LoHoSearch在离线评测中的实施流程详解

文章导读
在离线评测中,LoHoSearch 的实施流程可以拆成四个环节:固定评测用例、锁定索引快照、批量查询采集、指标与人工复审。这部分不依赖线上流量,适合在版本迭代后快速判断相关性是否回退,也适合对比不同配置参数。下面只覆盖可复现的通用步骤,具体路径和字段需要结合你本地的 API 结构确认。
📋 目录
  1. A 一、先明确评测目标和用例集
  2. B 二、部署静态环境和数据快照
  3. C 三、批量查询采集与结果保存
  4. D 四、指标计算与人工复审
  5. E 五、常见误区与边界
A A

在离线评测中,LoHoSearch 的实施流程可以拆成四个环节:固定评测用例、锁定索引快照、批量查询采集、指标与人工复审。这部分不依赖线上流量,适合在版本迭代后快速判断相关性是否回退,也适合对比不同配置参数。下面只覆盖可复现的通用步骤,具体路径和字段需要结合你本地的 API 结构确认。

离线评测的可行性取决于是否有稳定用例集和统一打分标准。建议把评测脚本、用例集、快照版本一起归档;用相同查询比较配置差异,而不是追求单个指标的绝对值。离线结果不能覆盖缓存、点击反馈等线上因素,最终仍需要小规模线上验证。

一、先明确评测目标和用例集

评测目标决定了用例集怎么收。如果是版本回归,可以用上一版线上搜索日志里的高频查询;如果是长尾词优化,就要加入同义词、口语化表达、错别字改写等难例。用例集至少要包含三列:查询词、预期结果 ID 列表、预期结果是否唯一。预期结果应由业务方或资深运营确认,不要只用当前线上返回结果当作标准。

{
  "query": "退款多久到账",
  "expected_ids": ["doc_1024", "doc_1025"],
  "answer_type": "help_center",
  "note": "用户高频问题"
}

建议把用例集按数据类别分开:品牌词、功能词、问题词、长尾词。每个类别的结果单独统计,避免某一类特别多时把整体指标拉平。同时要标记用例的收集时间,后续评估旧版本时能知道这份用例集是否适合当时的知识库。

二、部署静态环境和数据快照

离线测评要有刻意隔离的环境。先给当前搜索服务打一个稳定镜像,把索引、词典、排序配置全部固化下来。如果 LoHoSearch 使用外部数据库或向量文件,也要同步固定快照。不要在生产环境直接跑,因为在线更新和用户流量会干扰结果。

LoHoSearch在离线评测中的实施流程详解

有两类设置需要特别留意:一是把个性化开关关掉,比如基于用户历史的加权逻辑;二是把相关性排序公式里的实时反馈按钮关掉。这样每次查询的输出只受查询词和静态数据影响。部署完成后,用一个冒烟测试查询确认服务能返回结果,再开始跑批量评测。

三、批量查询采集与结果保存

写一个批量脚本循环发送查询,把原始结果保存下来。不要直接打印在终端里,要存成 JSONL 或 CSV,每条记录包含查询词、预期 ID、实际返回 ID、排序分值、耗时(可选)。这样后续可以用不同算法重新计算指标,不需要再次请求服务。

import requests, json, time

def load_cases(path):
    with open(path, 'r', encoding='utf-8') as f:
        for line in f:
            yield json.loads(line)

cases = list(load_cases('cases.jsonl'))
output = []
for case in cases:
    payload = {"query": case["query"], "top_k": 10}
    resp = requests.post("http://127.0.0.1:8080/api/search", json=payload)
    data = resp.json()
    hits = [h["id"] for h in data.get("hits", [])]
    output.append({
        "query": case["query"],
        "expected": case["expected_ids"],
        "hits": hits,
        "scores": [h.get("score") for h in data.get("hits", [])]
    })
    time.sleep(0.03)  # 根据服务能力调整,别把评测服务打挂

with open('result.jsonl', 'w', encoding='utf-8') as f:
    for item in output:
        f.write(json.dumps(item, ensure_ascii=False) + '\n')

脚本里的 API 路径和字段名要替换成你本地 LoHoSearch 的真实接口。运行前先用单条查询验证返回结构,再放开全量。如果查询量很大,可以分批跑,每批之间留几秒间隔,同时观察服务日志有没有报错。

四、指标计算与人工复审

常用指标包括 Recall@k、MRR、nDCG。Recall@k 看目标结果是否进前 k,MRR 看第一个目标结果排在第几位,nDCG 更关注排序是否符合预期。计算时直接用上一步保存的 result.jsonl,不要再请求服务。

LoHoSearch在离线评测中的实施流程详解
import json

def recall_at_k(item, k):
    hit = set(item["hits"][:k]) & set(item["expected"])
    return len(hit) / len(set(item["expected"])) if item["expected"] else 0

with open('result.jsonl') as f:
    items = [json.loads(line) for line in f]

for k in [1, 3, 5]:
    group_recall = [recall_at_k(it, k) for it in items]
    print(f"Recall@{k}: {sum(group_recall) / len(group_recall):.3f}")

指标是粗筛选,人工复审才是关键。把每个查询的实际排序和预期结果拉到一个表格里,按“完全命中、部分命中、未命中”分类。优先看未命中的案例,判断是索引缺词、分词语义不一致,还是排序公式对某些字段权重不对。对于部分命中的,看预期结果是否排在可接受范围内。

五、常见误区与边界

第一,离线评测结果不反映真实用户体验,因为线上有缓存、热门点击反馈、个性化信号、网络延迟等。离线指标上升不代表线上效果一定好,只能作为版本是否可上线的参考信号。第二,不要只用单一指标。例如 Recall@5 提升但 MRR 下降,说明预期结果进入了前五但整体排名更靠后,这时需要结合排序优化单独看。第三,用例集不是越大越好,几百条真实用例如果覆盖均匀,往往比几千条重复用例更能发现回归。

建议把评测环境(镜像版本、索引日期、配置文件名)记录在 result.jsonl 的元数据字段里。后续每当 LoHoSearch 升级或调整参数,都在同一套用例集上重跑,再与历史结果比较。这样离线评测就能形成可追溯的回归基线,而不是一次性的试验。