跳到主要内容

混合检索与融合重排 — Dealer 核心

30 秒导读: 这是 RAGFlow「读取线」的高潮。用户问一句话,系统要从千万级 chunk 里挑出最相关的几段喂给大模型。RAGFlow 的做法是双路召回 + 融合重排:一路把问句拆成带权重的关键词布尔查询(BM25 全文检索),一路把问句编码成向量做 cosine 近邻(稠密检索),两路的候选合并后,再按用户设定的权重把「词面相似度」和「向量相似度」加权融合,排序、切页、过阈值,最后把答案切片匹配回原始 chunk 生成可点击的引用编号。全部逻辑收在一个叫 Dealer 的类里。

本章聚焦「文本 + 向量的通用混合检索」。结构化召回(RAPTOR 树、GraphRAG 图谱、查询分解)是另一套东西,留给 04-advanced-retrieval-raptor-graphrag.md。写入线怎么把 chunk 分词、算向量、建索引,见 02-ingestion-write-path.md


1. 这是什么(零基础也能懂)

  • 一句话定义: 把用户的自然语言提问,变成「既看关键词、又看语义」的检索请求,召回一批候选 chunk,再重新打分排序,交给 LLM 生成答案。

  • 解决什么问题: 纯关键词检索(BM25)召不回同义改写(问「怎么退货」,文档写「退款流程」);纯向量检索(语义)又常常在专有名词、数字、代号上翻车(把「iPhone 15」和「iPhone 14」当成几乎一样)。两者各有盲区,合起来才稳。 这就是「混合检索(hybrid retrieval)」。

  • 给谁用: 上层的对话服务(api/db/services/dialog_service.py)、Chunk 检索 API、Agent 的 Retrieval 工具,统一调 Dealer.retrieval(...) 这一个入口。

  • 用起来什么样: 一次真实调用长这样(来自 rag/benchmark.py:58):

# settings.retriever 就是 search.Dealer 的单例
ranks = await settings.retriever.retrieval(
query, # 用户问句
embd_mdl, # 嵌入模型(把文本变向量)
tenant_id, [kb_id], # 租户 + 知识库
page=1, page_size=30, # 要第几页、每页几条
similarity_threshold=0.2, # 低于这个分就丢
vector_similarity_weight=0.3, # 向量占 30%,词面占 70%
)
# ranks["chunks"] 是排好序的候选:每条带 similarity / vector_similarity / term_similarity
  • 一句话直觉: 把它想成一个二次面试流程。第一轮(召回)靠数据库快速海选,尽量别漏;第二轮(重排)在应用侧用更讲究的公式重新打分,决定最终名次。数据库快而糙,应用侧慢而精。

本节不碰代码细节。记住一件事:Dealer 就是这个「双路海选 + 精细重排」的执行者


2. 顶层全景(它大概怎么转)

一次 retrieval() 从提问到结果,数据流是这样(从左到右,命中即往下):

用户问句 "怎么给知识库配置重排模型?"


┌───────────────────────────────────────────────┐
│ ① 查询构造 FulltextQueryer.question │ rag/nlp/query.py
│ 中英分流 → 去停用词 → 切词/term_weight 加权 │
│ → 同义词扩展 → 拼成带 ^权重 的布尔串 │
│ 产出: MatchTextExpr(BM25) + keywords 列表 │
└───────────────────────────────────────────────┘
│ │
│ ② 稠密召回 │ ① 文本召回
▼ get_vector ▼
MatchDenseExpr (同一个 MatchTextExpr)
(q_<dim>_vec, cosine) │
└───────────┬─────────────┘

┌───────────────────────────────────────────────┐
│ ③ 双路融合入口 Dealer.search │ rag/nlp/search.py
│ [matchText, matchDense, FusionExpr] 一次下发 │
│ 数据库(ES/Infinity/OceanBase)海选出候选窗口 │
│ → SearchResult(ids, field, query_vector...) │
└───────────────────────────────────────────────┘

┌───────────────────────────────────────────────┐
│ ④ 重排(三选一,按后端 + 是否外置模型) │ rag/nlp/search.py
│ a. 外置模型 rerank_by_model │
│ b. ES 路径 _knn_scores + rerank_with_knn │
│ c. Infinity 直接用 _score / OceanBase 走 rerank│
│ 都叠加 rank_feature(PAGERANK 加权) │
└───────────────────────────────────────────────┘

