RAG设计之混合检索的ES字段详解
文章目录
先回忆一下上一篇讲分词架构,留了一个小尾巴,ES 字段咋设计,因为这个设计原则确实影响了我们如何设计分词,所以本文就记录一下我们RAG 项目里 ES 字段的设计思路,抛砖引玉一下😯
在做 RAG 的混合检索时,首先要明白一点:每个 chunk 入库时,不只把原文存进去,还需要额外存一批专门给检索用的字段。
通常ES 里一条 document 基本对应一个 chunk。
主链路是:
|
|
文件上传后的解析在file_parse.py 统一根据文件类型进行分发parser
1. 写入哪些核心字段
当前 process_items() 会构造这些字段:
|
|
注意:q_1024_vec 不是固定名字,它取决于 embedding 维度:
|
|
如果 embedding 是 1024 维,就是:
|
|
如果是 1536 维,就是:
|
|
2. 为啥是这些字段
content_with_weight
这是 chunk 原文。
它主要给大模型生成答案用,也给前端展示引用用。这个字段在 mapping 里属于 *_with_weight,设置了:
|
|
所以它不参与 ES 检索,只是存储和返回!!!
这是为啥🤏?
因为原文通常很长,直接让 ES 对原文做中文 analyzer 不稳定;我们自己设计了分词器进行分词,把可检索 token 放到 粗粒度content_ltks/细粒度content_sm_ltks,这俩的详细解释看之前分词器架构那篇就👌了。
-
粗粒度做正文全文检索,查询字段是:
content_ltks^2:正文粗粒度命中有一定权重,x2,但低于标题、关键词字段。 -
细粒度查询字段里是:
content_sm_ltks: 没有显式 boost,默认权重 1。 为什么权重低?
因为细粒度字段是兜底召回。它能解决用户问得不完整的问题,比如用户只问“据点奖励”,但如果给太高权重,会把很多只包含“据点”“奖励”的泛内容召回来。 -
文件名/标题分词字段
title_tks^10
|
|
为什么标题权重大?
因为标题命中通常比正文偶然命中更可靠。比如 chunk 来自《血盟据点战玩法说明》,用户问“据点战奖励”,标题命中说明这个文档主题就是它。
important_kwd / important_tks这两个是重要关键词字段。
查询侧权重很高:
|
|
如果离线阶段抽取了 chunk 的核心关键词,比如:
|
|
那么用户 query 命中这些字段时,应该比正文普通命中更强。
question_kwd / question_tks
这是 FAQ/问答型 chunk 预留字段。
查询侧权重:
|
|
如果某些资料本身是问答形式,比如:
|
|
可以把问题部分单独写入 question_tks,用户 query 命中问题字段时权重更高。
q_1024_vec
这是向量字段,用于 dense vector 相似度检索。
|
|
也就是说,embedding 是基于 chunk 原文 content_with_weight 生成的。
为什么需要向量字段?
因为用户 query 和文档表达不一定同词。
比如用户问:
|
|
文档可能写:
|
|
全文检索不一定强命中,但向量语义能召回。
doc_id / docnm_kwd / kb_id
这些是过滤、聚合、引用字段。
|
|
搜索结果返回后,retrieve_content() 会拿:
|
|
组装引用信息。
create_time / create_timestamp_flt
这是时间字段。
用于排序、调试、后续按时间过滤。*_flt 会被 ES mapping 识别成 float。
3. ES mapping 怎么识别这些字段
mapping 在 mapping.json,大量依赖字段后缀自动映射:
|
|
但是为什么 *_tks 和 *_ltks 用 whitespace analyzer?
因为系统已经用 RagTokenizer 提前分好词了:
|
|
ES 不需要再做中文分词!!!只需要按空格切 token。
这样做的好处是:
|
|
4. 怎么写入 ES
最终写入在 es_conn.py:
|
|
通过批量 bulk 写入,这里有个细节:
|
|
id 不作为普通字段写入 _source,而是作为 ES 的 _id。
所以 ES 里的结构是:
|
|
chunk_id 的生成方式是:
|
|
这样同一个 chunk 在同一个 index 里有稳定 ID。
5. 查询时用哪些字段,怎么用
查询字段在 query.py:
|
|
这些字段被包装成:
|
|
然后在 [es_conn.py]里变成 ES query_string:
|
|
所以用户问:
|
|
会先被分词、同义词扩展、细粒度扩展,然后去这些字段里查:
|
|
命中标题分高,命中重要词更高,命中正文粗粒度次之,命中细粒度最低。
同时,向量检索会用:
|
|
在 [search_v2.py]service/core/rag/nlp/search_v2.py:46) 里,query 也会生成 embedding,然后自动拼出同维度字段名:
|
|
然后 ES 做 KNN:
什么时候选暴力 KNN,什么时候 HNSW? 向量数量 <5 万:直接暴力 KNN(Flat),简单、无精度损失 10 万~千万级向量:优先 HNSW(RAG 主流标配) 亿级超大向量:IVF-PQ / DiskANN(磁盘型图索引)
|
|
也就是说:
|
|
当前融合权重是:
|
|
可以理解为:
|
|
ES相当于是一个大的仓库,分门别类的存储了很多类型的东西:
- 原文类字段:content_with_weight,用于回答和引用
- 检索类字段:content_ltks/content_sm_ltks/title_tks/important_tks/question_tks,用于 BM25/query_string
- 向量类字段:q_1024_vec,用于语义相似度 KNN
- 元数据字段:doc_id/docnm_kwd/kb_id/create_timestamp_flt,用于过滤、聚合、排序、引用
所以把一个 chunk 写入 ES 时,会同时写原文、粗粒度 token、细粒度 token、标题 token、元数据和 embedding 向量。 原文不直接参与检索,只用于回答和引用; token 字段用 whitespace analyzer,保证离线文档分词和在线 query 分词一致; 向量字段用于语义召回; 查询时先构造 query_string 查多字段并带不同 boost,再用 dense vector 做 KNN,最后融合和 rerank。 这样既能保证游戏领域术语的精确命中,也能覆盖用户表达不一致时的语义召回。
文章作者 沐桢