init: RAG 知识库服务初始提交

- 后端 API(Flask + Gunicorn)
- RAG 引擎(混合检索 + 云端 Reranker + 引用溯源)
- 文档解析(MinerU + 多格式支持)
- Docker 生产部署配置
- 排除前端项目、敏感配置、模型文件
This commit is contained in:
lacerate551
2026-06-04 17:35:27 +08:00
commit 100d1a06eb
158 changed files with 64534 additions and 0 deletions

382
knowledge/search.py Normal file
View File

@@ -0,0 +1,382 @@
"""
知识库管理器 - 检索功能 Mixin
提供多源融合检索功能,支持:
- 单向量库检索:向量检索 + BM25 混合检索RRF 融合排序
- 多向量库并行检索:跨多个向量库并行检索并合并结果
- 废止版本检测:查找与查询相关的已废止文档
检索流程:
1. 向量检索:使用 cosine 相似度在 ChromaDB 中检索
2. BM25 检索:使用关键词匹配在 BM25 索引中检索
3. RRF 融合:使用 Reciprocal Rank Fusion 合并两路结果
4. 过滤:排除已废止/已替代的文档
主要方法:
- search_single: 单向量库检索
- search_multiple: 多向量库并行检索
- find_deprecated_versions: 查找已废止版本
"""
import logging
from typing import List, Tuple, Dict, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
from .base import SearchResult
logger = logging.getLogger(__name__)
class SearchMixin:
"""
检索功能 Mixin
提供混合检索(向量 + BM25和多库并行检索能力。
依赖属性(需由主类提供):
- self.get_collection: 获取向量库集合的方法
- self.get_bm25_index: 获取 BM25 索引的方法
"""
def search_single(
self,
kb_name: str,
query_vector: List[float],
query_text: str,
top_k: int = 5,
use_bm25: bool = True,
include_deprecated: bool = False
) -> Optional[SearchResult]:
"""
单向量库检索
对单个向量库执行混合检索:向量检索 + BM25 检索,
使用 RRF (Reciprocal Rank Fusion) 融合排序。
Args:
kb_name: 向量库名称
query_vector: 查询向量(由 embedding 模型生成)
query_text: 查询文本(用于 BM25 检索)
top_k: 返回结果数量(默认 5
use_bm25: 是否启用 BM25 混合检索(默认 True
include_deprecated: 是否包含已废止/已替代的文档(默认 False
Returns:
SearchResult 对象,包含:
- ids: 文档 ID 列表
- documents: 文档内容列表
- metadatas: 元数据列表
- distances: 距离/分数列表
- collection_name: 向量库名称
向量库为空时返回 None
"""
collection = self.get_collection(kb_name)
if not collection or collection.count() == 0:
return None
where_filter = None
if not include_deprecated:
where_filter = {"status": "active"}
vector_result = collection.query(
query_embeddings=[query_vector],
n_results=top_k,
where=where_filter
)
if not use_bm25:
return SearchResult(
ids=vector_result['ids'][0] if vector_result['ids'] else [],
documents=vector_result['documents'][0] if vector_result['documents'] else [],
metadatas=vector_result['metadatas'][0] if vector_result['metadatas'] else [],
distances=vector_result['distances'][0] if vector_result['distances'] else [],
collection_name=kb_name
)
bm25_index = self.get_bm25_index(kb_name)
bm25_ids, bm25_docs, bm25_metas, bm25_scores = bm25_index.search(
query_text, top_k=min(top_k * 2, 20)
)
if not include_deprecated and bm25_metas:
filtered_bm25 = []
for i, meta in enumerate(bm25_metas):
if meta.get('status', 'active') == 'active':
filtered_bm25.append((bm25_ids[i], bm25_docs[i], bm25_metas[i], bm25_scores[i]))
if filtered_bm25:
bm25_ids, bm25_docs, bm25_metas, bm25_scores = zip(*filtered_bm25)
else:
bm25_ids, bm25_docs, bm25_metas, bm25_scores = [], [], [], []
return self._merge_results(
vector_result,
(list(bm25_ids), list(bm25_docs), list(bm25_metas), list(bm25_scores)),
top_k=top_k,
collection_name=kb_name
)
def search_multiple(
self,
kb_names: List[str],
query_vector: List[float],
query_text: str,
top_k: int = 5,
use_bm25: bool = True
) -> SearchResult:
"""
多向量库并行检索
同时在多个向量库中检索,使用线程池并行执行,
最终合并去重并按分数排序。
Args:
kb_names: 向量库名称列表
query_vector: 查询向量
query_text: 查询文本
top_k: 每个库返回的数量
use_bm25: 是否启用 BM25
Returns:
合并后的 SearchResult 对象
"""
if not kb_names:
return SearchResult(
ids=[], documents=[], metadatas=[], distances=[]
)
results = []
with ThreadPoolExecutor(max_workers=len(kb_names)) as executor:
futures = {
executor.submit(
self.search_single,
kb_name,
query_vector,
query_text,
top_k,
use_bm25
): kb_name for kb_name in kb_names
}
for future in as_completed(futures):
result = future.result()
if result:
results.append(result)
return self._merge_multiple_results(results, top_k)
def _merge_results(
self,
vector_result: dict,
bm25_result: Tuple,
top_k: int,
collection_name: str
) -> SearchResult:
"""
RRF 融合向量检索和 BM25 检索结果
使用 Reciprocal Rank Fusion 算法合并两路检索结果,
综合考虑向量相似度和 BM25 分数进行排序。
Args:
vector_result: ChromaDB 向量检索结果
bm25_result: BM25 检索结果元组 (ids, docs, metas, scores)
top_k: 返回数量
collection_name: 向量库名称
Returns:
融合后的 SearchResult 对象
Note:
RRF 参数 k=60向量权重 0.5BM25 权重 0.5。
"""
k = 60 # RRF 参数
doc_scores = {}
# 向量检索结果
if vector_result['ids'] and vector_result['ids'][0]:
for rank, (doc_id, doc, meta, dist) in enumerate(zip(
vector_result['ids'][0],
vector_result['documents'][0],
vector_result['metadatas'][0],
vector_result['distances'][0]
)):
rrf_score = 1 / (k + rank + 1)
sim_score = 1 - dist
combined = rrf_score * 0.5 + sim_score * 0.5
doc_scores[doc_id] = {
'score': combined,
'doc': doc,
'meta': meta
}
# BM25 结果
bm25_ids, bm25_docs, bm25_metas, bm25_scores = bm25_result
for rank, (doc_id, doc, meta, score) in enumerate(zip(
bm25_ids, bm25_docs, bm25_metas, bm25_scores
)):
rrf_score = 1 / (k + rank + 1)
norm_score = score / 10.0 if score > 0 else 0
combined = rrf_score * 0.5 + norm_score * 0.5
if doc_id in doc_scores:
doc_scores[doc_id]['score'] += combined
else:
doc_scores[doc_id] = {
'score': combined,
'doc': doc,
'meta': meta
}
# 排序
sorted_items = sorted(
doc_scores.items(),
key=lambda x: x[1]['score'],
reverse=True
)[:top_k]
return SearchResult(
ids=[item[0] for item in sorted_items],
documents=[item[1]['doc'] for item in sorted_items],
metadatas=[item[1]['meta'] for item in sorted_items],
distances=[item[1]['score'] for item in sorted_items],
collection_name=collection_name
)
def _merge_multiple_results(
self,
results: List[SearchResult],
top_k: int
) -> SearchResult:
"""
合并多个向量库的检索结果
将多个向量库的检索结果合并、去重、排序。
Args:
results: 各向量库的检索结果列表
top_k: 最终返回数量
Returns:
合并后的 SearchResult 对象
"""
if not results:
return SearchResult(
ids=[], documents=[], metadatas=[], distances=[]
)
if len(results) == 1:
return results[0]
all_items = []
for result in results:
for i, doc_id in enumerate(result.ids):
all_items.append({
'id': doc_id,
'doc': result.documents[i],
'meta': result.metadatas[i],
'score': result.distances[i],
'collection': result.collection_name
})
all_items.sort(key=lambda x: x['score'], reverse=True)
seen = set()
unique_items = []
for item in all_items:
if item['id'] not in seen:
seen.add(item['id'])
unique_items.append(item)
unique_items = unique_items[:top_k]
return SearchResult(
ids=[item['id'] for item in unique_items],
documents=[item['doc'] for item in unique_items],
metadatas=[item['meta'] for item in unique_items],
distances=[item['score'] for item in unique_items],
collection_name="multiple"
)
def find_deprecated_versions(
self,
kb_names: List[str],
query_vector: List[float],
top_k: int = 3
) -> List[Dict]:
"""
查找与查询相关的已废止版本
在指定向量库中搜索已废止的文档,
当相似度 >= 0.7 时返回废止提示信息。
Args:
kb_names: 向量库名称列表
query_vector: 查询向量
top_k: 每个库返回的数量
Returns:
废止提示列表,每个元素包含:
- document: 文档来源
- collection: 向量库名称
- status: 状态("deprecated"
- deprecated_date: 废止日期
- deprecated_reason: 废止原因
- similarity: 相似度分数
- snippet: 内容摘要
- message: 废止提示消息
"""
hints = []
for kb_name in kb_names:
collection = self.get_collection(kb_name)
if not collection:
continue
result = collection.query(
query_embeddings=[query_vector],
n_results=top_k,
where={"status": "deprecated"}
)
if result['ids'] and result['ids'][0]:
for doc, meta, score in zip(
result['documents'][0],
result['metadatas'][0],
result['distances'][0]
):
sim_score = 1 - score
if sim_score >= 0.7:
hints.append({
"document": meta.get("source", ""),
"collection": kb_name,
"status": "deprecated",
"deprecated_date": meta.get("deprecated_date", ""),
"deprecated_reason": meta.get("deprecated_reason", ""),
"similarity": sim_score,
"snippet": doc[:100] + "..." if len(doc) > 100 else doc,
"message": self._build_deprecation_hint(meta)
})
return hints
def _build_deprecation_hint(self, metadata: Dict) -> str:
"""
构建废止提示消息
Args:
metadata: 切片元数据,包含 deprecated_date 和 deprecated_reason
Returns:
格式化的废止提示消息
"""
deprecated_date = metadata.get("deprecated_date", "")
deprecated_reason = metadata.get("deprecated_reason", "")
date_str = deprecated_date[:10] if deprecated_date else "未知日期"
reason_str = f",原因:{deprecated_reason}" if deprecated_reason else ""
return f"⚠️ 该文档已于 {date_str} 废止{reason_str},内容不再有效"