Elasticsearch搜索优化实战:从基础查询到精准排序的完整实践

目录

  • Query DSL 优化
    • match
  • 引入排序逻辑
    • 时间衰减因子的引入
  • 进阶:BM25模型调优
  • 总结

在最近的项目中,我们基于Elasticsearch(以下简称ES)搭建了一套全文检索系统。开发过程中发现经常搜不到想要的内容,或者明明关键词匹配,结果却杂乱无章,新内容被旧内容压在后面,核心字段匹配的文档反而排在边缘。经过一系列针对性优化,搜索准确率提升了60%以上。这篇文章就把整个优化过程拆解开来,希望能够帮助到在看文章的你。

Query DSL 优化

Query DSL是ES查询的核心,接下来我们一步步看如何从 "能搜到" 到 "搜得准"。

match

构建一个搜索的最小版本很简单,只需要下面一段话:

{
  "query": {
    "match": {
      "content": "智能手表续航"
    }
  }
}

虽然已经能够实现全文的检索了,但效果肯定不尽人意。

这种方式有三个严重问题:

  • 分词不匹配:比如 "续航" 会被分词为 "续" 和 "航"。
  • 字段权重失衡:标题与正文采用相同权重计算,导致标题含关键词但正文无关的文档排名落后
  • 条件过滤缺失:未排除其他类别的条目,比如搜索苹果,可能出现苹果手机

针对分词不匹配问题,我们首先优化字段映射与查询类型:

  1. 重构索引映射:将核心字段拆分为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的搜索方案,有时间再继续和大家分享。

ElasticSearch
2025 © Yeliheng的技术小站 版权所有