图书管理员老周的桌上摊着一本翻得卷边的“词语索引本”。每个词条下密密麻麻列着书号:翻开“麦田”,写着 37 号、58 号、102 号……读者问“哪本书记载了麦田”,老周翻翻本子几秒就能作答。这个神奇的索引本,就是搜索引擎里“倒排索引”最朴素的模样。 直到某天,一位读者要“找一本写了'金色的麦田'这五个字连贯出现的书”——注意,不是分别有“金色”和“麦田”,而是它们必须肩并肩,一字不差。老周傻眼了:索引本只告诉他哪些书提过哪个词,却答不出词与词是否挨在一起。被逼无奈,老周开始给索引本“升级”:每个词条下除了书号,还要记录“这本书里的第几个词位置出现了这个词”。 这版升级后的“词语索引本”,就是位置索引。有了它,老周可以回答“金色后面的那个词是不是麦田”这样的短语查询;再放宽一点,还能回答“金色和麦田相隔不超过三个词”的近邻查询。代价是索引本厚了好几倍,老周不得不研究各种压缩技巧。本文将沿着老周的升级之路,把位置索引的结构、匹配算法、空间优化与中文场景讲个明白。

岗位关联

对 AI 从业者来说,短语查询与位置索引是搜索引擎、数据库全文检索、RAG 召回精排的底层基石。理解它,你才能真正读懂 Elasticsearch 的 match_phrase 为什么快、为什么贵,并在面试与系统设计中游刃有余。

目录
  1. 可视化演示
  2. 倒排索引回顾
  3. 位置索引结构
  4. 短语匹配算法
  5. 近邻查询处理
  6. 空间效率优化
  7. 中文分词应用
  8. 混合策略选择
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

演示位置索引的存储结构、短语查询的位置匹配算法,以及近邻查询中词项距离的计算过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

位置索引存储结构 每个词项关联文档列表与位置数组 位置信息支持短语和近邻查询 空间开销约为基础索引2-4倍 term→[(doc,[pos1,pos2...])] 倒排表扩展位置数组记录词项出现位置

倒排索引回顾

老周的“词语索引本”其实有讲究:他不是把所有藏书从头翻到尾,而是先编一本“词典”——把馆里所有书里出现过的词去重整理成词条;每个词条下挂着“倒排表”,也就是出现过这个词的书号清单。词典加倒排表,合起来就是倒排索引。老周的管理台账,正是信息检索里最经典的数据结构。

搜索引擎的倒排索引和它一模一样。建索引时,系统把每篇文档切分成词项,统计每个词项出现在哪些文档里;查询时,用户输入的词直接在词典里查,拿到倒排表。比如查询“金色 AND 麦田”,系统分别取“金色”和“麦田”的倒排表,再做集合求交,结果就是同时包含两个词的文档。这种“两个名单对一对”的思路,就是布尔查询的合并过程。

除了文档编号,倒排表通常还会记录词频 tf(词在文档中出现了几次)和文档频率 df(有多少文档包含该词),用于后续相关性排序。两个倒排表的合并是检索的核心操作,最朴素的做法是双指针扫描:两个表都按文档号升序排列,谁小谁前进,相等就输出。

可是老周渐渐发现,这个索引本有个致命盲区:它记录的是“书号”这个粗粒度位置,而不是“书里第几个词”这个细粒度位置。读者真正想找的往往是“一句话”而不仅仅是“一个词”。于是,升级索引本迫在眉睫。

I(q1∧q2)=I(q1)∩I(q2)\displaystyle I(q_1 \wedge q_2) = I(q_1) \cap I(q_2)
两个查询词的倒排表取交集,命中的文档必须同时包含两个词——但“同时包含”和“连在一起”是两回事。
  1. 倒排表符号$I(\cdot)$ — 输入一个查询词,返回该词的倒排表(文档编号集合)
    I(q1)I(q_1)
  2. 合取语义$\wedge$ — 表示“且”,两个查询词的条件必须同时满足
    q1∧q2q_1 \wedge q_2
  3. 集合求交$\cap$ — 求两个倒排表共同的文档编号,得到布尔 AND 的检索结果
    I(q1)∩I(q2)I(q_1) \cap I(q_2)

