""" لایه‌ی نازک روی Qdrant: ساختِ collection، upsert نقاط، و جستجوی برداری. برداشتِ پاک‌سازی‌شده از subjects/services/qdrant.py مخزنِ Rag_backend. هیچ کلاینتِ سطحِ ماژولی ساخته نمی‌شه؛ همه از get_qdrant_client() lazy. """ from django.conf import settings from . import clients def ensure_collection(name: str, vector_size: int | None = None) -> None: client = clients.get_qdrant_client() vector_size = vector_size or settings.OPENAI_EMBEDDING_DIM existing = {c.name for c in client.get_collections().collections} if name in existing: return from qdrant_client.models import Distance, VectorParams client.create_collection( collection_name=name, vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE), ) def upsert_points(collection: str, points: list[dict]) -> int: """points: [{id, vector, payload}, ...]""" if not points: return 0 client = clients.get_qdrant_client() from qdrant_client.models import PointStruct structs = [ PointStruct(id=str(p['id']), vector=p['vector'], payload=p['payload']) for p in points ] client.upsert(collection_name=collection, points=structs, wait=True) return len(structs) def delete_points(collection: str, point_ids: list[str]) -> int: """ پاک‌کردنِ نقاط با شناسه — برای جبرانِ upsertِ نیمه‌کاره. `wait=True` در upsert یعنی تایم‌اوتِ کلاینت لزوماً به معنیِ ننوشتنِ Qdrant نیست؛ ممکن است نوشته باشد و فقط جوابش دیر رسیده باشد. در آن حالت ردیف‌های DB پس گرفته می‌شوند و نقطه‌ها بی‌صاحب می‌مانند. این تابع همان را جمع می‌کند. """ if not point_ids: return 0 client = clients.get_qdrant_client() client.delete(collection_name=collection, points_selector=[str(i) for i in point_ids], wait=True) return len(point_ids) def set_payload_fields(collection: str, point_ids: list[str], fields: dict) -> int: """ فقط چند کلیدِ payload را روی نقاطِ موجود می‌نویسد، بدونِ دست زدن به بردار. برای همگام‌سازیِ برچسب‌ها همین کافی است و از upsertِ کامل خیلی ارزان‌تر: نه بردار دوباره فرستاده می‌شود نه embedding دوباره خریده می‌شود. """ if not point_ids or not fields: return 0 client = clients.get_qdrant_client() client.set_payload( collection_name=collection, payload=fields, points=[str(i) for i in point_ids], wait=True, ) return len(point_ids) def search(collection: str, query_vector: list[float], *, lesson_filter_value=None, lesson_filter_key: str | None = None, limit: int = 4) -> list[dict]: """ نزدیک‌ترین نقاط به بردارِ پرسش، در صورتِ نیاز محدود به یک درس. خروجی: [{score, payload}, ...] — payload همون چیزیه که تیمِ دیگه ذخیره کرده (معمولاً شاملِ summary / lesson_title / lesson_number). """ client = clients.get_qdrant_client() query_filter = None if lesson_filter_value is not None and lesson_filter_key: from qdrant_client.models import FieldCondition, Filter, MatchValue # دادهٔ تیمِ دیگه lesson_number/chapter را همیشه به‌صورتِ رشته ذخیره # کرده ("5")، پس مقدار را به رشته تبدیل می‌کنیم تا فیلتر بخورد. query_filter = Filter(should=[ FieldCondition(key=lesson_filter_key, match=MatchValue(value=str(lesson_filter_value))), FieldCondition(key=lesson_filter_key, match=MatchValue(value=lesson_filter_value)), ]) response = client.query_points( collection_name=collection, query=query_vector, limit=limit, query_filter=query_filter, ) return [{'score': p.score, 'payload': p.payload or {}} for p in response.points]