
百万级文档秒匹配?HNSW 让向量数据库在 RAG 中封神
向量数据库如何为检索增强生成(RAG)高效匹配数据
包含4个层级和30个文档的HNSW图
检索增强生成(RAG)是向大型语言模型(LLMs)添加外部知识的重要工具。
几乎每个RAG系统都包含一个向量数据库来执行语义搜索。在这种搜索方式中,存储在向量数据库中的文档嵌入会与用户的查询嵌入进行比较。
一个基本的RAG设置包括一个嵌入模型、一个向量数据库和一个大型语言模型。向量数据库用于找到与查询最匹配的前K个文档
在实际应用中,将一个查询向量与数据库中数百万个嵌入向量进行比较以找到完美匹配是非常缓慢的。为了提高速度,这些向量数据库通常会返回足够接近的结果。
让我们仔细看看向量数据库是如何工作的,并探讨为当今许多RAG系统提供支持的分层可导航小世界(HNSW) 搜索算法。
基本语义搜索
在语义搜索中,嵌入模型会将文本转换为一个密集向量,该向量捕捉了文本的含义。
通过使用同一个嵌入模型将查询和文档都转换为向量,我们可以通过识别查询向量的最近邻来执行语义搜索。这个过程被称为K近邻(KNN) 搜索。
在一个RAG系统中,检索器的工作是找到与用户查询最相似的文档。在这个简单的例子中,在10个文档中,文档1、5和7是三个最接近的匹配。
为了找到最近邻,我们需要测量两个向量之间的距离。通常使用的距离度量是余弦距离,它关注两个向量之间的夹角,或者点积。
余弦距离公式是1减去两个向量v和w的余弦相似度。
然后,我们按距离排序并选择K个最接近的匹配。
然而,搜索所需的计算量会随着向量数据库中条目的数量线性增加。例如,有15个文档时,必须计算15个距离。
如果数据库包含数百万个文档,那么每次查询时我们都需要计算数百万个距离。
为了提高效率并解决这个问题,我们可以改用近似最近邻(ANN) 搜索。
ANN比KNN高效得多。然而,顾名思义,这些算法不能保证返回最接近的匹配。但在实际应用中,它们通常足够接近。
Navigable Small World (NSW)
NSW 算法[2]是一种基于图的ANN算法。图是一种由边和顶点组成的数据类型。
首先,我们计算所有文档之间的距离,这只需要做一次。接下来,我们构建一个邻近图,其中数据库中的每个文档对应一个顶点。每个文档通过边连接到其几个最近的邻居。超参数M
决定了每个文档的最大连接数。
使用上面的相同示例,下面是NSW邻近图的样子:
一个包含10个文档的NSW邻近图,每个顶点最多有M=3条边。
图构建完成后(只需要做一次),就可以用于高效搜索了。NSW是一种贪心算法,在每一步都做出局部最优选择。
NSW背后的理念是,从任何一个文档都可以通过几次“跳跃”到达另一个文档。
为了找到与查询最接近的文档,我们随机选择一个文档作为起点,并计算查询与当前文档的所有相连邻居之间的距离。然后,我们选择距离最短的文档。
如果查询与所选文档之间的距离小于查询与当前文档之间的距离,我们就移动到所选文档。
然后,算法重复此过程。如果查询与当前文档之间的距离小于与所有邻居的距离,则找到了局部最小值,并返回前K个文档。
以下是一个示例查询的NSW算法:
一个带有查询和随机选择起点的贪心搜索示例。
我们也可以用不同的随机起点重复整个过程,以找到更好的解决方案。
Hierarchical Navigable Small World (HNSW)
HNSW在NSW的基础上增加了分层结构,从而加快了搜索算法的速度[3]。
HNSW使用多层图。最低层,即第0层,包含完整的NSW图和所有文档。
在HNSW中,会创建多个层级,每个层级进一步减少文档的数量。在一个简单的例子中,我们可能只使用两个层级,如下图所示。
一个包含两个层级和10个文档的HNSW图。
搜索算法从最高层开始,其操作方式与NSW类似。在某个层级中找到局部最小值后,我们将该文档作为下一个较低层级的起点。
最终,我们到达第0层并返回前K个文档。
这个过程就像最初先缩小范围在最高层级找到一个大致匹配,然后随着层级降低不断放大范围以找到更好的匹配。
虽然HNSW在搜索方面比KNN快得多,但创建多层邻近图需要大量计算。当从数据库中插入、更新或删除文档时,还需要进行额外的计算来更新或重新创建图。
此外,像HNSW这样的基于图的搜索算法需要更多的内存来存储图结构和向量嵌入。
HNSW得到了流行向量数据库的支持,如Chroma、Qdrant、Weaviate、Milvus、Faiss、Vespa、Pinecone和Elasticsearch,并且它通常被用作语义搜索的默认ANN算法。
实际应用中的HNSW
HNSW通常是大多数向量数据库的默认选择。这使得它在实际应用中非常容易使用。
例如,我们将在Python中使用开源向量数据库Chroma,可以使用pip install chromadb
命令安装它。
首先,创建一个本地Chroma数据库,然后为你的数据创建一个新的集合。在这里,我们可以显式地设置HNSW算法的超参数,例如距离度量space=cosine
和图中每个文档的最大邻居数max_neighbors=16
。
import chromadb
# 初始化Chroma客户端
client = chromadb.Client()
# 在Chroma中创建一个带有显式HNSW配置的集合
collection = client.create_collection(
name="my-collection",
cnotallow={"hnsw": {"space": "cosine", "max_neighbors": 16}},
)
有趣的是,Chroma使用L2范数(即欧氏距离)作为默认的距离度量。然而,大多数嵌入模型都是针对余弦距离进行优化的。
接下来,我们将文档添加到集合中,并在后台使用HNSW搜索算法搜索数据库。
# 创建示例文档
documents = [
"这是第一个文档。",
"这个文档是关于机器学习的。",
"这是第三个关于自然语言处理的文档。",
]
# 将文档插入集合
collection.add(ids=["doc1", "doc2", "doc3"], documents=documents)
# 搜索最相似的1个文档
search_results = collection.query(query_texts=["自然语言处理"], n_results=1)
# 打印搜索结果
print(search_results)
这应该会返回第三个文档。
结论
随着大型语言模型的兴起,向量数据库变得越来越重要,现在几乎每个RAG系统都在使用它们。
大多数流行的向量数据库都支持HNSW,它通常被用作默认的搜索算法。HNSW和NSW都是用于找到足够接近的匹配的近似最近邻算法。
对于大小为N的数据集,HNSW和NSW的搜索复杂度都是log(N),而K近邻搜索的复杂度是线性的。这在数据库中有数百万或更多条目的情况下会产生巨大的差异。
时间复杂度增长:O(N)随着输入大小线性增加,而O(log N)的增长要慢得多。
参考文献
[1] Dr. Leon Eversberg (2025), Understanding Large Language Models and Generative AI: Inside the Technology Behind the Hype
[2] A. Ponomarenko et al. (2011), Approximate Nearest Neighbor Search Small World Approach, International Conference on Information and Communication Technologies & Applications
[3] Yu. A. Malkov, and D. A. Yashunin (2018), Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs, IEEE Transactions on Pattern Analysis and Machine Intelligence
本文转载自AIGC深一度