位置索引结构

老周在“金色”词条下加了一列小字:37 号书,出现在第 1、8、15 个词的位置;58 号书,出现在第 0、5、9 个词的位置。“麦田”词条同样处理。这相当于给每个词在书里装上了“页码行号”,原先的粗粒度书号变成了精确定位。老周的新本子,就是位置索引的雏形。

位置索引和普通倒排索引的唯一区别是:每个倒排表项不再只是一个文档编号 docID,而是“docID + 位置列表”。位置列表中的每个整数,代表词项在该文档分词后的第几个词位出现,一般从 0 开始编号。比如“金色”在 58 号书的第 0、5、9 个词位出现,就记作 [0, 5, 9]。

位置列表按照词位严格递增排列,这一点至关重要——查询时用双指针比较,两个位置值谁小谁往后走,才能在线性时间内完成相邻性检查。位置列表的长度等于该词在该文档中的出现次数,也就是词频;把所有文档所有词项的位置数相加,就得到位置索引的总规模,它大致等于语料库的总词数。

有了“页码行号”,老周回答“金色后面是不是麦田”就不再靠猜了:翻开两个词条,逐本书比对位置。不过,肉眼比对效率太低,他需要一套明确的比对规则——这就是下一节的短语匹配算法。

posting(t)={(d,Pt,d)∣d∈D, t∈d},Pt,d={p1<p2<⋯<pk}\displaystyle \mathrm{posting}(t) = \left\{ (d, P_{t,d}) \mid d \in \mathcal{D},\ t \in d \right\},\quad P_{t,d} = \{ p_1 < p_2 < \cdots < p_k \}
一个词项的倒排表由若干“文档号+位置集合”对组成,位置集合记录该词在文档中的所有出现位置,并且严格递增排列。
  1. 倒排表项$\mathrm{posting}(t)$ — 检索词项 t 得到的完整倒排表,是所有“文档-位置”对的集合
    posting(t)\mathrm{posting}(t)
  2. 位置集合$P_{t,d}$ — 词 t 在文档 d 中所有出现位置组成的集合,k 是出现次数
    Pt,dP_{t,d}
  3. 文档限定$t \in d$ — 只为真正包含词 t 的文档记录位置,不出现的文档不进集合
    d∈D, t∈dd \in \mathcal{D},\ t \in d
  4. 递增排列$p_1 < p_2 < \cdots < p_k$ — 位置按词位升序存储,这是双指针合并的前提
    p1<p2<⋯<pkp_1 < p_2 < \cdots < p_k

短语匹配算法

老周像校准两串珍珠项链一样,把“金色”和“麦田”两条位置列表在同一个文档里逐位对齐:金色在第 0 个词位,那么麦田必须正好在第 1 个词位;金色在 0 和 3 有两个位置,麦田在 1 和 8,那么“0 对 1”这一对就算对上,文档命中。老周的手指在两个词条上左右滑动,就是最原始的短语匹配。

计算机里的短语查询,正是这个“位置对齐”过程的程序化。给定短语 $w_1 w_2 \cdots w_m$,先在倒排索引里查得每个 $w_j$ 的倒排表,然后对这些倒排表做文档级求交;对共同出现的文档,再用位置列表做相邻性检查:第一个词在位置 p,那么第二个词必须在 p+1,第三个词必须在 p+2,以此类推。顺序和相邻缺一不可。

实现上可以采用双指针合并。比如对两词短语,两个位置列表都从 0 号元素开始:若第二个位置等于第一个位置加 1,命中;若第二个位置更小,第二个指针后移;否则第一个指针后移。整个过程每个位置最多被访问一次,时间复杂度与位置总数成线性关系,非常高效。

