首页 / 文章 / 关联检索:一种基于记忆的向量搜索心智模型

关联检索:一种基于记忆的向量搜索心智模型

以人类记忆为指引,了解嵌入、语义相似度、近似最近邻搜索以及元数据过滤器是如何协同工作的。

2273 词

多年过去,可能完全不会想起曾经的童年老师。然而,一阵粉笔灰的味道、学校食堂的气味,或是回家路上常听到的歌曲,就能立刻让那个人清晰地浮现在脑海中。人们从未刻意去查找过那个人的名字,只是类似的事物出现时,记忆便自行浮现。

这正是语义搜索、检索增强生成(RAG)以及人工智能推荐系统试图复制的效果——尽管并不完美,但态度十分认真。而实现这一功能的核心组件就是向量数据库。本文以“关联回忆”作为思维模型,解释什么是向量、如何衡量相似度、最近邻搜索的扩展方式,以及这些要素如何构成一个检索流程。同时,它也指出了这种类比失效的地方,因为恰恰在这些地方,实际系统才容易出错。

按含义而非名称存储

人类的记忆没有字母顺序索引。并不存在名为“食物”的心理文件夹,其中又包含名为“意大利菜”的子文件夹,而该子文件夹下还有名为“披萨”的文件。传统数据库正是通过这种严格的层级结构来组织数据的:每条记录都位于特定的地址,可通过精确的键值找到。

而记忆则是通过含义和关联来组织的:依据上下文、感受,以及事物之间的相互关系。披萨这个概念会让人联想到周五的夜晚、那不勒斯的旅行、令人舒适的食物、层层叠叠的奶酪、牛至的气味,或许还有那个毁掉所有自制面团的大学室友。想到披萨并不会打开某个单独的文件,而是会唤醒一系列相互关联的记忆,其中关联最紧密的记忆会首先浮现。

向量数据库正是运用了这一原理。数据项是根据其含义来存储的,检索时则是根据它们与查询内容的相似度来判断,而非依赖键是否完全匹配。

向量所编码的内容

要理解这种数据库,首先需要明白它存储了什么。这里的向量其实就是代表某种含义的有序数字列表。

机器本身并无法理解“披萨”究竟是什么。通过处理海量文本,嵌入模型能够学习到单词所代表的概念。“披萨”会出现在“奶酪”、“意大利”、“面团”、“烤箱”和“切片”等词附近。这些词的关联度与“寿司”不同,但与“扁面包”或“卡隆内饼”的关联度则很高。

该模型会将这些模式压缩成固定长度的数字列表,通常包含几百到几千个数值;384和1,536是常见的长度。单个数字并无可读的标签,但它们共同作用能让该元素在高维空间中精确定位。关键在于:含义相似的输入对应的向量会彼此靠近。“披萨”的向量会位于“扁平面包”附近,而与“季度盈利报告”的向量相距甚远。

换言之,含义被转化为距离。本文的其余内容皆源于这一转换。

相似性是连续谱,而非简单匹配

传统的查询是二元的:一行数据要么满足条件,要么不满足。如果按“狗”进行过滤,得到的将是包含“狗”这个词的行,而不会出现“幼犬”、“金毛猎犬”或“忠诚的四足伙伴”等相关内容。

语义相似度则用一个分数来替代这种非黑即白的答案,该分数表示两种含义的相似程度。以一个示例性评分标准来看,“幼犬”相对于“狗”的得分可能是0.94,“狼”的得分约为0.71,而“发票”的得分则在0.08左右。这种度量方法通常是余弦相似度,或是点积、欧几里得距离等相关的距离度量,具体选择取决于嵌入模型的训练方式。

这类似于粉笔灰效应:并非精确匹配,而是某个线索触发了附近的记忆,这些记忆又会进一步激活其邻近的记忆。在向量数据库中,其机制是数值化的。查询会被转换成向量,数据库则会返回那些向量与之最为接近的存储项。所谓“接近”指的是含义相似。

正因如此,搜索“如何修复缓慢的数据库查询”时,即便这两个短语几乎没有任何共同词汇,也可能会找到标题为“大规模下优化查询性能”的文档。因为它们表达的是相同的意图,所以其向量彼此接近。

关于数值的注意事项

