数据工程设计模式——数据检索模式

108 阅读13分钟

引言

在前面的章节里,我们聚焦于基于主键与二级键的各种数据检索设计模式。这类查询通常依赖传统的数据结构(如查找索引、哈希索引等),其返回结果是确定性的——也就是说,结果会与用户给定的谓词精确匹配。

然而,许多现代用例并不能通过“精确匹配”来解决。用户往往希望得到与输入相似但不完全一致的结果。本章将进一步探讨基于相似度匹配模糊匹配的检索场景。

数据工程师传统上使用全文搜索数据库来在文本中查找与输入文本“相似”的内容。近年来,随着大语言模型和基于 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) :将单词转为小写,并移除标点。例如 Thetheriverbed!riverbed
  • 停用词去除(Stop-word Removal) :去掉对检索贡献不大的常见词,如 the, a, with 等。
  • 词干提取(Stemming) :将单词化为词干,如 startsstartsearchingsearch

完成上述预处理后,会创建索引条目。比如 policeofficerriverbed 等词会被写入倒排索引,并指向相应的电影 ID。

预处理带来的好处

  • 避免将“the / a”等无关词纳入索引并影响排序权重,从而凸显 policeriverbed 等更有辨识度的词。
  • 通过词干化减少前后缀造成的不匹配。例如用户查询“the police searches riverbed”,而剧情中是“searching”,词干化后都归一到 search,避免“看起来应当匹配却未命中”的情况。

全文搜索示例

流行的开源全文搜索引擎包括 Elasticsearch(Elastic)Apache SolrTypesense 等,它们为全文搜索提供了丰富的高级特性(本章后续会讨论)。

在实践中,数据工程师通常选用一个事务型数据库(如 MySQL、DynamoDB)来存储电影的结构化信息(如上映日期、导演、制片人等),而需要全文检索的字段(如剧情)会同时存于事务库与搜索引擎中:事务库的剧情用于页面展示;搜索库的剧情用于建立索引并支持检索。

Elasticsearch 为例,基本步骤如下:

  1. 部署 Elasticsearch 服务器。

  2. 创建索引(示例 REST 命令):

    curl -X PUT "localhost:9200/movie_plot_index" -H 'Content-Type: application/json'
    
  3. 添加文档到索引(示例 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."
    }'
    
  4. 每当有新电影,持续增量写入索引。

  5. 当用户检索时执行搜索(示例:在剧情中搜索“detective”):

    curl -X POST "localhost:9200/movie_plot_index/_search" -H 'Content-Type: application/json' -d '
    {
      "query": {
        "match": {
          "plot": "detective"
        }
      }
    }'
    

image.png

图 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 citizenscommits crimes 不是同词也非严格同义;faces legal consequencesends up in jail 句法不同但语义相近)。此时全文检索就吃力了,而向量搜索可以胜任。

向量简介

向量是一种对非结构化数据(文本、图像、视频等)的数学表示。要点如下:

  • 通过**向量嵌入模型(embedding model)**将非结构化数据转换为向量表示;
  • 向量通常是浮点数数组
  • 同一嵌入模型生成的所有向量维度一致
  • 向量长度即向量维度(dimensionality)

向量相似度搜索

嵌入模型基于海量数据学习到的启发式,将文本或图像的关键特征编码为向量;相似样本在向量空间中彼此接近,不相似的则距离较远

检索时,将用户输入编码为向量,然后在库中查找与其距离最近的向量。常见相似度/距离度量包括:

  • 欧氏距离
  • 余弦相似度/夹角余弦

寻找最近向量的算法通常称为 k 近邻(KNN)

向量数据库与向量索引

向量数据库专门为向量存取与相似度计算而优化(计算距离、执行 KNN 等),不一定对传统事务型操作做了同样程度的优化。它们支持创建向量索引,加速相似度检索。为提升速度,很多实现采用 近似最近邻(ANN) 技术,以一定精度损失换取性能。

如何使用向量数据库

与全文检索类似,向量数据库面向向量检索场景,提供专用能力;非向量数据仍建议保存在事务型数据库中以支持基于 ID 等的快速查找。常见做法:

  • 在事务库中存储原始非结构化数据(文本、图片等)及其向量嵌入
  • 构建数据摄取管道将嵌入写入向量数据库;
  • 向量库仅保存嵌入并基于向量索引提供检索。

image.png

图 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 的输出质量。做法是:

  1. 检索:先从向量数据库中检索与用户问题高度相关的资料;
  2. 生成:把这些资料作为上下文与用户问题一起喂给 LLM,让其生成更好的回答。

仍以在线电影库为例:
若要“生成对某部电影导演所受批评的总结”,LLM 本身可能没有足够的定向信息。网站中保存了用户评论,数据工程师可用向量检索从评论中找出批评导演的片段,把这些片段作为上下文提供给 LLM,从而得到更贴合问题的总结。
这一“先检索再生成”的流程,即为 RAG

RAG 两步法:

  1. 检索:在向量库中寻找与问题最相关的评论/片段;
  2. 生成:将检索到的内容与问题一起给到 LLM,生成改进后的答案。

image.png

(图 15.3:检索增强生成流程示意)

小结

本章围绕“如何找到更相关的数据”介绍了多种搜索技术与模式:

  • 先从全文检索入手,理解其基础与在 Elasticsearch 上的实现,以及编辑距离、同义词等高级特性
  • 指出全文检索在语义相似方面的不足,引入向量搜索,讲解向量表示、相似度度量与向量数据库的用法及局限;
  • 为兼顾两者优缺点,引出混合搜索并给出实作示例;
  • 最后说明如何用向量检索为 LLM 提供高相关上下文,从而通过 RAG 提升生成质量。

下一章将讨论多种领域特定的设计模式,包括时序数据弱网环境等场景。