take the points back too when an upsert fails

بررسیِ collectionها بعد از ingestِ پایه نهم نشان داد Qdrant ۲۸۷ نقطه دارد ولی
دیتابیس ۲۷۹ ردیف. هشت نقطه‌ی اضافه، همه از صفحه‌ی ۱۲۵ — همان صفحه‌ای که دو بار
تایم‌اوت داد و بار سوم گرفت. سه مسئله سه نسخه داشتند و دو نقطه هم بی‌صاحب بود.

علت: `upsert` با `wait=True` ممکن است نوشته باشد و فقط جوابش تایم‌اوت شده
باشد. مسیرِ جبران فقط ردیف‌های DB را پس می‌گرفت، نه نقطه‌ها؛ و چون هر اجرای
دوباره `qdrant_point_id` تازه می‌سازد، هر retry یک نسخه‌ی تکراری اضافه می‌کرد.

اثرش در بازیابی دیده می‌شود نه در لاگ: top-k با سه نسخه از یک مسئله پر می‌شود
و بقیه‌ی متنِ مرتبط بیرون می‌ماند — یعنی دستیار بافتِ کمتری می‌بیند و هیچ‌کس
هم متوجه نمی‌شود.

حالا در صورتِ شکست، نقطه‌ها هم پاک می‌شوند. اگر خودِ پاک‌سازی هم شکست بخورد
لاگ می‌شود ولی خطای اصلی پوشانده نمی‌شود، چون صفحه باید ناتمام بماند تا اجرای
بعدی دوباره سراغش برود.

داده‌ی موجود هم تمیز شد: ۸ نقطه‌ی بی‌صاحب پاک شدند و حالا ۲۷۹ = ۲۷۹.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 23:00:09 +03:30
parent 1131569b76
commit 92dc2242f8
3 changed files with 81 additions and 1 deletions

View File

@@ -19,7 +19,7 @@ from .config import build_collection_name, resolve_grade_title
from .embedding import embed_text
from .known_collections import is_math_subject, resolve_math_chapter
from . import pdf as pdf_service
from .qdrant_store import ensure_collection, upsert_points
from .qdrant_store import delete_points, ensure_collection, upsert_points
from .textbooks import Textbook, get_textbook
logger = logging.getLogger(__name__)
@@ -140,7 +140,20 @@ def _persist_and_upsert(rows: list[dict], *, lesson_id, grade_id, subject,
for c, v in zip(created, vectors)
])
except Exception:
# ⚠️ نقطه‌ها را هم پس می‌گیریم، نه فقط ردیف‌ها. upsert با wait=True ممکن
# است نوشته باشد و فقط جوابش تایم‌اوت شده باشد؛ آن‌وقت پاک کردنِ صرفِ
# ردیف‌ها نقطه‌ی بی‌صاحب جا می‌گذارد، و چون اجرای بعدی شناسه‌ی تازه
# می‌سازد، هر تلاشِ دوباره یک نسخه‌ی تکراری اضافه می‌کند. روی صفحه‌ی ۱۲۵
# کتابِ نهم همین شد: سه نسخه از یک مسئله و دو نقطه‌ی یتیم.
point_ids = [str(c.qdrant_point_id) for c in created]
LessonChunk.objects.filter(pk__in=[c.pk for c in created]).delete()
try:
delete_points(collection, point_ids)
except Exception:
logger.exception(
'rag: پاک‌سازیِ %d نقطه‌ی نیمه‌نوشته در %r هم شکست خورد؛ '
'ممکن است نقطه‌ی بی‌صاحب بماند.', len(point_ids), collection,
)
logger.exception(
'rag: upsert به collection %r شکست خورد؛ %d ردیفِ همین دسته پس گرفته شد',
collection, len(created),

View File

@@ -38,6 +38,22 @@ def upsert_points(collection: str, points: list[dict]) -> int:
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]:
"""