import logging import uuid from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue from config import settings logger = logging.getLogger(__name__) class VectorStore: def __init__(self): self.client = None self.collection = settings.QDRANT_COLLECTION # "tk_qc_issues" def initialize(self): self.client = QdrantClient(url=settings.QDRANT_URL) self._ensure_collection() def _ensure_collection(self): collections = [c.name for c in self.client.get_collections().collections] if self.collection not in collections: # bge-m3 기본 출력 = 1024 dims self.client.create_collection( collection_name=self.collection, vectors_config=VectorParams(size=1024, distance=Distance.COSINE), ) @staticmethod def _to_uuid(doc_id) -> str: """문자열/정수 ID → UUID5 변환 (Qdrant 호환)""" return str(uuid.uuid5(uuid.NAMESPACE_URL, str(doc_id))) def upsert( self, doc_id: str, document: str, embedding: list[float], metadata: dict = None, ): point_id = self._to_uuid(doc_id) payload = {"document": document, "original_id": str(doc_id)} if metadata: payload.update(metadata) self.client.upsert( collection_name=self.collection, points=[PointStruct(id=point_id, vector=embedding, payload=payload)], ) def query( self, embedding: list[float], n_results: int = 5, where: dict = None, ) -> list[dict]: query_filter = self._build_filter(where) if where else None try: response = self.client.query_points( collection_name=self.collection, query=embedding, limit=n_results, query_filter=query_filter, with_payload=True, ) except Exception as e: logger.error(f"Qdrant search failed: {e}", exc_info=True) return [] items = [] for hit in response.points: payload = hit.payload or {} item = { "id": payload.get("original_id", str(hit.id)), "document": payload.get("document", ""), "distance": round(1 - hit.score, 4), # cosine score → distance "metadata": {k: v for k, v in payload.items() if k not in ("document", "original_id")}, "similarity": round(hit.score, 4), } items.append(item) return items @staticmethod def _build_filter(where: dict) -> Filter: """ChromaDB 스타일 where 조건 → Qdrant Filter 변환""" conditions = [] for key, value in where.items(): conditions.append(FieldCondition(key=key, match=MatchValue(value=value))) return Filter(must=conditions) def delete(self, doc_id: str): point_id = self._to_uuid(doc_id) self.client.delete( collection_name=self.collection, points_selector=[point_id], ) def count(self) -> int: info = self.client.get_collection(collection_name=self.collection) return info.points_count def stats(self) -> dict: return { "total_documents": self.count(), "collection_name": self.collection, } vector_store = VectorStore()