--- id: backend.elasticsearch.search-hybrid title: Elasticsearch:倒排索引、映射与中文分词、BM25、kNN 向量检索、混合检索和别名重建 type: lesson domain: backend stack: [elasticsearch] tags: [Elasticsearch, 倒排索引, BM25, IK 分词, cjk, dense_vector, kNN, RRF, 混合检索, 别名, reindex] level: intermediate prerequisites: [ai.rag.hybrid-rerank] est_hours: 3 related: [backend.elasticsearch.scenario.kb-search-sync, ai.rag.index, backend.elasticsearch.interview] week: [6, 14] order: 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](examples/hybrid-search/es_hybrid.py)(需要 `ES_URL`),实际输出: ```text 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 步用的是玩具嵌入,向量结果没有语义意义,只演示流程;真实项目换成嵌入模型。) ```python 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? - 怎么在不停机的情况下修改映射?