本文是 搜推算法笔记 第 7 篇。索引见 搜推算法 · 知识索引。
上一篇:Python 搜推技术栈
Elasticsearch 是工业界最常用的全文检索引擎,擅长倒排检索、BM25 打分与组合过滤。本文从搜推工程视角介绍 ES 的用法,与 ES 存储引擎系列(引擎原理)互补。
1. ES 在搜推中的角色
Elasticsearch 擅长:
- 全文检索(倒排 + BM25)
- 组合过滤(类目、价格、店铺、库存状态)
- 排序与(有限度的)聚合
- (可选)knn 向量检索
在架构里常见位置:
1 | 用户搜索 → ES 全文检索 |
复杂排序通常交给业务侧 LTR 精排——ES 专注「强过滤 + 文本召回」,避免在 ES 内堆叠复杂 function_score。
2. 倒排索引与查询逻辑
1 | 正向:doc → terms |
Postings List 结构:
1 | term "蓝牙" → [ (doc_id=1, tf=2, pos=[3,15]), (doc_id=5, tf=1, pos=[0]), ... ] |
大规模索引中 postings 按 doc_id 排序,配合 Skip List / 跳表 加速 AND 合并——两路 postings 求交集时,小 doc_id 侧可跳过不可能匹配的块,避免逐 doc 比较。
TF 存储: 不必存原始词频,可存 (\log(1+tf)) 或量化值以省空间;BM25 计算时再还原。
一次查询:
1 | 1. 分析器分析 Query → tokens |
分析器意识
中文检索效果很大程度取决于分词——索引时怎么切、查询时怎么切、品牌词是否进词典。索引分析与查询分析不一致会导致「有文档却搜不到」。
工程上,分词策略调整往往是搜索效果优化的第一步。几个关键认知:
索引与查询分析器要一致。 索引时用 ik_max_word,查询时用 ik_smart,或者两边都用同一套——不一致会导致「库里有却搜不到」。
品牌/SKU 进自定义词典。 「华为手机」不能被切成「华为 / 手机 / 华 / 为」——否则精确匹配和 BM25 都会受影响。
多字段策略。 标题、SKU、规格、类目可能用不同 analyzer 或不同 boost 权重。SKU 字段通常需要更细粒度的分词。
1 | { |
(具体配置因业务而异,关键是理解「分析器决定能不能搜到」。)
3. BM25 与业务排序
ES 默认相似度多为 BM25(公式见 Query 理解与多路召回)。它给出的是 文本相关分,不等于最终业务序。
业务排序常见做法:
- 纯 BM25:简单搜索
- function_score:相关分 × 业务因子(销量、新鲜度)——详见 ES 相关度评分
- 先 ES 召回,再业务 LTR 精排(更现代)
BM25 在 ES 中的计算
ES/Lucene 对每个 shard 独立算 BM25,再归一化合并(shard 间 IDF 用全局统计近似)。Query 侧 boost 可给不同 field 加权:
1 | { |
title^3 表示 title 字段命中词的 TF 分量 ×3——field boost 是乘法,不是简单加固定分。
function_score 与 BM25 的关系:
[
\text{final_score} = \text{BM25}(q,d) \times f(\text{sales}, \text{freshness}, \ldots)
]
乘法模型:相关分接近 0 时,业务因子拉不动;相关分高时,业务因子放大差异。加法模型 (\text{BM25} + \alpha \cdot \text{sales}) 则可能让高销量低相关文档霸榜——搜推场景乘法更常见。
4. 过滤、聚合与性能直觉
| query | filter | |
|---|---|---|
| 目的 | 算相关分 | 是/否过滤 |
| 缓存 | 不强调 | 常可缓存 bitset |
| 例子 | match「蓝牙耳机」 | term 有库存 |
能 filter 的别写成 query,通常更快更稳。
聚合(Aggregation)可做类目统计、价格直方图,但大数据量 + 高基数聚合很贵。榜单类需求常改为 预聚合写入,在线直接读。
5. 分页方案对比
| 方案 | 原理 | 深分页 | 跳页 | 数据一致性 | 适用场景 |
|---|---|---|---|---|---|
| from/size | 跳过前 N 条 | 差(越翻越慢) | 支持 | 实时 | 前 1~3 页 |
| scroll | 快照上下文遍历 | 好 | 不支持 | 快照(非实时) | 数据导出/批处理 |
| search_after | 游标取下一批 | 好 | 不支持 | 实时 | 用户翻页推荐 |
| PIT + search_after | 时间点快照 + 游标 | 最好 | 不支持 | 快照窗口内一致 | 深翻页 + 一致性要求高 |
更深入的 PIT 方案见 ES 深度分页:PIT 详解。
from / size(浅分页)
1 | page 1: from=0, size=20 |
深分页为什么慢?
ES 是分布式的:每个分片先取 from+size 条,再在协调节点归并排序,丢掉前 from 条。
1 | from=100000, size=10 |
适合前几页。不适合翻到很深。
scroll
为 快照式遍历/导出 设计,保留上下文。不适合普通用户翻页。
search_after(深翻页推荐)
用上一页最后一条的排序值当游标:
1 | { |
| from/size | search_after | |
|---|---|---|
| 深分页 | 差 | 好 |
| 随机跳到第 N 页 | 容易 | 不擅长 |
| 实时用户翻页 | 浅页可用 | 推荐 |
排序必须加唯一键(如 id asc),否则同分不稳定,游标可能重复/漏数据。
6. PageToken 与查询指纹
用户第一页用「销量排序」,翻到第 3 页时改成「价格排序」,若仍用旧游标 → 结果错乱。
做法:把查询条件 + 排序规则做成指纹(hash),写入 pageToken:
1 | token = base64({ |
指纹一致 → 继续 search_after;指纹变了 → 旧 token 失效,从第一页重来。
7. 搜索走 ES、详情走 DB
| 路径 | 存储 | 原因 |
|---|---|---|
| 搜索/列表 | ES | 倒排检索、过滤、排序适合搜索引擎 |
| 详情 | MySQL 等 | 强一致、字段完整、事务、复杂关联 |
ES 与 DB 之间存在同步延迟 → 短暂不一致。应对:MQ 近实时同步、离线对账修数据、关键字段以 DB 为准。
8. 同步一致性与版本
ES 近实时(默认 refresh 约 1s 量级),不是事务强一致。
Binlog/CDC 同步可能乱序——用 external version:仅当新版本 > 当前版本才写入,否则拒绝(409),避免旧数据覆盖新数据。
定期用 DB 权威数据对比 ES,修复漏同步/错同步。「只靠 MQ」或「只靠对账」都不够,通常两者互补。
9. 常见性能优化
mget 消除 N+1
1 | 坏:for id in ids: GET /index/_doc/id |
只取需要的字段
_source filtering,避免大文档整包返回。
避免深度聚合与深度分页
榜单预计算;翻页 search_after。
向量 knn 与全文检索同集群?
要评估 CPU/内存/线程池争用。全文检索延迟敏感时,向量检索独立更稳(见 向量检索与 ANN)。
ES 三种排序路径对比
| 路径 | 做法 | 延迟 | 灵活性 | 适用 |
|---|---|---|---|---|
| 纯 BM25 | ES 默认 score 排序 | 低 | 低 | 简单搜索 |
| function_score | 相关分 × 业务因子 | 中 | 中 | 销量/新鲜度加权 |
| ES 召回 + LTR 精排 | ES 取 TopK → Python/Java 精排 | 中~高 | 高 | 现代搜推主流 |
全文检索走 ES 召回 + 过滤,复杂排序交给业务侧 LTR——ES 专注「强过滤 + 文本召回」,不在 ES 里堆复杂 function_score。
本篇收尾
这是搜推算法笔记工程线的最后一篇。建议从 知识索引 回顾整体脉络。


