DLAI-向量数据库笔记-全-
DLAI 向量数据库笔记(全)
001:Weaviate简介


在本节课中,我们将要学习向量数据库的基础概念及其在现代人工智能应用中的核心作用,特别是它在检索增强生成技术中的关键地位。
大型语言模型催生了许多令人兴奋的新应用。然而,语言模型的一个短板是,一个训练好的模型不具备对近期事件的了解,也无法获取其训练数据之外的专有文档信息。
为了解决这个问题,可以使用检索增强生成技术。RAG的一个关键组件就是向量数据库。专有或最新的数据首先被存储在这个向量数据库中。当用户提出涉及这些信息的查询时,该查询会被发送到向量数据库。数据库随后检索出相关的文本数据,最后,这些检索到的文本可以被包含在给语言模型的提示中,为其提供回答问题的上下文。
向量数据库实际上早于最近的生成式AI热潮。它们长期以来一直是语义搜索应用的重要组成部分。这类应用基于词或短语的含义进行搜索,而不是寻找精确匹配的关键词搜索。向量数据库也常用于推荐系统中,用于寻找相关物品推荐给用户。
作为一名AI开发者,理解向量数据库的工作原理、其内部机制,将有助于你在自己的项目中更有效地使用它。例如,你将知道何时应用稀疏搜索(如关键词搜索)、密集搜索(即通过向量相似性实现)或结合两者的混合搜索。理解不同的相似度计算方法将帮助你选择最佳的距离算法。理解向量数据库和搜索的扩展挑战,将帮助你在不同的嵌入搜索算法之间做出选择。
本课程的讲师是Sebastian Vitas,他是Weaviate的开发者关系负责人,在指导用户构建和使用向量数据库方面拥有丰富的经验。

感谢Andrew。能与你合作这门课程,我感到非常荣幸。

在本课程结束时,你将理解并实现构成向量数据库的许多要素。例如,嵌入——代表短语含义的密集向量;距离度量,如点积或余弦距离;不同类型的向量搜索,如查看数据库中所有条目的线性搜索,或通过允许近似结果来加速搜索的近似搜索;以及不同的搜索范式,如稀疏搜索、密集搜索和混合搜索。最后,你将构建向量数据库的真实世界应用,创建一个具备混合搜索和多语言搜索功能的RAG系统。
内容非常丰富。希望我们通过Weaviate介绍的一些想法,能激励你继续自己的语言模型和机器学习之旅,在向量数据库的基础上进行构建。许多人参与了本课程的贡献,我们感谢Zen Hasan。

Weaviate团队,以及来自DeepLearning.AI的Jeff Ludwig和Ismail Ggari。
希望本短期课程中涵盖的一些示例能激励你继续自己的大语言模型之旅,在向量数据库的基础上进行构建。

让我们进入下一个视频,开始学习。


002:嵌入向量从何而来?🔍


在本节课中,我们将学习向量数据库中的向量是如何产生的。我们将从神经网络如何将数据表示为数字(即嵌入)开始,并动手构建一个自编码器架构,将图像嵌入为向量。接着,我们将探讨数据对象之间相似或相异的含义,以及如何利用数据的向量表示来量化这种关系。


自编码器的工作原理 🤖
上一节我们概述了学习目标,本节中我们来看看自编码器是如何工作的。为了说明其原理,我们将使用MNIST手写数字数据集。当我们输入一张数字图像(例如一个数字“0”的图像,其尺寸为28x28像素,即784维)时,编码器会将其压缩,然后解码器会将其解压缩,最终输出另一张图像。
你可以看到,输入图像和输出图像并不完全一致。这就是为什么我们需要在多个训练样本上运行此过程。每次运行时,内部的权重都会得到调整,每次匹配都会越来越好,直到模型训练完成,我们对输入输出的结果感到满意。
这里需要注意的关键点是,输出仅由中间的那个向量生成。因此,该向量包含了该图像的“含义”,我们称之为嵌入向量。稍后我们将进行编码实现,但这就是模型内部的结构。

构建自编码器模型 🏗️
上一节我们介绍了自编码器的概念,本节中我们来构建其具体结构。我们可以看到一组密集连接层。当图像通过密集层时,它被压缩到256维和128维,直到我们到达2维的瓶颈层。同样地,解码器会将这个2维的嵌入向量扩展到128维、256维,直到最终输出。
我们选择让中间的嵌入向量只有2维,纯粹是为了在本课程中便于可视化。实际上,向量嵌入的维度通常远不止于此,经常达到100维或更多。
这是一个很好的例子,展示了我们如何获取任何类型的数据(例如一张图像或一整段文本),并将其转换为机器可以理解的向量嵌入。向量嵌入捕捉了底层数据的含义,你可以将其视为数据的机器可理解格式。
代码实现:数据准备与模型设置 💻
现在,让我们看看这一切在代码中是如何工作的。首先,我们需要加载一些库,我们将使用TensorFlow。如前所述,我们将使用MNIST加载数据集。这将为我们提供一个训练集和一个测试集。
接下来,我们需要对数据进行归一化处理。实际上,我们是将28x28的图像展平成一个结构。如果我们打印处理前后的形状,会看到训练数据从60000个28x28的对象,变成了60000个784维的对象,测试数据同理。
现在,为我们的模型设置一些参数:批量大小设为100,训练50个周期。隐藏状态的初始维度为256,目标是生成2维的向量嵌入。
让我们看一个输入图像的示例,它看起来很像数字“0”。接下来,我们需要构建一个采样函数,以便在训练阶段抓取一定数量的图像。
构建编码器与解码器 ⚙️


