Files
zhuangang/topics/backend/elasticsearch/01-search-basics-hybrid.md
T

4.9 KiB
Raw Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
id title type domain stack tags level prerequisites est_hours related week order
backend.elasticsearch.search-hybrid Elasticsearch:倒排索引、映射与中文分词、BM25、kNN 向量检索、混合检索和别名重建 lesson backend
elasticsearch
Elasticsearch
倒排索引
BM25
IK 分词
cjk
dense_vector
kNN
RRF
混合检索
别名
reindex
intermediate
ai.rag.hybrid-rerank
3
backend.elasticsearch.scenario.kb-search-sync
ai.rag.index
backend.elasticsearch.interview
6
14
1

Elasticsearch:倒排索引、映射与中文分词、BM25、kNN 向量检索、混合检索和别名重建

BOSS直聘 2026-10-23 的调研中,多个 RAG 岗位提到“ES 混合检索”。示例基于 Elasticsearch 9.5.1 + Python 客户端 9.5.1 实际运行通过。功能可用性与许可证有关(例如本地 basic 许可下服务端 RRF 返回 403),以官方文档为准:https://www.elastic.co/docs 。

解释说明

倒排索引和 BM25

  • 倒排索引:词 → 包含这个词的文档列表。查询时先分词,再查倒排表,速度与文档总数关系不大。
  • BM25:ES 默认的相关性算法,考虑词频(有饱和上限)、逆文档频率(越稀有的词越重要)和文档长度。
  • 擅长:精确词、专有名词、错误码、型号;不擅长:同义表达(“不想要了” vs “退货”)。

映射和分词

字段类型 用途
text 全文检索,写入时分词
keyword 精确匹配、过滤、聚合、排序(租户、状态、标签)
dense_vector 向量,支持 kNN(HNSW)

中文分词:内置 cjk 分析器按二元切分,可用但粗糙;生产常用 IK 分词插件(ik_max_word 写入、ik_smart 查询)并维护业务词典和同义词,插件版本必须与 ES 版本匹配(以插件文档为准)。映射一旦创建,已有字段的类型和分词器不能修改,只能新建索引并重建数据。

查询

  • bool 查询:must(参与算分)、filter(不算分、可缓存,用于租户 / 权限 / 状态)、should、must_not。
  • multi_match 多字段检索,title^2 提高标题权重。
  • knn:向量近似检索,num_candidates 越大越准越慢;filter 在检索过程中生效。

混合检索

关键词和向量各检索一次,用 RRF(倒数排名融合) 合并:只看名次,不需要把 BM25 分数和余弦相似度放在同一尺度上。ES 提供服务端的 retriever.rrf(许可证要求以官方文档为准),也可以在应用层融合(示例中的做法,任何许可都能用)。融合后通常再接重排模型(rerank)。

别名和零停机重建

业务只访问别名;修改映射时新建 v2 索引 → 全量导入 + 追增量 → 校验 → 原子切换别名 → 保留旧索引一段时间以便回滚。

案例

知识库问答:用户问“错误码 E1024 是什么”时关键词检索命中准确;问“商品不想要了怎么退”时依赖向量检索;两者 RRF 融合后再重排,取前几条给大模型。租户条件放在 filter 中,在两路检索中都生效。

代码示例

examples/hybrid-search/es_hybrid.py(需要 ES_URL),实际输出:

1. 关键词检索擅长精确词(错误码):E1024 → ['d6']
2. 租户过滤生效:t1 搜“退货” → ['d1'](没有 t2 的 d5)
3. 向量检索(玩具嵌入):['d6', 'd2', 'd1']
4. RRF 混合检索:['d1', 'd2', 'd6']
5. 服务端 RRF 不可用(AuthorizationException: ...current license is non-compli...),使用客户端 RRF
6. 别名原子切换到 v2,查询不中断

(第 3 步用的是玩具嵌入,向量结果没有语义意义,只演示流程;真实项目换成嵌入模型。)

def rrf(*rankings: list[str], k: int = 60) -> list[str]:
    score: dict[str, float] = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            score[doc_id] = score.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(score, key=lambda d: (-score[d], d))

fused = rrf(bm25(q, tenant, 5), knn(q, tenant, 5))

易错点

  • 租户 / 权限条件写在 must 中(参与算分)或者在检索后才过滤(可能导致结果不足)。
  • 把需要精确匹配的字段设成 text。
  • 直接把 BM25 分数和向量相似度加权相加,尺度不同,权重很难调。
  • 修改映射后直接改原索引(做不到),或者没有别名导致切换时停机。
  • 用 ES 作为唯一的数据存储:ES 是检索引擎,数据源应在数据库中,ES 可以重建。
  • 本地关闭安全认证的配置被带到生产。

面试常问

  • 倒排索引是什么?BM25 和 TF-IDF 的区别?
  • text 和 keyword 的区别?
  • 混合检索怎么做?为什么用 RRF?
  • 怎么在不停机的情况下修改映射?