search.py — vektor qidiruv
Buni o'zingiz yozasiz. RAG ning "R" harfi — Retrieval.
Endi bazada vektorlar bor. Savol ham vektorga aylanadi. Qolgani — eng yaqinlarini topish.
Asosiy SQL
SELECT ch.id, ch.chunk_text, d.title, 1 - (ch.embedding_local <=> :vec) AS similarity FROM rag.chunks ch JOIN rag.documents d ON d.id = ch.document_id WHERE ch.embedding_local IS NOT NULL ORDER BY ch.embedding_local <=> :vec LIMIT 10;
Butun RAG shu besh qatorda. Qolgani — atrofidagi bezak.
<=> — cosine masofa. pgvector qo'shgan operator.
ORDER BY ... <=> — masofa bo'yicha tartiblash. Kichik masofa =
yaqinroq = yuqorida.
1 - masofa — masofani o'xshashlikka aylantiradi. Odamga
"o'xshashlik 0.87" degani "masofa 0.13" dan tushunarliroq.
Muhim: ORDER BY va indeks
ORDER BY ch.embedding_local <=> :vec -- indeks ISHLAYDI ORDER BY 1 - (ch.embedding_local <=> :vec) DESC -- indeks ISHLAMAYDI
Ikkalasi bir xil natija beradi. Lekin ikkinchisida HNSW indeks ishlatilmaydi — PostgreSQL hisoblangan ustun bo'yicha tartiblaydi va barcha qatorlarni ko'radi.
100 ta chunk'da sezmaysiz. 100 000 ta chunk'da qidiruv 50 ms o'rniga 5 sekund bo'ladi.
Shuning uchun: ORDER BY da doim xom operator, SELECT da esa
chiroyli ko'rinish.
Tekshirish:
EXPLAIN ANALYZE SELECT id FROM rag.chunks ORDER BY embedding_local <=> '[0.1,0.2,...]'::vector LIMIT 10;
Natijada Index Scan using idx_chunks_embedding_local_hnsw
ko'rinishi kerak. Agar Seq Scan chiqsa — indeks ishlamayapti.
Kod
from sqlalchemy import text from app.database import SessionLocal from app.config import settings from scraper.embedder import embed_text def detect_language(matn: str) -> str: """Kirill harflar ko'p bo'lsa — rus tili.""" kirill = sum(1 for c in matn if "Ѐ" <= c <= "ӿ") harflar = sum(1 for c in matn if c.isalpha()) if harflar and kirill / harflar > 0.3: return "ru" return "uz" def search_chunks(session, query: str, top_k: int | None = None, threshold: float | None = None) -> list[dict]: top_k = top_k or settings.rag_top_k threshold = threshold if threshold is not None else settings.rag_similarity_threshold vektor = embed_text(query) if vektor is None: return [] til = detect_language(query) ustun = "embedding_local" if settings.is_local else "embedding_cloud" sql = f""" SELECT ch.id, ch.chunk_text, ch.language, d.title, d.source_url, 1 - (ch.{ustun} <=> :vec) AS similarity FROM rag.chunks ch JOIN rag.documents d ON d.id = ch.document_id WHERE ch.{ustun} IS NOT NULL ORDER BY ch.{ustun} <=> :vec LIMIT :lim """ try: qatorlar = session.execute( text(sql), {"vec": str(vektor), "lim": top_k} ).fetchall() finally: pass natija = [] for q in qatorlar: if q.similarity < threshold: continue natija.append({ "id": str(q.id), "chunk_text": q.chunk_text, "title": q.title, "source_url": q.source_url, "language": q.language, "similarity": float(q.similarity), }) return natija
Threshold nega kerak
Vektor qidiruv doim natija qaytaradi. Savol butunlay begona bo'lsa ham, eng yaqin 10 tasini beradi.
Savol: "Marsda hayot bormi?" Natija: "Standart tarif 250 000 som" similarity 0.11
Bu chunk hech qanday aloqasi yo'q. Lekin u eng yaqini, chunki boshqasi yo'q.
Agar buni LLM ga bersangiz — u tarif haqida javob yozadi. Foydalanuvchi esa Mars haqida so'ragan edi.
if q.similarity < settings.rag_similarity_threshold: continue
Threshold past natijalarni tashlaydi. Hech narsa qolmasa — "bilmayman" deymiz. Bu noto'g'ri javobdan yaxshiroq.
Threshold qanday tanlanadi
Taxmin qilmang — o'lchang:
savollar = ["Nihol narxi qancha?", "Marsda hayot bormi?"] for s in savollar: n = search_chunks(db, s, top_k=3) print(f"\n{s}") for x in n: print(f" {x['similarity']:.3f} {x['chunk_text'][:50]}")
To'g'ri savollarning eng past ballini va noto'g'ri savollarning eng yuqori ballini ko'ring. Threshold — ular orasida.
Bizda 0.3. Lekin bu sizning ma'lumotingizga bog'liq. Har bir
loyihada qaytadan o'lchang.
Til filtri — ehtiyot bo'ling
Vasvasa: "o'zbekcha savolga o'zbekcha chunk"
WHERE ch.language = :til -- EHTIYOT BO'LING
bge-m3 ko'p tilli — o'zbekcha savol ruscha chunk'ni topa
oladi va bu ko'pincha foydali. Til bo'yicha filtrlash kerakli
ma'lumotni yo'qotishi mumkin.
Tavsiya: filtrlamang, lekin natijada tilni ko'rsating. Foydalanuvchi o'zi hal qilsin.
Sinang
python -c " from app.database import SessionLocal from app.services.search import search_chunks db = SessionLocal() for x in search_chunks(db, 'Nihol narxi qancha?'): print(round(x['similarity'], 3), x['chunk_text'][:60]) db.close() "
Yuqoridagi natijalar savolga mos bo'lishi kerak. Bo'lmasa: embedding'lar bormi? Threshold juda balandmi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.