现在,我们来构建编码器。正如之前提到的,它将有两个密集层:第一层256维,第二层128维。
接着,我们需要构建一个匹配的解码器。以类似的方式,这次从2维开始,扩展到128维,再到256维。最终,我们可以创建解码器函数。
这是用于训练自编码器(也称为变分自编码器)的损失函数。其基本思想是优化模型,使其能很好地匹配输入和输出。

模型训练与可视化 📊
现在我们有了所有组件,可以开始训练了。训练将运行50个周期,每次训练100个对象,这需要几分钟时间。

训练完成后,我们可以可视化我们的数据。首先构建一个扁平化的编码器,然后添加一段代码将我们的向量嵌入绘制到图表上。



你可以看到,相似的向量在向量嵌入空间中聚集在一起。例如,所有的“0”聚集在这里,所有的“1”聚集在那里。整个空间在二维中展示,这两个维度就是我们嵌入向量内部的维度。

比较向量嵌入:距离度量 📏
现在我们可以进入比较向量嵌入的阶段。让我们选取三张不同的图像:一张“0”(0_a),另一张“0”(0_b),和一张代表数字“1”的图像。
如果我们获取这三个对象,并调用函数生成向量嵌入,那么0_a、0_b和1将包含我们需要的向量值。打印它们,你可以看到两个“0”的向量彼此相似,而代表数字“1”的向量则相当不同。
我们也可以对文本嵌入做类似的事情。使用一个句子转换器,抓取几个句子,就可以为每个句子生成向量嵌入。每个向量有384维。

为了以视觉方式表示这些向量,我将把它们绘制成条形码。可以看到,前两个向量彼此相似,而第三个向量则相当不同。
现在,我们将讨论距离度量,以及如何计算不同图像或句子(即代表它们的向量嵌入)之间的距离。我们将看四种不同的方法:欧几里得距离、曼哈顿距离、点积和余弦距离。
以下是四种距离度量的核心概念:
- 欧几里得距离:计算两点之间的最短直线距离。
- 公式:
distance = sqrt(sum((vector_a - vector_b)^2))
- 公式:
- 曼哈顿距离:计算两点在网格状路径上,只能沿轴移动的距离之和。
- 公式:
distance = sum(abs(vector_a - vector_b))
- 公式:
- 点积:衡量一个向量在另一个向量上投影的大小。
- 公式:
dot_product = sum(vector_a * vector_b)
- 公式:
- 余弦距离:通过计算两个向量之间夹角的余弦值来衡量方向相似性。
- 公式:
cosine_similarity = dot_product / (norm(vector_a) * norm(vector_b))
- 公式:
让我们从欧几里得距离开始。计算0_a和0_b之间的欧几里得距离,结果约为0.6。NumPy也有内置方法可以计算。计算所有距离后,我们可以看到两个“0”之间的距离很小,而它们与数字“1”的距离则很远。这从数学上证明了两张“0”的图像非常相似。
接下来看曼哈顿距离。计算0_a和0_b之间的距离,得到这个值。同样,NumPy提供了简便的计算方法。比较所有距离后,再次看到0_a和0_b非常接近,但与数字“1”相距甚远。
然后看点积。0_a和0_b的点积是3.6。计算所有三个向量的点积,可以看到0_a和0_b的点积是3.6,而“0”与“1”比较的值是负数。与之前例子中“距离越小匹配越好”不同,对于点积,值越高通常意味着匹配越好,而负值通常意味着它们相距甚远。
最后看余弦距离。其思想是相似的向量彼此之间的夹角会很小。对于两个“0”,余弦值非常接近1,这表明匹配度非常高。如果我们观察0_a除以0_b的幅度,会发现它们非常接近,这意味着两个向量沿着非常相似的方向延伸。
让我们将余弦距离计算封装成一个函数。计算所有距离后,可以看到0_a和0_b之间的夹角很小,而0_a与1之间的余弦值则低得多,这再次证明了我们的观点。
应用于文本嵌入 📝
现在回到句子嵌入的例子。有趣的是,在所有距离度量中,点积和余弦距离在自然语言处理领域非常常用。
例如,我们可以尝试使用点积来比较所有这些向量。可以看到前两个句子之间的点积非常高,这表明前两个句子非常相似,而其他句子则不那么相似。
如果用余弦距离做同样的计算,我们再次可以看到前两个句子之间的夹角指示器非常接近,而其他的则相距较远。但同时,前两个句子也并非完美匹配。
总结 🎯

本节课中,我们一起学习了向量数据库的核心基础——嵌入向量的生成与比较。我们首先了解了自编码器如何将高维数据(如图像)压缩成低维的向量表示,并动手实现了这一过程。接着,我们深入探讨了如何量化向量之间的相似性,介绍了欧几里得距离、曼哈顿距离、点积和余弦距离这四种关键的距离度量方法,并通过代码示例展示了它们在图像和文本数据上的应用。理解这些向量表示和距离度量,是后续在向量数据库中进行高效搜索和检索的基石。在下一课中,我们将运用这里学到的知识,学习如何在大量向量中进行搜索。
003:暴力kNN算法


在本节课中,我们将通过暴力K最近邻算法,直观地理解向量搜索或语义搜索的原理。你将编写一个暴力KNN算法的实现,并了解如何用它来准确获取嵌入空间中与查询向量最接近的向量。然后,我们将探讨暴力KNN算法在时间复杂度方面存在的问题。这将引导我们认识近似最近邻算法,这类算法是向量数据库技术的核心。现在,让我们开始学习。
向量能够捕捉数据背后的含义。因此,为了找到在含义上与我们的查询相似的数据点,我们可以在向量空间中搜索并检索最接近的对象,然后返回它们。这个过程被称为语义搜索或向量搜索。这里的语义搜索,指的是利用词语或图像本身含义进行的搜索。










暴力搜索的原理与步骤
寻找相似向量的一种方法是暴力搜索,它遵循以下步骤:
- 计算距离:给定一个查询,计算所有向量与查询向量之间的距离。
- 排序距离:对所有计算出的距离进行排序。
- 返回结果:返回距离最小的前K个最佳匹配对象。
这种方法在经典机器学习中被称为K最近邻算法。然而,暴力搜索伴随着巨大的计算成本。可以看到,总体查询时间随着我们存储中对象数量的增加而增长。如果数据量随时间翻倍或增至三倍,查询时间也会相应翻倍或增至三倍。
代码演示与规模扩展
接下来,我们将在代码中演示这个算法,并尝试在数据点数量和维度上进行扩展。
我们已经将一些库加载到笔记本中,其中值得关注的是 nearest neighbor 库,我们将用它来演示暴力搜索算法及其工作原理。
首先,我们生成20个二维的随机点。
# 生成20个二维随机点示例代码
import numpy as np
points = np.random.rand(20, 2)
然后,我们可以将它们漂亮地绘制在图表上,以便观察它们在屏幕和向量空间中的分布情况。
现在,让我们将所有数据点添加到最近邻索引中。可以看到,这里我们使用的是暴力算法。运行后,它会返回一个可供查询的索引。
现在,我们执行一个查询,寻找最近的4个向量(因为K设置为4)。这是我们正在寻找的查询向量。运行后,我们会看到向量10、4、19和15是最近的四个,并附有相应的距离。
尽管我们只有20个对象,但我们已经可以测量这次查询花费了多长时间。如果我们运行这段代码并记录查询前后的时间,会发现搜索20个向量只需要极短的时间。
测试大规模数据集的时间复杂度
现在,让我们看看对于比20个对象大得多的数据集,暴力搜索的时间复杂度如何。为此,我们有一个方便的函数 speed_test,它接收要测试的对象数量作为参数。
它分三步工作:
- 首先,根据数量随机生成相应数量的对象。
- 然后,再次使用最近邻方法构建索引。
- 最后,我们测量实际查询的时间,并将其作为结果返回。
让我们在20,000个对象上测试一下,可以看到运行速度相当快。但为了真正测试其极限,让我们在更大的数据集上运行:200,000、2百万、2千万、2亿个对象。你已经可以看到,随着对象数量的增加,查询时间越来越长。即使在2百万到2千万之间(增加了10倍),时间也显著增长。对于2亿个对象,查询耗时12秒。可以想象,如果我们实际有十亿或更多对象,情况会很快变得难以处理。
我们刚刚看到的复杂度仅针对二维情况。那么,如果增加向量嵌入的维度会发生什么?让我们将维度增加到768,看看结果如何。
高维向量下的性能挑战
首先,我们生成1000个768维的文档向量。在这里,我们生成这些向量并进行归一化处理。同时,我们有一个用于测试性能的查询向量。
现在,我们运行一个查询:开始时启动计时器,然后使用点积计算查询向量与所有一千个向量嵌入之间的距离。最后,得到结果后,我们对所有距离进行排序,然后停止计时器。这将给出找到前五个最近结果所需的时间。我们可以看到,搜索1000个向量嵌入花费了大约0.5毫秒,并得到了最近的匹配项。
现在,让我们真正测试一下在768维下,对1000、1万、10万、50万个对象运行一次向量查询需要多长时间。可以看到,直到10万个对象,返回速度都相当快,但50万个对象需要的时间稍长。一次针对50万个向量的查询就花费了近2秒。如果我们要对50万个对象运行1000次查询,总共将花费大约半小时,这并不理想。
总结
本节课中,我们一起学习了向量数量如何影响查询时间。向量越多,查询完成所需的时间就越长。当我们接近现实场景时,问题变得尤为棘手:在我们的例子中,向量维度为768。在这种情况下,一旦对象数量达到50万,暴力搜索就无法胜任了,因为每次查询需要近2秒。而在现实场景中,你可能需要处理数千万甚至数亿个对象。在下一课中,我们将介绍不同的方法,教你如何在查询大量向量时,仍然能在合理的时间内返回结果。


004:近似最近邻算法 🧠

在本节课中,我们将学习近似最近邻算法的理论与实践。你将理解ANN算法如何通过牺牲少量精度来换取巨大的性能提升。我们将重点探讨分层可导航小世界算法,并了解它如何驱动世界上最强大的向量数据库。我们还将演示HNSW的可扩展性,以及它如何解决暴力KNN算法的时间复杂度问题。



理论:从精确到近似

上一节我们介绍了向量搜索的基本概念。本节中,我们来看看当数据量变大时,精确搜索面临的问题。
观察一个包含20个向量的例子,搜索最近邻可能不是大问题。然而,一旦数据量达到数千或数百万,这就变成了一个巨大的难题,精确搜索变得不可行。
许多算法可以让我们以更高效的方式找到近似最近邻向量。解决此问题的算法之一是HNSW,它基于人类社会网络中的“小世界现象”。其核心思想是,平均而言,我们之间都通过“六度分隔”理论相连。
因此,你可能有一个朋友的朋友的朋友……最终连接到目标人物。整个想法是,你可能认识一个认识所有人的人,而那个人可能也认识一个社交广泛的人。通过这种方式,你实际上可以在六步之内找到与目标人物的联系。我们可以将同样的概念应用到向量嵌入上,如果我们能建立这种连接。
构建可导航小世界
现在,让我们看看可导航小世界算法,它允许我们在不同节点之间构建这些连接。
以下是构建过程的步骤:
- 从向量0开始,尚无连接。
- 添加向量1,唯一可能的连接是到向量0。
- 添加向量2,可以建立到向量1和0的两个连接。
- 继续此方法,为向量3建立到向量2和0的连接。
- 为向量4建立到向量2和0的连接。
- 为向量5建立到向量2和0的连接。
- 为向量6建立到向量2和4的连接。
- 最后,为向量7建立到向量5和3的连接。
就这样,我们构建了一个可导航小世界。请注意,在此示例中,我们仅为每个向量尝试建立两个连接,但在现实中,根据向量数量,你可以有8个、32个甚至更多连接。
在NSW中进行搜索
现在让我们看看如何在这个可导航小世界中进行搜索。
在这个例子中,我们有一个位于左侧的查询向量。我们大致可以猜到向量6将是最近的向量。通常,使用NSW进行搜索时,我们从一个随机入口节点开始,并尝试朝着最近邻的方向移动。
搜索路径如下:
- 从节点7开始,它连接到节点3和5。可以看到节点5比节点7更近,因此移动到节点5。
- 从节点5,可以看到我们还连接到节点0和2,而节点2明显更近,因此移动到节点2。
- 从节点2,有多个候选节点,最佳选项是节点6,因此移动到节点6。
- 在节点6,不再有更好的候选节点,因此查询在此结束。
这就是我们找到最佳匹配(恰好是我们寻找的最近邻向量)的方式。
近似结果与分层结构
然而,使用NSW的搜索并不总是能找到最佳匹配。让我们看另一个例子。
如果从节点0开始:
- 潜在的候选节点是这些,此步骤的最佳选择是向量1。
- 从向量1开始,不再有任何更好的候选节点,因此搜索在此结束。
在这种情况下,我们没有找到最佳可能结果,但我们找到了近似最近邻,这仍然是一个相当好的结果,但不一定是完美结果。
现在是时候学习分层可导航小世界了,它在彼此之上放置了多层可导航小世界。你可以这样想象:如果你要去世界上的某个地方,首先你可能会乘飞机到离目的地最近的机场,然后可能换乘火车到达你想去的城镇,最后,一旦到了底层,你会步行或乘出租车前往最终目的地。
需要指出的是,HNSW每一层的构建方式与NSW非常相似,因此我们不再深入探讨。HNSW的查询方式同样是:从一个随机节点开始,我们只能从最高层可用的节点中选择,然后在该层内移动到最近的一个。一旦到达那里,我们可以在下一层找到最佳匹配。最终,一旦我们到达底层,我们就可以前往最接近查询向量的对象,这将帮助我们完成搜索的“最后一公里”。
节点被分配到不同层的方式是通过随机生成一个数字,该数字将该节点分配到该层及以下所有层。值得注意的是,节点出现在较高层的概率对数性地低于出现在较低层的概率。因此,顶层的节点数量将远少于底层的节点数量。
例如:
- 如果随机数是0,则该节点仅存在于底层(第0层)。
- 如果随机数是2,则该节点存在于第0、1和2层。
HNSW的特性
以下是HNSW的一些特性:
- 如前所述,节点存在于更高层的可能性要低得多。
- 查询时间呈对数增长,这意味着随着数据点数量的增加,执行向量搜索所需的比较次数仅呈对数增长。在计算机科学中,这被称为 O(log n) 时间复杂度。
- 这种特性可以很好地可视化:随着数据点数量的增长,速度并不会随时间受到太大影响。从图中可以看出,如果向量数量从50万增加到100万,运行时间的增加是最小的。
代码实践:构建与搜索
现在让我们看看这一切在代码中是如何工作的。
在这个笔记本中,我们将从40个二维向量开始,并将最近邻连接数设置为2。我们可以随机构建这些向量。
首先,添加一个位于 [0.5, 0.5] 的查询向量。我们创建一个包含该查询向量的节点列表,然后使用networkx库进行可视化。接着,我们打印节点并为后续绘图块创建查询向量的位置。
接下来,我们将运行暴力算法来找到我们搜索的最佳可能向量嵌入,并将其绘制在图表上。在这种情况下,我们可以看到我们的查询在这里,而最佳匹配就在它旁边。
在这一步,我们构建HNSW层,然后在循环中逐一打印层ID,并显示每一层的所有节点和连接。让我们看一下:
- 在顶层,我们可以看到节点20、34、28和39已经相互连接。
- 当我们到达第2层时,有更多节点和更多连接。
- 在第1层,几乎所有节点都已重新连接。
- 最后在第0层,所有节点都存在并连接到它们的最近邻。
执行HNSW搜索查询
现在我们已经建立了跨所有层的整个网络,可以运行实际的HNSW搜索查询。
首先,我们得到一个搜索路径图数组,其中包含跨所有层的旅行路径图。接下来,我们有一个入口图数组,它为我们提供了图的入口点。然后,我们在循环中遍历所有层,逐层绘制所有结果以进行可视化。
搜索过程如下:
- 从顶层节点39开始。从39,我们可以移动到节点20,这使我们更接近查询。一旦我们在20,不再有任何20的邻居节点能使我们更接近查询。
- 然后我们移动到第2层。从第2层的节点20,可以带我们到节点16。但节点16没有其他能使我们更接近查询的候选节点,这使我们进入下一层。
- 从第1层,我们可以从节点16移动到2。从节点2,不再有任何其他候选节点能使我们更接近查询。
- 所以我们最终移动到底层。然后从节点2,我们可以一路走到节点25,而它恰好是我们查询的完美匹配。
就这样,我们跨所有层执行了HNSW查询,并返回了最接近的匹配。
使用向量数据库进行搜索
现在让我们看看如何使用向量数据库执行向量搜索,它几乎包含了所有这些功能。
为此,我们将使用Weaviate,一个开源向量数据库。Weaviate提供的模式之一是嵌入式选项,允许我们在笔记本内运行向量数据库。
第一步,我们需要创建数据模式(或我称之为数据集合)。我们将它命名为“my_collection”,向量化器设置为“none”,这基本上意味着我们只想使用纯向量搜索,并且我们希望使用的距离度量设置为“cosine”。
运行后,我们将在数据库中获得一个新的空集合。如果你想稍后重新运行相同的示例并需要重新创建集合,我留给你一段代码,允许你在集合存在时删除它然后重新创建,但你不必觉得必须一遍又一遍地重新运行它。
现在是时候将一些数据导入向量数据库了。假设我们有这五个具有标题、完整值和向量嵌入的随机对象。
以下代码将帮助我们获取数据对象并将其加载到数据库中。我们设置了批量加载过程,这是一种最佳实践,尽管我们只处理五个对象,但通常如果你加载成千上万或数百万对象,使用批量加载过程实际上是有益的。然后,我们实际运行一个循环遍历所有数据项,构建一个属性对象,然后运行client.batch.add_data_object,将对象插入数据库。
我们需要添加集合名称(称为“my_collection”),数据对象是我们拥有的属性,向量实际上存在于item.vector中。这就是我们如何将向量传递到数据库中的方式。
现在让我们检查数据库中有多少个对象。我们可以在集合上运行此查询,然后只询问内部对象的计数。运行后,我们可以看到我们的集合包含五个对象。
查询数据库
现在让我们实际查询数据库。查询如下:我们想说,嘿,我想从“my_collection”中搜索,我想取回标题,我想用这个向量运行它。这只是一个跨越六个维度的随机向量,以匹配我们的原始数据。通过这样说,我们告诉Weaviate只获取两个最佳匹配。如果我运行这个,它会告诉我们第二个对象和第四个对象匹配我们的结果。
如果你想查看所有匹配对象的向量嵌入,可以复制此代码并添加这一行,它基本上告诉我们也获取距离、向量和数据的ID。现在我们可以看到第一个对象的距离计算为0.65,这是匹配的向量,第二个匹配向量也是如此。
由于我们使用的是向量数据库,我们可以做所有额外的事情,比如对特定属性进行过滤。
在这种情况下,我们可以添加一点额外的代码,告诉数据库只搜索full_value大于44的对象,并且只搜索预过滤的对象。像这样,你可以看到我唯一匹配到的对象是那些full_value确实大于44的对象。
我们可以在向量搜索中做的另一件事是,基于提供的对象ID查找其他相似对象。在这里,我们只是从前一个查询中获取第一个结果,并寻找三个匹配此对象的对象。在这种情况下,你当然会找到它自己以及第四个和第一个对象。
总结

