Reranking (Qayta Tartiblash)
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):
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.
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.
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
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.
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 out3. To'liq pipeline
Diqqat: rerankerga ko'p nomzod bering (20–30), chunki uning ishi — yomonlarini tashlash. 5 ta bersangiz, tanlash uchun hech narsa qolmaydi.
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}")# 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.
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 outFallback 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
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
query: foydalanuvchi savoli.documents: chunk matnlari ro'yxati.top_n: nechta eng tegishli chunk qaytarish.- 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.
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.
# 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).
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):
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
- Similarity bo'yicha eng yuqori 3 chunk har doim olinadi -- vector search allaqachon yaxshi tartiblagan.
- 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,servergahammasiserverprefiksiga mos keladi. - 4-o'rindan boshlab qolgan chunk-lar tekshiriladi: matni yoki sarlavhasida kamida bitta kalit so'z bo'lsa, kontekstga qo'shiladi.
- 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:
-
top_k juda kichik: Agar faqat 3-5 chunk qaytarilsa, reranking uchun material yetarli emas -- qayta tartiblashning ma'nosi yo'q.
-
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.
-
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.
-
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):
# 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
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.