引言
在前面的章节里,我们聚焦于基于主键与二级键的各种数据检索设计模式。这类查询通常依赖传统的数据结构(如查找索引、哈希索引等),其返回结果是确定性的——也就是说,结果会与用户给定的谓词精确匹配。
然而,许多现代用例并不能通过“精确匹配”来解决。用户往往希望得到与输入相似但不完全一致的结果。本章将进一步探讨基于相似度匹配或模糊匹配的检索场景。
数据工程师传统上使用全文搜索数据库来在文本中查找与输入文本“相似”的内容。近年来,随着大语言模型和基于 AI 的检索方案的普及,使用向量数据库进行相似度搜索也迅速流行起来。
本章将介绍全文搜索数据库与向量数据库;我们会学习如何调优全文搜索与相似度搜索的结果——由于数据集差异及底层实现的非确定性,结果质量往往需要按照场景来调校。接着我们会了解将全文搜索结果与向量搜索结果结合的混合搜索。最后,我们将以**检索增强生成(RAG)**模式收尾,学习如何借助向量数据库找到尽可能契合的答案。
结构
本章涵盖以下主题:
- 全文搜索
- 向量搜索
- 混合搜索
- 检索增强生成(RAG)
目标
读完本章后,你将理解文本相似检索、模糊检索与混合检索的多种用例,了解用于执行此类检索查询的软件工具,并掌握提升检索质量的调优方法。最后,你将理解现代 AI 应用如何通过 RAG 模式提升检索结果质量。
全文搜索
设想你与朋友聊电影,想起了一个精彩的剧情片段,大家都很喜欢,但当被问到片名时你却想不起来——这时,你会希望有一个能按剧情搜索电影的网站。这样的场景正是全文搜索软件的用武之地:以存放在电影信息中的非结构化文本为依据进行检索。
现代电影网站可以允许用户基于剧情文本、影评等进行搜索,这类高级检索能返回更相关的结果。下面我们以基于剧情的检索为例,介绍全文搜索数据库。
全文搜索数据库不同于传统结构化数据库,它们专为文本检索质量而设计。它们的工作过程大致如下:
-
全文搜索数据库存储电影列表以及对应的剧情文本。
-
为了高效搜索,数据工程师需要创建合适的全文索引(倒排索引) ,将剧情中的各个词语映射到对应的电影 ID。
-
在创建索引时,数据库会遍历所有电影的剧情文本,并对每段剧情:
- 分词/标记化(Tokenization)
- 预处理:如规范化(Normalization) 、停用词去除(Stop-word Removal) 、**词干提取(Stemming)**等
- 将词语与电影 ID 的映射写入索引
-
当用户按剧情文字搜索时,用户输入也会进行分词与规范化,然后在索引上进行查找。
-
检索目标是返回最相关的结果,并按照相关性得分进行排序。
术语与技术
- 分词 / 标记化(Tokenization) :将全文拆解为单词(token)。例如句子“the movie starts with a police officer searching the riverbed!”会被拆分为:the, movie, starts, with, a, police, officer, searching, the, riverbed!
- 规范化(Normalization) :将单词转为小写,并移除标点。例如 The → the,riverbed! → riverbed。
- 停用词去除(Stop-word Removal) :去掉对检索贡献不大的常见词,如 the, a, with 等。
- 词干提取(Stemming) :将单词化为词干,如 starts → start,searching → search。
完成上述预处理后,会创建索引条目。比如 police、officer、riverbed 等词会被写入倒排索引,并指向相应的电影 ID。
预处理带来的好处
- 避免将“the / a”等无关词纳入索引并影响排序权重,从而凸显 police、riverbed 等更有辨识度的词。
- 通过词干化减少前后缀造成的不匹配。例如用户查询“the police searches riverbed”,而剧情中是“searching”,词干化后都归一到 search,避免“看起来应当匹配却未命中”的情况。
全文搜索示例
流行的开源全文搜索引擎包括 Elasticsearch(Elastic) 、Apache Solr、Typesense 等,它们为全文搜索提供了丰富的高级特性(本章后续会讨论)。
在实践中,数据工程师通常选用一个事务型数据库(如 MySQL、DynamoDB)来存储电影的结构化信息(如上映日期、导演、制片人等),而需要全文检索的字段(如剧情)会同时存于事务库与搜索引擎中:事务库的剧情用于页面展示;搜索库的剧情用于建立索引并支持检索。
以 Elasticsearch 为例,基本步骤如下:
-
部署 Elasticsearch 服务器。
-
创建索引(示例 REST 命令):
curl -X PUT "localhost:9200/movie_plot_index" -H 'Content-Type: application/json' -
添加文档到索引(示例 REST 命令):
curl -X POST "localhost:9200/movie_plot_index/_doc" -H 'Content-Type: application/json' -d ' { "movieID": "Movie-1", "plot": "Two detectives travel from New York to San Francisco in search of a criminal." }' -
每当有新电影,持续增量写入索引。
-
当用户检索时执行搜索(示例:在剧情中搜索“detective”):
curl -X POST "localhost:9200/movie_plot_index/_search" -H 'Content-Type: application/json' -d ' { "query": { "match": { "plot": "detective" } } }'
图 15.1:使用全文搜索数据库
- 用户应用对事务型数据库发起事务性工作负载;
- 对全文搜索数据库发起全文检索工作负载;
- 在事务库与搜索库之间建立数据摄取管道,确保两边数据的一致性。
全文搜索的高级特性
如前所述,全文搜索基于相似进行文本检索,匹配结果不一定是精确匹配。因此,系统通常提供一些高级能力,帮助返回更相关的结果。常见特性包括:
编辑距离(Edit distance)
编辑距离允许在检索时忽略输入文本与存储文本之间的细小差异,以应对用户输入或存储数据中的拼写错误。继续我们的例子:用户想搜 detective(侦探)相关电影,却输入了 detectiv(末尾少了字母 e)。设置编辑距离后,系统仍能找到包含 detective 的文档。
在 Elasticsearch 中,可在查询里通过 fuzziness 参数指定编辑距离。例如:
curl -X POST "localhost:9200/movie_plot_index/_search" -H 'Content-Type: application/json' -d '
{
"query": {
"match": {
"plot": {
"query": "detectiv", // 注意少了末尾的 "e"
"fuzziness": "1"
}
}
}
}'
上例中 fuzziness: 1 表示输入与存储文本相差 1 个字符也视为匹配,因此能命中文档里含有 detective 的记录。
同义词(Synonyms)
用户检索“侦探”电影时,可能不输入 detective,而输入其同义词 police(警察)。从语义上讲,包含 detective 的文档也应匹配。为此,全文搜索引擎支持在索引创建时配置同义词。
下面的 REST 命令演示了在 Elasticsearch 中通过自定义分析器(analyzer)启用同义词:
curl -X PUT "localhost:9200/movie_plot_index" -H 'Content-Type: application/json' -d '
{
"settings": {
"analysis": {
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"police, detective"
]
}
},
"analyzer": {
"synonym_analyzer": {
"tokenizer": "standard",
"filter": [
"lowercase",
"synonym_filter"
]
}
}
}
},
"mappings": {
"properties": {
"plot": {
"type": "text",
"analyzer": "synonym_analyzer"
}
}
}
}'
有了上述同义词分析器,用户以 police 作为查询词时,剧情中含 detective 的电影也会被返回。
上述两点都还是围绕“词”的匹配。但在一些场景里,用户不追求具体词语的匹配,而是更关注语义是否相近。这类语义搜索通常由向量搜索来解决。
向量搜索
在上一节的例子中,用户通过单词或同义词(police / detective)可以检索到大量结果。但如果用户输入的是完整句子,并希望按语义寻找最相近的剧情怎么办?
例如有一段剧情:“一名腐败的警察不断骚扰无辜市民,最终面临法律制裁。”
用户的查询是:“一名警察犯罪并最终入狱。”
两句在词面上重合很少(troubling innocent citizens 与 commits crimes 不是同词也非严格同义;faces legal consequences 与 ends up in jail 句法不同但语义相近)。此时全文检索就吃力了,而向量搜索可以胜任。
向量简介
向量是一种对非结构化数据(文本、图像、视频等)的数学表示。要点如下:
- 通过**向量嵌入模型(embedding model)**将非结构化数据转换为向量表示;
- 向量通常是浮点数数组;
- 同一嵌入模型生成的所有向量维度一致;
- 向量长度即向量维度(dimensionality) 。
向量相似度搜索
嵌入模型基于海量数据学习到的启发式,将文本或图像的关键特征编码为向量;相似样本在向量空间中彼此接近,不相似的则距离较远。
检索时,将用户输入编码为向量,然后在库中查找与其距离最近的向量。常见相似度/距离度量包括:
- 欧氏距离;
- 余弦相似度/夹角余弦。
寻找最近向量的算法通常称为 k 近邻(KNN) 。
向量数据库与向量索引
向量数据库专门为向量存取与相似度计算而优化(计算距离、执行 KNN 等),不一定对传统事务型操作做了同样程度的优化。它们支持创建向量索引,加速相似度检索。为提升速度,很多实现采用 近似最近邻(ANN) 技术,以一定精度损失换取性能。
如何使用向量数据库
与全文检索类似,向量数据库面向向量检索场景,提供专用能力;非向量数据仍建议保存在事务型数据库中以支持基于 ID 等的快速查找。常见做法:
- 在事务库中存储原始非结构化数据(文本、图片等)及其向量嵌入;
- 构建数据摄取管道将嵌入写入向量数据库;
- 向量库仅保存嵌入并基于向量索引提供检索。
图 15.2:使用向量数据库进行检索
应用将用户查询编码为向量,提交给向量库执行相似度搜索,同时事务库仍用于常规数据管理。
向量搜索示例(以 Elasticsearch 为例)
我们在事务库保存电影详情,并保存由剧情生成的向量嵌入;同时把这些嵌入写入向量库(这里以 Elastic 的向量能力为例;开源替代还包括 Milvus、Qdrant、PGVector 等)。
创建向量索引:
PUT /movie-plot-index
{
"mappings": {
"properties": {
"title": { "type": "text" },
"movie_plot_embedding": {
"type": "dense_vector",
"dims": 128,
"index": true,
"similarity": "cosine"
}
}
}
}
写入向量数据:
POST /movie-plot-index/_doc/<MovieId>
{
"title": "Movie Title",
"movie_plot_embedding": <array of 128 floating point numbers>
}
按剧情向量检索(KNN):
POST /movie-plot-index/_search
{
"knn": {
"movie_plot_embedding": {
"vector": <Vector embedding for the user query>,
"k": 10
}
}
}
注意:查询向量必须由与索引入库时相同的嵌入模型生成,否则向量空间不一致将导致效果下降。
向量检索质量
影响向量检索效果的因素包括:
- 嵌入质量(模型能力、训练语料等);
- 向量维度(维度越高,表达能力通常更强,但也可能带来检索开销与“维度灾难”问题);
- 近邻搜索算法与索引结构(是否 ANN、参数设置等)。
需要注意的局限:
- 向量化过程中可能产生信息损失;
- 采用 ANN 会带来近似误差。
这意味着向量搜索与全文搜索各有取舍。为了扬长避短,工程上常采用混合搜索来综合两者优势。
混合搜索(Hybrid search)
混合搜索指把全文检索与向量检索的结果合并,以获得更相关的返回。以“按剧情找电影”为例:
- 向量检索会返回与用户查询语义最相近的剧情;
- 全文检索会按用户查询中的关键词返回相似结果。
理论上,两种检索各自选出一组文档;混合搜索引擎将这两组合并与排序,形成最终结果。在 Elasticsearch 中,可以通过 boost 参数调节权重,提升全文检索相对向量检索在最终排序中的影响力。
注:Elasticsearch 会按“相关性分数”对文档排序;
boost用来放大由match(或其他查询子句)命中的文档分数。
创建混合索引示例:
PUT hybrid_index
{
"mappings": {
"properties": {
"plot": { "type": "text" },
"plot_vector_embedding": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
}
}
}
}
执行混合搜索示例(通过 boost 强化全文匹配的权重):
POST hybrid_index/_search
{
"size": 5,
"query": {
"bool": {
"should": [
{
"match": {
"plot": {
"query": <user query text>,
"boost": 1.5
}
}
},
{
"knn": {
"plot_vector_embedding": {
"vector": <user query vector embedding>,
"k": 10,
"num_candidates": 100
}
}
}
]
}
}
}
近年来,大语言模型(LLM)因能解决多种问题而广泛应用。LLM 生成答案的质量高度依赖于上下文的相关性。一种常见做法是先用向量检索找到更相关的材料,再将其提供给 LLM——这就是 RAG 模式。
检索增强生成(Retrieval-Augmented Generation, RAG)
RAG 是一种数据工程设计模式,目标是提升 LLM 的输出质量。做法是:
- 检索:先从向量数据库中检索与用户问题高度相关的资料;
- 生成:把这些资料作为上下文与用户问题一起喂给 LLM,让其生成更好的回答。
仍以在线电影库为例:
若要“生成对某部电影导演所受批评的总结”,LLM 本身可能没有足够的定向信息。网站中保存了用户评论,数据工程师可用向量检索从评论中找出批评导演的片段,把这些片段作为上下文提供给 LLM,从而得到更贴合问题的总结。
这一“先检索再生成”的流程,即为 RAG。
RAG 两步法:
- 检索:在向量库中寻找与问题最相关的评论/片段;
- 生成:将检索到的内容与问题一起给到 LLM,生成改进后的答案。
(图 15.3:检索增强生成流程示意)
小结
本章围绕“如何找到更相关的数据”介绍了多种搜索技术与模式:
- 先从全文检索入手,理解其基础与在 Elasticsearch 上的实现,以及编辑距离、同义词等高级特性;
- 指出全文检索在语义相似方面的不足,引入向量搜索,讲解向量表示、相似度度量与向量数据库的用法及局限;
- 为兼顾两者优缺点,引出混合搜索并给出实作示例;
- 最后说明如何用向量检索为 LLM 提供高相关上下文,从而通过 RAG 提升生成质量。
下一章将讨论多种领域特定的设计模式,包括时序数据与弱网环境等场景。