一、当搜索结果开始"跳街舞"

最近我负责的电商站内搜索出了怪现象:用户搜索"运动鞋"时,首页居然出现了三年前下架的商品,而热销新款却躲在第三页。就像超市把过期牛奶放在货架最显眼位置,这体验实在让人抓狂。

问题的根源在于Elasticsearch默认的BM25评分算法。这个算法主要考虑:

  • 词频(TF):关键词在文档中出现的次数
  • 逆文档频率(IDF):关键词在整个索引中的稀有程度
  • 字段长度:较短字段的匹配更有价值

但当遇到以下场景时,默认排序就会"抽风":

  1. 商品标题包含多个同义词
  2. 库存状态/上架时间影响业务权重
  3. 用户个性化需求(如地域偏好)

二、给搜索引擎"植入价值观"

2.1 基础调参法:query调优

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "防水运动鞋",
      "fields": ["title^3", "description"], // 标题权重是描述的3倍
      "type": "best_fields"
    }
  }
}

通过字段权重调整,让标题匹配的结果获得更高评分。但这种方法就像给所有用户发同样的问卷,无法应对个性化需求。

2.2 组合拳:bool查询混搭

{
  "query": {
    "bool": {
      "must": [
        { "match": { "category": "运动鞋" } }
      ],
      "should": [
        { "term": { "tags": "防水" } }, // 精确匹配标签加0.5分
        { "range": { "stock": { "gt": 0 } } } // 有库存加1.0分
      ]
    }
  }
}

通过should子句添加业务规则,但各条件的权重比例需要反复调试,就像调鸡尾酒时各种基酒的比例拿捏。

2.3 终极大招:function_score

{
  "query": {
    "function_score": {
      "query": { "match": { "title": "跑步鞋" } },
      "functions": [
        {
          "filter": { "range": { "sales": { "gte": 1000 } } }, // 销量大于1000
          "weight": 2
        },
        {
          "field_value_factor": { 
            "field": "rating",   // 用户评分
            "modifier": "log1p",
            "missing": 3
          }
        },
        {
          "gauss": {
            "release_date": { 
              "origin": "now",   // 新品优先
              "scale": "30d"
            }
          }
        }
      ],
      "boost_mode": "sum" // 各维度分数相加
    }
  }
}

这是最灵活的调优方式,相当于给每个文档打综合评分。但需要注意:

  • 不同函数的分值区间要统一
  • 避免某个函数权重过高导致结果偏差
  • 对冷门字段要设置missing参数

三、实战中的"平衡艺术"

3.1 电商搜索优化示例

{
  "query": {
    "function_score": {
      "query": { "multi_match": { /* 基础查询 */ } },
      "functions": [
        {
          "filter": { "term": { "is_sponsored": true } },
          "weight": 5 // 广告商品加权
        },
        {
          "script_score": {
            "script": {
              "source": """
                double score = 0;
                // 库存系数
                score += Math.log1p(doc['stock'].value);
                // 新品系数(发布时间在3天内)
                score += (new Date().getTime() - doc['publish_date'].value.getMillis()) < 259200000 ? 3 : 0;
                // 促销活动叠加
                score += params.promotions.get(doc['item_id'].value) != null ? 2 : 0;
                return score;
              """,
              "params": {
                "promotions": {"A001":1, "B202":1} // 当前促销商品列表
              }
            }
          }
        }
      ]
    }
  }
}

这个方案实现了:

  • 广告位的商业价值
  • 库存和时效性的平衡
  • 动态促销信息的结合

3.2 社区论坛优化示例

{
  "query": {
    "function_score": {
      "query": { "match": { "content": "编程技巧" } },
      "functions": [
        {
          "field_value_factor": {
            "field": "like_count",
            "modifier": "sqrt", // 点赞数开平方防刷分
            "factor": 0.5
          }
        },
        {
          "filter": { "range": { "comment_count": { "gt": 10 } } },
          "weight": 2 // 热门讨论加成
        },
        {
          "gauss": {
            "create_time": {
              "origin": "now",
              "scale": "7d", // 7天内新帖优先
              "decay": 0.5
            }
          }
        }
      ]
    }
  }
}

这个设计巧妙平衡了内容质量与时效性,避免老帖长期霸榜。

四、排序优化的"双刃剑"

4.1 技术优势

  • 多维度决策支持:可以融合业务指标、用户行为、实时数据
  • 动态权重调整:通过脚本实现实时策略变更
  • 精度可控:支持从简单加权到复杂算法的平滑过渡

4.2 需要警惕的坑

  • 性能成本:每个function_score函数都会增加计算量
  • 权重失衡:某维度权重过高会导致结果极化
  • 冷启动问题:新文档缺乏统计特征数据
  • 排序抖动:频繁调整策略可能导致结果不稳定

五、实施前的checklist

  1. 明确业务目标:是追求点击率?转化率?还是内容质量?
  2. 建立评分沙盒:在Kibana中创建多个评分策略对比测试
  3. 监控排序健康度:通过埋点统计前10结果的用户互动率
  4. 设置保护阈值:比如确保广告商品不超过结果总数的30%
  5. 定期策略review:根据用户行为变化调整权重比例

六、写在最后

调整Elasticsearch相关性排序就像给智能音箱训练语音识别——既需要算法基础,更要理解业务场景。经过多个项目的实践,我总结出三个心法:

  1. 二八原则:80%的效果来自20%的核心参数调整
  2. 动态平衡:不要追求绝对的"正确排序",而要维持生态健康
  3. 数据说话:每天查看搜索terms的点击热图,比任何算法都有说服力

记住,最好的排序策略是那个能让用户忘记排序存在的策略。当用户自然地找到想要的内容时,就是我们搜索工程师的最高成就。