20.1 倒排索引、Analyzer 与相关性
按主键定位商品时,B+ 树能直接缩小查找范围;用户输入“轻便防水外套”时,还需要分词、召回和排序。搜索系统会预先构造词项及其统计信息,让查询从候选文档开始。
LIKE '%词%' 到底缺什么
前导通配符通常不能直接利用普通 B+ 树前缀顺序,需要扫描大量文本。数据库的 trigram、倒排全文索引等扩展可以解决部分场景,所以不能说“数据库索引都搜不了关键词”。
专用搜索系统的优势在于组合能力:
- analyzer、同义词、词形还原与多语言;
- 倒排索引和短语位置;
- 布尔过滤、相关性、字段权重和 function score;
- 聚合、highlight、suggest 与分布式查询;
- 大规模索引生命周期和近实时更新。
若需求只是少量精确子串或站内小表搜索,数据库原生全文能力可能更简单。
倒排索引保存 term 到 postings
先给定三篇文档的词项序列,位置从 1 开始。这里是手工分词示意,不代表某个中文 analyzer 的实际输出:
d1: 防水 外套 轻量
d2: 防水 背包
d3: 外套 通勤倒排表可以概念化为:
防水 -> (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 是索引契约
典型分析链:
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,便于观察英文词项:
{
"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:
{ "field": "name", "text": "Waterproof Jacket" }再依次向 PUT /products-lab/_doc/1?refresh=wait_for、/2?refresh=wait_for、/3?refresh=wait_for 写入下面三个请求体。后两条路径同样以 /products-lab/_doc 开头:
{ "name": "Waterproof Jacket", "status": "available", "price": 80 }{ "name": "Waterproof Backpack", "status": "available", "price": 65 }{ "name": "Jacket Commute", "status": "available", "price": 120 }此练习假设自动 refresh 已启用,wait_for 的等待边界见下一节。价格单位在这里约定为元;scaled_float 按乘以 100 后取整的方式存储,不能替代交易库的金额校验。
向 POST /products-lab/_search 发送:
{
"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”解释成相关概率。
词法相关性还应加入业务信号,但要控制尺度:
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 变化。
下一节进入生产路径:新文档何时可搜、索引如何分片、权威数据库怎样与搜索投影保持可追踪同步。