老周的珍珠项链校准法完美解决了“金色麦田”;但另一位读者走来问:“找不到'金色的麦田'没关系,'金色'和'麦田'隔不超过三个字我也接受。”这就是把“紧挨着”放宽成“隔不远”——近邻查询登场了。

ptj=pt1+(j−1),j=1,2,…,m\displaystyle p_{t_j} = p_{t_1} + (j - 1),\quad j = 1, 2, \ldots, m
短语匹配要求每个词的位置恰好与第一个词的位置对齐:第二个词在 p+1,第三个词在 p+2,依次错开一位。
  1. 短语长度$m$ — 短语包含的词项总数,两词短语时 m = 2
    mm
  2. 基准位置$p_{t_1}$ — 短语首个词在文档中的某个出现位置,作为对齐基准
    pt1p_{t_1}
  3. 位置增量$j - 1$ — 第 j 个词必须位于基准之后第 j-1 个词位,同时保证顺序与相邻
    j−1j - 1
  4. 全程校验$j = 1, 2, \ldots, m$ — 对短语的每个词逐一检查,全部通过才算命中
    j=1,2,…,mj = 1, 2, \ldots, m

近邻查询处理

老周这次不再要求两词“手牵手”,而是允许“一个手臂的距离”:只要“麦田”出现在“金色”之后、且位置差不大于 k 个词位,就算命中。读者给了 k=3,于是“金色 的 麦田”这种中间隔着“的”的文档也能被找到。老周在索引本上比划着两列数字之间的距离,心里有了新的规则。

这就是近邻查询——短语查询的“宽松版”。它依然利用位置索引,但把“位置差恒等于 1”放宽为“位置差 ≤ k”。按方向要求不同,又分为有序近邻(第二个词必须在前一个之后)和无序近邻(两词谁先谁后无所谓)。前者适合“温故知新”这种讲究顺序的表达,后者适合“噪音污染”这种位置不敏感的同现。

实现近邻查询时,可以对两个位置列表做窗口滑扫:维护两个指针指向两词当前考察的位置,记录位置差,若差在允许范围内则命中,然后移动较前的那个指针,像一个滑动窗口沿着文档从左到右扫过。窗口滑扫只需要线性扫描两个位置列表各一次,比嵌套循环快得多。

老周的“手臂距离”规则大大提高了召回:读者想找“金色麦田”周边的话题,不必死扣字眼。但索引本也因此被查得更频繁——加上位置信息本身就体积膨胀,老周必须想想怎么给索引本“减肥”了。

0<pt2−pt1≤k\displaystyle 0 < p_{t_2} - p_{t_1} \le k
有序近邻的判定:第二个词的位置减去第一个词的位置必须大于 0(保证顺序),且不超过 k(保证距离)。
  1. 顺序约束$p_{t_2} - p_{t_1} > 0$ — 第二个词必须排在第一个词之后,顺序不可颠倒
    pt2−pt1>0p_{t_2} - p_{t_1} > 0
  2. 距离上界$\le k$ — 两词位置差不得超过用户设定的窗口大小 k
    ≤k\le k
  3. 严格为正$0 <$ — 排除位置相同或逆序的非法情形,保证两词确实占据不同词位
    0<0 <
  4. 无序变体$|p_{t_2} - p_{t_1}| \le k$ — 无序近邻不考虑先后,用绝对值判断距离即可
    ∣pt2−pt1∣≤k|p_{t_2} - p_{t_1}| \le k

空间效率优化

升级后的索引本让老周哭笑不得:原来 300 页的本子,记完位置信息后变成了 900 页。毕竟普通倒排表每个词记一个书号,位置索引却要记下每个词在每本书里的每一次出现——总条数等于整座图书馆所有书的字数之和。这个代价,在搜索引擎里叫“索引体积膨胀”。

