跳到内容

第 5 卷 / 第 20 章 / 第 1 课

20.1 倒排索引、Analyzer 与相关性 ​

按主键定位商品时,B+ 树能直接缩小查找范围;用户输入“轻便防水外套”时,还需要分词、召回和排序。搜索系统会预先构造词项及其统计信息,让查询从候选文档开始。

LIKE '%词%' 到底缺什么 ​

前导通配符通常不能直接利用普通 B+ 树前缀顺序,需要扫描大量文本。数据库的 trigram、倒排全文索引等扩展可以解决部分场景,所以不能说“数据库索引都搜不了关键词”。

专用搜索系统的优势在于组合能力:

  • analyzer、同义词、词形还原与多语言;
  • 倒排索引和短语位置;
  • 布尔过滤、相关性、字段权重和 function score;
  • 聚合、highlight、suggest 与分布式查询;
  • 大规模索引生命周期和近实时更新。

若需求只是少量精确子串或站内小表搜索,数据库原生全文能力可能更简单。

倒排索引保存 term 到 postings ​

先给定三篇文档的词项序列,位置从 1 开始。这里是手工分词示意,不代表某个中文 analyzer 的实际输出:

text
d1: 防水 外套 轻量
d2: 防水 背包
d3: 外套 通勤

倒排表可以概念化为:

text
防水 -> (d1, tf=1, positions=[1]), (d2, tf=1, positions=[1])
外套 -> (d1, tf=1, positions=[2]), (d3, tf=1, positions=[1])

实际索引会对文档 ID 差值、词频、位置等编码压缩,并维护字段长度、doc values 等其他结构。是否保存 positions、offsets、term vectors 会影响短语查询、高亮、磁盘和写入成本。

Analyzer 是索引契约 ​

典型分析链:

text
character filters -> tokenizer -> token filters
  • character filter 在切词前规范化原文;
  • tokenizer 划分 token 边界;
  • token filter 做小写化、停用词、同义词、词干等处理。

中文没有天然空格边界,按单字、词典、统计模型或子词切分会产生不同召回。商品名、型号、代码和自然语言应按查询语义选择 analyzer,不宜未经检验套用同一配置。

standard analyzer 组合标准分词与小写化;下面的练习会直接检查其输出。

索引 analyzer 与查询 analyzer 可以不同,但必须语义兼容。修改 analyzer 通常不会自动重写已有 postings,需要建立新索引并 reindex。

text 与 keyword 是两条检索路径 ​

以下请求体适用于 Elasticsearch 8.x/9.x REST API,需要已可访问的练习集群及创建、写入和查询索引的权限。先确认 products-lab 不存在,再把此 JSON 作为 PUT /products-lab 的请求体;不要覆盖已有业务索引。这里显式使用 standard analyzer,便于观察英文词项:

json
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "raw": { "type": "keyword" }
        }
      },
      "status": { "type": "keyword" },
      "price": { "type": "scaled_float", "scaling_factor": 100 }
    }
  }
}

name 用于全文匹配,name.raw 用于精确过滤、排序或聚合。把任意动态字段自动映射进索引可能造成 mapping explosion;生产环境应控制模板、字段数量和类型演进。

Query 与 Filter 分工 ​

先发送 POST /products-lab/_analyze,请求体如下,预期 token 为 waterproof、jacket:

json
{ "field": "name", "text": "Waterproof Jacket" }

再依次向 PUT /products-lab/_doc/1?refresh=wait_for、/2?refresh=wait_for、/3?refresh=wait_for 写入下面三个请求体。后两条路径同样以 /products-lab/_doc 开头:

json
{ "name": "Waterproof Jacket", "status": "available", "price": 80 }
json
{ "name": "Waterproof Backpack", "status": "available", "price": 65 }
json
{ "name": "Jacket Commute", "status": "available", "price": 120 }

此练习假设自动 refresh 已启用,wait_for 的等待边界见下一节。价格单位在这里约定为元;scaled_float 按乘以 100 后取整的方式存储,不能替代交易库的金额校验。

向 POST /products-lab/_search 发送:

json
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": { "query": "waterproof jacket", "operator": "and" } } }
      ],
      "filter": [
        { "term": { "status": "available" } },
        { "range": { "price": { "lte": 10000 } } }
      ]
    }
  }
}

预期仅命中文档 1,因为两个词项都必须出现。match 默认使用 OR;删掉 operator: "and" 后,这组三条文档都会匹配至少一个词,价格条件也都通过。它也不要求两个词相邻;短语顺序要用 match_phrase 等相应查询表达。

全文 query 参与相关性评分,结构化 filter 只判断是否匹配。权限、租户、库存状态等硬条件必须放在过滤边界内,不能指望低分文档自然排到后面。

BM25 在平衡什么 ​

Lucene BM25 的直觉由三部分组成:

  • 词在当前文档出现越多,贡献增加但逐渐饱和;
  • 在整个语料中越稀有,区分度通常越高;
  • 长文档有更多自然命中机会,需要长度归一化。

得分不是跨查询、跨索引可直接比较的概率。分片级统计、字段 boost、同义词展开和索引变化都会影响 _score。不要把“分数 > 1”解释成相关概率。

词法相关性还应加入业务信号,但要控制尺度:

text
final_score = lexical relevance
            + bounded freshness boost
            + bounded popularity boost

若销量直接无上限相加,热门商品会永久压制更相关的新商品。

相关性必须有评价集 ​

先为代表性查询建立人工相关性标注集,覆盖热门、长尾、错别字、多义词、零结果和权限过滤。常用离线指标:

  • Precision@k:前 k 条中相关结果比例;
  • Recall@k:相关结果在前 k 条被召回多少;
  • MRR:先算每个查询首个相关结果排名的倒数,再对查询取平均;无相关结果时该项记 0;
  • nDCG@k:考虑多级相关性和排名折损。

例如标注相关文档为 d1、d3,返回顺序为 d1、d2、d3,则 Precision@2 = 1/2,Recall@2 = 1/2,首个相关结果倒数为 1。若另一查询首个相关结果排第三,两个查询的 MRR = (1 + 1/3) / 2 = 2/3。Recall 的分母来自标注范围,漏标相关文档会影响指标解释。

离线指标通过后,再以 A/B 测试观察点击、转化、零结果率和 reformulation。点击日志有位置偏差,不能直接当无偏真值。

本节检查点 ​

  • 能解释倒排表、词频、位置和字段长度分别服务什么。
  • 能为自然语言、精确过滤和排序设计不同字段。
  • 能区分召回、过滤与排序,不把 _score 当概率。
  • 能用 judgement set 评价 analyzer 和 ranking 变化。

下一节进入生产路径:新文档何时可搜、索引如何分片、权威数据库怎样与搜索投影保持可追踪同步。

Built with VitePress | Software Systems Atlas