┌───────────────────────────────────────────────┐
│ ⑤ 排序整形 稳定排序→阈值过滤→切页→doc_aggs聚合 │ rag/nlp/search.py retrieval()
└───────────────────────────────────────────────┘

ranks["chunks"] → 喂给 LLM 生成答案

┌───────────────────────────────────────────────┐
│ ⑥ 可溯源引用 insert_citations │ rag/nlp/search.py
│ 把答案切句,按 hybrid_similarity 匹配回 chunk │
│ 在句尾插 [ID:3] 这样的引用标记 │
└───────────────────────────────────────────────┘

各部件一句话职责:

部件干什么在哪
FulltextQueryer.question问句 → 带权重的 BM25 布尔查询 + 关键词rag/nlp/query.py:42
term_weight.Dealer给每个词算 IDF×词性×命名实体权重rag/nlp/term_weight.py:164
synonym.Dealer.lookup同义词扩展(自定义词典 + WordNet)rag/nlp/synonym.py:80
Dealer.get_vector问句 → 稠密向量近邻表达式rag/nlp/search.py:53
Dealer.search下发双路召回,组装 SearchResultrag/nlp/search.py:132
Dealer.retrieval顶层编排:分页、重排、阈值、聚合rag/nlp/search.py:573
rerank_* / _knn_scores三条重排路径rag/nlp/search.py:443-540
insert_citations答案切片回匹 chunk,插引用rag/nlp/search.py:242

主线走一遍(不进代码):问句进来 → 拆成关键词布尔查询 + 一条向量 → 数据库双路海选出一个候选窗口 → 应用侧按用户权重重新打分 → 排序切页过阈值 → 返回带三种相似度的 chunk 列表。下面逐段拆开。


3. 核心机制(逐个拆解)

3.1 查询构造 — 把一句话拆成带权重的布尔查询

要解决的小问题: BM25 检索不认识「整句话」,它认「词 + 每个词多重要」。所以第一步是把 "怎么给知识库配置重排模型?" 这种口语,变成数据库能吃的 (重排^2.1 (rerank)^0.5) OR (模型^1.3 ...) OR ("重排 模型"~2)^1.5 这种带权重、带同义词、带短语的布尔串

入口: FulltextQueryer.question(txt, min_match=0.6),rag/nlp/query.py:42。注意 search() 实际传的是 min_match=0.3(search.py:173),比默认宽松。

处理流水线(顺序执行):

原始问句
│ add_space_between_eng_zh 中英之间补空格 "iPhone15手机"→"iPhone15 手机"
│ strQ2B + tradi2simp 全角转半角、繁体转简体
│ 正则清洗 干掉 Infinity 词法器会误判的转义字符 :^"'~*?( 等
│ rmWWW 去疑问词/停用词 "怎么/如何/what/is/the..."

is_chinese? ──否──▶ 英文路径(query.py:59-95)
│是

中文路径(query.py:104-181)

关键设计一:每个词都带权重 ^weight 权重来自 term_weight.Dealer.weights(term_weight.py:164),公式是 (0.3·IDF_freq + 0.7·IDF_df) × ner(t) × postag(t)——即「逆文档频率 × 命名实体类型 × 词性」。人名/地名/机构名(ns/ntcorp/loca)权重放大到 3,虚词(代词/连词 r/c/d)压到 0.3。所以专有名词天然比「的、了」重要。

关键设计二:同义词扩展,但权重打折。 每个词查 syn.lookup(query.py:71137),命中的同义词以原权重的 0.2~0.25 倍挂上去(英文路径 w/4.,query.py:73;中文路径 (...)^0.2,query.py:151)。既扩了召回,又不让同义词盖过原词。

关键设计三:相邻词组成短语,权重翻倍。 相邻两个词会拼成 "重排 模型"~2 这样的邻近短语(~2 允许间隔 2 个词),权重取两词较大值 ×2(英文 query.py:78-89)或 ×1.5(中文整句 query.py:160)。命中连续短语的 chunk 应该排更前,这是词序信息。

