بررسیِ collectionها بعد از ingestِ پایه نهم نشان داد Qdrant ۲۸۷ نقطه دارد ولی دیتابیس ۲۷۹ ردیف. هشت نقطهی اضافه، همه از صفحهی ۱۲۵ — همان صفحهای که دو بار تایماوت داد و بار سوم گرفت. سه مسئله سه نسخه داشتند و دو نقطه هم بیصاحب بود. علت: `upsert` با `wait=True` ممکن است نوشته باشد و فقط جوابش تایماوت شده باشد. مسیرِ جبران فقط ردیفهای DB را پس میگرفت، نه نقطهها؛ و چون هر اجرای دوباره `qdrant_point_id` تازه میسازد، هر retry یک نسخهی تکراری اضافه میکرد. اثرش در بازیابی دیده میشود نه در لاگ: top-k با سه نسخه از یک مسئله پر میشود و بقیهی متنِ مرتبط بیرون میماند — یعنی دستیار بافتِ کمتری میبیند و هیچکس هم متوجه نمیشود. حالا در صورتِ شکست، نقطهها هم پاک میشوند. اگر خودِ پاکسازی هم شکست بخورد لاگ میشود ولی خطای اصلی پوشانده نمیشود، چون صفحه باید ناتمام بماند تا اجرای بعدی دوباره سراغش برود. دادهی موجود هم تمیز شد: ۸ نقطهی بیصاحب پاک شدند و حالا ۲۷۹ = ۲۷۹. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
"""
|
|
لایهی نازک روی 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 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]
|