倒排索引:全文检索是如何工作的
在一个包含数十亿网页的集合中,输入几个关键词,搜索引擎能在零点几秒内返回最相关的结果。在一个存储了数亿条日志的系统中,输入一个错误码,几秒钟内就能找到所有包含它的记录。电商网站的商品搜索、文档管理系统的内容检索、代码托管平台的代码搜索,背后都依赖着同一种核心数据结构:倒排索引(Inverted Index)。
本文从全文检索面临的问题出发,介绍倒排索引的结构与构建过程、文本分析的各个环节、查询的执行方式、相关性评分模型、索引的压缩与更新机制,以及近年来与之互补的向量检索技术。
一、为什么需要倒排索引
顺序扫描的局限
查找包含某个词的文档,最直接的方法是逐个扫描所有文档,检查其中是否包含该词。对于少量文本,这种方法足够快;但当文档数量达到百万、亿级时,每次查询都要读取全部数据,耗时将以分钟甚至小时计,完全无法满足交互式搜索的需求。
关系型数据库的局限
关系型数据库中可以使用模糊匹配查询,例如查找某个字段中包含特定子串的记录。但这类查询通常无法利用 B+ 树索引:B+ 树按照字段值的字典序组织,只能加速前缀匹配,对于出现在字段中间的子串,仍然需要全表扫描。
此外,全文检索还有许多关系型查询难以满足的需求:
- 查询"跑步"时,希望也能找到包含"跑"或"跑了"的文档;
- 查询多个关键词时,希望返回同时包含这些词、且这些词出现得越集中的文档越靠前;
- 结果需要按照相关程度排序,而不仅仅是简单地过滤。
正排与倒排
文档本身的组织方式可以称为正排:从文档出发,记录每个文档包含哪些词。
文档 1 → [数据库, 索引, 查询]
文档 2 → [索引, 结构, 搜索]
文档 3 → [数据库, 搜索, 引擎]
倒排索引则将这一关系反过来:从词出发,记录每个词出现在哪些文档中。
数据库 → [1, 3]
索引 → [1, 2]
查询 → [1]
结构 → [2]
搜索 → [2, 3]
引擎 → [3]
有了倒排索引,查找包含"搜索"的文档,只需直接找到"搜索"这个词,读取其对应的文档列表即可,无需扫描任何文档内容。查询耗时只与结果的数量相关,而与文档总数基本无关。
倒排索引这个思想古已有之:书籍末尾的索引页,按字母或笔画顺序列出关键词及其出现的页码,本质上就是一个倒排索引。
二、倒排索引的结构
一个完整的倒排索引由两部分组成。
词典
词典(Term Dictionary)记录了所有出现过的词项(Term),以及每个词项对应的倒排列表在存储中的位置。
词典需要支持快速查找。对于有数千万乃至上亿个不同词项的大型索引,词典本身也可能非常大,常见的组织方式有:
- 有序数组加二分查找:将词项排序存储,查找时进行二分查找。结构简单,但需要整体加载;
- B+ 树:适合存储在磁盘上的大型词典;
- 哈希表:精确查找速度最快,但不支持前缀查找和范围查找;
- 前缀树或有限状态转换器:利用词项之间共享的前缀和后缀进行压缩,内存占用很小,同时支持前缀查找、模糊查找等操作。
在一些实现中,会在内存中保存一个稀疏的词典索引,只记录部分词项及其在磁盘上的位置。查找时先通过内存中的索引定位到磁盘上的一小块区域,再读取该区域进行精确查找,以较少的内存换取较少的磁盘读取。
倒排列表
倒排列表(Posting List)记录了包含某个词项的所有文档。列表中的每一项称为一个倒排项(Posting),通常包含以下信息:
- 文档编号:包含该词项的文档的唯一标识。倒排列表通常按文档编号升序排列;
- 词频:该词项在这个文档中出现的次数,用于计算相关性;
- 位置:该词项在文档中出现的位置序号,用于支持短语查询和邻近查询;
- 偏移量:该词项在原始文本中的字符起止位置,用于搜索结果的高亮显示。
并不是所有字段都需要存储全部信息。只需要过滤、不需要评分的字段,可以只存储文档编号;不需要短语查询的字段,可以省略位置信息。省略不需要的信息,可以显著减小索引的体积。
三、文本分析
在构建倒排索引之前,需要先将原始文本切分为一个个词项,这个过程称为文本分析(Text Analysis)。文本分析的质量直接决定了检索的效果:同样的查询,分析方式不同,可能检索出完全不同的结果。
文本分析通常由以下几个环节组成。
字符过滤
在切分之前,对原始文本进行预处理,例如:
- 去除 HTML 标签;
- 将全角字符转换为半角字符;
- 将繁体字转换为简体字;
- 将特定符号替换为文字,例如将"&“替换为"和”。
分词
分词(Tokenization)是将文本切分为一个个词元的过程。
对于英文等以空格分隔单词的语言,分词相对简单,主要依据空格和标点符号进行切分,再处理连字符、缩写、所有格等特殊情况。
对于中文、日文等词与词之间没有分隔符的语言,分词是一个困难得多的问题。同样一句话,可能有多种切分方式,不同的切分会导致不同的含义。常见的中文分词方法包括:
- 基于词典的匹配:维护一个词典,按照一定的规则在文本中查找词典中的词,例如从左到右尽可能匹配最长的词。方法简单、速度快,但无法识别词典之外的新词,也难以处理歧义;
- 基于统计模型:利用大规模语料训练统计模型,根据字与字之间组合成词的概率进行切分,能够较好地处理歧义和识别新词;
- N 元切分:不尝试识别真正的词,而是将文本切分为所有长度为 N 的连续字符片段,例如二元切分将"倒排索引"切分为"倒排"“排索”“索引”。这种方法不依赖词典,召回率高,不会因分词错误而漏掉结果,但索引体积大,且会产生许多无意义的片段,影响相关性排序。
实际系统中,通常会根据需要组合使用多种方法。例如,对同一字段同时建立基于词典的分词索引和 N 元切分索引,查询时综合两者的结果。
词元过滤
切分出的词元,还需要经过一系列标准化处理,才能成为最终写入索引的词项:
- 大小写归一:将所有字母转换为小写,使得查询"Database"也能匹配"database";
- 停用词过滤:去除"的"“了”“是”“the”“a”“of"等出现频率极高但几乎不携带信息的词。去除停用词可以减小索引体积,但也可能影响某些短语查询,例如查询"to be or not to be”。现代搜索系统往往倾向于保留停用词,而在评分阶段降低它们的权重;
- 词干提取:将单词还原为其词干形式,例如将"running"“runs”“ran"都还原为"run”。词干提取通常基于规则,速度快但较为粗糙,可能产生并非真实单词的结果;
- 词形还原:借助词典和语法分析,将单词还原为其标准词形。比词干提取更准确,但速度更慢;
- 同义词扩展:将同义词映射到相同的词项,或在索引中同时写入多个同义词,使得查询"电脑"也能找到包含"计算机"的文档;
- 拼音转换:为中文词项额外生成拼音形式,支持拼音搜索。
索引与查询使用一致的分析
一个重要的原则是:索引时和查询时,通常需要使用相同或相容的分析方式。如果索引时将"Running"转换为"run",而查询时没有进行相同的处理,查询"Running"就会找不到任何结果。
但在某些情况下,两者也会有意采用不同的处理。例如,同义词扩展可以只在查询时进行,这样修改同义词表时无需重建索引;又如,为了支持输入时的实时提示,索引时会生成词的所有前缀,而查询时不需要。
四、索引的构建
基本流程
构建倒排索引的基本流程如下:
- 为每个文档分配一个文档编号;
- 对文档的每个需要检索的字段进行文本分析,得到词项序列;
- 对每个词项,记录下文档编号、词频和位置信息;
- 将所有记录按照词项分组,同一词项下按文档编号排序,形成倒排列表;
- 将词典和倒排列表写入存储。
大规模索引构建
当数据量远超内存容量时,无法将所有记录一次性在内存中排序。常见的做法是分块构建再合并:
- 持续读取文档,在内存中构建一个小型的倒排索引;
- 当内存使用达到阈值时,将这个小索引排序后写入磁盘,形成一个段(Segment);
- 重复上述过程,产生多个段;
- 将多个段进行多路归并,合并为一个更大的索引。
由于每个段内部的词项都是有序的,多路归并只需要顺序读取各个段,效率很高。
对于网页搜索这样的超大规模场景,索引构建通常在分布式计算框架上完成:将文档分配到多台机器上并行处理,每台机器负责一部分文档的分析和索引构建。
五、查询的执行
单词项查询
最简单的查询只包含一个词项。执行过程是:对查询词进行与索引相同的文本分析,在词典中查找该词项,读取其倒排列表,返回其中的文档。
布尔查询
多个词项的组合查询,通过对倒排列表进行集合运算实现:
- 与(AND):求多个倒排列表的交集,返回同时包含所有词项的文档;
- 或(OR):求多个倒排列表的并集,返回包含任意一个词项的文档;
- 非(NOT):求差集,从结果中排除包含某个词项的文档。
由于倒排列表按文档编号有序排列,求交集可以使用类似归并排序的双指针方法:同时遍历两个列表,比较当前的文档编号,较小的一方向前移动,相等时记录为结果。这种方法的时间复杂度与两个列表的长度之和成正比。
交集的优化
当两个倒排列表长度相差悬殊时,例如一个常见词有一千万个文档,一个罕见词只有一百个文档,逐个遍历长列表是一种浪费。常见的优化手段有:
- 从最短的列表开始:多个词项求交集时,先处理最短的倒排列表,以其中的每个文档编号作为候选,在其他列表中查找,中间结果的规模始终不超过最短列表;
- 跳表:在倒排列表中每隔一定间隔设置一个跳跃指针,记录该位置的文档编号。在长列表中查找某个文档编号时,可以借助跳跃指针快速跳过大量不可能匹配的项,而无需逐项比较;
- 二分查找或指数搜索:对于短列表中的每个文档编号,在长列表中进行二分或指数搜索,定位到第一个不小于该编号的位置。
短语查询
短语查询要求多个词项按照指定的顺序连续出现,例如查询"倒排 索引",只匹配这两个词紧邻出现的文档,而不匹配两个词分散在文档不同位置的情况。
短语查询需要利用倒排项中的位置信息:
- 先对各词项的倒排列表求交集,找到同时包含所有词项的文档;
- 对每个候选文档,检查各词项的位置是否满足连续关系:第二个词项的某个位置,恰好等于第一个词项的某个位置加一,依此类推。
在此基础上,还可以支持邻近查询:允许多个词项之间相隔一定数量的其他词,例如两个词之间的距离不超过五个词。
前缀、通配符与模糊查询
- 前缀查询:查找以某个前缀开头的所有词项。如果词典有序或采用前缀树结构,可以快速找到所有匹配的词项,再合并它们的倒排列表;
- 通配符查询:例如查找符合"数*库"模式的词项。前导通配符的查询(如"*库")难以利用有序词典,常见做法是额外建立一个将每个词项反转后的索引,或者使用 N 元片段索引辅助查找;
- 模糊查询:查找与查询词的编辑距离在一定范围内的词项,用于容忍拼写错误。可以将查询词构造为一个能够接受所有编辑距离不超过阈值的字符串的自动机,再与词典结构进行交集运算,高效地找出所有候选词项。
需要注意的是,前缀、通配符和模糊查询可能展开为大量词项,每个词项都需要读取倒排列表,查询开销可能很大。实际系统中通常会限制展开的词项数量。
六、相关性评分
检索出匹配的文档只是第一步。对于返回成千上万条结果的查询,用户通常只会查看前几条,因此按照相关程度排序至关重要。
词频与逆文档频率
衡量一个文档与查询的相关程度,有两个直观的出发点:
词频(Term Frequency,TF):一个词项在文档中出现的次数越多,该文档越可能与这个词项相关。
逆文档频率(Inverse Document Frequency,IDF):一个词项出现在越多的文档中,它对区分文档的作用就越小。例如,"系统"这个词可能出现在大量技术文档中,而"倒排"这个词只出现在少数文档中,后者对于判断相关性更有价值。逆文档频率通常计算为总文档数与包含该词的文档数之比的对数。
将两者相乘,得到 TF-IDF 评分:一个词项在当前文档中出现得越频繁、在整个文档集合中出现得越稀少,它对当前文档的评分贡献越大。多词项查询的总评分,可以由各词项的评分相加得到。
向量空间模型
另一种理解方式是向量空间模型:将每个文档和查询都表示为一个高维向量,每一维对应一个词项,值为该词项的 TF-IDF 权重。文档与查询的相关程度,用两个向量之间夹角的余弦值来衡量。余弦值越大,说明文档与查询在词项分布上越相似。
BM25
TF-IDF 存在一些不足:
- 词频的影响是线性的,一个词出现一百次的文档,评分是出现十次的十倍。但实际上,词频从零增加到几次时,相关性提升明显;继续增加时,相关性的提升应当逐渐饱和;
- 没有考虑文档长度。长文档天然包含更多的词,词频也往往更高,在评分上占有不公平的优势。
BM25 是在概率检索模型基础上发展出来的评分函数,对上述问题进行了改进,目前是全文检索中最常用的默认评分算法:
- 词频饱和:词频对评分的贡献随词频增长而逐渐趋于一个上限,由一个参数控制饱和的速度;
- 文档长度归一化:将文档长度与集合中的平均文档长度进行比较,较长的文档会适当降低其词频的权重,由另一个参数控制归一化的强度;
- 改进的逆文档频率:采用了一种更平滑的逆文档频率计算方式。
BM25 的两个参数可以根据具体的文档集合进行调整,但其默认值在大多数场景下已经能够取得不错的效果。
评分的扩展
在实际的搜索系统中,仅凭文本相关性往往不足以得到理想的排序,通常还会结合其他因素:
- 字段权重:查询词出现在标题中,比出现在正文中更能说明相关性,可以为不同字段设置不同的权重;
- 词项邻近度:多个查询词在文档中出现的位置越接近,文档的相关性可能越高;
- 文档质量信号:例如网页的链接关系、文档的发布时间、商品的销量和评价、内容的点击率等;
- 个性化因素:用户的地理位置、历史行为、偏好等。
在大规模搜索系统中,常采用多阶段排序:第一阶段用倒排索引和 BM25 等轻量的评分方法,从海量文档中快速召回数百或数千个候选;第二阶段对这些候选使用更复杂的机器学习模型进行精细排序。这样既保证了召回的效率,又能在较小的候选集上使用计算开销较大的排序模型。
Top-K 检索的优化
用户通常只需要评分最高的前 K 个结果。如果对所有匹配文档都计算完整的评分再排序,在匹配文档很多时代价高昂。
一类重要的优化方法是动态剪枝:为每个词项预先计算它在任意文档中可能贡献的最大评分,存储在索引中。查询时维护当前前 K 个结果中的最低分作为阈值,如果某个文档即使在所有剩余词项上都取得最大评分,总分也不可能超过阈值,就可以直接跳过,无需计算其精确评分。更精细的实现会将倒排列表分块,为每一块记录块内的最大评分,从而跳过整块不可能进入前 K 的文档。
这类方法能够在保证结果完全正确的前提下,跳过大量文档的评分计算,显著提升查询速度。
七、索引压缩
倒排索引的体积可能非常大。对于一个包含数十亿文档的集合,常见词项的倒排列表可能包含数亿个文档编号。压缩倒排列表,不仅能够节省存储空间,更重要的是能减少磁盘读取和内存带宽的消耗,使更多的索引能够被缓存在内存中,从而提升查询速度。
差值编码
倒排列表中的文档编号是有序递增的。与其直接存储文档编号本身,不如存储相邻编号之间的差值:
原始编号: 3, 7, 15, 16, 28, 31
差值: 3, 4, 8, 1, 12, 3
对于常见词项,文档编号之间的差值往往很小;即使是罕见词项,差值通常也远小于编号本身。差值越小,表示它所需的位数越少。
变长整数编码
差值编码之后,需要用一种能够高效表示小整数的编码方式进行存储。
变长字节编码是最常用的一种:每个字节使用 7 位存储数值,最高位作为标志,表示后面是否还有更多字节。数值小于 128 时只需 1 个字节,小于 16384 时需要 2 个字节,以此类推。这种编码实现简单,解码速度快。
位打包与块编码
更进一步,可以将倒排列表分成固定大小的块,例如每块 128 个差值。对于每一块,找出其中最大的值需要多少位来表示,然后用这个位数紧凑地存储块内所有的值。
例如,如果一块中最大的差值是 13,只需 4 位即可表示,那么 128 个差值总共只需 64 字节。这种方法称为位打包(Bit Packing)。
位打包的一个问题是:如果块中有少数几个异常大的值,会迫使整块都使用较多的位数。一种改进是将这些异常值单独存储,其余值使用较少的位数,这类方法被称为带例外处理的块编码。
块编码的另一个重要优势是解码速度:由于块内所有值使用相同的位宽,解码过程高度规则,可以借助 SIMD 指令一次解码多个值,速度比逐个解码变长字节编码快得多。
位图与压缩位图
对于出现在大量文档中的常见词项,还可以使用位图表示倒排列表:用一个比特数组表示所有文档,包含该词项的文档对应的位为 1。位图的交集和并集运算可以通过按位与、按位或高效完成。
但对于稀疏的倒排列表,位图会浪费大量空间。压缩位图结合了多种表示方式的优点:将整个文档编号空间划分为固定大小的区块,对每个区块根据其中元素的密度,自适应地选择用有序数组存储(稀疏时)、用位图存储(稠密时),或用连续区间存储(存在大段连续编号时)。压缩位图在空间占用和运算速度上都有良好的表现,被广泛用于倒排列表、过滤条件缓存等场景。
词典压缩
词典中的词项在有序排列时,相邻词项往往有很长的公共前缀,例如"索引"“索引器”“索引结构”。可以使用前缀压缩,只存储每个词项与前一个词项不同的后缀部分及公共前缀的长度。更进一步,有限状态转换器同时共享前缀和后缀,能够以极小的空间表示大型词典,并支持高效的查找和遍历。
八、索引的更新
倒排索引的结构高度紧凑、有序,非常适合查询,但并不便于修改:在一个已经压缩存储的倒排列表中间插入一个文档编号,需要重写整个列表。
基于段的不可变索引
现代全文检索引擎普遍采用与 LSM 树类似的思路:索引由多个不可变的段组成。
- 新增文档:新文档首先被写入内存缓冲区。缓冲区达到一定大小,或者经过一定时间后,将其中的文档构建为一个新的段,写入存储。段一旦生成,就不再修改;
- 删除文档:由于段不可修改,删除操作只是在一个单独的删除标记文件中记录被删除的文档编号。查询时,这些文档会被过滤掉;
- 更新文档:实现为删除旧文档、再新增一个新版本的文档;
- 查询:同时在所有段上执行,再将各段的结果合并。
段合并
随着不断写入,段的数量会越来越多。过多的段会带来问题:每次查询都需要访问所有段,开销增大;已删除的文档仍然占用着存储空间,也需要在查询时被跳过。
因此,系统会在后台定期将多个小段合并为一个大段。合并时,被标记删除的文档会被真正清除。合并策略通常会选择大小相近的段一起合并,以控制写放大。
段合并会消耗大量的磁盘 I/O 和 CPU 资源,可能影响查询性能,因此通常需要对合并的速率加以限制。
近实时检索
基于段的架构带来了一个特点:新写入的文档,在形成新段并对查询可见之前,是无法被搜索到的。将内存中的文档生成一个可被查询的新段,这个操作通常称为刷新(Refresh)。
刷新的频率决定了数据从写入到可被检索之间的延迟。刷新越频繁,数据可见得越快,但会产生更多的小段,增加合并压力和查询开销。许多系统默认每秒刷新一次,提供所谓的近实时检索:数据在写入后约一秒内即可被搜索到。
在写入大量数据的场景,例如批量导入历史数据时,可以暂时降低刷新频率或关闭自动刷新,以提高写入吞吐量,导入完成后再恢复。
需要注意的是,刷新只是让段对查询可见,并不一定意味着数据已被持久化到磁盘。为了防止宕机时丢失数据,系统通常会配合使用预写日志:每次写入同时追加到日志中,在段被真正持久化之前,可以通过重放日志恢复数据。
九、分布式检索
当数据量超出单台机器的处理能力时,需要将索引分布到多台机器上。
按文档分片
最常见的方式是按文档分片:将文档集合划分为多个分片,每个分片包含一部分文档的完整倒排索引,分布在不同的节点上。
查询时:
- 协调节点将查询发送到所有分片;
- 每个分片在本地执行查询,计算评分,返回本地的前 K 个结果;
- 协调节点合并各分片返回的结果,选出全局的前 K 个。
按文档分片的优点是各分片完全独立,增加数据时只需增加分片,并且每个分片都可以独立地并行处理查询。缺点是每个查询都需要访问所有分片,查询的延迟取决于最慢的那个分片。
一个值得注意的细节是逆文档频率的计算:每个分片只能看到自己的那部分文档,计算出的逆文档频率与全局值可能存在差异。当数据在分片之间分布均匀时,这种差异通常可以忽略;但在分片较少或数据分布不均时,可能导致评分不一致。一些系统提供了先收集全局统计信息再执行评分的查询模式,以提高准确性,代价是需要额外的一轮网络通信。
按词项分片
另一种方式是按词项分片:将词典划分为多个部分,每个节点负责一部分词项的完整倒排列表。
查询时只需访问包含查询词项的节点,对于只包含少数几个词项的查询,涉及的节点很少。但多词项查询需要在节点之间传输倒排列表进行交集运算,网络开销较大;热门词项所在的节点会承受很大的负载;文档更新时,一个文档的多个词项分布在不同节点上,需要同时更新多个节点。
由于这些问题,按词项分片在实际系统中较少使用。
副本
每个分片通常会有多个副本,分布在不同的节点上。副本既能在节点故障时保证数据不丢失、服务不中断,也能分担查询负载:同一个查询可以发送到任意一个副本上执行,增加副本数就能提高系统整体的查询吞吐量。
十、向量检索与混合检索
倒排索引的局限
基于倒排索引和 BM25 的检索方法,本质上是词汇匹配:只有当查询和文档中出现相同的词项时,才能建立联系。这带来了一个固有的局限,即所谓的词汇鸿沟问题:
- 查询"如何修复笔记本电脑无法开机",可能无法匹配到一篇题为"便携式计算机电源故障排查"的文章,尽管它们讨论的是同一件事;
- 用户的提问方式,往往与文档的表述方式存在差异。
同义词扩展可以在一定程度上缓解这个问题,但难以覆盖语言中丰富多样的表达方式。
稠密向量检索
近年来,随着深度学习的发展,向量检索(也称语义检索或稠密检索)成为倒排索引的重要补充。
其基本思路是:使用一个训练好的神经网络模型,将文档和查询都映射为固定维度的稠密向量,通常有数百到上千维。模型经过训练,使得语义上相近的文本,其向量在空间中也彼此靠近,即使它们使用了完全不同的词语。检索时,计算查询向量与文档向量之间的相似度,返回最相似的文档。
在大规模数据上精确地计算查询向量与所有文档向量的相似度,开销是不可接受的。因此,向量检索通常依赖近似最近邻搜索算法,例如基于图结构的索引:将向量组织成一个多层的近邻图,查询时从入口节点出发,沿着图中的边不断向更接近查询向量的节点移动,在较少的距离计算次数内找到近似最优的结果。另一类方法则先对向量进行聚类,查询时只在最近的若干个聚类中搜索,并结合向量量化技术压缩存储。
有趣的是,基于聚类的近似最近邻索引,其结构与倒排索引非常相似:每个聚类中心相当于一个"词项",属于该聚类的向量列表相当于"倒排列表"。
两者的互补
向量检索和词汇检索各有所长:
- 向量检索擅长理解语义,能够匹配表述不同但含义相近的内容,对自然语言形式的提问效果较好;
- 词汇检索擅长精确匹配,对于专有名词、产品型号、错误码、人名、罕见术语等,往往比向量检索更可靠。向量模型可能从未在训练中见过某个特定的型号或代码,难以为其生成有意义的向量表示;
- 词汇检索的结果更容易解释,用户能够清楚地看到哪些词匹配了;
- 词汇检索的索引构建和更新开销通常更低,不需要模型推理。
因此,许多现代检索系统采用混合检索:同时执行词汇检索和向量检索,再将两路结果进行融合。一种简单而有效的融合方法是倒数排名融合:每个文档的最终得分,等于它在各路检索结果中排名的倒数之和。这种方法只依赖排名,不需要对不同检索方式的原始评分进行归一化,实现简单,效果稳健。
此外,还有一类学习型稀疏检索方法,利用神经网络模型为文档和查询生成稀疏的词项权重,包括扩展出原文中没有出现但语义相关的词项,然后仍然使用倒排索引进行检索。这类方法试图在保留倒排索引高效性和可解释性的同时,引入语义理解能力。
在检索增强生成等应用中,混合检索已经成为一种常见的做法:先从知识库中检索出相关的文档片段,再将其作为上下文提供给语言模型生成回答。检索质量的高低,直接影响着最终生成内容的准确性。
十一、实践建议
根据数据特点选择分析器。 不同语言、不同领域的文本需要不同的分析方式。对于中文内容,分词器的选择和词典的维护对检索效果影响很大;对于代码、日志、产品编号等非自然语言内容,通用的分词器可能会错误地切分,需要定制分析规则。在确定分析方式之前,应当用真实的数据和查询进行测试。
只索引需要检索的字段。 每个被索引的字段都会增加存储和写入开销。对于只需要展示、不需要检索的字段,可以只存储而不建立索引;对于只需要精确过滤、不需要全文检索的字段,例如状态、类别、标签,可以作为不分词的关键字字段处理。
区分过滤与评分。 对于不需要影响排序的条件,例如时间范围、类别筛选,应当作为过滤条件而非评分条件执行。过滤条件不需要计算评分,并且其结果可以被缓存复用,执行效率更高。
谨慎使用开销大的查询。 前导通配符查询、大范围的模糊查询、深度分页等操作可能非常昂贵。深度分页尤其需要注意:在分布式环境中,获取第一万页的结果,需要每个分片都返回其前若干万条结果再由协调节点合并,开销随页数线性增长。对于需要遍历大量结果的场景,应当使用基于游标的翻页方式。
合理设置刷新与合并参数。 根据业务对数据实时性的要求设置刷新间隔,避免不必要的频繁刷新。在批量写入时调整刷新和合并策略,以提高吞吐量。
评估检索效果。 检索系统的优劣不能仅凭直觉判断。应当建立一套包含典型查询及其期望结果的评测集,使用准确率、召回率、排序质量等指标量化评估,并在调整分析方式、评分参数或引入向量检索时,用这套评测集进行对比验证。
结语
倒排索引的思想非常朴素:与其在每次查询时扫描所有文档,不如预先记录每个词出现在哪里。但围绕这一朴素的思想,发展出了一整套精巧的技术体系:文本分析决定了什么样的词能够被检索到;集合运算和跳跃结构让多词查询高效执行;位置信息支持了短语和邻近查询;TF-IDF 和 BM25 等评分模型让结果按照相关程度排序;差值编码和位打包让庞大的索引得以紧凑存储;不可变段和后台合并让索引能够持续更新;分片和副本让检索系统扩展到海量数据。
近年来,向量检索为文本检索带来了语义理解的能力,但它并没有取代倒排索引,而是与之形成了互补。在精确匹配、可解释性和效率方面,倒排索引依然具有不可替代的优势。理解倒排索引的原理,仍然是理解现代检索系统的基础。
- 点赞
- 收藏
- 关注作者
评论(0)