Files
tk-factory-services/ai-service/db/vector_store.py
Hyungi Ahn 85f674c9cb feat: ai-service를 ds923에서 맥미니로 이전
- ChromaDB → Qdrant 전환 (맥미니 기존 인스턴스, tk_qc_issues 컬렉션)
- Ollama 임베딩/텍스트 생성 URL 분리 (임베딩: 맥미니, 텍스트: GPU서버)
- MLX fallback 제거, Ollama 단일 경로로 단순화
- ds923 docker-compose에서 ai-service 제거
- gateway/system3-web nginx: ai-service 프록시를 ai.hyungi.net 경유로 변경
- resolver + 변수 기반 proxy_pass로 런타임 DNS 해석 (컨테이너 시작 실패 방지)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 15:36:42 +09:00

103 lines
3.3 KiB
Python

import uuid
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue
from config import settings
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:
results = self.client.search(
collection_name=self.collection,
query_vector=embedding,
limit=n_results,
query_filter=query_filter,
)
except Exception:
return []
items = []
for hit in results:
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()