在最近的项目中,我们基于Elasticsearch(以下简称ES)搭建了一套全文检索系统。开发过程中发现经常搜不到想要的内容,或者明明关键词匹配,结果却杂乱无章,新内容被旧内容压在后面,核心字段匹配的文档反而排在边缘。经过一系列针对性优化,搜索准确率提升了60%以上。这篇文章就把整个优化过程拆解开来,希望能够帮助到在看文章的你。
Query DSL 优化
Query DSL是ES查询的核心,接下来我们一步步看如何从 "能搜到" 到 "搜得准"。
match
构建一个搜索的最小版本很简单,只需要下面一段话:
{
"query": {
"match": {
"content": "智能手表续航"
}
}
}
虽然已经能够实现全文的检索了,但效果肯定不尽人意。
这种方式有三个严重问题:
- 分词不匹配:比如 "续航" 会被分词为 "续" 和 "航"。
- 字段权重失衡:标题与正文采用相同权重计算,导致标题含关键词但正文无关的文档排名落后
- 条件过滤缺失:未排除其他类别的条目,比如搜索苹果,可能出现苹果手机
针对分词不匹配问题,我们首先优化字段映射与查询类型:
- 重构索引映射:将核心字段拆分为text(分词检索)和keyword(精确匹配)双字段
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word", // 细粒度分词,提升召回率
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"content": {
"type": "text",
"analyzer": "ik_smart" // 粗粒度分词,减少噪音
}
}
}
}
关键点在于配置好分词器,可以使用ik或者根据需求使用其他分词器。
2、使用分层查询结构:对标题采用 match_phrase(短语匹配)提升精准度,正文用match保证召回率
{
"query": {
"bool": {
"should": [
{
"match_phrase": { // 要求关键词连续出现,提升标题匹配权重
"title": {
"query": "智能手表续航",
"slop": 1 // 允许1个词的间隔
}
}
},
{
"match": { // 正文分词匹配
"content": "智能手表续航"
}
}
]
}
}
}
3、引入布尔查询结合boost参数调整权重,过滤无关结果。
{
"query": {
"bool": {
"must": [
{ "term": { "status": { "value": "online", "boost": 3 } }}
],
"should": [
{ "match_phrase": { "title": { "query": "智能手表", "boost": 5 }}}, // 标题权重最高
{ "match": { "tags": { "query": "智能手表", "boost": 3 }}}, // 标签次之
{ "match": { "content": { "query": "智能手表", "boost": 1 }}} // 正文权重最低
]
}
}
}
这里的关键逻辑是:
- 用must子句强制过滤商品状态
- should子句中,标题匹配权重是正文的 5 倍,确保核心字段匹配的文档优先展现
注意,线上要慎用包含通配符前缀的查询(如*手表),尽量使用前缀查询来替代:
// 不要用
{ "wildcard": { "title": "*手表" }}
// 正确的方式
{ "prefix": { "title.keyword": "手表" }} // 仅对keyword字段做前缀匹配
引入排序逻辑
默认情况下,ES仅根据_score(相关性得分)排序,但在实际业务中暴露出两个问题:
- 新上架的热门商品(如刚发布的智能手表)因索引时间短,
_score较低而排名靠后 - 同分数文档的排序随机,导致用户刷新页面时结果可能不一致
时间衰减因子的引入
为解决 "新内容被压制" 的问题,我们在排序中加入时间衰减因子,让近期内容获得额外权重:
{
"query": {
"function_score": {
"query": { "match": { "title": "智能手表" }},
"functions": [
{
"gauss": { // 高斯衰减函数,越新权重越高
"publish_time": {
"origin": "now", // 以当前时间为原点
"scale": "7d", // 7天内的内容权重衰减缓慢
"decay": 0.3 // 超过7天后权重衰减至30%
}
}
}
],
"boost_mode": "multiply" // 衰减分数与原始分数相乘
}
},
"sort": [
{ "_score": { "order": "desc" }},
{ "publish_time": { "order": "desc" }}
]
}
进阶:BM25模型调优
BM25是ES 7.0后的默认的相关性评分算法。具体细节自行查阅相关资料。
对于BM25核心公式而言,其中的关键参数如下:
- k1:控制词频饱和效应(默认 1.2),值越大词频影响越显著
- b:控制文档长度对评分的影响(默认 0.75),值越小长文档优势越弱
初始阶段,我们发现两类文档的评分异常:
- 长文档(如商品详情页)因包含更多分词,即使核心关键词出现次数少,_score仍偏高
- 短文档(如商品标题)因词频低,_score被严重低估
针对此,我们通过以下步骤调优:
- 分析文档长度分布:计算
avgdl(平均文档长度),发现正文字段的avgdl为800字,而标题仅为 20 字 - 调整b参数:降低长文档的长度优势
{
"mappings": {
"properties": {
"content": {
"type": "text",
"similarity": {
"my_bm25": {
"type": "BM25",
"b": 0.4, // 从默认0.75降至0.4,削弱长文档优势
"k1": 1.2
}
}
}
}
}
}
- 提升k1参数:增强核心词频的影响
{
"similarity": {
"my_bm25": {
"type": "BM25",
"b": 0.4,
"k1": 1.8 // 从1.2提升至1.8,让高频核心词获得更高权重
}
}
}
总结
ES搜索优化不是一蹴而就的,而是发现问题-调整参数-验证效果的循环过程,评分机制是可拆解、可调控的,只要结合业务场景一点点打磨,就能让搜索结果真正贴合用户需求。
后续可以讲讲解决近义词搜不到的问题和Emoji的搜索方案,有时间再继续和大家分享。