相似度分数是相对的,而非绝对的。某个嵌入模型给出的0.8分无法与另一个模型给出的0.8分直接比较,即便在同一模型内,其典型范围也会因应用领域而异。应将这些分数视为对候选项进行排序的依据,若要设定阈值,应基于自身数据进行调整,而非选择某个整数。

一个相关的补充说明:单纯的基于关键词的SQL语言无法表达语义相似性,但这并不意味着SQL数据库不能用于此用途。如下文将介绍的pgvector这类扩展功能,可为Postgres添加向量列和相似度运算符,从而使这种能力内置于关系型数据库之中。

大规模下的最近邻搜索

数据库究竟是如何找到最接近的向量的?其核心操作被称为最近邻搜索,而提升该操作的效率正是向量数据库存在的主要原因。

可以将每个存储的项想象成银河系中的一颗星星,根据其含义进行排列,相关项则聚集在同一区域。当有查询时,就相当于向该银河系中加入了一颗新星,系统会找出距离它最近的五颗现有星星。

简单的处理方法是为查询项与每个存储的向量计算距离,对结果进行排序后返回最接近的前几项。对于数千个向量来说,这种方法速度很快且完全足够。但一旦数量达到数百万甚至数十亿,与所有向量进行比较就会迅速变得效率低下。

因此,生产系统依赖于近似最近邻(ANN)算法。它们不会逐一检查每一颗星体,而是通过索引将搜索范围缩小到有希望的区域,从而以一定的精度损失换取速度上的巨大提升。目前最常用的算法是HNSW,即分层可导航小世界算法。虽然使用向量数据库时无需了解其内部机制,但应当知道正是这一算法使得对上亿个嵌入向量的搜索能够在毫秒级时间内完成。

“近似”这一概念值得重视。人工神经网络索引有时会错过真正的最近邻,而其找到真正最佳结果的比率——即召回率——取决于那些在内存、延迟与准确性之间进行权衡的索引参数。如果在大规模应用中检索质量出现莫名其妙的波动,那就值得检查索引设置;我们的针对生产环境RAG优化HNSW索引指南对相关参数进行了深入讲解。

嵌入存储的内部机制

从根本上说,向量数据库是一种为特定任务优化过的存储方式:用于快速保存向量并执行最近的邻搜索。

想象这样一个图书馆,它不采用杜威十进制分类法,而是根据书籍所传达的情绪来摆放。关于悲伤的书籍与关于失去的书籍放在一起,这些书又与关于孤独的书籍相邻,接着是关于孤立的书籍,再往后则是独自探险的回忆录。这些分类并非由人工设定,而是因为喜欢某一类书籍的读者往往也会想要其他相关的书籍,从而形成了这样的排列。

Pinecone、Weaviate、Chroma和Qdrant都是这类图书馆的例子。你可以将文档嵌入、产品描述、转换为向量的图像或用户行为模式加载到这些系统中,它们会维护索引,以便在藏品不断增加时仍能实现快速检索。

每条存储的记录通常包含三部分:

  • 唯一标识该项目的ID。
  • 代表其含义的向量。
  • 可选的元数据,如标题、日期、类别或来源URL,可用于筛选结果。

元数据的价值远超表面所见。一个实际的查询需求可能是:找到与查询最相似的五份文档,但仅限过去30天内、且仅来自工程团队知识库中的文档。这就是向量搜索与元数据过滤的结合,大多数生产系统都依赖这种组合方式。过滤的应用方式也很重要:在相似度搜索之后再进行过滤可能会导致结果数量少于预期,因此请检查数据库是否在搜索过程中就进行了过滤。

从始至终的检索流程

