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>
This commit is contained in:
@@ -1,18 +1,31 @@
|
||||
import chromadb
|
||||
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 = None
|
||||
self.collection = settings.QDRANT_COLLECTION # "tk_qc_issues"
|
||||
|
||||
def initialize(self):
|
||||
self.client = chromadb.PersistentClient(path=settings.CHROMA_PERSIST_DIR)
|
||||
self.collection = self.client.get_or_create_collection(
|
||||
name="qc_issues",
|
||||
metadata={"hnsw:space": "cosine"},
|
||||
)
|
||||
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,
|
||||
@@ -21,11 +34,13 @@ class VectorStore:
|
||||
embedding: list[float],
|
||||
metadata: dict = None,
|
||||
):
|
||||
self.collection.upsert(
|
||||
ids=[doc_id],
|
||||
documents=[document],
|
||||
embeddings=[embedding],
|
||||
metadatas=[metadata] if metadata else 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(
|
||||
@@ -34,42 +49,53 @@ class VectorStore:
|
||||
n_results: int = 5,
|
||||
where: dict = None,
|
||||
) -> list[dict]:
|
||||
kwargs = {
|
||||
"query_embeddings": [embedding],
|
||||
"n_results": n_results,
|
||||
"include": ["documents", "metadatas", "distances"],
|
||||
}
|
||||
if where:
|
||||
kwargs["where"] = where
|
||||
query_filter = self._build_filter(where) if where else None
|
||||
try:
|
||||
results = self.collection.query(**kwargs)
|
||||
results = self.client.search(
|
||||
collection_name=self.collection,
|
||||
query_vector=embedding,
|
||||
limit=n_results,
|
||||
query_filter=query_filter,
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
items = []
|
||||
if results and results["ids"] and results["ids"][0]:
|
||||
for i, doc_id in enumerate(results["ids"][0]):
|
||||
item = {
|
||||
"id": doc_id,
|
||||
"document": results["documents"][0][i] if results["documents"] else "",
|
||||
"distance": results["distances"][0][i] if results["distances"] else 0,
|
||||
"metadata": results["metadatas"][0][i] if results["metadatas"] else {},
|
||||
}
|
||||
# cosine distance → similarity
|
||||
item["similarity"] = round(1 - item["distance"], 4)
|
||||
items.append(item)
|
||||
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):
|
||||
self.collection.delete(ids=[doc_id])
|
||||
point_id = self._to_uuid(doc_id)
|
||||
self.client.delete(
|
||||
collection_name=self.collection,
|
||||
points_selector=[point_id],
|
||||
)
|
||||
|
||||
def count(self) -> int:
|
||||
return self.collection.count()
|
||||
info = self.client.get_collection(collection_name=self.collection)
|
||||
return info.points_count
|
||||
|
||||
def stats(self) -> dict:
|
||||
return {
|
||||
"total_documents": self.count(),
|
||||
"collection_name": "qc_issues",
|
||||
"collection_name": self.collection,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user