Asosiy qismga o'tish

Reranking (Qayta Tartiblash)

8-bob 13 kod bloki 2503 so'z

Vector search matematikaviy o'xshashlikni topadi, lekin o'xshashlik har doim ham relevantlik emas. Reranking -- retrieval natijalarini yana bir bor tekshirish va haqiqatan ham tegishli chunk-larni ajratish qadami. Bu sifat nazoratining oxirgi bosqichi -- LLM-ga yuborishdan oldin eng yaxshi materiallarni tanlash.


8.1 Muammo: O'xshash != Tegishli

Real misol Nihol loyihasidan. Foydalanuvchi yozadi: "Ucell loyihasi haqida gapirib bering".

Vector search natijasi (cosine similarity bo'yicha):

matn5 qator
Rank 1: "Ucell uchun billing tizimi modernizatsiyasi..." (similarity: 0.82) -- TEGISHLI
Rank 2: "Sharq Telekom uchun tarmoq kengaytirish..." (similarity: 0.79) -- TEGISHLI EMAS
Rank 3: "Ucell abonentlari uchun SMS gateway..." (similarity: 0.77) -- TEGISHLI
Rank 4: "EMAN GROUP telekommunikatsiya yechimlari..." (similarity: 0.75) -- TEGISHLI EMAS
Rank 5: "Ucell tarmoq monitoringi loyihasi..." (similarity: 0.74) -- TEGISHLI

Muammo nimada? Sharq Telekom va EMAN GROUP ham telekommunikatsiya sohasida, shuning uchun ularning embedding-lari Ucell chunk-lariga yaqin. Lekin foydalanuvchi faqat Ucell haqida so'ragan. Reranking-siz LLM bu shovqinli chunk-larni ham ko'radi va javobiga Sharq Telekom ma'lumotlarini qo'shishi mumkin.

Analogiya: Kutubxonada "Python dasturlash" kitobini so'radingiz. Kutubxonachi "Python" javonidan hammasi -- Python haqidagi kitoblar, Python yiloni haqidagi kitoblar, va "Monty Python" filmi haqidagi kitoblarni olib keldi. Hammasi "python" so'ziga tegishli, lekin sizga faqat dasturlash kerak edi. Reranker -- aqlli kutubxonachi, u sizning savolingiz kontekstida qaysi kitoblar haqiqatan kerakligini tushunadi.


8.2 Bi-Encoder vs Cross-Encoder

Retrieval va reranking orasidagi asosiy farq -- model arxitekturasi.

Bi-Encoder (Embedding Model)

Query va hujjat alohida encoding qilinadi, keyin vektorlar taqqoslanadi.

matn16 qator
Bi-Encoder jarayoni:

  Savol: "Ucell loyihasi"          Hujjat: "Ucell billing tizimi..."
         |                                  |
         v                                  v
  +----------------+               +----------------+
  | Encoder (BERT) |               | Encoder (BERT) |
  +----------------+               +----------------+
         |                                  |
         v                                  v
    [0.12, -0.45, 0.78, ...]        [0.15, -0.42, 0.81, ...]
         |                                  |
         +----------> cosine sim <----------+
                         |
                         v
                   similarity: 0.82

Afzalliklari: - Hujjat vektorlarini oldindan hisoblash mumkin (offline). Bazada saqlanadi. - Qidirish tez -- faqat savol encoding + vektor taqqoslash. - Minglab hujjatlar ichidan millisekundlarda topadi.

Kamchiliklari: - Query va hujjat bir-birini "ko'rmaydi" -- kontekst yo'qoladi. - "Bank loyihasi" va "Bank binosi qurilishi" -- vektorlari yaqin, lekin ma'no boshqa.

Cross-Encoder (Reranker Model)

Query va hujjat birga encoding qilinadi, bitta relevantlik ball qaytariladi.

matn17 qator
Cross-Encoder jarayoni:

  +-----------------------------------------+
  | Input: [CLS] Ucell loyihasi [SEP]       |
  |        Ucell billing tizimi             |
  |        modernizatsiyasi... [SEP]        |
  +-----------------------------------------+
                    |
                    v
          +------------------+
          | Transformer      |
          | (12-24 layers)   |
          | Full attention   |
          +------------------+
                    |
                    v
            relevance: 0.94

Afzalliklari: - Query va hujjat bir-birini "ko'radi" -- to'liq kontekst. - Aniqroq relevantlik bahosi. - "Bank loyihasi" va "Bank binosi qurilishi" farqini tushunadi.

Kamchiliklari: - Har bir (query, hujjat) juftligi uchun alohida hisoblash. - 1000 ta hujjat = 1000 ta forward pass. Sekin. - Offline precomputation mumkin emas.

Shuning uchun ikkalasi birga ishlatiladi: bi-encoder tezda 100-1000 chunk orasidan top 10-20 ni topadi, keyin cross-encoder bu 10-20 ni sinchiklab qayta tartiblaydi.


8.3 Jina Reranker v2 (Cloud Yondashuv)

Kodlar: Reranking — cross-encoder bilan qayta tartiblash 8-bob · Jina / BGE / LLM · ~90 qator

1. Ikki bosqichli qidiruv

Bi-encoder tez lekin qo'pol, cross-encoder aniq lekin sekin. Shuning uchun ularni ketma-ket ishlatamiz:

  • 1-bosqich: vector search 100 000 chunk dan 20 tasini oladi (~20 ms)
  • 2-bosqich: reranker shu 20 tasini qayta baholaydi va 5 tasini qoldiradi (~300 ms)

20 ta hujjatni cross-encoder bilan baholash mumkin. 100 000 tasini — yo'q. Mana shuning uchun ikki bosqich.

2. Jina Reranker — cloud varianti

O'z modelingizni ishga tushirmasdan ishlatish mumkin. Ko'p tilli, o'zbekcha matnda ham ishlaydi.

python25 qator
import requests

JINA_URL = "https://api.jina.ai/v1/rerank"

def jina_rerank(question: str, chunks: list[dict], top_n: int = 5) -> list[dict]:
    docs = [c["content"] for c in chunks]
    resp = requests.post(
        JINA_URL,
        headers={"Authorization": f"Bearer {settings.jina_api_key}"},
        json={
            "model": "jina-reranker-v2-base-multilingual",
            "query": question,
            "documents": docs,
            "top_n": top_n,
        },
        timeout=20,
    )
    resp.raise_for_status()

    out = []
    for item in resp.json()["results"]:
        row = dict(chunks[item["index"]])
        row["rerank_score"] = item["relevance_score"]
        out.append(row)
    return out

3. To'liq pipeline

Diqqat: rerankerga ko'p nomzod bering (20–30), chunki uning ishi — yomonlarini tashlash. 5 ta bersangiz, tanlash uchun hech narsa qolmaydi.

python21 qator
def search_and_rerank(session, question: str,
                      candidates: int = 20, top_n: int = 5) -> list[dict]:
    emb, _ = generate_query_embedding(question)
    hits = retrieve_chunks(session, emb, top_k=candidates)
    if not hits:
        return []
    try:
        return jina_rerank(question, hits, top_n=top_n)
    except Exception as exc:
        print(f"Reranker ishlamadi ({exc}) — vector tartibida qaytaramiz")
        return hits[:top_n]


question = "Ucell loyihasi haqida gapirib bering"
before = retrieve_chunks(session, generate_query_embedding(question)[0], 5)
after = search_and_rerank(session, question)

print(f"{'#':<3}{'VECTOR':<42}{'RERANK':<42}")
print("-" * 87)
for i in range(5):
    print(f"{i+1:<3}{before[i]['title'][:40]:<42}{after[i]['title'][:40]:<42}")
Natija
#  VECTOR                                    RERANK
---------------------------------------------------------------------------------------
1  Ucell billing tizimi modernizatsiyasi     Ucell billing tizimi modernizatsiyasi
2  Sharq Telekom tarmoq kengaytirish         Ucell SMS gateway integratsiyasi
3  Ucell SMS gateway integratsiyasi          Ucell tarmoq monitoringi loyihasi
4  EMAN GROUP telekom yechimlari             Ucell billing hisobot moduli
5  Ucell tarmoq monitoringi loyihasi         Sharq Telekom tarmoq kengaytirish

4. Nima o'zgardi

Sharq Telekom 2-o'rindan 5-o'ringa tushdi, EMAN GROUP umuman chiqib ketdi. Cross-encoder savol va hujjatni birga o'qigani uchun "bu Ucell haqida emas" degan xulosaga keldi.

Bi-encoder buni qila olmaydi — u hujjatni savoldan alohida ko'rgan.

5. Lokal variant — BGE reranker

Internet yoki API kaliti bo'lmasa, o'z serveringizda ishlatish mumkin. Birinchi yuklanish ~1.1 GB, keyin offline ishlaydi.

python19 qator
from sentence_transformers import CrossEncoder

_bge = None

def bge_rerank(question: str, chunks: list[dict], top_n: int = 5) -> list[dict]:
    global _bge
    if _bge is None:
        _bge = CrossEncoder("BAAI/bge-reranker-v2-m3", max_length=512)

    pairs = [(question, c["content"]) for c in chunks]
    scores = _bge.predict(pairs)

    ranked = sorted(zip(chunks, scores), key=lambda x: x[1], reverse=True)
    out = []
    for chunk, score in ranked[:top_n]:
        row = dict(chunk)
        row["rerank_score"] = float(score)
        out.append(row)
    return out
Qachon reranking kerak emas: korpus kichik bo'lsa (500 chunk gacha) va vector search allaqachon to'g'ri javob bersa — reranker faqat 300 ms qo'shadi, sifatni oshirmaydi.

Fallback majburiy. Yuqoridagi try/except ni olib tashlamang: Jina API tushsa, butun chat ishlamay qolmasligi kerak — vector tartibida javob bergan yaxshiroq.

Jina Reranker v2 -- cross-encoder asosidagi cloud reranking servisi. 100 dan ortiq tilni qo'llab-quvvatlaydi, jumladan rus va o'zbek tillari.

API chaqiruvi

python27 qator
import httpx

def _jina_rerank(query: str, documents: list[str], top_n: int = 5) -> list[dict]:
    resp = httpx.post(
        "https://api.jina.ai/v1/rerank",
        headers={
            "Authorization": "Bearer jina_xxxxxxxxxxxx",
            "Content-Type": "application/json",
        },
        json={
            "model": "jina-reranker-v2-base-multilingual",
            "query": query,
            "documents": documents,
            "top_n": top_n,
        },
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    return [
        {
            "index": r["index"],
            "relevance_score": r["relevance_score"],
            "document": documents[r["index"]],
        }
        for r in data["results"]
    ]

Ishlash tartibi

  1. query: foydalanuvchi savoli.
  2. documents: chunk matnlari ro'yxati.
  3. top_n: nechta eng tegishli chunk qaytarish.
  4. API har bir chunk uchun relevance_score (0 dan 1 gacha) qaytaradi.

Jina Reranker afzalliklari: - Maxsus relevance judgment uchun train qilingan. - Multilingual -- rus va o'zbek matnlarni tushunadi. - Tez -- 20 chunk uchun 100-300 ms. - Arzon -- kichik loyihalar uchun bepul tarif bor.


8.4 Boshqa Reranker Variantlari

Cohere Rerank

Eng sifatli tijorat yechimlardan biri. API Jina-ga o'xshash. Narxi yuqoriroq, lekin aniqlik ham yuqori. Enterprise loyihalar uchun mos.

python12 qator
def _cohere_rerank(query: str, documents: list[str], top_n: int = 5):
    resp = httpx.post(
        "https://api.cohere.ai/v1/rerank",
        headers={"Authorization": "Bearer cohere_api_key"},
        json={
            "model": "rerank-multilingual-v3.0",
            "query": query,
            "documents": documents,
            "top_n": top_n,
        },
    )
    return resp.json()["results"]

BGE Reranker (Open Source)

BAAI tomonidan ishlab chiqilgan. Lokal serverda ishga tushirish mumkin. Internet kerak emas. Sifat Jina va Cohere-dan pastroq, lekin bepul.

matn2 qator
# Ollama orqali ishga tushirish
ollama pull bge-reranker-v2-m3

ColBERT (Late Interaction)

ColBERT -- bi-encoder va cross-encoder orasidagi "oltin o'rta". U query va hujjat token-larini alohida encoding qiladi (bi-encoder singari), lekin taqqoslash token darajasida amalga oshiriladi (cross-encoder singari).

python16 qator
ColBERT jarayoni:

  Savol token-lari:     Hujjat token-lari:
  [q1, q2, q3]          [d1, d2, d3, d4, d5]
       |                        |
       v                        v
  Alohida encoding       Alohida encoding
       |                        |
       v                        v
  [vq1, vq2, vq3]       [vd1, vd2, vd3, vd4, vd5]
       |                        |
       +--- MaxSim matching ----+
       |
       v
  Har bir qi uchun eng yaqin dj topiladi:
  score = sum(max(sim(qi, dj) for j) for i)

Afzalliklari: hujjat token vektorlarini oldindan hisoblash mumkin (bi-encoder singari tez), lekin token-level matching cross-encoder-ga yaqin aniqlik beradi.

Kamchiliklari: xotira ko'p talab qiladi (har bir token uchun vektor saqlanadi).


8.5 Heuristik Chunk Tanlash (Bizning Joriy Yondashuvimiz)

Nihol loyihasining oldingi versiyasida biz LLM-ni reranker sifatida sinab ko'rdik (LLM-as-Judge). Lekin har bir savolga qo'shimcha 1-3 soniya LLM inference qo'shilishi lokal serverda sezilarli sekinlashuvga olib keldi. Shuning uchun joriy production pipeline-da reranking bosqichi olib tashlandi va uning o'rniga tez, deterministik heuristik tanlash ishlatiladi -- select_context_chunks. Bu funksiya qo'shimcha LLM chaqiruvisiz, mikrosekundlarda ishlaydi.

Joriy koddagi haqiqiy funksiya (app/services/chat.py):

python17 qator
def select_context_chunks(chunks: list[dict], question: str) -> list[dict]:
    top_chunks = chunks[:3]

    q_lower = question.lower()
    key_terms = [w[:6] if len(w) > 6 else w for w in q_lower.split() if len(w) > 3]

    extra = []
    seen_ids = {c["chunk_id"] for c in top_chunks}
    for chunk in chunks[3:]:
        text_lower = chunk["chunk_text"].lower()
        title_lower = (chunk.get("title") or "").lower()
        if any(term in text_lower or term in title_lower for term in key_terms):
            if chunk["chunk_id"] not in seen_ids:
                extra.append(chunk)
                seen_ids.add(chunk["chunk_id"])

    return (top_chunks + extra)[:8]

Qanday ishlaydi

  1. Similarity bo'yicha eng yuqori 3 chunk har doim olinadi -- vector search allaqachon yaxshi tartiblagan.
  2. Savoldan kalit so'zlar ajratiladi: 3 belgidan uzun so'zlar olinadi va 6 belgigacha qisqartiriladi. Qisqartirish o'zbek va rus tillaridagi qo'shimchalarni bevosita stemming-siz "yutish" uchun -- masalan serverlar, serverni, serverga hammasi server prefiksiga mos keladi.
  3. 4-o'rindan boshlab qolgan chunk-lar tekshiriladi: matni yoki sarlavhasida kamida bitta kalit so'z bo'lsa, kontekstga qo'shiladi.
  4. Jami maksimum 8 chunk qaytariladi.

Nega LLM-as-Judge emas?

Mezon LLM-as-Judge (eski) Heuristik tanlash (joriy)
Tezlik 1-3 soniya (qo'shimcha inference) < 1 ms
Deterministik Yo'q (LLM har safar boshqacha) Ha
Qo'shimcha resurs GPU band qiladi Yo'q
Aniqlik Yuqoriroq bo'lishi mumkin Vector search sifatiga tayanadi

Konfiguratsiyada RERANK_MODEL (dengcao/Qwen3-Reranker-8B) parametri saqlanib qolgan -- kelajakda haqiqiy cross-encoder reranker qo'shish uchun zaxira, lekin hozircha pipeline-da ishlatilmaydi.

LLM-as-Judge qanday ishlagan edi (ma'lumot uchun)

Agar o'z loyihangizda sinab ko'rmoqchi bo'lsangiz, yondashuvning mohiyati: har bir chunk-dan 500 belgigacha preview olinadi, [0], [1] indekslar bilan raqamlanadi, LLM-ga savol bilan birga yuboriladi va undan faqat eng tegishli indekslarning JSON array-i so'raladi (masalan [2, 0, 4]). JSON parse bo'lmasa yoki indekslar noto'g'ri bo'lsa -- fallback sifatida dastlabki top_n chunk qaytariladi. Kamchiliklari: chat modellar relevance judgment uchun maxsus train qilinmagan, va har savolga 1-3 soniya qo'shiladi -- aynan shu sabab biz undan voz kechdik.


8.6 RRF Formulasi -- To'liq Hisob Misoli

Faraz qiling, hybrid search natijasi bor: vector search 10 chunk qaytardi, keyword search 8 chunk qaytardi. Ba'zi chunk-lar ikkalasida ham bor.

Chunk Vector Rank Keyword Rank RRF Score
A 1 3 1/61 + 1/63 = 0.01639 + 0.01587 = 0.03226
B 2 - 1/62 + 1/1060 = 0.01613 + 0.00094 = 0.01707
C 5 1 1/65 + 1/61 = 0.01538 + 0.01639 = 0.03177
D 3 2 1/63 + 1/62 = 0.01587 + 0.01613 = 0.03200
E - 4 1/1060 + 1/64 = 0.00094 + 0.01563 = 0.01657
F 4 7 1/64 + 1/67 = 0.01563 + 0.01493 = 0.03056

Yakuniy tartib (RRF bo'yicha): A (0.032) > D (0.032) > C (0.032) > F (0.031) > B (0.017) > E (0.017)

Diqqat: Chunk A faqat vector-da 1-o'rinda, C faqat keyword-da 1-o'rinda. Lekin D ikkalasida ham yuqori (2 va 3-o'rin), shuning uchun RRF uni ham yuqoriga ko'taradi. B faqat vector-da bor (keyword-da yo'q), shuning uchun RRF ball past.

k = 60 qiymati nima uchun? Bu "smoothing" parametr. Kichik k (masalan 1) rank farqlarini kuchaytiradi -- 1-o'rin va 2-o'rin orasida katta farq. Katta k (masalan 60) rank farqlarini tekislaydi -- 1-o'rin va 5-o'rin orasida kichik farq. k = 60 -- empirik tarzda topilgan optimal qiymat.


8.7 Qachon Reranking Kerak Emas

Har doim ham reranking kerak emas. Uni o'tkazib yuborish mumkin:

  1. top_k juda kichik: Agar faqat 3-5 chunk qaytarilsa, reranking uchun material yetarli emas -- qayta tartiblashning ma'nosi yo'q.

  2. Barcha chunk-lar aniq tegishli: Agar savol juda aniq va barcha chunk-lar bitta mavzudan bo'lsa. Masalan: "Uzpromstroybank uchun IBM klaster loyihasi" -- barcha qaytgan chunk-lar aynan shu loyiha haqida.

  3. Tezlik muhim: Real-time chatbot-da har bir millisekund muhim. Reranking 1-3 soniya qo'shadi. Agar foydalanuvchi tajribasi (UX) birinchi o'rinda bo'lsa, reranking-ni o'chirish mumkin.

  4. Resurslar cheklangan: LLM-as-judge yondashuvi qo'shimcha inference talab qiladi. Agar server allaqachon yukli bo'lsa, reranking-ni vaqtincha o'chirish mantiqiy.


8.8 To'liq Pipeline -- Retrievaldan Javobgacha

Nihol loyihasidagi joriy jarayon (app/routes/chat.py dagi haqiqiy oqim):

python21 qator
# 1. Til aniqlanadi (kirill yoki lotin)
lang_code = detect_lang(question)

# 2. Savol LLM yordamida kengaytiriladi (temperature=0)
expanded_query = expand_query(question)

# 3. Query embedding (alohida EMBED_OLLAMA_URL, keep_alive=-1)
embedding = generate_query_embedding(expanded_query)

# 4. Pgvector cosine search: top_k=15, til va rubrika filtrlari
chunks = retrieve_chunks(
    session, embedding,
    top_k=rag["top_k"], threshold=rag["similarity_threshold"],
    lang_code=lang_code, rubrics=effective_rubrics,
)

# 5. Heuristik tanlash: top-3 + kalit so'z mosliklari, maksimum 8
used = select_context_chunks(chunks, question)

# 6. Prompt yaratish va LLM-ga yuborish (temperature=0)
messages = build_prompt(chunks, question, None)

Qadam-baqadam: 1. detect_lang: kirill yoki lotin -- til aniqlanadi. 2. expand_query: LLM savolni qidiruvga mos kengaytirilgan so'rovga aylantiradi (sinonimlar, qisqartmalar ochiladi). 3. generate_query_embedding: kengaytirilgan so'rov 4096 o'lchamli vektorga aylantiriladi. Embedding alohida Ollama instansiyaga boradi (EMBED_OLLAMA_URL), keep_alive=-1 bilan model doim xotirada turadi. 4. retrieve_chunks: pgvector cosine search, 15 tagacha chunk. Til va rubrika filtrlari SQL darajasida qo'llanadi. 5. select_context_chunks: heuristik tanlash -- eng yaxshi 3 + kalit so'zga mos kelganlar, jami 8 tagacha. 6. build_prompt: tanlangan chunk-lar kontekst sifatida LLM-ga yuboriladi.

Bu pipeline-da har bir qadam oldingi qadam natijasini yaxshilaydi. Query expansion retrieval sifatini oshiradi, language filtering shovqinni kamaytiradi, heuristik tanlash kontekstni ixcham saqlaydi. Natijada LLM faqat eng sifatli materialni ko'radi -- va butun jarayon bitta qo'shimcha LLM chaqiruvi (expand_query) bilan bajariladi.


</> Loyihaning haqiqiy kodi — bu bob amalda qanday ishlaydi

app/services/chat.py — heuristik chunk tanlash

python17 qator
def select_context_chunks(chunks: list[dict], question: str) -> list[dict]:
    top_chunks = chunks[:3]

    q_lower = question.lower()
    key_terms = [w[:6] if len(w) > 6 else w for w in q_lower.split() if len(w) > 3]

    extra = []
    seen_ids = {c["chunk_id"] for c in top_chunks}
    for chunk in chunks[3:]:
        text_lower = chunk["chunk_text"].lower()
        title_lower = (chunk.get("title") or "").lower()
        if any(term in text_lower or term in title_lower for term in key_terms):
            if chunk["chunk_id"] not in seen_ids:
                extra.append(chunk)
                seen_ids.add(chunk["chunk_id"])

    return (top_chunks + extra)[:8]

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar