从向量搜索到强大的 REST API,Elasticsearch 为开发人员提供了最全面的搜索工具包。探索 GitHub 上的示例笔记本,尝试新事物。您也可以立即开始免费试用或在本地运行 Elasticsearch。
本文是三篇系列文章中的第一篇,将深入探讨矢量搜索(也称语义搜索)的复杂性以及如何在 Elasticsearch 中实现。
第一部分重点介绍嵌入向量的基础知识以及向量搜索的工作原理。
有了在第一篇文章中学到的所有知识,第二部分将指导你如何在 Elasticsearch 中设置矢量搜索。
在第三部分中,我们将利用在前两部分中学到的知识,在此基础上深入研究如何在 Elasticsearch 中创建强大的混合搜索查询。
在进入本文的正题之前,让我们回顾一下向量的历史,向量是语义搜索的一个关键概念。
病媒并不新鲜
相信每个人都会同意,自 2022 年 11 月 ChatGPT 问世以来,没有一天不听到或读到关于 "矢量搜索 "的消息。它无处不在,如此普遍,以至于我们经常会觉得这是一项刚刚问世的新尖端技术,但事实上,这项技术已经存在了六十多年!对这一主题的研究始于 20 世纪 60 年代中期,1978 年,信息检索专家杰拉德-萨尔顿和他在康奈尔大学的同事发表了第一批研究论文。萨尔顿在密集和稀疏向量模型方面的研究成果是现代向量搜索技术的基础。
在过去的 20 年里,基于他的研究成果,许多不同的矢量数据库管理系统相继问世并推向市场。其中包括由 Apache Lucene 项目提供支持的 Elasticsearch,该项目于 2019 年开始致力于矢量搜索。
矢量现在无处不在,非常普遍,因此在使用矢量之前,首先要充分掌握其基本理论和内部工作原理。在深入探讨之前,让我们先快速回顾一下词法搜索和矢量搜索之间的区别,以便更好地理解它们的不同之处以及如何相互补充。
矢量搜索与词法搜索
介绍向量搜索的一个简单方法是将其与你可能已经习惯的更传统的词法搜索进行比较。矢量搜索(通常也称为语义搜索)和词法搜索的工作原理截然不同。词法搜索是我们在 Elasticsearch 中使用多年的一种搜索方式。简而言之,它并不试图理解所索引和查询内容的真正含义,而是努力将用户在查询中键入的单词或其变体(如词干、同义词等)的字面意思与之前已通过相似性算法(如 TF-IDF)索引到数据库中的所有字面意思进行词法匹配。

图 1:一个简单的词库搜索示例
我们可以看到,左上方的三份文档已经进行了标记化和分析。然后,将得到的术语编入倒排索引,该索引只是将分析术语映射到包含这些术语的文档 ID。请注意,所有术语都只出现一次,没有任何文件共享这些术语。搜索 "优秀的德国教师 "时,三份文档的匹配得分各不相同,但没有一份真正符合查询的真正含义。
如下图 2 所示,在处理多义词或同形词(即拼写相同但含义不同的词(right、palm、bat、mean 等))时,情况会变得更加棘手。

图 2:搜索同源词
搜索"我不对",返回的文档与第一个返回结果的含义完全相反。如果搜索完全相同的术语,但为了产生不同的含义而对它们进行不同的排序,例如"向右转 "和" 右转",则会得到完全相同的结果(即第三份文档 "向右转")。当然,我们的查询过于简化,没有使用短语匹配等更高级的查询,但这有助于说明词法搜索并不了解索引内容和搜索内容背后的真正含义。如果还不清楚,也不要着急,我们将在第三篇文章中再次讨论这个例子,看看矢量搜索在这种情况下如何提供帮助。
对词法搜索来说,当你能够控制如何为结构化数据建立索引(如映射、文本分析、摄取管道等)以及如何制作查询(如巧妙制作的 DSL 查询、查询词分析等)时,你就能利用词法搜索引擎创造奇迹,这是毋庸置疑的!Elasticsearch 的词法搜索能力令人惊叹。在过去的几年里,它所取得的成就以及它对词法搜索领域的普及和改进确实令人瞩目。
但是,如果要为需要提出自由文本问题的用户提供非结构化 数据(如图像、视频、音频、原始文本等)的查询支持,词法搜索就显得力不从心了。此外,有时查询的内容甚至不是文本,而可能是图片,我们很快就会看到。在这种情况下,词法搜索无法胜任的主要原因是,非结构化数据既无法像结构化数据那样被索引,也无法像结构化数据那样被查询。在处理非结构化数据时,语义学开始发挥作用。语义学是什么意思?很简单,就是这个意思!
让我们以简单的图像搜索引擎(如谷歌图像搜索或 Lens)为例。您只需拖放一张图片,谷歌语义搜索引擎就会找到并返回与您查询的图片最相似的图片。在下图 3 中,我们可以看到左侧是一张德国牧羊犬的图片,右侧是所有已检索到的相似图片,第一个结果是与所提供图片相同的图片(即最相似的图片)。

图 3:搜索图片。
资料来源谷歌图片搜索,https://www.google.com/imghp
尽管这对我们人类来说听起来简单且合乎逻辑,但对计算机来说却是另一回事。这正是矢量搜索所能实现和帮助实现的。矢量搜索所释放的能量是巨大的,全世界最近都见证了这一点。现在,让我们揭开引擎盖,看看下面隐藏着什么。
嵌入向量
正如我们在前面所看到的,通过词法搜索引擎,文本等结构化数据可以很容易地被标记为在搜索时可以匹配的术语,而不管这些术语的真正含义是什么。然而,非结构化数据可以采用不同的形式,例如大型二进制对象(图像、视频、音频等),而且完全不适合采用相同的标记化流程。此外,语义搜索的整个目的就是为数据编制索引,以便根据数据所代表的意义进行搜索。我们如何做到这一点?答案就在两个字里:机器学习!或者更准确地说,是深度学习!
深度学习是机器学习的一个特定领域,它依赖于基于人工神经网络的多层处理模型,可以逐步提取数据的真实含义。这些神经网络模型的工作方式在很大程度上受到了人脑的启发。下图 4 显示了神经网络的外观,包括输入层、输出层和多个隐藏层:

图 4:神经网络层。
神经网络的真正功能在于,它能够将单个非结构化数据转化为浮点数值序列,这些浮点数值被称为嵌入向量或简称嵌入。作为人类,只要将向量在二维或三维空间中形象化,我们就能很好地理解向量是什么。矢量的每个分量代表二维 x-y 平面或三维 x-y-z 空间中的一个坐标。
然而,神经网络模型所依赖的嵌入向量可能有几百甚至上千个维度,而且仅仅代表多维空间中的一个点。每个向量维度代表非结构化数据的一个特征或特性。让我们用一个将图像转化为 2048 维嵌入向量的深度学习模型来说明这一点。该模型将把我们在图 3 中使用的德国牧羊犬图片转化为下表所示的嵌入向量。请注意,我们只显示了前三个元素和后三个元素,但表格中还有 2,042 个列/维度。
| is_red | 是狗 | blue_sky | … | no_gras | 德国牧羊犬 | is_tree | |
|---|---|---|---|---|---|---|---|
| 德国牧羊犬 嵌入 | 0.0121 | 0.9572 | 0.8735 | … | 0.1198 | 0.9712 | 0.0512 |
每一列是模型的一个维度,代表底层神经网络试图建模的一个特征或特性。根据输入与 2048 个维度中每个维度的相似程度,对模型的每个输入进行特征描述。因此,嵌入向量中每个元素的值表示该输入与特定维度的相似度。在这个例子中,我们可以看到模型检测到狗和德国牧羊犬之间的高度相似性,以及一些蓝天的存在。
与词法搜索相比,矢量搜索能让我们更好地了解非结构化数据与模型支持的每个维度的相似程度。因此,嵌入向量可以作为非结构化数据的绝佳语义表示。
秘制酱汁
既然我们已经知道深度学习神经网络是如何将非结构化数据切成嵌入向量的,这些嵌入向量可以从多个维度捕捉数据的相似性,那么我们就需要了解这些向量的匹配是如何进行的。事实证明,答案非常简单。相互接近的嵌入向量代表语义相似的数据片段。因此,当我们查询矢量数据库时,搜索输入(图像、文本等)首先会使用索引所有非结构化数据时使用的相同模型转化为嵌入矢量,最终目标是找到与该查询矢量最近的相邻矢量。因此,我们需要做的就是找出如何测量查询向量与数据库中索引的所有现有向量之间的 "距离 "或 "相似性",仅此而已。
距离和相似性
幸运的是,由于有了向量算术,测量两个向量之间的距离是一个很容易解决的问题。因此,让我们来看看现代矢量搜索数据库(如 Elasticsearch)支持的最常用的距离和相似性函数。警告,前方有数学题!
L1 距离
两个向量 x 和 y 的 L1 距离(也称为曼哈顿距离)是通过将它们所有元素的成对绝对差值相加来测量的。显然,距离 d 越小,两个矢量就越接近。计算公式非常简单,如下所示:

下图 5 可以直观地说明 L1 距离:

图 5:两个向量之间的 L1 距离可视化
假设有两个向量 x 和 y,例如 x = (1, 2) 和 y = (4, 3),那么这两个向量的 L1 距离为 | 1 - 4 | + | 2 - 3 | = 4。
L2 距离
测量两个向量 x 和 y 的 L2 距离(也称欧几里得距离)的方法是,首先求出它们所有元素成对差值的平方和,然后取其平方根。它基本上是两点之间最短的路径(也称为斜边)。与 L1 类似,距离 d 越小,两个向量越接近:

L2 距离如下图 6 所示:

图 6:两个向量之间的 L2 距离可视化
让我们重新使用与 L1 距离相同的两个样本向量 x 和 y,现在我们可以计算出 L2 距离为。10 的平方根就是 3.16。
林夫距离
两个向量 x 和 y 的 Linf(表示 L 无穷大)距离,也称为切比雪夫距离或棋盘距离,简单地定义为这两个向量的任意两个元素之间的最长距离,或沿其中一个轴/维测量的最长距离。计算公式非常简单,如下所示:

林夫距离的表示方法如下图 7 所示:

图 7:两个向量之间的 Linf 距离可视化
同样,取相同的两个样本向量 x 和 y,我们可以计算出无穷大距离为 max ( | 1 - 4 | , | 2 - 3 | ) = max (3, 1) = 3。
余弦相似性
与 L1、L2 和 Linf 不同的是,余弦相似度不是测量两个向量 x 和 y 之间的距离,而是测量它们的相对角度,即它们是否都指向大致相同的方向。相似度 s 越高,两个向量就越 "接近"。计算公式同样非常简单,如下所示:

两个向量的余弦相似度的表示方法如下图 8 所示:

图 8:两个向量的余弦相似度可视化
此外,由于余弦值始终处于[-1, 1]区间,-1 表示相似度相反(即两个矢量之间的夹角为 180°),0 表示相似度不相关(即夹角为 90°),1 表示相似度相同(即夹角为 0°),如下图 9 所示:

图 9:余弦相似性频谱
让我们再次使用相同的样本向量 x 和 y,用上述公式计算余弦相似度。首先,我们可以计算出两个向量的点积为。然后,我们将两个向量的长度(也称为幅值)相乘:最后,我们用点积除以乘积长度 10 / 11.18034 = 0.894427(即 26°角),这个值非常接近 1,因此可以认为这两个矢量非常相似。
点积相似性
余弦相似度的一个缺点是,它只考虑两个矢量之间的角度,而不考虑它们的大小(即长度),这意味着如果两个矢量大致指向同一方向,但其中一个矢量比另一个矢量长很多,那么这两个矢量仍然会被认为是相似的。点积相似度(也称为标量或内积)考虑了向量的角度和大小,从而改进了点积相似度,提供了更精确的相似度量。
点积相似性的计算有两个等价公式。第一种情况与我们之前在余弦相似性的分子中看到的相同:

第二个公式只是将两个矢量的长度乘以它们之间夹角的余弦:

下图 10 展示了点积相似性:

图 10:两个向量点积相似度的可视化
最后,我们取样 x 和 y 向量,使用第一个公式计算它们的点积相似度,就像之前计算余弦相似度一样,即 (1) + (2) = 10。
利用第二个公式,我们将两个矢量的长度相乘:+(4^2 + 3^2)^{1/2} = 11.18034,再乘以两个向量之间 26°夹角的余弦,就得到 11.18034(26°) = 10。
值得注意的一点是,如果首先对所有向量进行归一化处理(即长度为 1),那么点积相似度就会与余弦相似度(因为 |x| |y| = 1)完全相同,即两个向量之间夹角的余弦值。正如我们稍后将看到的,对向量进行归一化处理是一种很好的做法,这样可以使向量的大小变得无关紧要,从而使相似性只集中在角度上。它还能加快索引和查询时的距离计算速度,这在处理数十亿矢量时可能是个大问题。
快速回顾
哇,到目前为止,我们已经了解了很多信息,让我们稍停片刻,快速回顾一下目前的状况。我们了解到
- ...语义搜索基于深度学习神经网络模型,擅长将非结构化数据转化为多维嵌入向量。
- ......模型的每个维度都代表了非结构化数据的一个特征或特性。
- ......嵌入向量是一串相似度值(每个维度一个),表示给定的非结构化数据与每个维度的相似程度。
- ......两个向量越 "接近"(即近邻),它们代表的语义概念就越相似。
- ......距离函数(L1、L2、Linf)可以测量两个向量的距离。
- ......相似函数(余弦和点积)使我们能够测量两个向量朝同一方向移动的程度。
现在,我们需要深入研究的最后一个部分是矢量搜索引擎本身。当收到查询时,首先对查询进行矢量化,然后矢量搜索引擎会找到与查询矢量最近的相邻矢量。测量查询向量与数据库中所有向量之间的距离或相似性的粗暴方法可以适用于小型数据集,但随着向量数量的增加,这种方法很快就会失效。换句话说,我们如何才能索引数百万、数十亿甚至数万亿的向量,并在合理的时间内找到查询向量的近邻?这就需要我们发挥聪明才智,找出索引矢量的最佳方法,以便在不降低精度的情况下,尽可能快地找到最近的邻近矢量。
矢量搜索算法和技术
多年来,许多不同的研究团队投入了大量精力,开发出了非常聪明的向量搜索算法。在此,我们将简要介绍主要的几种。根据不同的使用情况,有的比有的更适合。
线性搜索
在前面提到将查询向量与数据库中的所有向量进行比较的粗暴方法时,我们简要地提到了线性搜索或平面索引。虽然它在小型数据集上运行良好,但随着向量和维数的增加,性能会迅速下降(复杂度为 O(n))。
幸运的是,有一种更有效的方法被称为近似近邻(ANN),在这种方法中,嵌入向量之间的距离是预先计算出来的,相似向量的存储和组织方式可以使它们靠近在一起,例如使用聚类、树、哈希或图。这些方法被称为 "近似 "方法,因为它们通常不能保证 100% 的准确性。最终目标是尽可能快地缩小搜索范围,以便只关注最有可能包含相似向量的区域,或者降低向量的维度。
K 维树
K 维树(或 KD 树)是二进制搜索树的一种概括,它在 k 维空间中存储点,并通过将搜索空间连续分成较小的左树和右树来索引向量。搜索时,算法只需访问查询向量(图 11 中的红点)周围的几个树枝,就能找到最近的邻居(图 11 中的绿点)。如果请求的邻居超过 k 个,黄色区域就会扩大,直到算法找到更多邻居。
KD 树算法的最大优势在于,它可以让我们迅速只关注一些局部树枝,从而将大部分向量排除在外。不过,这种算法的效率会随着维数的增加而降低,因为需要访问的分支要比低维空间多得多。
反转文件索引
倒置文件索引(IVF)方法也是一种空间分区算法,它将彼此接近的向量分配到它们的共享中心点上。在二维空间中,使用 Voronoi 图最直观,如图 12 所示:

图 12:二维空间中倒置文件索引的 Voronoi 表示法。
来源:https://docs.zilliz.com/docs/vector-index-basics-and-the-inverted-file-index
我们可以看到,上述二维空间被划分为 20 个聚类,每个聚类的中心点用黑点表示。空间中的所有嵌入向量都被分配到其中心点最接近的聚类中。在搜索时,算法首先通过找到与查询向量最接近的中心点来确定要关注的集群,然后就可以简单地将该区域归零,必要时还可以将周围的区域归零,以便找到最近的邻居。
这种算法在高维空间中使用时,也存在与 KD 树相同的问题。这就是所谓的 "维度诅咒"(curse of dimensionality),当空间体积大幅增加,以至于所有数据都显得稀疏,而要获得更精确的结果所需的数据量却呈指数级增长时,就会出现这种情况。当数据稀疏时,这些空间划分算法就很难将数据组织成群。幸运的是,还有其他算法和技术可以缓解这一问题,详情如下。
量化
量化是一种基于压缩的方法,通过降低嵌入向量的精度,我们可以减小数据库的总大小。这可以通过将浮点矢量值转换为整数值的标量量化 (SQ)来实现。这不仅将数据库的大小减少了 8 倍,还降低了内存消耗,并加快了搜索时向量间距离的计算速度。
另一种技术称为乘积量化(PQ),它首先将空间划分为低维子空间,然后使用聚类算法(类似于 k-means)在每个子空间中对相近的向量进行分组。
请注意,量化不同于降维,后者是减少维数,即简单地缩短向量。
分层导航小世界(HNSW)
如果光看名字就觉得很复杂,别担心,其实并不复杂!简而言之,"分层可导航小世界 "是一种基于多层图的算法,非常流行且高效。包括 Apache Lucene 在内的许多不同的矢量数据库都在使用它。下文图 13 显示了国家污水处理厂的概念图。

图 13:分层导航的小世界。
在顶层,我们可以看到一个由极少数向量组成的图,这些向量之间的链接最长,也就是说,这是一个相似性最小的连接向量图。我们越深入下层,发现的向量就越多,图形就越密集,越来越多的向量相互靠近。在最底层,我们可以找到所有矢量,其中最相似的矢量彼此距离最近。
搜索时,算法从顶层的任意入口点开始,找到最接近查询向量的向量(如灰色点所示)。然后,它再向下移动一层,从它留在上一层的同一个向量开始,重复同样的过程,如此逐层进行,直到到达最底层,并找到与查询向量最近的邻居。
位置敏感散列(LSH)
与迄今为止介绍的所有其他方法一样,对位置敏感的哈希算法试图大幅缩小搜索空间,以提高检索速度。通过这种技术,嵌入向量被转化为哈希值,同时保留了相似性信息,因此搜索空间最终变成了一个可以查询的简单哈希表,而不是一个需要遍历的图或树。基于哈希值的方法的主要优势在于,包含任意(大量)维度的向量可以映射为固定大小的哈希值,这大大加快了检索时间,同时又不会牺牲太多精度。
一般来说,散列数据,特别是嵌入向量,有许多不同的方法,但本文不会深入探讨每种方法的细节。传统的散列方法通常会对看似非常相似的数据产生截然不同的散列值。由于嵌入向量是由浮点数值组成的,因此我们取两个在向量运算中被认为非常接近的浮点数值样本(例如 0.73 和 0.74),然后通过几个常见的散列函数对它们进行运算。从下面的结果来看,普通的散列函数显然无法保留输入之间的相似性。
| 散列功能 | 0.73 | 0.74 |
|---|---|---|
| MD5 | 1342129d04cd2924dd06cead4cf0a3ca | 0aec1b15371bd979cfa66b0a50ebecc5 |
| SHA1 | 49d2c3e0e44bff838e1db571a121be5ea874e8d9 | a534e76482ade9d9fe4bff3035a7f31f2f363d77 |
| SHA256 | 99d03fc3771fe6848d675339fc49eeb1cb8d99a12e6358173336b99a2ec530ea | 5ecbc825ba5c16856edfdaf0abc5c6c41d0d8a9c508e34188239521dc7645663 |
传统的散列方法试图尽量减少相似数据之间的散列碰撞,而位置敏感散列的主要目标恰恰相反,即尽量增加散列碰撞,从而使相似数据以很高的概率落在同一个桶中。通过这种方法,多维空间中相距较近的嵌入向量将被哈希到属于同一个桶的固定大小的值。由于 LSH 允许这些散列向量保持其邻近性,因此这种技术在数据聚类和近邻搜索中非常有用。
所有繁重的工作都是在编制索引时进行的,此时需要计算哈希值,而在搜索时,我们只需要对查询向量进行哈希处理,以便查找包含最接近嵌入向量的桶。找到候选桶后,通常会进行第二轮,以确定与查询向量最近的相邻向量。
结束语
为了介绍矢量搜索,我们必须在这篇文章中介绍一些内容。在比较了词法搜索和向量搜索的区别之后,我们了解了深度学习神经网络模型是如何捕捉非结构化数据的语义,并将其意义转码为高维嵌入向量的。值得注意的是,向量搜索和词法搜索并不是相互竞争的信息检索技术,而是相辅相成的(我们将在本系列的第三部分深入探讨混合搜索)。
之后,我们介绍了向量搜索的基本构件,即距离(和相似性)函数,它允许我们测量两个向量的接近程度,并评估它们所代表概念的相似性。
最后,我们回顾了各种最流行的向量搜索算法和技术,它们可以基于树、图、簇或哈希值,其目标是快速缩小多维空间中特定区域的范围,以便找到最近的邻居,而无需像线性暴力搜索那样访问整个空间。
如果您喜欢现在阅读的内容,请务必查看本系列的其他部分:
常见问题
矢量搜索和词法搜索有什么区别?
词法搜索并不试图理解所索引和查询内容的真正含义,而是匹配单词的字面意思或其变体。与此相反,矢量搜索对数据进行索引的方式可以根据数据所代表的意义进行搜索。





