97 lines
4.9 KiB
Markdown
97 lines
4.9 KiB
Markdown
---
|
||
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?
|
||
- 怎么在不停机的情况下修改映射?
|