import asyncio from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker from kbqa.documents.models import Chunk, Document from kbqa.rag.embeddings import EmbeddingProvider from kbqa.rag.types import SourceCandidate from kbqa.rag.vector_store import MilvusVectorStore class KnowledgeRetriever: def __init__( self, embeddings: EmbeddingProvider, vector_store: MilvusVectorStore, session_factory: async_sessionmaker, ) -> None: self.embeddings = embeddings self.vector_store = vector_store self.session_factory = session_factory async def retrieve(self, query: str, top_k: int) -> list[SourceCandidate]: vector = await self.embeddings.embed_query(query) candidate_limit = max(top_k * 3, top_k) hits = await asyncio.to_thread(self.vector_store.search, vector, candidate_limit) if not hits: return [] ids = [hit.chunk_id for hit in hits] async with self.session_factory() as session: rows = await session.execute( select(Chunk, Document) .join(Document, Document.id == Chunk.document_id) .where(Chunk.id.in_(ids), Document.status == "indexed") ) lookup = {chunk.id: (chunk, document) for chunk, document in rows} sources: list[SourceCandidate] = [] for hit in hits: record = lookup.get(hit.chunk_id) if record is None: continue chunk, document = record sources.append( SourceCandidate( chunk_id=chunk.id, document_id=document.id, filename=document.filename, order_index=chunk.order_index, page_number=chunk.page_number, content=chunk.content, score=hit.score, ) ) if len(sources) == top_k: break return sources