位置索引的体积膨胀是主要成本:位置总数约等于语料库全部文档的词语总数,通常是普通倒排表规模的数倍。工程上常用的压缩思想是“把大整数变成小整数”:位置值本身很大,但相邻位置的间隔很小,存间隔显然更省。老周很快想到了这个主意:与其写“第 3、7、12、20 个词位”,不如写“第 3 个词位,之后隔了 3 个、再隔了 4 个、再隔了 7 个”。

位置差分编码正是记录相邻位置的间隔:$g_i = p_i - p_{i-1} - 1$,减 1 是因为位置连续时间隔记为 0。由于多数文档里同一词的出现位置不会太疏,间隔值通常很小,再配合变长字节编码(小整数用 1 个字节、大整数用多个字节)就能大幅压缩。此外还有跳跃指针:倒排表每隔若干项放一个“快进点”,加速求交时一次性跳过大段无关文档。

老周用“记录间隔”这招把索引本又压回了 400 页。他正得意,一位中文系的读者走进来,抱着一本没有空格的小说:“麻烦帮我查查'金色的麦田'在哪几本书里?”老周这才意识到:中文书根本没有空格分词,他的索引本在编本子之前就卡住了。

gi=pi−pi−1−1\displaystyle g_i = p_i - p_{i-1} - 1
位置差分的本质是用“相邻位置的间隔”代替“绝对位置”,间隔小、编码短、好压缩。
  1. 参考位置$p_{i-1}$ — 列表中上一个出现位置,作为差分的参考基准
    pi−1p_{i-1}
  2. 当前位置$p_i$ — 当前出现位置,与参考位置相减得到间隔
    pip_i
  3. 间隔修正$-1$ — 位置连续时原始差值为 1,减 1 后间隔变为 0,数值更小更易压缩
    −1-1
  4. 反向还原$p_i = p_{i-1} + g_i + 1$ — 查询时从第一个位置起累加间隔,还原全部绝对位置
    pi=pi−1+gi+1p_i = p_{i-1} + g_i + 1

中文分词应用

英文书里词与词之间有天然的空格,老周照抄空格就能编词条;可中文书里“金色的麦田”五个汉字挤在一起,如果不先切分,整个句子就会变成“金”“色”“的”“麦”“田”这样的单字。索引本是建了,却答不出“金色麦田”这种词级查询。中文搜索引擎的文档处理流水线因此多了一步:分词。

建索引前,先用分词器把连续的汉字切成词序列:“金色的麦田” → “金色 / 的 / 麦田”,然后按词建立位置索引。这样词项是词而非单字,位置编号也是词在句子中的序号。用户查询“金色麦田”时同样先分词,再把切分结果拿到索引里做短语匹配。查询改写切分让用户与文档在同一个“词序列空间”里对齐。

如果采用字级索引(以单个汉字为词项),查询“金色麦田”只是四个单字的共现,会召回“金色/的/麦田里”等大量语义无关的文档,噪声很大;词级索引更精确,但依赖分词器的词典质量,未登录词可能被切错。位置索引在中文的妙处还在于支持词级模糊命中:查询“金色麦田”分词为“金色”“麦田”,而文档“金色的麦田”分词为“金色”“的”“麦田”,词位差为 1,用近邻查询或查询改写就能灵活召回。

老周欣慰地发现,一旦中文文档完成了分词,他的位置索引本照样适用,甚至能发现“金色的麦田”与“金色麦田”两种表达之间的微妙位置关系。不过,面对浩如烟海的藏书,他也在反思:是不是每本书、每个词都值得记录精确位置?

