从查询走到结果:AI搜索排序笔记

听起来很高端,但实际做下来踩了不少坑,特别是从传统BM25到learning-to-rank的过渡阶段。

最近接了个需求,要把我们产品的搜索结果排序从纯规则改成AI驱动的学习排序。

为什么要写这篇文章

最近接了个需求,要把我们产品的搜索结果排序从纯规则改成AI驱动的学习排序。听起来很高端,但实际做下来踩了不少坑,特别是从传统BM25到learning-to-rank的过渡阶段。

最痛苦的是:网上讲算法原理的很多,但讲"怎么在生产环境中落地"的很少。很多教程都是跑个demo就完事了,没人告诉你真实环境中有哪些坑——数据质量、特征工程、模型部署、线上效果验证,每一步都有细节。

所以写下这篇文章,把整个从查询到结果的实践过程记录下来,希望能帮到同样在做搜索排序的同学。

背景:为什么需要学习排序

我们原来的搜索很简单:用户输入关键词,我们用BM25计算相关性分数,然后按分数降序返回结果。这在大词量查询时效果还行,但遇到一些场景就暴露问题了:

场景1:用户搜索"最佳相机"

  • BM25只会关注"最佳"和"相机"的词频匹配
  • 但用户真正关心的是哪款相机最好,而不是哪个页面包含"最佳相机"这个词

场景2:搜索历史行为的影响

  • 老用户经常搜索技术文档,应该优先返回权威内容
  • 新用户可能更适合返回入门教程
  • BM25完全不知道用户是谁

场景3:时效性内容

  • 搜索"最新iPhone价格",BM25可能返回2023年的老文章
  • 我们需要知道内容是何时发布的、是否还在更新

这些问题都指向一个核心:我们需要一个能综合考虑多维度因素的排序模型。

需求分析

在动手之前,先梳理清楚我们的需求:

核心需求

  1. 提升搜索准确率,特别是长尾查询
  2. 支持个性化排序(基于用户历史行为)
  3. 考虑内容质量(原创性、时效性、互动数据)
  4. 性能要可控,不能增加太多延迟

非功能需求

  1. 线上推理时间 < 100ms
  2. 支持A/B测试,可对比新旧排序效果
  3. 模型训练周期可控(不超过1天)
  4. 支持特征回溯,便于问题排查

实现方案

整体架构

先看整体流程图:

graph TD A[用户查询] --> B[检索阶段] B --> C[BM25粗排] C --> D[召回Top100] D --> E[特征提取] E --> F[LambdaMART模型] F --> G[精排Top10] G --> H[结果展示] H --> I[用户行为埋点] I --> J[训练数据收集] J --> K[模型重训练] K --> F

检索阶段

我们采用两阶段检索:粗排+精排。

粗排阶段还是用BM25,但做了点优化:

  • 加入了N-gram匹配(2-gram和3-gram)
  • 对长查询做query expansion
  • 过滤明显不相关的内容