本节课中,我们一起学习了HNSW的工作原理,如何构建HNSW层并在所有层中进行搜索,同时也学习了如何在生产就绪的数据库中使用类似算法。下一节课,你将学习如何将向量数据库与像OpenAI这样的机器学习模型一起使用,如何向量化数据以及如何向量化查询,并且还将深入探讨用于创建、读取、更新和删除对象的CRUD操作。
005:L4_对象与向量




在本节课中,我们将介绍 Weaviate,一个开源的向量数据库,并讨论如何使用它执行语义搜索,以及它如何支持 CRUD 操作(即创建、读取、更新和删除)。我们还将检查存储在数据库中的对象和向量。本节课将为你提供向量数据库入门的基础知识,甚至包括一些高级主题,例如执行过滤搜索。




在这个项目中,我们将使用一个包含一组《危险边缘》问答的示例数据集。我们的想法是,我们将拥有类似“类别”、“问题”和“答案”的数据,并将其加载到向量数据库中,然后对其执行语义搜索查询。
与上一课类似,我们将设置一个 Weaviate 的嵌入式实例,但这里的一个不同之处是,我们将使用 OpenAI 来生成我们的向量嵌入。为此,我们需要加载一个 OpenAI API 密钥。如果你在自己的环境中运行此项目,可能需要将其替换为你自己的 API 密钥。但出于本教程的目的,你可以保持原样。如果你看到此类警告信息,不必担心,这纯粹是信息性的,一切工作正常。