Q↦⟨w1,w2,…,wm⟩,wi∈V\displaystyle Q \mapsto \langle w_1, w_2, \ldots, w_m \rangle,\quad w_i \in \mathcal{V}
用户查询先被切分为词典中的词序列,然后才能与文档词序列对齐做位置匹配。
  1. 查询切分$Q \mapsto \langle w_1, w_2, \ldots, w_m \rangle$ — 原始查询串被分词器映射成有序词序列
    Q↦⟨w1,w2,…,wm⟩Q \mapsto \langle w_1, w_2, \ldots, w_m \rangle
  2. 词序保留$\langle w_1, w_2, \ldots, w_m \rangle$ — 尖括号表示保留顺序的词序列,位置匹配依赖这个顺序
    ⟨w1,w2,…,wm⟩\langle w_1, w_2, \ldots, w_m \rangle
  3. 词典约束$w_i \in \mathcal{V}$ — 切分出的每个词必须来自词项词典,未登录词无法被正确切出
    wi∈Vw_i \in \mathcal{V}
  4. 对齐匹配$\mathrm{pos}(w_j) = \mathrm{pos}(w_1) + (j - 1)$ — 切分后的词序列进入短语匹配,逐个与文档词位对齐
    pos(wj)=pos(w1)+(j−1)\mathrm{pos}(w_j) = \mathrm{pos}(w_1) + (j - 1)

混合策略选择

老周最后总结出一条经验:对于“的”“了”“和”这种高频虚词,记录它们的位置纯属浪费纸面——几乎每页都有它们,位置列表长得吓人却几乎没有查询价值;而“金色麦田”这种固定搭配,与其查两个词再比对位置,不如直接把它当做一个词条编进索引本。这叫按词性、按频次区别对待。

这正是工业界搜索系统的常规思路:混合索引。对高频常见短语,直接建立短语索引(把整个短语作为一个词项);对一般内容,才使用位置索引支持任意短语与近邻查询;还可以做字段级索引,比如对标题字段建位置索引、对正文只建普通倒排索引。Lucene/Elasticsearch 甚至可以配置关闭位置信息,牺牲短语能力换取更小的索引体积。

空间与时间的权衡贯穿始终。位置索引让短语查询变快,但索引变大、写入变慢;关闭位置则索引瘦身,短语查询退化为词项共现查询。设计时通常结合业务:电商的“品牌+类目”短语、法律的“法条引用”场景必须开位置索引,而日志检索这种以单词过滤为主的场景可以关闭。跳跃指针、变长编码、位置截断(每个文档只保留前 K 个位置)等技巧进一步细化这个取舍。

老周的图书馆终于形成了自己的“检索手册”:高频虚词只记书号,关键词记页码行号,固定搭配直接编成新词条。搜索引擎的设计同样如此——没有银弹,只有基于数据分布与业务场景的精细权衡。

s=⌊N⌋\displaystyle s = \lfloor \sqrt{N} \rfloor
跳跃指针的经典步长取倒排表长度的平方根,让求交扫描的期望步数最小。
  1. 表长定义$N$ — 词项倒排表中的文档项总数,跳跃表建立在它之上
    NN
  2. 均匀采样$\sqrt{N}$ — 开平方根给出理论最优步长,平衡“跳过消费”与“比较代价”
    N\sqrt{N}
  3. 取整处理$\lfloor \cdot \rfloor$ — 向下取整得到整数步长,保证跳跃点不越界
    ⌊⋅⌋\lfloor \cdot \rfloor
  4. 跳跃步长$s = \lfloor \sqrt{N} \rfloor$ — 每隔 s 个文档放一个跳跃指针,求交时可大跨度跳过无关区间
    s=⌊N⌋s = \lfloor \sqrt{N} \rfloor

AI 应用场景

场景1:电商搜索的精准短语召回

阶段:查询理解与检索召回

解决的问题:用户在淘宝搜索“苹果手机壳”时,期望召回标题里“苹果 手机壳”连续出现、顺序一致的商品。传统倒排索引只能保证两词共现,会召回“苹果味手机壳”“苹果 手机 壳”等词序错乱或不相邻的结果;位置索引精确检查词位连续,召回精度大幅提升。