def retrieve_bm25(query: str, top_k: int = 100) -> List[Dict]:
    # 基础BM25检索
    base_results = bm25.search(query, k=top_k * 2)

    # Query expansion for long queries
    if len(query.split()) > 3:
        expanded_terms = extract_expanded_terms(query)
        expanded_results = bm25.search(" ".join(expanded_terms), k=top_k // 2)
        base_results = merge_results(base_results, expanded_results)

    # 过滤低质量内容
    filtered = filter_low_quality(base_results)

    return filtered[:top_k]

特征工程

这是最耗时也最重要的部分。我们提取了以下几类特征:

文本特征

  • BM25分数
  • 查询词在标题/正文中的位置
  • 查询词与标题的语义相似度(用sentence-transformers计算)
  • 文本长度、段落数

内容质量特征

  • 原创性评分(通过比对相似内容)
  • 发布时间、更新时间
  • 阅读量、点赞数、评论数
  • 站内点击率历史

用户特征

  • 用户历史点击的相关内容
  • 用户兴趣标签
  • 搜索历史
  • 地理位置匹配度
def extract_features(query: str, doc: Dict, user: Dict) -> np.ndarray:
    features = []

    # 文本特征
    features.append(calculate_bm25(query, doc))
    features.append(query_title_similarity(query, doc['title']))
    features.append(term_position_score(query, doc['content']))

    # 质量特征
    features.append(doc['view_count'] / max(doc['view_count'], 1))
    features.append(time_decay(doc['publish_date']))
    features.append(originality_score(doc))

    # 用户特征
    features.append(user_interest_match(user, doc))
    features.append(click_history_similarity(user, doc))

    return np.array(features)

模型选择

试了几个模型:

模型优点缺点最终选择
Linear Regression训练快,解释性强表达能力弱
Random Forest非线性能力强推理慢,难以部署
XGBoost性能好,速度快特征重要性解释一般备选
LambdaMART专为排序设计训练稍复杂选中

最后选择LambdaMART,原因是:

  • 专门为排序任务设计
  • 直接优化排序指标(NDCG、MRR)
  • XGBoost实现成熟,性能好
  • 社区支持好,资料多

模型训练

训练数据是关键。我们采用了两种方式:

方式1:人工标注数据

让内部员工对搜索结果进行 relevance 打分(0-4分)

  • 优点:数据质量高
  • 缺点:成本高,规模小

方式2:弱监督学习

基于用户行为自动生成训练数据:

  • 点击率高的结果 -> relevance高
  • 停留时间长的 -> relevance高
  • 转化成功的 -> relevance最高
def generate_training_data(click_logs: List[Dict]):
    training_data = []

    for session in group_by_session(click_logs):
        query = session['query']
        results = session['results']

        # 计算每个结果的pseudo relevance
        relevance_scores = []
        for result in results:
            score = 0
            if result['clicked']:
                score += 1
            if result['dwell_time'] > 30:
                score += 0.5
            if result['converted']:
                score += 2

            relevance_scores.append(score)

        # 归一化到0-4
        relevance_scores = normalize_to_0_4(relevance_scores)

        training_data.append({
            'query': query,
            'results': results,
            'relevance': relevance_scores
        })

    return training_data

训练代码:

import xgboost as xgb
from sklearn.metrics import ndcg_score

def train_lambdamart(training_data):
    # 准备训练样本
    X_train = []
    y_train = []
    qids = []

    qid = 0
    for data in training_data:
        features = [extract_features(data['query'], doc) for doc in data['results']]
        relevance = data['relevance']

        X_train.extend(features)
        y_train.extend(relevance)
        qids.extend([qid] * len(features))
        qid += 1

    # 转换为DMatrix
    dtrain = xgb.DMatrix(X_train, label=y_train)
    dtrain.set_group([len(data['results']) for data in training_data])

    # 训练参数
    params = {
        'objective': 'rank:pairwise',
        'metric': 'ndcg',
        'max_depth': 6,
        'eta': 0.1,
        'gamma': 1.0,
        'min_child_weight': 0.1,
        'subsample': 0.8,
        'colsample_bytree': 0.8
    }

    # 训练
    model = xgb.train(params, dtrain, num_boost_round=100)

    return model

踩坑记录

坑1:特征爆炸

刚开始一下子提取了200多个特征,结果:

  • 训练时间爆炸(从1小时变成6小时)
  • 推理延迟超标(从50ms变成200ms)
  • 很多特征噪音很大

解决方案

  • 用SHAP值分析特征重要性
  • 剔除相关性高的冗余特征(保留一个即可)
  • 降维:PCA到50维(但解释性变差)

坑2:训练数据分布偏差

初期只用了点击数据,结果模型偏向"标题党":

  • 标题夸张的点击率高 -> 被认为是好结果
  • 但用户实际停留时间短 -> 体验差

解决方案

  • 引入停留时间、跳出率等用户满意度指标
  • 人工抽样检查训练数据质量
  • 加入对抗样本(标题夸张但内容差的负样本)

坑3:冷启动问题

新内容没有点击数据,模型倾向于排到后面:

  • 导致新内容很难被展示
  • 陷入恶性循环

解决方案

  • 新内容给予"探索性曝光"(展示给部分用户)
  • 使用内容特征预测潜在质量
  • 降权时间衰减因子

坑4:线上特征不一致

训练时的特征和线上推理的特征不一致:

  • 训练时用了"过去7天点击率",但线上是"实时点击率"
  • 导致模型效果下降

解决方案

  • 统一特征计算逻辑
  • 做特征一致性监控
  • 训练时模拟线上环境

结果评估

离线指标

指标BM25LambdaMART提升
NDCG@100.620.71+14.5%
MRR0.480.55+14.6%
Precision@50.560.63+12.5%

离线三项指标的提升幅度相近,说明 LambdaMART 在整体排序质量上是均衡改善而非单点优化:

BM25 与 LambdaMART 在 NDCG@10、MRR、Precision@5 上的离线指标对比

三项离线指标均提升约 12–15%,为后续线上 A/B 测试提供了可信的离线信号。

线上指标

A/B测试结果(7天数据):

指标对照组实验组提升
搜索点击率18.3%20.1%+9.8%
平均停留时长42s48s+14.3%
搜索转化率3.2%3.7%+15.6%
无结果率8.5%7.1%-16.5%

性能指标

  • 端到端延迟:从 80ms 降至 95ms(可接受)
  • QPS:支持 2000 QPS
  • GPU使用率:30%(部署在单卡T4)

遗留问题

虽然效果有提升,但还有一些问题没解决:

1. 长尾查询效果仍不理想

  • 对冷门查询,BM25召回质量就不够好
  • 学习排序只是在现有召回基础上优化
  • 需要优化查询理解和召回策略

2. 多意图查询处理

  • “苹果"是水果还是公司?
  • 当前只按单一意图排序
  • 需要做意图识别和结果分组

3. 实时性不够

  • 热点事件需要几个小时才能体现在排序中
  • 需要实现流式特征更新和热部署

4. 模型漂移

  • 随时间推移,用户行为分布变化
  • 需要定期监控和重训练
  • 目标是自动化模型更新流水线

总结

从BM25到Learning-to-Rank的实践,踩了不少坑,但收获也很多:

关键成功因素

  1. 先把召回做好,精排才有基础
  2. 特征工程比模型选择更重要
  3. 监控数据质量比模型复杂度更关键
  4. A/B测试是验证效果的唯一标准

如果重来一次

  • 先做好数据标注管道,而不是后期补
  • 更早关注特征一致性监控
  • 从第一天就设计好实验方案
  • 投入更多时间做特征工程

下一步计划

  • 引入深度学习模型(如BERT reranker)
  • 优化长尾查询和多意图处理
  • 实现自动化模型训练和部署
  • 探索联邦学习,保护用户隐私

搜索排序是个持续优化的过程,没有一劳永逸的方案。重要的是建立一套可靠的实验和迭代机制,让数据驱动决策。

希望能给同样在做搜索排序的同学一些参考,少走弯路。如果你也在做类似的工作,欢迎交流讨论!

版权声明: 本文首发于 指尖魔法屋-从查询走到结果:AI搜索排序笔记https://blog.thinkmoon.cn/post/328-ai-search-ranking-query-result-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!