无论它存在于RAG系统、语义搜索功能中,还是任何利用存储知识的应用程序里,检索都遵循相同的四个步骤。

  1. 嵌入内容。所有需要支持搜索的文档、文章、产品描述或记录都会经过嵌入模型处理,生成的向量会与原始内容一同存储。较长的文档通常需要先被分割成多个片段,因为用一个向量来表示整份手册会导致太多信息相互混淆。
  2. 使用相同的模型对查询进行嵌入。这并非可选项。不同的嵌入模型会在互不相关的空间中生成向量,因此用某个模型的查询去对比另一个模型的文档所得出的距离毫无意义。更换模型意味着必须重新对整个集合进行嵌入处理。
  3. 执行搜索。查询向量会被送入数据库,通过最近邻搜索找到存储中最接近的向量,随后返回最相关的结果,通常还会附带相似度得分。
  • 利用这些结果。在RAG流程中,检索到的段落会被作为上下文传递给语言模型,因此答案基于真实文档而非猜测。在推荐系统中,这些结果就是推荐内容;在搜索功能中,则是展示给用户的匹配结果。
  • 从用户的角度来看,相关答案会在瞬间出现。其背后是先将含义转化为数字,再在数百万条存储内容中进行比对,返回最接近的匹配项,最终用真实内容生成响应。

    为何向量搜索如今无处不在

    不久前,向量数据库还只是用于构建语义搜索或专用推荐系统时的小众工具。如今它们已成为许多人工智能应用的标准组成部分。RAG需要一个地方来存储和查询文档嵌入向量;智能体则通过语义方式查询知识库;大规模推荐系统依赖向量相似度运行;而多模态搜索,比如根据文本描述查找图片或从上传的照片中检索产品,也同样依靠向量技术实现。

    如果你正在基于大型语言模型进行实际应用开发,很有可能你已经在使用向量搜索,或者很快就会依赖它。令人欣慰的是,其核心理念其实早已为人所熟知:自从人类诞生以来,记忆系统就一直在为我们的感官所接收的信息检索最相关的关联内容。向量数据库只是用数字代替了信息载体,并能在毫秒级时间内处理数百万条数据,实现同样的功能。

    记忆类比的局限性

    将大脑比作记忆有助于直观理解,但实际应用中存在一些差异:

    • 记忆会持续适应;而嵌入模型则是固定的。如果所在领域的词汇发生变化,向量本身不会自动更新。
    • 记忆能轻松融合上下文;向量仅能捕捉输入的内容。分割不当或存在噪声的源文本会导致生成的向量质量低下。
    • 相似性并不等同于相关性。两段文本可能在含义上相近,但只有一段真正能回答问题,正因如此许多系统会额外加入关键词搜索或重排步骤。

    动手实践

    无需构建任何基础设施即可进行实验:

    • ChromaDB 可在 Python 环境中本地运行,无需账户、API 密钥或部署流程,安装和初始化只需几行代码。它非常适合学习和小型项目使用。
    • Qdrant 提供免费的云服务层级以及简洁的 Python 客户端,若您希望获得接近生产环境的功能而又不想自行管理服务器,它是不错的选择。在实际使用前请先了解当前的层级限制。
    • pgvector 是 Postgres 的扩展功能。如果您已经在使用 Postgres,它无需额外创建数据库即可实现向量搜索,从而保持技术栈的简洁性。

    无论选择哪种方案,工作流程都是一样的:选定嵌入模型、对内容进行向量化处理、存储这些向量,再对输入的查询进行向量化并执行搜索。核心概念保持不变,只有客户端库会有所不同。

    分数两侧必须是同一个模型

    余弦相似度只有在查询和片段由同一模型嵌入时才有意义。

    def cosine(a: list[float], b: list[float]) -> float:
        dot = sum(x * y for x, y in zip(a, b))
        na = sum(x * x for x in a) ** 0.5
        nb = sum(y * y for y in b) ** 0.5
        if na == 0 or nb == 0:
            return 0.0
        return dot / (na * nb)
    
    # query and passage must come from the same embedding model
    score = cosine(embed(query), embed(passage))

    元数据过滤发生在最近邻搜索之前,它决定谁可以进入候选集。

    hits = collection.query(
        query_embeddings=[embed(query)],
        n_results=8,
        where={"tenant_id": tenant_id},
    )

    关键要点

    • 嵌入模型将含义转化为位置,从而使相似的项彼此靠近。
    • 相似度分数用于对候选项进行排序;需根据自身数据调整阈值。
    • 如HNSW这样的近似最近邻索引会在一定程度上牺牲召回率,以换取大规模应用时的更高速度。
    • 元数据过滤器可将原始相似度转化为符合时间、来源及访问规则的答案。
    • 查询与文档必须使用相同的嵌入模型,检索质量不仅取决于数据库本身,还与数据分块方式及数据质量密切相关。