具体案例:Elasticsearch 的 match_phrase 查询就是位置索引的典型应用。用户输入“深度学习 入门”,索引记录每篇文档分词后“深度学习”与“入门”的词位,只有位置差为 1 的文档才进入候选。

场景2:RAG 知识库的短语级段落召回

阶段:检索增强生成(RAG)的召回阶段

解决的问题:问答系统常需找到文档中与问题字面高度重合的句子片段(如“新能源汽车购置税减免”)。仅靠向量检索可能召回语义相似但关键短语错位的段落;叠加位置索引的短语/近邻匹配,能有效提升回答的引用准确性。

具体案例:某法律知识库问答系统中,用户问“合同解除的法定条件”,系统先分词为“合同/解除/法定/条件”,再用近邻查询(k=2)匹配包含相近词序的条款原文,精准定位到法条段落。

场景3:新闻转载与洗稿检测

阶段:内容理解与去重

解决的问题:判断一篇文章是否转载另一篇,关键证据是“连续若干词完全一致”的片段。位置索引让系统快速找出两篇文章共享的短语窗口,比逐句比对高效得多。

具体案例:某内容平台用位置索引对百万篇新闻做 8 词以上长短语交叉匹配,命中片段占比超过 30% 即判定转载,有效识别洗稿账号。

Python 演示

python75 行
"""位置索引:用"书页坐标"实现短语查询与近邻查询"""class PositionalIndex:    def __init__(self):        self.index = {}        # 词项 -> {文档ID: [位置列表]}        self.doc_lengths = {}  # 文档ID -> 词数     def add_document(self, doc_id, tokens):        """向索引中添加一篇文档,记录每个词项的出现位置"""        self.doc_lengths[doc_id] = len(tokens)        for pos, token in enumerate(tokens):            if token not in self.index:                self.index[token] = {}            if doc_id not in self.index[token]:                self.index[token][doc_id] = []            self.index[token][doc_id].append(pos)     def phrase_query(self, phrase):        """短语查询:词项必须按顺序连续出现"""        if not phrase:            return []        if any(t not in self.index for t in phrase):            return []        common = set(self.index[phrase[0]].keys())        for t in phrase[1:]:            common &= set(self.index[t].keys())        results = []        for doc_id in sorted(common):            if self._aligned(phrase, doc_id):                results.append(doc_id)        return results     def _aligned(self, phrase, doc_id):        """检查短语在文档中是否存在连续对齐的位置"""        for p in self.index[phrase[0]][doc_id]:            ok = True            for j in range(1, len(phrase)):                if (p + j) not in self.index[phrase[j]][doc_id]:                    ok = False                    break            if ok:                return True        return False     def proximity_query(self, w1, w2, k, ordered=True):        """近邻查询:w1 与 w2 的距离不超过 k 个词位"""        if w1 not in self.index or w2 not in self.index:            return []        results = []        for doc_id in sorted(set(self.index[w1]) & set(self.index[w2])):            hit = False            for a in self.index[w1][doc_id]:                for b in self.index[w2][doc_id]:                    gap = b - a                    if (ordered and 0 < gap <= k) or (not ordered and abs(gap) <= k):                        hit = True                        break                if hit:                    break            if hit:                results.append(doc_id)        return results  if __name__ == "__main__":    docs = {        0: ["金色", "的", "麦田", "真", "美"],        1: ["金色", "麦田", "里", "有", "丰收"],        2: ["麦田", "守望", "者", "金色", "麦田"],    }    pi = PositionalIndex()    for doc_id, tokens in docs.items():        pi.add_document(doc_id, tokens)    print("位置索引:", pi.index)    print("短语查询 [金色, 麦田]:", pi.phrase_query(["金色", "麦田"]))    print("近邻查询 金色~麦田(有序,k=2):", pi.proximity_query("金色", "麦田", 2, ordered=True))
目的:用约 70 行代码实现一个微型位置索引,演示短语查询如何通过“位置对齐”区分“金色的麦田”与“金色麦田”,并展示近邻查询如何放宽匹配条件。
  1. if token not in self.index:判断词项是否首次出现,首次则新建词条,相当于老周在索引本上开新页
  2. self.index[token][doc_id].append(pos)把出现位置记入位置列表,这就是位置索引的“页码行号”
  3. common &= set(self.index[t].keys())对多个词项的倒排表做集合求交,只保留同时包含所有查询词的文档
  4. if (p + j) not in self.index[phrase[j]][doc_id]:校验短语第 j 个词是否恰好位于首个词位置之后 j 个词位,完成“珍珠项链对齐”
  5. gap = b - a计算两个词的位置差,它是近邻查询的核心度量
  6. if (ordered and 0 < gap <= k) or (not ordered and abs(gap) <= k):有序与无序近邻的统一判定:顺序正确且距离不超窗口就算命中