如果你好奇嵌入式实例内部有哪些可用功能,基本上 Weaviate 提供了这个模块化系统。它允许你使用诸如与 OpenAI 的生成式搜索,或者通过 Cohere、Hugging Face 或 OpenAI 运行文本向量化等功能。这就像是其背后的强大动力,因为它允许你跳过手动向量化,让数据库为你处理。这就是我想在接下来的步骤中向你展示的。
与上一课一样,我们需要从创建一个新的集合开始。我们将其命名为 question。这次,我们将使用 text2vec-openai 向量化器。这是一个非常强大的模块,允许你在导入数据时以及每次查询时自动生成向量嵌入。向量数据库将获取必要的输入,然后将其发送给 OpenAI 进行向量化。
作为提醒,让我们打印一个数据对象,以便了解其数据结构。然后,我们可以将该数据对象导入到我们的 questions 集合中。这就是我们将要做的:我们将以每批 5 个的方式导入数据,基本上对于每个对象,我们会说“嘿,我们正在导入这个问题”,用答案、问题和类别构建我们的对象,然后将其传递到数据库中。
请注意,我们这次没有传递向量嵌入,因为这正是 text2vec-openai 模块应该做的事情,它将为每个对象生成向量嵌入。如果你运行此代码,向量化就完成了。
为了验证,我们可以在 question 集合上运行这个快速的聚合查询,可以看到我们确实有 10 个对象。
现在,我们可以做的是,也许从我们的 question 集合中获取一个对象。让我们看看它有什么类别、问题和答案。但更重要的是,让我们看看为该特定对象生成了什么向量嵌入。如果我们运行此代码,可以看到一个完整的向量嵌入,它相当长,应该是一个大约 1500 维的嵌入。
现在,让我们尝试使用语义搜索运行一个向量查询。我们将使用 nearText 操作符,并将我们的查询作为概念传入。查询本身是“biology”,这就是我们要找的。为了添加一些额外信息,我们还显示一个附加属性,即“距离”。然后,如果我们运行此查询,应该会得到两个与“biology”查询匹配的对象。
这就是我们的结果。由于底层模型使用余弦距离,较小的数字表示更好的匹配。因此,在这种情况下,0.19 和 0.2 实际上表明与我们的“biology”查询有很强的匹配度。
我们还可以运行一个查询来返回数据库中所有的对象,然后查看这里所有可用的距离。你可以看到,随着我们向下滚动,距离会增加。由于我们使用余弦距离度量,这基本上意味着最差的匹配在底部,最好的匹配在顶部。
现在,让我们再次尝试运行相同的查询。但问题是,我们并不总是知道有多少对象是最佳匹配。也许我们可以做的一件事是说,比如“我接受特定距离内的任何东西”。这次我可以说我的距离是 0.24,任何高于该距离的对象都应该被拒绝。这是一个很好的方法,可以说“我对结果质量有特定要求,任何超出该要求的都应该被忽略”。
就像你在这里看到的,最终结果在 0.23 处被截断。
由于我们正在使用向量数据库,这意味着我们还可以执行各种 CRUD 操作,如创建、读取、更新或删除。
要创建单个对象,我们需要做的就是调用 client.data_object.create(),然后我们可以在其中传入数据对象,并提供要插入的集合名称。同样,text2vec-openai 模块会为此对象生成向量嵌入。让我们添加这个对象,现在我们可以打印它的 UUID。
现在,让我们看一个读取示例,以读取我们在上一个代码块中刚刚创建的对象。我们将通过这个对象 ID 来获取它。然后,如果我们打印它,这就是我们的对象。如果你好奇想看看为它生成了什么向量嵌入,我们所要做的就是添加 include_vector=True,然后运行该命令将为我们提供包含所有信息及其向量嵌入的对象。
现在,让我们获取该对象并可能更新它。之前答案只是“Italy”,但让我们将其设置为“Florence in Italy”。如果我们运行此代码,对象将被更新。然后,我们可以再次通过其 ID 获取它,可以看到答案确实被更新了。
最后,我们到了只想删除示例对象的阶段。在这种情况下,我们首先要做的是检查之前有多少个对象。然后,我们可以根据其 ID 删除该对象。最后,我们将打印聚合结果,以验证我们只剩下一个对象。之前我们有 11 个,现在回到了 10 个。
本节课到此结束。在这里,你学习了如何使用向量数据库通过 OpenAI 自动向量化所有数据,并使用相同的机制向量化查询并执行各种搜索,包括向量搜索和过滤搜索。我们还介绍了如何使用各种 CRUD 操作,以便在应用程序的整个生命周期中维护数据。在下一课中,我们将介绍稀疏向量和稠密向量的概念,并了解混合搜索,它允许我们结合这两种方法来提供更好的结果。


006:混合搜索 🔍

在本节课中,我们将学习两种不同的搜索技术:密集向量搜索和稀疏向量搜索。我们将介绍它们各自的概念、实现方式以及优缺点。随后,我们将探讨如何通过混合搜索将两者结合,从而充分利用各自的优势,获得更优的搜索结果。


