Asosiy qismga o'tish

search.py — vektor qidiruv

11-bob 9 kod bloki 854 so'z

Buni o'zingiz yozasiz. RAG ning "R" harfi — Retrieval.

Endi bazada vektorlar bor. Savol ham vektorga aylanadi. Qolgani — eng yaqinlarini topish.

Asosiy SQL

sql7 qator
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

python2 qator
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:

python4 qator
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

python58 qator
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.

yaml2 qator
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.

python2 qator
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:

python6 qator
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"

python1 qator
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

bash8 qator
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.

Telegramda yozish Narxlar