常见误区

误区在很多初学者的理解里,短语查询不过是两个词项倒排表求交集,只要文档同时出现了两个词,就认为结果命中,根本不看词序和间距。

正解短语查询严格得多。它要求词项按查询顺序连续出现:两词短语的位置差必须恰好等于 1,多词短语则要求每个词的位置严格递增 1。如果文档是“金色的麦田”,搜“金色麦田”就搜不到,因为中间隔着一个“的”。用老周的比喻:倒排表求交只是“两个人都在图书馆里”,短语查询要求“两个人手牵着手排在同一列队伍里”。

为何常见:倒排索引的教学通常先从布尔查询讲起,布尔 AND 恰好就是集合求交,初学者容易把“共现”误当成全部,忽略了位置这一维信息。

误区许多人以为位置索引里存的是“这个词从文档第几个字节开始”,毕竟字节偏移才是计算机视角里真正的“位置”。

正解位置索引通常记录的是词位序号——也就是该词是文档分词后的第几个词,而不是字节偏移。词位序号直接服务于“相邻/间隔 N 个词”这种语义判断;字节偏移主要用于结果高亮等展示功能。Lucene 的 term position 就是词位,与字符偏移分开存储,各司其职。

为何常见:一提到“位置”,编程直觉立刻想到字节下标、数组索引,忽略了信息检索里“位置”是为词距语义服务的抽象单位。

误区有人认为位置索引不过是在倒排表里多存一个整数,体积大概翻一倍,不影响大局。

正解位置索引的体积大约与语料库的全部词项出现次数成正比,而普通倒排表只与“文档-词项”对的个数成正比;同一词在一篇文档出现 10 次就要记 10 个位置,膨胀通常是数倍而非一倍。这也是工程上要用差分编码、变长字节、位置截断来控制体积的原因。

为何常见:估算时下意识只算“每篇文档多一个坐标”,忘了每个词每次出现都要记一个位置,而文档里高频词重复出现极多。

误区以为只要给中文做了分词,建好位置索引,中文短语查询就和英文完全一样了,不存在任何差异。

正解分词本身的歧义会沿着流水线传导:查询“金色麦田”分词成“金色/麦田”,但文档“金色的麦田”分词成“金色/的/麦田”,位置差是 2 而非 1,普通短语查询照样匹配不上。中文场景往往需要配合近邻查询、查询改写与词典更新,才能实现灵活匹配。分词质量直接决定位置索引的上限。

为何常见:把“分词”看作一个已经解决的预处理步骤,忽略它引入的边界噪声与未登录词问题,误以为词序列一旦生成就和英文一样干净。

面试题

Q1. 倒排索引的倒排表记录的主要内容是什么?

单选题 · easy

  • A.文档的完整内容
  • B.词项所在的文档编号← 正确答案
  • C.词项的拼音标注
  • D.文档的创建时间

答案:B。倒排表是词项指向文档的映射,记录包含该词项的文档编号。用老周的比喻,每个词条下挂的就是“书号列表”。A 是全文存储,C 和 D 与检索无关。

