2026-04-21 11:02:16 +08:00
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
Qdrant 混合检索器模块(完全异步)
|
2026-04-21 11:02:16 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
提供基于 Qdrant 的混合检索(Dense + Sparse)功能,包括:
|
|
|
|
|
|
- 纯混合检索(无子父文档)
|
|
|
|
|
|
- 父子文档混合检索(先检索子文档,再返回父文档)
|
2026-04-21 11:02:16 +08:00
|
|
|
|
|
|
|
|
|
|
核心原理:
|
2026-05-04 02:54:37 +08:00
|
|
|
|
- 使用 Qdrant Universal Query API (query_points)
|
|
|
|
|
|
- 使用 Prefetch 并行检索多个源
|
|
|
|
|
|
- 使用 RRF 分数融合
|
2026-04-21 11:02:16 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
from typing import Dict, Any, Optional, List
|
2026-05-04 14:33:12 +08:00
|
|
|
|
from qdrant_client import AsyncQdrantClient, models
|
2026-04-21 11:02:16 +08:00
|
|
|
|
from qdrant_client.http.exceptions import UnexpectedResponse
|
2026-05-04 02:01:22 +08:00
|
|
|
|
from langchain_core.documents import Document
|
2026-04-21 11:02:16 +08:00
|
|
|
|
from langchain_core.embeddings import Embeddings
|
2026-05-04 14:33:12 +08:00
|
|
|
|
from langchain_core.retrievers import BaseRetriever
|
2026-05-04 02:54:37 +08:00
|
|
|
|
from pydantic import Field, PrivateAttr
|
2026-04-21 11:02:16 +08:00
|
|
|
|
|
2026-05-04 14:33:12 +08:00
|
|
|
|
from rag_core import QdrantHybridStore, get_sparse_embedder, create_docstore
|
|
|
|
|
|
from rag_core.client import create_async_qdrant_client
|
2026-04-29 10:52:01 +08:00
|
|
|
|
from app.model_services import get_embedding_service
|
2026-05-04 02:01:22 +08:00
|
|
|
|
from app.logger import info, warning, debug
|
|
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
|
|
|
|
|
|
# 模块级常量
|
|
|
|
|
|
DEFAULT_SEARCH_K = 20
|
2026-05-04 02:01:22 +08:00
|
|
|
|
DEFAULT_PARENT_SEARCH_K = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HybridRetriever(BaseRetriever):
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
混合检索器:稠密向量 + BM25 稀疏向量 RRF 分数融合(异步)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
使用 Qdrant Universal Query API (query_points)
|
2026-05-04 02:01:22 +08:00
|
|
|
|
"""
|
2026-05-04 02:54:37 +08:00
|
|
|
|
collection_name: str = Field(description="Qdrant 集合名称")
|
|
|
|
|
|
search_k: int = Field(default=DEFAULT_SEARCH_K, description="检索返回结果数")
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
_vector_store: Any = PrivateAttr()
|
|
|
|
|
|
_client: Any = PrivateAttr()
|
|
|
|
|
|
_sparse_embedder: Any = PrivateAttr()
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
collection_name: str,
|
2026-05-04 14:33:12 +08:00
|
|
|
|
vector_store: QdrantHybridStore,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: int = DEFAULT_SEARCH_K,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Args:
|
|
|
|
|
|
collection_name: Qdrant 集合名称
|
2026-05-04 14:33:12 +08:00
|
|
|
|
vector_store: QdrantHybridStore 实例
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: 检索返回结果数
|
|
|
|
|
|
"""
|
2026-05-04 02:54:37 +08:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
collection_name=collection_name,
|
|
|
|
|
|
search_k=search_k
|
|
|
|
|
|
)
|
|
|
|
|
|
self._vector_store = vector_store
|
2026-05-04 14:33:12 +08:00
|
|
|
|
self._client = vector_store.get_async_qdrant_client()
|
2026-05-04 02:54:37 +08:00
|
|
|
|
self._sparse_embedder = get_sparse_embedder()
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
|
|
|
|
|
def _get_relevant_documents(
|
|
|
|
|
|
self, query: str, *, run_manager: Any = None
|
|
|
|
|
|
) -> List[Document]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
同步检索(不推荐使用,仅供兼容性)
|
|
|
|
|
|
|
|
|
|
|
|
注意:在异步环境中请使用 _aget_relevant_documents 或 ainvoke
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
try:
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
# 已有事件循环,使用 create_task
|
|
|
|
|
|
task = loop.create_task(self._aget_relevant_documents(query))
|
|
|
|
|
|
return loop.run_until_complete(task)
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
# 没有事件循环,创建新的
|
|
|
|
|
|
return asyncio.run(self._aget_relevant_documents(query))
|
|
|
|
|
|
|
2026-05-04 14:33:12 +08:00
|
|
|
|
async def _aget_relevant_documents(
|
2026-05-04 17:58:10 +08:00
|
|
|
|
self, query: str, *, run_manager: Any = None
|
2026-05-04 02:01:22 +08:00
|
|
|
|
) -> List[Document]:
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
异步混合检索相关文档
|
2026-05-04 02:01:22 +08:00
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
# 1. 生成查询向量
|
2026-05-04 17:58:10 +08:00
|
|
|
|
dense_query = await self._vector_store.aembed_query(query)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
sparse_query = self._sparse_embedder.embed_query(query)
|
|
|
|
|
|
sparse_vec = models.SparseVector(
|
|
|
|
|
|
indices=sparse_query["indices"],
|
|
|
|
|
|
values=sparse_query["values"]
|
|
|
|
|
|
)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 14:33:12 +08:00
|
|
|
|
# 2. 使用 Qdrant 的 query_points API
|
|
|
|
|
|
response = await self._client.query_points(
|
2026-05-04 02:01:22 +08:00
|
|
|
|
collection_name=self.collection_name,
|
2026-05-04 14:33:12 +08:00
|
|
|
|
prefetch=[
|
2026-05-04 02:54:37 +08:00
|
|
|
|
models.Prefetch(
|
|
|
|
|
|
query=dense_query,
|
2026-05-04 14:33:12 +08:00
|
|
|
|
using="dense",
|
2026-05-04 02:54:37 +08:00
|
|
|
|
limit=self.search_k
|
|
|
|
|
|
),
|
|
|
|
|
|
models.Prefetch(
|
|
|
|
|
|
query=sparse_vec,
|
2026-05-04 14:33:12 +08:00
|
|
|
|
using="sparse",
|
2026-05-04 02:54:37 +08:00
|
|
|
|
limit=self.search_k
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
2026-05-04 14:33:12 +08:00
|
|
|
|
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
|
|
|
|
|
limit=self.search_k,
|
2026-05-04 02:54:37 +08:00
|
|
|
|
with_payload=True
|
2026-05-04 02:01:22 +08:00
|
|
|
|
)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
# 3. 转换结果
|
2026-05-04 02:01:22 +08:00
|
|
|
|
results = []
|
2026-05-04 02:54:37 +08:00
|
|
|
|
for point in response.points:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
doc = Document(
|
2026-05-04 02:54:37 +08:00
|
|
|
|
page_content=point.payload.pop("page_content", point.payload.pop("text", "")),
|
2026-05-04 02:01:22 +08:00
|
|
|
|
metadata=point.payload
|
|
|
|
|
|
)
|
|
|
|
|
|
results.append(doc)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
|
|
|
|
|
debug(f"混合检索返回 {len(results)} 个文档")
|
2026-05-04 02:01:22 +08:00
|
|
|
|
return results
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
|
|
|
|
|
|
class ParentHybridRetriever(BaseRetriever):
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
父子文档混合检索器(异步):
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
1. 先用混合检索找到相关子文档
|
|
|
|
|
|
2. 根据子文档的 parent_id 找到对应的父文档
|
|
|
|
|
|
3. 去重并返回父文档
|
|
|
|
|
|
"""
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
collection_name: str = Field(description="Qdrant 集合名称")
|
|
|
|
|
|
search_k: int = Field(default=DEFAULT_PARENT_SEARCH_K, description="检索返回结果数")
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
_vector_store: Any = PrivateAttr()
|
|
|
|
|
|
_client: Any = PrivateAttr()
|
|
|
|
|
|
_sparse_embedder: Any = PrivateAttr()
|
|
|
|
|
|
_docstore: Any = PrivateAttr()
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
collection_name: str,
|
2026-05-04 14:33:12 +08:00
|
|
|
|
vector_store: QdrantHybridStore,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: int = DEFAULT_PARENT_SEARCH_K,
|
|
|
|
|
|
docstore: Optional[Any] = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Args:
|
|
|
|
|
|
collection_name: Qdrant 集合名称
|
2026-05-04 14:33:12 +08:00
|
|
|
|
vector_store: QdrantHybridStore 实例
|
2026-05-04 02:54:37 +08:00
|
|
|
|
search_k: 最终返回的父文档数量
|
2026-05-04 02:01:22 +08:00
|
|
|
|
docstore: 文档存储(如果父文档在 PostgreSQL),可选
|
|
|
|
|
|
"""
|
2026-05-04 02:54:37 +08:00
|
|
|
|
super().__init__(
|
|
|
|
|
|
collection_name=collection_name,
|
|
|
|
|
|
search_k=search_k
|
|
|
|
|
|
)
|
|
|
|
|
|
self._vector_store = vector_store
|
2026-05-04 14:33:12 +08:00
|
|
|
|
self._client = vector_store.get_async_qdrant_client()
|
2026-05-04 02:54:37 +08:00
|
|
|
|
self._sparse_embedder = get_sparse_embedder()
|
|
|
|
|
|
self._docstore = docstore
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
|
|
|
|
|
def _get_relevant_documents(
|
|
|
|
|
|
self, query: str, *, run_manager: Any = None
|
|
|
|
|
|
) -> List[Document]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
同步检索(不推荐使用,仅供兼容性)
|
|
|
|
|
|
|
|
|
|
|
|
注意:在异步环境中请使用 _aget_relevant_documents 或 ainvoke
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
try:
|
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
|
task = loop.create_task(self._aget_relevant_documents(query))
|
|
|
|
|
|
return loop.run_until_complete(task)
|
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
|
return asyncio.run(self._aget_relevant_documents(query))
|
|
|
|
|
|
|
2026-05-04 14:33:12 +08:00
|
|
|
|
async def _aget_relevant_documents(
|
2026-05-04 17:58:10 +08:00
|
|
|
|
self, query: str, *, run_manager: Any = None
|
2026-05-04 02:01:22 +08:00
|
|
|
|
) -> List[Document]:
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
异步检索相关父文档
|
2026-05-04 02:01:22 +08:00
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
# 1. 生成查询向量
|
2026-05-04 17:58:10 +08:00
|
|
|
|
dense_query = await self._vector_store.aembed_query(query)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
sparse_query = self._sparse_embedder.embed_query(query)
|
|
|
|
|
|
sparse_vec = models.SparseVector(
|
|
|
|
|
|
indices=sparse_query["indices"],
|
|
|
|
|
|
values=sparse_query["values"]
|
|
|
|
|
|
)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 2. 多取一些子文档,避免去重后数量不足
|
|
|
|
|
|
search_limit = self.search_k * 2
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
# 3. 使用 query_points API 进行混合检索
|
2026-05-04 14:33:12 +08:00
|
|
|
|
response = await self._client.query_points(
|
2026-05-04 02:01:22 +08:00
|
|
|
|
collection_name=self.collection_name,
|
2026-05-04 02:54:37 +08:00
|
|
|
|
prefetch=[
|
|
|
|
|
|
models.Prefetch(
|
|
|
|
|
|
query=dense_query,
|
|
|
|
|
|
using="dense",
|
|
|
|
|
|
limit=search_limit
|
|
|
|
|
|
),
|
|
|
|
|
|
models.Prefetch(
|
|
|
|
|
|
query=sparse_vec,
|
|
|
|
|
|
using="sparse",
|
|
|
|
|
|
limit=search_limit
|
|
|
|
|
|
)
|
|
|
|
|
|
],
|
|
|
|
|
|
query=models.FusionQuery(fusion=models.Fusion.RRF),
|
|
|
|
|
|
limit=search_limit,
|
|
|
|
|
|
with_payload=True
|
2026-05-04 02:01:22 +08:00
|
|
|
|
)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
if not response.points:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
debug("混合检索未找到任何文档")
|
|
|
|
|
|
return []
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 4. 收集 parent_id 和对应最高得分
|
|
|
|
|
|
parent_score_map = {}
|
|
|
|
|
|
parent_ids = set()
|
|
|
|
|
|
child_point_map = {} # 保存子文档点用于降级
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
for point in response.points:
|
|
|
|
|
|
payload_copy = point.payload.copy()
|
|
|
|
|
|
parent_id = payload_copy.get("parent_id", point.id)
|
2026-05-04 02:01:22 +08:00
|
|
|
|
score = point.score
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
if parent_id not in parent_score_map or score > parent_score_map[parent_id]:
|
|
|
|
|
|
parent_score_map[parent_id] = score
|
|
|
|
|
|
parent_ids.add(parent_id)
|
|
|
|
|
|
child_point_map[parent_id] = point
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 5. 批量查询父文档
|
|
|
|
|
|
parent_docs = []
|
|
|
|
|
|
found_parent_ids = set()
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 14:33:12 +08:00
|
|
|
|
# 先尝试从 Qdrant 直接查询(如果父文档也在 Qdrant 中)
|
2026-05-04 02:01:22 +08:00
|
|
|
|
try:
|
2026-05-04 14:33:12 +08:00
|
|
|
|
parent_points = await self._client.retrieve(
|
2026-05-04 02:01:22 +08:00
|
|
|
|
collection_name=self.collection_name,
|
|
|
|
|
|
ids=list(parent_ids),
|
|
|
|
|
|
with_payload=True
|
|
|
|
|
|
)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
for point in parent_points:
|
2026-05-04 02:54:37 +08:00
|
|
|
|
payload_copy = point.payload.copy()
|
2026-05-04 02:01:22 +08:00
|
|
|
|
doc = Document(
|
2026-05-04 02:54:37 +08:00
|
|
|
|
page_content=payload_copy.pop("page_content", payload_copy.pop("text", "")),
|
|
|
|
|
|
metadata=payload_copy
|
2026-05-04 02:01:22 +08:00
|
|
|
|
)
|
|
|
|
|
|
parent_docs.append(doc)
|
|
|
|
|
|
found_parent_ids.add(point.id)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
except Exception as e:
|
2026-05-04 17:58:10 +08:00
|
|
|
|
warning(f"从 Qdrant 查询父文档失败: {e}")
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 6. 如果有 docstore,尝试从 docstore 查询剩余的父文档
|
2026-05-04 02:54:37 +08:00
|
|
|
|
if self._docstore and len(found_parent_ids) < len(parent_ids):
|
2026-05-04 02:01:22 +08:00
|
|
|
|
missing_parent_ids = parent_ids - found_parent_ids
|
|
|
|
|
|
try:
|
2026-05-04 14:33:12 +08:00
|
|
|
|
docstore_docs = await self._docstore.amget(missing_parent_ids)
|
2026-05-04 02:01:22 +08:00
|
|
|
|
for doc_id, doc in zip(missing_parent_ids, docstore_docs):
|
|
|
|
|
|
if doc is not None:
|
|
|
|
|
|
parent_docs.append(doc)
|
|
|
|
|
|
found_parent_ids.add(doc_id)
|
|
|
|
|
|
except Exception as e:
|
2026-05-04 17:58:10 +08:00
|
|
|
|
warning(f"从 docstore 查询父文档失败: {e}")
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 7. 降级:对于仍未找到的父文档,用子文档本身代替
|
|
|
|
|
|
missing_parent_ids = parent_ids - found_parent_ids
|
|
|
|
|
|
if missing_parent_ids:
|
2026-05-04 17:58:10 +08:00
|
|
|
|
warning(f"以下 parent_id 未找到对应的父文档,将返回子文档本身: {missing_parent_ids}")
|
2026-05-04 02:01:22 +08:00
|
|
|
|
for parent_id in missing_parent_ids:
|
|
|
|
|
|
child_point = child_point_map.get(parent_id)
|
|
|
|
|
|
if child_point:
|
2026-05-04 02:54:37 +08:00
|
|
|
|
payload_copy = child_point.payload.copy()
|
2026-05-04 02:01:22 +08:00
|
|
|
|
doc = Document(
|
2026-05-04 02:54:37 +08:00
|
|
|
|
page_content=payload_copy.pop("page_content", payload_copy.pop("text", "")),
|
|
|
|
|
|
metadata=payload_copy
|
2026-05-04 02:01:22 +08:00
|
|
|
|
)
|
|
|
|
|
|
parent_docs.append(doc)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 8. 按照得分降序排序,返回前 k 个
|
|
|
|
|
|
parent_docs_with_scores = [
|
2026-05-04 14:33:12 +08:00
|
|
|
|
(doc, parent_score_map.get(doc.metadata.get("id", doc.id if hasattr(doc, "id") else ""), 0.0))
|
2026-05-04 02:01:22 +08:00
|
|
|
|
for doc in parent_docs
|
|
|
|
|
|
]
|
|
|
|
|
|
parent_docs_with_scores.sort(key=lambda x: x[1], reverse=True)
|
2026-05-04 17:58:10 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
final_docs = [doc for doc, _ in parent_docs_with_scores[:self.search_k]]
|
2026-05-04 17:58:10 +08:00
|
|
|
|
debug(f"父子文档混合检索返回 {len(final_docs)} 个父文档")
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
return final_docs
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
def create_hybrid_retriever(
|
2026-04-21 11:02:16 +08:00
|
|
|
|
collection_name: str,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: int = DEFAULT_SEARCH_K,
|
|
|
|
|
|
embeddings: Optional[Embeddings] = None,
|
2026-04-21 11:02:16 +08:00
|
|
|
|
) -> BaseRetriever:
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
创建混合检索器(稠密向量 + BM25 稀疏向量)- 异步版本。
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
这是默认推荐的检索方式,效果最优。
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
Args:
|
2026-04-29 10:52:01 +08:00
|
|
|
|
collection_name: Qdrant 集合名称
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: 检索返回结果数
|
|
|
|
|
|
embeddings: 可选的嵌入模型实例。若未提供,将自动获取统一嵌入服务。
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
Returns:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
HybridRetriever 实例
|
2026-04-21 11:02:16 +08:00
|
|
|
|
"""
|
2026-04-29 10:52:01 +08:00
|
|
|
|
if embeddings is None:
|
|
|
|
|
|
embeddings = get_embedding_service()
|
2026-05-04 14:33:12 +08:00
|
|
|
|
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 17:58:10 +08:00
|
|
|
|
vector_store = QdrantHybridStore(collection_name=collection_name)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
try:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
vector_store.get_client().get_collection(collection_name)
|
2026-04-21 11:02:16 +08:00
|
|
|
|
except UnexpectedResponse as e:
|
|
|
|
|
|
if e.status_code == 404:
|
2026-04-29 10:52:01 +08:00
|
|
|
|
warning(f"⚠️ Qdrant 集合 '{collection_name}' 不存在,请先创建并索引文档")
|
|
|
|
|
|
raise ValueError(f"Qdrant 集合 '{collection_name}' 不存在")
|
2026-04-21 11:02:16 +08:00
|
|
|
|
raise
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
info(f"✅ Qdrant 混合检索器初始化成功(search_k={search_k})")
|
|
|
|
|
|
return HybridRetriever(
|
2026-04-21 11:02:16 +08:00
|
|
|
|
collection_name=collection_name,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
vector_store=vector_store,
|
|
|
|
|
|
search_k=search_k
|
2026-04-21 11:02:16 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
def create_parent_hybrid_retriever(
|
2026-04-21 11:02:16 +08:00
|
|
|
|
collection_name: str,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
search_k: int = DEFAULT_PARENT_SEARCH_K,
|
|
|
|
|
|
embeddings: Optional[Embeddings] = None,
|
|
|
|
|
|
use_docstore: bool = True,
|
2026-04-21 11:02:16 +08:00
|
|
|
|
) -> BaseRetriever:
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
创建父子文档混合检索器(默认推荐)- 异步版本。
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
检索流程:
|
|
|
|
|
|
1. 混合检索找到相关子文档
|
|
|
|
|
|
2. 根据 parent_id 找到对应的父文档
|
|
|
|
|
|
3. 去重并返回父文档
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
Args:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
collection_name: Qdrant 集合名称
|
2026-05-04 02:54:37 +08:00
|
|
|
|
search_k: 最终返回的父文档数量
|
2026-05-04 02:01:22 +08:00
|
|
|
|
embeddings: 可选的嵌入模型实例
|
|
|
|
|
|
use_docstore: 是否使用 PostgreSQL docstore 存储父文档
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-04-21 11:02:16 +08:00
|
|
|
|
Returns:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
ParentHybridRetriever 实例
|
2026-04-21 11:02:16 +08:00
|
|
|
|
"""
|
2026-05-03 17:58:21 +08:00
|
|
|
|
if embeddings is None:
|
|
|
|
|
|
embeddings = get_embedding_service()
|
2026-05-04 14:33:12 +08:00
|
|
|
|
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 17:58:10 +08:00
|
|
|
|
vector_store = QdrantHybridStore(collection_name=collection_name)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-03 17:46:38 +08:00
|
|
|
|
try:
|
2026-05-04 02:01:22 +08:00
|
|
|
|
vector_store.get_client().get_collection(collection_name)
|
2026-05-03 17:58:21 +08:00
|
|
|
|
except UnexpectedResponse as e:
|
|
|
|
|
|
if e.status_code == 404:
|
|
|
|
|
|
warning(f"⚠️ Qdrant 集合 '{collection_name}' 不存在,请先创建并索引文档")
|
|
|
|
|
|
raise ValueError(f"Qdrant 集合 '{collection_name}' 不存在")
|
|
|
|
|
|
raise
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
docstore = None
|
|
|
|
|
|
if use_docstore:
|
|
|
|
|
|
try:
|
|
|
|
|
|
docstore, _ = create_docstore()
|
|
|
|
|
|
info("✅ 文档存储初始化成功(PostgreSQL)")
|
|
|
|
|
|
except Exception as e:
|
2026-05-04 14:33:12 +08:00
|
|
|
|
warning(f"⚠️ 文档存储初始化失败,将不使用 docstore: %s", e)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
info(f"✅ Qdrant 父子文档混合检索器初始化成功(search_k={search_k})")
|
|
|
|
|
|
return ParentHybridRetriever(
|
2026-05-03 18:12:20 +08:00
|
|
|
|
collection_name=collection_name,
|
2026-05-04 02:01:22 +08:00
|
|
|
|
vector_store=vector_store,
|
|
|
|
|
|
search_k=search_k,
|
|
|
|
|
|
docstore=docstore
|
2026-05-03 17:58:21 +08:00
|
|
|
|
)
|
2026-05-03 17:46:38 +08:00
|
|
|
|
|
2026-05-03 18:12:20 +08:00
|
|
|
|
|
2026-05-04 02:54:37 +08:00
|
|
|
|
def create_base_retriever(
|
|
|
|
|
|
collection_name: str,
|
|
|
|
|
|
search_k: int = DEFAULT_SEARCH_K,
|
|
|
|
|
|
embeddings: Optional[Embeddings] = None,
|
|
|
|
|
|
) -> BaseRetriever:
|
|
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
创建基础检索器(向后兼容)- 实际上返回混合检索器。
|
2026-05-04 02:54:37 +08:00
|
|
|
|
"""
|
2026-05-04 14:33:12 +08:00
|
|
|
|
return create_hybrid_retriever(collection_name, search_k, embeddings)
|
2026-05-04 02:54:37 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 02:01:22 +08:00
|
|
|
|
# 别名:默认就是父子文档混合检索
|
|
|
|
|
|
create_retriever = create_parent_hybrid_retriever
|