教学示例(演示中文路径的产物,# 示意,非源码):

# 输入: "重排模型怎么配置" → rmWWW 去掉"怎么" → 剩 "重排模型 配置"
# 每个切词块 tt 产出一个子查询,最后用 OR 连起来:
q = [
"((重排^2.1 OR (rerank)^0.2) OR \"重排\"~2)^5 OR (\"重新排序\")^0.7", # 词 + 细粒度 + 同义词
"(配置^1.3 ...)^5 OR (\"设置\")^0.7",
]
query = " OR ".join(f"({t})" for t in q) # 各子查询 OR 连接
# 打包成 MatchTextExpr,附带 minimum_should_match 门槛

真实产物: 函数返回 MatchTextExpr(query_fields, query, 100, {"minimum_should_match": min_match, ...})keywords 列表(query.py:178-180)。其中 query_fields 是带字段权重的检索目标(query.py:32-40):

字段权重含义
important_kwd^3030人工/抽取的关键词,最高
question_tks^2020chunk 关联的问题
title_tks^1010标题
content_ltks^22正文分词
content_sm_ltks1正文细粒度分词

minimum_should_match(最少命中比例) 是召回宽严的总闸门:0.3 意味着布尔串里的 should 子句至少要命中 30%。ES 侧把浮点翻译成百分比字符串 "30%"(rag/utils/es_conn.py:204-206)。

英文 vs 中文两条路的差异:

维度英文路径(query.py:59-95)中文路径(query.py:104-181)
判定is_chinese 为假(非中文词占比低)为真
切词rag_tokenizer.tokenize 直接切tw.split 先按 NER 合并再切
细粒度fine_grained_tokenize 拆子词(query.py:117-119)
短语相邻词 bigram "a b"^w(:78)整句 "..."~2^1.5(:160)
minimum_should_match不设(靠字段权重)min_match(:179)

坑:两条路都对候选词做了 [:256] 截断(query.py:68106),关键词列表另有 < 32 的上限(query.py:112133 等)。超长问句的尾部词会被丢弃,这是刻意的性能护栏。

3.2 稠密召回 — 问句变一条向量

要解决的小问题: 语义匹配。BM25 那套只认字面;这一路把整句问话编码成一个高维向量,去和每个 chunk 的向量算 cosine,找语义最近的。

真实实现: Dealer.get_vector,rag/nlp/search.py:53-61:

qv, _ = await thread_pool_exec(emb_mdl.encode_queries, txt) # 问句 → 向量
embedding_data = [get_float(v) for v in qv]
vector_column_name = f"q_{len(embedding_data)}_vec" # 如 q_1024_vec
return MatchDenseExpr(vector_column_name, embedding_data,
'float', 'cosine', topk, {"similarity": similarity})

三个要点:

  1. 列名带维度: q_1024_vec——不同嵌入模型维度不同,列名把维度写进去,一个索引里能共存多种维度。写入线用同样规则建列(见 02 章)。
  2. 距离度量 cosine: 余弦相似度,范围规约到 [0,1]
  3. similarity 是召回门槛: 低于它的直接不返回。默认 0.1(search.py:181),比最终 similarity_threshold(默认 0.2)宽松——海选阶段先放进来,精排再筛。

3.3 双路融合入口 — Dealer.search 与候选窗口分页

要解决的小问题: 把 3.1 的文本表达式和 3.2 的向量表达式一次性下发给数据库,让数据库返回一个融合后的候选窗口。同时解决一个隐蔽的工程问题:深翻页怎么不丢结果

双路一次下发: search.py:192-193:

fusionExpr = FusionExpr("weighted_sum", topk, {"weights": "0.05,0.95"})
matchExprs = [matchText, matchDense, fusionExpr] # 文本 + 向量 + 融合规则
res = await thread_pool_exec(self.dataStore.search, ...)

这里的 weights="0.05,0.95"第一轮海选时数据库内部融合文本分和向量分的权重(向量占 95%),不是最终展示的相似度。最终相似度由第 3.4 节的重排用用户的 vector_similarity_weight 重新算。别混淆这两个权重。

空结果降级重试: 若第一轮 total==0,把 min_match 从 0.3 放宽到 0.1、similarity 从 0.1 放宽到 0.17 再来一次(search.py:200-212)。宁可放宽也别返回空。

候选窗口 + 分块分页(最烧脑的一段): 用户要「第 3 页,每页 10 条」,但重排必须在一个足够大的候选池里做才准。RAGFlow 的解法是引入一个 RERANK_LIMIT 窗口(约 64,_rerank_window,search.py:548-571):

RERANK_LIMIT (窗口, ~64, 必须是 page_size 整数倍)
├──────────────────────────────────────┤
全局偏移 global_offset = (page-1) * page_size

├─ 取哪个块: req["page"] = global_offset // RERANK_LIMIT + 1
└─ 块内起点: begin = global_offset % RERANK_LIMIT
page_idx = valid_idx[begin : begin + page_size]

为什么窗口必须是 page_size 的整数倍: 因为「取第几块」用整除、「块内切页」用取余,两者共用同一个 RERANK_LIMIT。若不是整数倍,块边界和页边界会错位,深翻页会静默丢结果、返回短页(_rerank_window 的 docstring search.py:558-565 明确警告)。所以窗口用 math.ceil(64/page_size)*page_size 向上取整到整页(search.py:568)。当启用外置重排模型时,窗口还会被 top 上限夹住(search.py:570)。

三个关键旋钮:

参数默认作用位置
similarity_threshold0.2最终分数低于它就丢retrieval() 形参 search.py:581
vector_similarity_weight0.3向量分占比,词面占 1-它search.py:582 / 629
top / topk1024稠密召回的候选上限search.py:584 / 145

SearchResult 是这一步的产物(search.py:42-51),关键字段:ids(候选 chunk id)、field(每个 chunk 的字段字典,含 _score)、query_vector(问句向量,后续 KNN 二次打分要用)、keywords

3.4 三条重排路径 — 融合的核心分野

要解决的小问题: 数据库海选给出的分数不够可信(各后端算法不一、量纲不一)。应用侧要用统一公式重新打分:最终分 = 词面权重·term相似度 + 向量权重·向量相似度 + rank_feature

但「向量相似度从哪来」因后端而异,于是分出三条路(retrieval() 里的分支,search.py:641-680):

是否配置了外置 rerank 模型?
│是 │否
▼ ▼
a. rerank_by_model 按 DOC_ENGINE 分:
模型直接给 query-doc 分 ├─ Infinity → 直接用 _score(库内已归一化融合)
├─ OceanBase → rerank(结果里带向量,本地算)
└─ ES/其它 → _knn_scores + rerank_with_knn

三条路对照:

路径向量相似度怎么来词面相似度怎么来代码
a. 外置模型rerank_mdl.similarity(query, docs),已归一化到 [0,1]token_similarityrerank_by_model search.py:513
b. ES 路径二次 KNN-only 调用取纯 cosine(_knn_scores)token_similarityrerank_with_knn search.py:443
c. Infinity不重排,直接用库返回的 _score同上(tsim=vsim=sim)search.py:651-654
c'. OceanBase结果里带 chunk 向量,本地 hybrid_similarity 算 cosinetoken_similarityrerank search.py:474

ES 路径为什么要「二次 KNN」(最巧的一处): 第一轮 search 里,ES 把文本分和向量分混在一起给了个 _score,拿不到纯净的 cosine。而且新版为了性能不再把 chunk 向量传回应用(search.py:184-188 注释)。解法:发第二次检索,只做 KNN、只对第一轮的候选 id 打分,让 ES 在库内算好 cosine 再返回纯分数(_knn_scores,search.py:367-400;similarity 设 0.0 保证每个候选都有分)。分数由 get_scores_id → _score 提取(common/doc_store/es_conn_base.py:305)。向量始终不出库,只在需要引用时才按需回取(fetch_chunk_vectors,search.py:402)。

融合公式(ES 路径,rerank_with_knn search.py:467-471):

tksim = token_similarity(keywords, ins_tw) # 词面相似度
vtsim = [knn_scores.get(cid, 0.0) for cid in ids] # 纯 cosine
rank_fea = self._rank_feature_scores(rank_feature, sres) # PageRank + 标签特征
sim = tkweight * tksim + vtweight * vtsim + rank_fea

词面相似度 token_similarity 怎么算(query.py:193-208): 把词序列变成加权字典——每个词记 0.4·权重,相邻词对记 0.6·max(两词权重)(捕捉词组)。然后 similarity 用一个非对称重叠度:查询词里有多少(按权重)在文档里出现,除以查询总权重(query.py:210-222)。注意它只看查询覆盖率,不惩罚文档长度——刻意偏向召回。

重排时给字段加权重(rerank_with_knn search.py:460-465): 拼 token 时 title×2 + important_kwd×5 + question_tks×6,标题和关键词被复制多份,等于在词面相似度里放大它们的贡献。

rank_feature — PageRank 加权(_rank_feature_scores search.py:334-365): 这是叠加在融合分之上的第三项。

  • PageRank(PAGERANK_FLD): 每个 chunk 有个「重要度」分,直接加到最终分上(search.py:339-343365)。retrieval 默认传 {PAGERANK_FLD: 10}(search.py:588)。
  • 标签特征(TAG_FLD): 查询的标签向量和 chunk 的标签向量做归一化点积,×10 再加(search.py:357-365)。标签怎么来见 tag_query(search.py:849)。
  • ES 侧对 PageRank 之外的标签字段用 rank_feature 查询直接在库内加权(es_conn.py:226-230)。

Infinity 为什么不重排: Infinity 的 FusionExprweighted_sum + normalize="atan"库内就把两路分数归一化融合好了(rag/utils/infinity_conn.py:220-222),_score 已经可用,应用侧再算一遍是浪费。

3.5 排序整形 — 稳定排序、阈值、切页、聚合

拿到融合分 sim 后,retrieval() 收尾(search.py:682-771):

  1. 稳定排序(search.py:688): np.argsort(sim * -1, kind='stable')——分数相同的候选保持原顺序,结果可复现。
  2. 阈值过滤(search.py:691-693): 保留 sim >= post_threshold 的。特例:当 vector_similarity_weight <= 0(纯词面),阈值对词面分无意义,直接置 0 不过滤。
  3. 切页(search.py:701-703): 用 3.3 的 begin = global_offset % RERANK_LIMIT,从过阈值后的有序列表里切出这一页 page_size 条。
  4. 结果整形(search.py:721-745): 每条 chunk 拼成字典,三种相似度都带上:similarity(融合分)、vector_similarity(vsim)、term_similarity(tsim)——分开返回,前端能显示各占多少
  5. 文档聚合 doc_aggs(search.py:747-767): 统计每个文档命中了几个 chunk,按命中数降序——「这次检索主要来自哪几篇文档」。

删除安全网:重排前会先 _prune_deleted_chunks 剔掉「DB 里文档已删、但向量库残留」的僵尸 chunk(search.py:76-118624)。注释明说这是兜底,不是主删除路径。

3.6 可溯源引用 — insert_citations

要解决的小问题: LLM 生成答案后,怎么标出「这句话是根据哪个 chunk 说的」,给用户可点击的 [ID:3] 引用?

思路: 把答案切成句子,每句再和候选 chunk 做一次 hybrid 相似度匹配,够像就在句尾插引用编号。注意:引用匹配是「答案 vs chunk」,不是「问题 vs chunk」——匹配的是模型实际写出来的内容。

真实实现: insert_citations,search.py:242-332。流程:

答案文本
│ 1. 按代码块 ``` 和句末标点切片(含中英/阿拉伯语标点, search.py:262-270)
│ 2. 丢掉长度 < 5 的碎片

每个答案切片 piece
│ embd_mdl.encode(pieces) → 切片向量 ans_v
│ hybrid_similarity(ans_v[i], chunk_v, 切片词, chunk词, tkweight=0.1, vtweight=0.9)
│ ↑ 注意向量权重高达 0.9,引用匹配更信语义

阈值 thr 从 0.63 起,匹配不到就 ×0.8 逐步放宽到 0.3(search.py:300-314)
取相似度 > 0.99·最大值 的 chunk,最多 4 个,插 [ID:c]

两个细节:

  • 代码块保护(search.py:247-259): 先用 ```` 三反引号把答案里的代码块整段抠出来,不在代码块中间切句、不插引用,避免破坏代码。
  • 动态阈值(search.py:300): 从严(0.63)到宽(0.3)逐步降,保证「宁可用低置信引用,也别一个引用都没有」。向量权重 0.9 远高于检索时的默认 0.7,因为引用是语义级对齐。

chunk_v(chunk 向量)通过 fetch_chunk_vectors 按需回取(search.py:402),只在这里才把向量拉回应用。


4. 巧妙之处(可带走的技术)

妙在哪一句话依据
向量不出库ES 路径把 cosine 计算留在库内,靠二次 KNN-only 调用取纯分,向量只在引用时按需回取,省大量网络传输_knn_scores search.py:367;fetch_chunk_vectors search.py:402
窗口=页整数倍用一个 RERANK_LIMIT 同时当「取块步长」和「切页模数」,强制整除对齐,深翻页不丢结果_rerank_window search.py:548-571
权重三层叠加字段权重(important^30)→ 词权重(IDF×NER×词性)→ 融合权重(user 可调)→ rank_feature(PageRank),四层各管一段query.py:32term_weight.py:164search.py:471
同义词打折扩召回同义词以原词权重的 0.2 倍挂上,既扩召回又不喧宾夺主query.py:73151
三相似度分开返回similarity/vector_similarity/term_similarity 都给前端,可解释「为什么这条排前面」search.py:731-733
空结果自动降级min_match 0.3→0.1、similarity 0.1→0.17 重试,宁松勿空search.py:200-212
引用动态阈值0.63 逐步降到 0.3,保证答案尽量有据可依search.py:300

5. 边界与局限(诚实)

  • 本章只覆盖「文本 + 向量」的通用混合检索。 RAPTOR 树召回、GraphRAG 图谱 n-hop、查询分解走的是另一套代码(rag/graphrag/search.pyKGSearch,继承 Dealer 但重写检索),见 04 章
  • 三条重排路径行为不完全一致。 Infinity 直接用库内 _score 不做应用侧重排,ES 做二次 KNN,OceanBase 用本地向量——同一份数据换后端,最终排序可能不同。切换后端时要注意。
  • 候选词有硬截断。 问句切词 [:256]、关键词 < 32(query.py:68112),超长问句尾部被丢,长文档式提问会掉召回。
  • token_similarity 只看查询覆盖率,不惩罚文档长度(query.py:210-222),对超长 chunk 偏宽松。
  • _prune_deleted_chunks 是兜底不是主删除路径(search.py:77-81),依赖它清理僵尸 chunk 会有性能代价(每次检索多一次 DB 查文档存在性)。
  • 同义词依赖 Redis 词典 + WordNet(synonym.py:3195);Redis 不可用时自定义词典失效,中文同义词退化。

6. 横向对比

  • 对比纯向量库(如只用 FAISS/Milvus 做语义检索): RAGFlow 坚持双路,补上了向量检索在专有名词/数字/代号上的盲区,代价是查询构造和重排的工程复杂度高得多。
  • 对比只用 BM25 的传统全文检索: 加了稠密召回和同义词扩展,召回率显著提升;融合权重可调,能按场景在「精确」和「语义」间滑动。
  • shelf 内兄弟: 写入线怎么产出这些可检索字段(分词、向量、important_kwd)见 02-ingestion-write-path.md;DeepDoc 怎么把 PDF 变成结构化 chunk 见 01-deepdoc-document-understanding.md;全景与阅读地图见 index.md

7. 代码地图(导航索引)

用符号名 grep 比行号抗漂移。

主题文件符号
顶层检索入口rag/nlp/search.pyDealer.retrieval
双路召回组装rag/nlp/search.pyDealer.search
稠密向量表达式rag/nlp/search.pyDealer.get_vector
候选窗口/分页对齐rag/nlp/search.pyDealer._rerank_window
查询构造(中英分流)rag/nlp/query.pyFulltextQueryer.question
去停用词/疑问词common/query_base.pyQueryBase.rmWWW
中英分流判定common/query_base.pyQueryBase.is_chinese
词权重(IDF×NER×词性)rag/nlp/term_weight.pyDealer.weights / Dealer.split
同义词扩展rag/nlp/synonym.pyDealer.lookup
词面相似度rag/nlp/query.pyFulltextQueryer.token_similarity / similarity
向量+词面融合rag/nlp/query.pyFulltextQueryer.hybrid_similarity
外置模型重排rag/nlp/search.pyDealer.rerank_by_model
ES 二次 KNN 打分rag/nlp/search.pyDealer._knn_scores
ES 融合重排rag/nlp/search.pyDealer.rerank_with_knn
OceanBase 本地重排rag/nlp/search.pyDealer.rerank
PageRank/标签特征分rag/nlp/search.pyDealer._rank_feature_scores
可溯源引用rag/nlp/search.pyDealer.insert_citations
引用向量按需回取rag/nlp/search.pyDealer.fetch_chunk_vectors
ES 查询翻译(min_should_match/knn/rank_feature)rag/utils/es_conn.pyESConnection.search
ES 纯分数提取common/doc_store/es_conn_base.pyget_scores
Infinity 库内归一化融合rag/utils/infinity_conn.pyInfinityConnection.search(FusionExpr normalize="atan")
表达式数据类common/doc_store/doc_store_base.pyMatchTextExpr / MatchDenseExpr / FusionExpr