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

97 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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?
- 怎么在不停机的情况下修改映射?