密集搜索与稀疏搜索的区别
上一节我们介绍了向量搜索的基本概念,本节中我们来看看两种主要的搜索方法。
密集搜索使用数据的向量嵌入表示来执行搜索。它依赖于数据的语义含义来匹配查询。例如,搜索“小狗”可能会返回关于“幼犬”的信息。然而,这种方法有其局限性。如果使用的模型是在完全不同的领域上训练的,查询的准确性就会很差。这就像问一位医生如何修理汽车引擎,医生很可能无法给出好答案。另一个例子是处理序列号或看似随机的文本字符串时,像“43300”这样的代码本身没有太多语义含义,使用语义搜索引擎可能无法返回高质量的结果。
因此,我们需要为这类情况寻找不同的方向,尝试使用关键词搜索,也称为稀疏搜索。
稀疏搜索允许你利用关键词在所有内容中进行匹配。一个例子是使用词袋模型。其核心思想是,对于数据中的每一段文本,提取所有单词并不断扩展可用词汇表。例如,在一个句子中,“extremely”出现一次,“word”出现两次,我们就可以为这个对象构建一个稀疏嵌入向量。之所以称为“稀疏”,是因为在整个数据集中,词汇量可能非常大,但代表某段具体数据的向量中,绝大多数位置(对应未出现的词)的计数都是0。
一个优秀的关键词搜索算法是最佳匹配25,也称为 BM25。它在处理大量关键词搜索时表现优异。其核心思想是:统计查询短语中每个词的出现频率,出现频率高的词在匹配时权重较低,而罕见的词如果匹配成功,则得分会高得多。
我们不必在两者中选择其一,这就是混合搜索的用武之地。
混合搜索的工作原理
混合搜索是一种在单次查询中同时运行稀疏向量搜索和密集向量搜索的方法。对于每种搜索,我们会得到不同的得分和结果。然后,我们可以将这些得分组合成一个综合得分,并据此对所有结果进行重新排序,最后返回给用户。
让我们看看如何在代码中实现这一切。
代码实践
我们将使用与上一课完全相同的数据集,因此不再赘述数据细节。让我们快速加载数据,创建一个新的Vectara实例。
# 创建Vectara实例
client = vectara_client
# 创建集合并导入数据
collection = client.create_collection()
collection.import_data(data)
现在,我们可以开始执行搜索查询了。
首先,执行一个你已经熟悉的查询,使用 near_text 进行密集向量搜索,我们搜索与“动物”相关的概念。
# 密集向量搜索
results_dense = collection.search(
query="animal",
search_type="near_text"
)


我们可以看到,语义上我们匹配到了像“哺乳动物”和“鳄鱼”这样的内容,当然也精确匹配到了“动物”本身。
接下来,尝试使用关键词搜索执行相同的查询。我们将添加 with_bm25 参数。
# 稀疏向量搜索 (BM25)
results_sparse = collection.search(
query="animal",
search_type="bm25"
)


这次,我们只得到一个结果,即精确匹配“动物”关键词的对象。

现在,进入最精彩的部分:执行混合搜索。
# 混合搜索
results_hybrid = collection.search(
query="animal",
search_type="hybrid",
alpha=0.5
)
这里有一个特殊参数 alpha,它决定了倾向于哪种搜索方式。alpha 越接近1,表示越倾向于密集向量搜索的得分;alpha 越接近0,则表示越倾向于关键词搜索的得分。
运行后,我们可以看到得到的结果与之前类似,但有趣的是,内部包含“animal”关键词的对象被排到了最前面,这使它进入了我们的首要关注区域,可以优先返回给用户。
让我们再尝试用不同的 alpha 值进行搜索。
# 纯关键词搜索倾向 (alpha=0)
results_hybrid_keyword = collection.search(
query="animal",
search_type="hybrid",
alpha=0
)
# 纯密集向量搜索倾向 (alpha=1)
results_hybrid_dense = collection.search(
query="animal",
search_type="hybrid",
alpha=1
)
当 alpha=0 时,我们只得到基于关键词搜索的有用响应,密集向量搜索的结果未被返回。当 alpha=1 时,这基本上就是纯粹的密集向量搜索。
这就是密集搜索、稀疏搜索以及通过混合搜索将两者结合起来的强大之处。
总结
本节课中,我们一起学习了:
- 密集向量搜索:基于语义相似性进行匹配,擅长处理有丰富含义的查询。
- 稀疏向量搜索(如BM25):基于关键词精确匹配,擅长处理术语、代码或特定名称的查询。
- 混合搜索:通过
alpha参数平衡两者,结合了语义理解和关键词精确匹配的优势,能够提供更全面、更准确的搜索结果。

在下一节课中,我们将深入探讨多语言搜索和检索增强生成。
007:多语言检索增强生成搜索 🔍

在本节课中,我们将探索多语言模型与向量数据库结合的灵活性,它允许你加载和查询多种语言的数据。我们还将介绍检索增强生成的概念,并探索如何在一个简单的查询中实现检索、推理和生成这个多步骤过程。




多语言搜索与RAG概述
上一节我们介绍了语义搜索的基本原理。本节中,我们来看看多语言搜索以及检索增强生成是如何工作的。
多语言搜索的原理与语义搜索非常相似。在语义搜索中,我们可以比较“狗”和“小狗”并找到高度匹配的结果。在多语言搜索中,你可以拥有不同语言的相同文本,这些文本会生成非常相似(即使不完全相同)的嵌入向量。通过这种方法,我们可以使用相同的技术来搜索任何语言的内容。
检索增强生成的基本思想是,它允许我们将向量数据库用作外部知识库。它不仅能检索相关信息并提供给大语言模型,还能与向量数据库中的数据协同工作。使用RAG的一个好处是,我们可以在自己的数据上运行应用,而无需重新训练或微调大语言模型。
你可以将RAG想象成去图书馆查阅资料。如果有人在你没有任何参考资料的情况下提问,你可能只能编造答案。但如果你去图书馆,你可以阅读书籍然后提供回答。这基本上就是RAG结合向量数据库所做的事情。
以下是RAG的一些关键优势:
- 它有助于减少大语言模型的“幻觉”(即生成不准确或虚构信息)。
- 它使大语言模型能够引用信息来源。
- 它可以解决知识密集型任务,特别是对于那些在公开数据中很难找到的信息。
RAG查询流程与代码示例
一个完整的RAG查询工作流程如下:
- 首先,向向量数据库发送查询,获取所有相关的源对象。
- 然后,将这些信息组合成一个提示。
- 最后,将提示发送给大语言模型,由其生成我们感兴趣的响应。
以下是一个执行RAG的快速代码示例:

