from langchain_core.documents import Document from langchain_qdrant import QdrantVectorStore from qdrant_client import QdrantClient from qdrant_client.models import Filter, FieldCondition, MatchValue, FilterSelector class RetrieverService: """Qdrant 벡터 검색 서비스. LangGraph Tool 및 직접 검색 모두 지원.""" def __init__( self, embeddings, qdrant_url: str, collection_name: str, top_k: int, reranker=None, rerank_fetch_k: int = 10, ): self._client = QdrantClient(url=qdrant_url) self._collection_name = collection_name self._store = QdrantVectorStore( client=self._client, collection_name=collection_name, embedding=embeddings, ) self._top_k = top_k self._reranker = reranker self._rerank_fetch_k = rerank_fetch_k def as_retriever(self): return self._store.as_retriever(search_kwargs={"k": self._top_k}) def search(self, query: str) -> list[Document]: fetch_k = self._rerank_fetch_k if self._reranker else self._top_k docs = self._store.similarity_search(query, k=fetch_k) if self._reranker: docs = self._reranker.rerank(query, docs, top_k=self._top_k) return docs def list_documents(self) -> list[str]: """Qdrant에 저장된 고유 파일 경로 목록을 반환한다.""" sources: set[str] = set() offset = None while True: results, next_offset = self._client.scroll( collection_name=self._collection_name, with_payload=True, limit=200, offset=offset, ) for point in results: src = (point.payload or {}).get("metadata", {}).get("source", "") if src: sources.add(src) if next_offset is None: break offset = next_offset return sorted(sources) def delete_document(self, source: str) -> None: """파일 경로로 저장된 모든 청크를 Qdrant에서 삭제한다.""" try: self._client.delete( collection_name=self._collection_name, points_selector=FilterSelector( filter=Filter( must=[FieldCondition( key="metadata.source", match=MatchValue(value=source), )] ) ), ) except Exception: pass