这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 相关性优化是搜索系统从“能搜到”走向“搜得好”的关键。它不只是调大某个字段权重,而是包含查询理解、召回、特征、排序、评估和持续迭代的完整过程。
27.1 什么是相关性
从用户视角看,相关结果应满足:
- 语义匹配:商品确实符合关键词;
- 意图匹配:理解品牌、型号、规格、用途;
- 可购买:有货、可售、价格合理;
- 可信:销量、评价、售后、商家服务;
- 可解释:排序符合用户直觉,异常结果能解释。
从系统视角看,排序由三部分组成:
相关性得分:BM25 / 向量相似度 / phrase / boost
业务得分:销量、转化、库存、信誉、时效
策略得分:置顶、活动、多样性、个性化
只优化 BM25 往往解决不了业务排序问题;只按销量排序又会牺牲文本相关性。
27.2 BM25 原理
BM25 是 Elasticsearch 默认文本评分算法。直观形式:
score =
IDF(term)
x TF(term in doc) / (TF + k1 * (1 - b + b * dl / avgdl))
x boost
三个核心因素:
| 因素 | 含义 | 影响 |
|---|---|---|
| IDF | 词在集合中越罕见,区分度越高 | 罕见词贡献更高 |
| TF | 词在文档中出现次数越多,越相关 | 受饱和效应限制 |
| 文档长度 norm | 文档越短,同样匹配越集中 | 长文档被适当惩罚 |
参数:
k1:控制词频饱和;b:控制字段长度归一化强度。
查看字段 similarity:
GET products-v3/_mapping/field/title
自定义 similarity:
PUT articles-v1
{
"settings": {
"index": {
"similarity": {
"default": {
"type": "BM25",
"k1": 1.2,
"b": 0.75
}
}
}
}
}
除非有评测集证明有效,不建议凭感觉改 BM25 参数。
27.3 字段权重
不同字段的重要性不同。标题、品牌、类目、运营词、详情应有不同权重:
GET products-v3/_search
{
"query": {
"multi_match": {
"query": "苹果手机",
"type": "best_fields",
"fields": [
"title^4",
"brand_name^3",
"keywords^2",
"description^1"
]
}
}
}
best_fields:取匹配效果最好的字段得分,适合“多个字段表达同一含义”。
most_fields:多个字段得分相加,适合字段互补,但容易让多字段弱匹配超过单字段强匹配。
cross_fields:把多个字段视作一个大字段,适合名、姓、地址拼接,但对中文复杂分词要谨慎。
| 类型 | 适合 | 风险 |
|---|---|---|
| best_fields | 标题优先的搜索 | 忽略多字段弱匹配 |
| most_fields | 多信号补充 | 重复加权 |
| cross_fields | 结构化姓名地址 | 依赖 analyzer 一致 |
| phrase | 精确顺序要求 | 召回变少,成本更高 |
27.4 Match 与 Phrase
match 不保证词序:
{ "match": { "title": "红色无线鼠标" } }
match_phrase 要求词项顺序和临近:
{ "match_phrase": { "title": "无线鼠标" } }
组合使用:
GET products-v3/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": { "query": "无线鼠标", "boost": 1 } } },
{ "match_phrase": { "title": { "query": "无线鼠标", "boost": 3, "slop": 1 } } }
],
"minimum_should_match": 1
}
}
}
这样能先保证召回,再让更精确的短语排序更靠前。
27.5 Function Score
Function Score 用于把业务因子加入相关性排序。
GET products-v3/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "笔记本",
"fields": ["title^4", "keywords^2"]
}
},
"functions": [
{
"filter": { "term": { "tags": "hot" } },
"weight": 1.2
},
{
"field_value_factor": {
"field": "sales_30d",
"modifier": "log1p",
"factor": 0.5,
"missing": 0
}
},
{
"gauss": {
"on_shelf_at": {
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
},
{
"filter": { "term": { "shop_grade": "a" } },
"weight": 1.1
}
],
"score_mode": "sum",
"boost_mode": "multiply",
"max_boost": 10
}
}
}
常用函数:
| 函数 | 用途 |
|---|---|
| weight | 固定加权 |
| field_value_factor | 根据数值字段加分 |
| decay | 时间、距离、价格接近度衰减 |
| random_score | 实验分流或打散 |
| script_score | 自定义公式或模型打分 |
score_mode 决定多个函数之间怎么合并;boost_mode 决定函数结果和原查询得分怎么合并。上线前必须固定排序版本并记录特征值。
27.6 Script Score
当公式复杂时,可以使用 script score:
GET products-v3/_search
{
"query": {
"script_score": {
"query": {
"multi_match": {
"query": "笔记本",
"fields": ["title^4", "keywords^2"]
}
},
"script": {
"source": """
double score = _score;
double sales = doc['sales_30d'].size() == 0 ? 0 : doc['sales_30d'].value;
double stock = doc['stock'].size() == 0 ? 0 : doc['stock'].value;
score += Math.log1p(sales) * params.salesWeight;
if (stock > 0) {
score *= params.stockBoost;
}
return score;
""",
"params": {
"salesWeight": 0.8,
"stockBoost": 1.05
}
}
}
}
}
注意:
- 脚本必须参数化;
- 优先使用 Painless;
- 限制脚本执行时间;
- 避免在脚本中访问超大嵌套结构;
- 脚本复杂时考虑外部重排;
- 需要记录 score 可解释性。
27.7 多路召回与 Rerank
复杂搜索常用多路召回:
query
-> 文本召回 BM25
-> 短语召回 phrase boost
-> 向量召回 semantic kNN
-> 运营/个性化召回
-> merge by id
-> first-stage rank
-> rerank model
混合检索示例:
GET products-v3/_search
{
"size": 50,
"query": {
"bool": {
"must": [
{ "match": { "title": "轻薄高续航笔记本" } }
],
"should": [
{ "match_phrase": { "title": { "query": "轻薄高续航笔记本", "boost": 2 } } }
],
"filter": [
{ "term": { "status": "on_sale" } }
]
}
},
"knn": {
"field": "embedding",
"query_vector": [0.01, 0.02, 0.03],
"k": 50,
"num_candidates": 200
}
}
不同版本对 query 与 kNN 的组合方式有差异,需要按当前版本验证。工程上更常见的做法是:
- 分别取 BM25 top N 和向量 top N;
- 应用层合并去重;
- 使用 RRF(Reciprocal Rank Fusion)或模型 rerank;
- 再叠加业务规则;
- 记录每个来源的分数和排名。
RRF 示例:
score(doc) = sum(1 / (k + rank_i(doc)))
其中 k 常取 60。RRF 不要求不同来源的分数可比,适合合并 BM25 和向量结果。
27.8 搜索评估
没有评估体系,相关性优化只能靠感觉。至少建立三层数据。
27.8.1 离线评测集
| query | 文档 | 相关等级 | 标注人 |
|---|---|---|---|
| 无线鼠标 | doc_1 | 3 | 搜索组 |
| 无线鼠标 | doc_2 | 2 | 业务组 |
| 无线鼠标 | doc_3 | 0 | 搜索组 |
常用指标:
- Precision@K;
- Recall@K;
- NDCG@K;
- MRR;
- MAP;
- 无结果率;
- 首屏点击率。
NDCG 关注高质量文档是否排在前面,适合搜索排序评估。
27.8.2 在线指标
- 点击率 CTR;
- 首屏点击率;
- 加购率;
- 转化率;
- 换词率;
- 无结果率;
- 搜索时长;
- 退货或投诉率。
27.8.3 分层指标
不同 query 应分开看:
| 类型 | 示例 | 重点 |
|---|---|---|
| 品牌词 | iPhone | 品牌、型号、正品 |
| 类目词 | 手机 | 类目、销量、多样性 |
| 属性词 | 轻薄 16G | 属性召回 |
| 长尾词 | 送女生的礼物 | 意图、推荐 |
| 错别字 | 显视器 | 纠错、同义词 |
全站平均指标可能掩盖局部劣化,必须按 query 分层评估。
27.9 点击日志与反馈闭环
搜索日志建议记录:
PUT search_logs-v1/_doc/search_log_001
{
"search_id": "search_log_001",
"user_id": "u_1001",
"query": "无线鼠标",
"rewritten_query": "无线 鼠标",
"result_ids": ["sku_1", "sku_2", "sku_3"],
"result_scores": [12.3, 11.8, 10.1],
"clicked_positions": [1],
"search_version": "rank-v3.4",
"experiment_bucket": "control",
"latency_ms": 48,
"result_count": 320,
"created_at": "2026-08-25T10:00:00Z"
}
这些数据可以用于:
- 统计点击率和换词率;
- 发现坏 query;
- 生成候选同义词;
- 构建训练样本;
- 验证排序版本;
- 排查线上排序问题。
注意不要把点击当成绝对真理:首屏位置本身会带来点击偏差,需要用位置偏差修正或 A/B 实验验证。
27.10 排名稳定性与多样性
搜索结果不稳定会损害用户信任。常见原因:
- 数据持续写入;
- refresh 后分片统计变化;
- 排序键重复;
- 随机分数未固定 seed;
- 个性化上下文变化;
- 实验分流变化。
建议:
- sort 加稳定 tie-breaker;
- 实验分桶在会话内稳定;
- 同一 query 短时间缓存;
- 新品或随机流量控制在固定比例;
- 避免无意义的频繁重排。
多样性策略:
- 同店铺、同品牌限量;
- 类目分桶;
- MMR 或规则打散;
- 置顶数量限制;
- 广告与自然结果区分标注。
27.11 排序治理流程
一次相关性优化应经过:
发现 bad case
-> 归因:召回 / 过滤 / 分词 / 排序
-> 构造离线评测集
-> 修改配置或模型
-> 离线回归
-> 小流量实验
-> 在线指标验证
-> 全量上线
-> 记录版本与回滚方案
禁止只看一个 query 的截图就上线权重改动。搜索是全局系统,一个 case 的提升可能带来一批 query 劣化。
27.12 相关性优化清单
- 标题、品牌、类目、属性字段的职责清晰;
- 同义词、停用词、拼音、纠错策略有版本管理;
- 权重改动必须通过离线评测;
- 每次请求记录排序版本和主要特征;
- 用
_explain和 profile 排查 bad case; - 业务因子可解释、可配置;
- 新排序支持灰度和回滚;
- 点击日志完整接入;
- 建立分层指标和固定评测集;
- 向量与文本混合检索有融合策略;
- 定期治理无结果和高换词 query。
本章小结
相关性优化是“查询理解 + 召回 + 排序 + 评估”的闭环。BM25 和字段权重只是起点,业务因子、点击反馈、评测集和版本治理才决定搜索质量能否持续提升。所有排序改动都要可解释、可回放、可回滚。
思考题
- BM25 中 IDF、TF 和文档长度分别如何影响得分?
best_fields和most_fields分别适合什么场景?- 为什么排序字段需要唯一 tie-breaker?
- BM25 分数和向量相似度为什么不能直接相加?
- 如果一个 query 效果变差,你会如何定位是分词、召回还是排序问题?