# 这是一个RAG查询的简化示例
results = vector_db.query(
query_text="你的查询问题",
top_k=5
)
prompt = construct_prompt(results, user_query)
response = llm.generate(prompt)
可以看到,这与我们过去执行的查询非常相似,只是这次我们增加了 generate 部分。不过,我们更倾向于在具体代码中实践它。

实战准备:环境与数据
现在,让我们开始动手构建。首先准备我们的环境。本次演示我们将使用一个已部署在云端的Weaviate实例,它利用了Cohere的多语言模型。
在这里,我们可以看到我们提供了两种类型的API密钥:
- Cohere API密钥:用于多语言搜索。
- OpenAI API密钥:用于生成式搜索。
让我们快速查看一下数据库中处理的对象数量。我们拥有大约430万篇维基百科文章可供使用,这非常棒。
多语言搜索实战


现在,让我们用这个大数据集进行一些有趣的查询。
首先,我们尝试搜索加利福尼亚州的度假胜地。运行这个查询并返回5个对象,你可以立即看到返回了几个英文对象,一个法语结果,一个西班牙语结果(实际上是两个)。你可能还注意到,我们在眨眼之间就完成了对430万个对象的查询。
为了让结果更清晰,我们添加一个过滤器,使返回的下一个结果都是英文的,并且只返回3个对象。查询本身仍然是多语言的。


那么,我们还能用这个系统做什么呢?我们尝试用不同的语言发送查询。例如,我们用波兰语询问“加利福尼亚州的度假胜地”。运行这个查询,我们仍然得到与之前相同的结果。
你可能会认为,用使用相似字母表的波兰语搜索并不那么令人印象深刻。那么,我们尝试用完全不同的字母表进行查询。我们可以用阿拉伯语运行相同的查询,它仍然表现得非常好,并且返回了谈论加利福尼亚州度假胜地的对象。
检索增强生成实战
现在,让我们做一些RAG示例。起点与我们之前所做的非常相似,是一个直接的语义查询。现在,我们可以添加一个提示。

我们构建一个提示,例如:“为我写一篇关于 {query_title} 的Facebook帖子,使用 {result_text} 内的信息。” 通过这样做,我们基本上根据查询结果构建了提示。

为了执行实际的生成查询,我们需要添加 .with_generate() 方法,并传入我们构建的单个提示。这样做,我们基本上是要求向量数据库为每个单独的对象传入相同的提示,因此我们应该得到三个不同的生成响应。

在这里,我们可以看到生成的结果,这是由GPT生成的全新内容。例如,“寻找阳光下的乐趣?不用再找了……” 这是它所基于的文本,同样的情况也发生在另外两个响应中。
另一种类型的RAG查询是分组任务,它基本上接受一个提示,然后运行查询,接着将所有结果作为单个查询发送给GPT。然后我们运行它,并期望只得到一个生成结果。例如,在这种情况下,我们想用两段话来总结这些帖子是关于什么的。

结果,我们得到了对原始查询返回的所有三个帖子的总结。

课程总结

本节课到此结束。在本节课中,你学习了如何使用多语言搜索,能够跨任何语言编写的内容进行搜索,并且可以用你需要的任何语言提供查询。我们还介绍了几个RAG查询的例子,在这些例子中,我们使用单个提示和分组任务,基于单个对象或生成集体响应来生成回答。
008:总结 🎯
在本节课中,我们将对向量数据库的核心知识进行总结。我们将回顾从向量嵌入到实际应用构建的完整流程,巩固所学内容。
课程回顾
上一节我们介绍了向量数据库支持的各种应用类型。现在,我们来对整个课程的核心要点进行总结。
以下是本课程涵盖的核心知识点列表:
- 向量嵌入:你理解了向量嵌入是什么。它们是将文本、图像等数据转换为高维空间中的数值向量的过程,其数学形式可表示为:
vector = embed(data)。这种表示能捕捉数据的语义特征。 - 搜索方法:你掌握了如何使用向量搜索、关键词搜索以及结合两者优势的混合搜索来检索数据。
- 索引算法:你了解了HNSW(Hierarchical Navigable Small World)这类近似最近邻搜索算法,它们能高效地在高维向量空间中进行相似性查找。
- 应用构建:你认识了所有能够基于向量数据库构建的应用程序类型。
总结与展望
本节课中,我们一起学习了向量数据库从基础概念到实际应用的完整知识体系。你现在已经掌握了向量嵌入的原理、多种数据检索方法、高效的索引技术以及广阔的应用场景。
我期待看到你将构建出何种创新的应用程序。🚀

浙公网安备 33010602011771号