Q2. 位置索引相比普通倒排索引,额外记录了哪种信息?

单选题 · easy

  • A.文档的大小信息
  • B.词项的位置序列← 正确答案
  • C.作者的写作时间
  • D.文档的语言种类

答案:B。位置索引在“文档编号”之外增加了“词项在文档中出现的位置列表”,相当于老周在书号后补上了页码行号。其余选项与位置索引的定义无关。

Q3. 对两词短语“金色麦田”,下列哪种情况才算短语命中?

单选题 · mid

  • A.两个词都出现即可
  • B.后词紧跟在前词后一位← 正确答案
  • C.前词位置小于后词
  • D.前词紧跟在后词后一位

答案:B。B 对应 pos(麦田) = pos(金色) + 1,顺序与相邻都满足。A 只保证共现;C 只保证顺序不保证相邻;D 顺序反了。

Q4. 有序近邻查询(k=3)要求两词满足什么关系?

单选题 · mid

  • A.两词任意顺序相距不超三
  • B.前词在前且距离不超三← 正确答案
  • C.两词必须完全相邻
  • D.后词必须在前词之前

答案:B。有序近邻要求第二个词在第一个词之后,且位置差不超过 k。A 是无序近邻的定义;C 是短语查询;D 顺序颠倒。

Q5. 位置差分编码中 gi = pi − p(i−1) − 1 的主要目的是什么?

单选题 · mid

  • A.对位置进行加密处理
  • B.让位置值变小利于压缩← 正确答案
  • C.对文档进行排序处理
  • D.提高查询匹配精度

答案:B。差分把大的绝对位置值转成小的间隔值,配合变长字节编码能显著压缩索引体积。它与加密、排序、匹配精度都没有关系。

Q6. 位置索引的总规模大致与什么成正比?

单选题 · hard

  • A.词典中词项的总数量
  • B.文档的词项出现总数← 正确答案
  • C.索引中的文档总数
  • D.不同词语的数量

答案:B。每个词每次出现都要记录一条位置,因此位置总量约等于语料库全部文档的词项出现总数(总词数),远大于词典规模或文档数量。

Q7. 下列哪些是位置索引的空间优化手段?

多选题 · mid

  • A.对位置做变长编码← 正确答案
  • B.存储位置差分值← 正确答案
  • C.为词保存完整文档
  • D.用跳跃指针加速← 正确答案

答案:ABD。变长编码与差分存储缩小整数占用,跳跃指针加速求交、间接降低扫描开销;C 保存完整文档是全文存储,反而更占空间。

Q8. 关于中文分词与位置索引,下列说法正确的是?

多选题 · extreme

  • A.中文必须先分词再建索引← 正确答案
  • B.查询分词后与文档对齐← 正确答案
  • C.中文无法使用位置索引
  • D.字级索引会产生大量噪声← 正确答案

答案:ABD。中文没有空格,按词建位置索引必须先分词;查询同样分词后才能与文档词序列对齐匹配。字级索引把词拆成单字,导致大量误召回,所以 D 正确、C 错误。

Q9. 无序近邻查询的距离判定条件是?

单选题 · mid

  • A.后词位置减前词位置为一
  • B.两词位置差绝对值不超 k← 正确答案
  • C.两词位置差恰等于窗口
  • D.两词必须位置完全相同

答案:B。无序近邻不要求先后顺序,因此用绝对值判断距离:|p1 − p2| ≤ k。A 和 C 是特定情形,D 要求同一词位,均不符合。

Q10. 位置索引常用于哪些检索场景?

多选题 · easy

  • A.支持短语查询匹配← 正确答案
  • B.支持近邻查询检索← 正确答案
  • C.支持中文分词对齐← 正确答案
  • D.直接压缩文档正文

答案:ABC。位置索引的核心价值在于短语匹配、近邻检索以及分词后的词序对齐;压缩文档正文属于存储压缩领域,与位置索引无关。