IlmHamroh
Data Science va sun'iy intellekt/Katta til modellari8/14-dars46 daqiqa
Mundarija (21)

25.8-dars: RAG — chunking va indeks

25-QISM — KATTA TIL MODELLARI · 8-dars


1. Kirish va motivatsiya

25.7-darsda savolga eng yaqin hujjatni topishni o'rgandik. Endi shu qidiruvni LLM bilan birlashtiramiz: RAG (Retrieval-Augmented Generation, qidiruv bilan boyitilgan generatsiya). G'oya oddiy: modelga savolni to'g'ridan-to'g'ri bermaymiz, avval o'z hujjatlarimizdan tegishli bo'laklarni topamiz va ularni promptga "manba" sifatida qo'shamiz; model javobni shu manbalarga tayanib yozadi va qaysi manbadan olganini ko'rsatadi.

Nega bu kerak? Birinchidan, bilim yangilanadi: model o'rgatilgandan keyin chiqqan buyruq yoki yangi tarif uning "boshida" yo'q, lekin indeksimizda bor — modelni qayta o'rgatmasdan (25.11-darsdagi fine-tuning dan ancha arzon) yangi bilim beramiz. Ikkinchidan, manba ko'rsatish: foydalanuvchi va auditor javob qaysi hujjatning qaysi bandidan olinganini tekshira oladi. Uchinchidan, gallyutsinatsiyani kamaytirish: model "eslab qolgan" narsani emas, oldida turgan matnni qayta aytadi (kamaytiradi, lekin yo'q qilmaydi — buni keyingi darsda o'lchaymiz).

RAG ning sifati ko'pincha modelga emas, quvurning oddiy bo'g'inlariga bog'liq: hujjat qanday o'qildi, qanday tozalandi, qanday bo'laklarga bo'lindi, qanday metadata saqlandi, indeks hujjatlar bilan sinxronmi. Bu darsda aynan shu bo'g'inlarni o'lchaymiz.

Real vaziyat. Kompaniya HR qoidalari bo'yicha ichki chat-bot ishga tushirdi. Bir oydan keyin shikoyatlar: bot ta'til muddatini noto'g'ri aytmoqda. Tekshiruv uchta sababni topdi. Birinchisi: PDF dan olingan matnda so'zlar qator oxirida defis bilan bo'lingan ("ta'-til"), va qidiruv bu bo'laklarni topmayapti. Ikkinchisi: hujjat 30 so'zlik qat'iy bo'laklarga bo'lingan — ba'zi javob jumlalari ikki bo'lak orasida qolgan, "Bu holatda muddat 14 kun" kabi jumlalar esa sarlavhasidan ajralib, nimaga tegishli ekani noma'lum bo'lib qolgan. Uchinchisi: yangi tahrirdagi nizom indeksga qo'shilgan, lekin eskisi o'chirilmagan — bot ikkalasini ham topadi va ko'pincha eskisidan javob beradi. Bu darsning to'rt misoli aynan shu uch muammoni o'lchaydi.

Bu darsda RAG quvurini boshidan oxirigacha quramiz: yuklash, tozalash, chunking strategiyalari va ularning retrieval sifatiga ta'siri, metadata va filtrlash, indeksni yangilash va kontekstni token byudjetiga sig'dirib, manba iqtiboslari bilan prompt yig'ish.

Bu darsda:

  • RAG nima va nima uchun
  • Quvur: yuklash → tozalash → chunking → embedding → indeks → retrieval → prompt
  • Chunking strategiyalari va javobning chunk chegarasida bo'linishi
  • Metadata va filtrlash: versiya, sana, bo'lim
  • Indeksni yangilash
  • Kontekstni prompt ga sig'dirish va manba iqtiboslari
  • Tuzoqlar

ℹ Mashinada LLM API yo'q. Retrieval qismi haqiqiy va o'lchanadi (TF-IDF belgi n-gramlari — 25.7-darsdagi kabi). Generator o'rnida — ekstraktiv javob tanlovchi: topilgan chunklardan savolga eng mos raqamli jumlani tanlaydi. U LLM emas va "LLM shunday javob beradi" degan da'vo emas; u retrieval natijasi javobga qanday ta'sir qilishini oddiy va takrorlanadigan tarzda ko'rsatadi. Korpus — kod ichida yaratilgan kompaniya ichki qoidalari (6 hujjat, 30 bo'lim, 180 fakt, har fakt uchun bitta savol — jami 180 savol).


2. Nazariya — chuqur tushuntirish

2.1. RAG nima va nima uchun

text
ODDIY LLM:     savol -> model -> javob (faqat o'rgatilgan bilim)
RAG:           savol -> retrieval (o'z hujjatlarimiz) -> top-k bo'lak
                     -> prompt = ko'rsatma + manbalar [1..k] + savol
                     -> model -> javob + iqtiboslar [n]

NIMA BERADI:
  bilim yangilanishi   - indeksni yangilash modelni qayta o'rgatishdan arzon va tez
  manba ko'rsatish     - har da'vo [n] bilan tekshiriladigan hujjatga bog'lanadi
  gallyutsinatsiya     - kamayadi (model oldidagi matnga tayanadi), lekin yo'qolmaydi
  maxfiylik/huquq      - kim qaysi hujjatni ko'ra oladi - metadata filtri bilan

NIMA BERMAYDI:
  retrieval topmagan narsani model ham bilmaydi (yoki o'ylab topadi)
  noto'g'ri/eskirgan hujjat topilsa - javob ham noto'g'ri/eskirgan
  murakkab xulosa (ko'p hujjatni birlashtirish) - retrieval va prompt dizayni talab qiladi

RAG va FINE-TUNING (LLM fine-tuning darsida batafsil):
  RAG         - FAKTLAR (tez o'zgaradi, manba kerak)
  fine-tuning - USLUB va FORMAT, vazifaga moslashuv (kamroq o'zgaradi)

RAG = qidiruv + prompt: javob sifati retrieval sifatidan yuqori bo'lolmaydi.

2.2. Quvur bo'g'inlari

text
1. YUKLASH      PDF, DOCX, HTML, wiki -> matn
                muammolar: sahifa sarlavhalari/raqamlari, defisli bo'linish,
                jadvallar, ikki ustunli sahifa, skanerlangan rasm (OCR)
2. TOZALASH     takrorlanuvchi sarlavha/footer ni olib tashlash,
                "ta'-\ntil" -> "ta'til", qator uzilishlarini birlashtirish,
                tuzilmani (## sarlavhalar) SAQLASH
3. CHUNKING     matnni qidiriladigan bo'laklarga bo'lish 2.3-bob
4. EMBEDDING    har chunk -> vektor 25.7-bob; hujjat va so'rov - bir xil model
5. INDEKS       vektorlar + matn + metadata (manba, bo'lim, sana, versiya)
6. RETRIEVAL    so'rov -> top-k chunk (filtrlar bilan)
7. PROMPT       ko'rsatma + raqamlangan manbalar + savol, token byudjeti ichida
8. GENERATSIYA  javob + [n] iqtiboslar; manbada yo'q bo'lsa - "bilmayman"

1-MISOL (180 savol, top-3):
  variant                          hit@3   javob to'g'ri
  xom matn + qat'iy 30 so'z        57.8%     57.8%
  tozalangan + qat'iy 30 so'z      66.1%     70.6%
  tozalangan + sarlavha + 3 jumla  96.7%     76.7%

Tozalash javob to'g'riligini +0.128 ga oshirdi (SE 0.039 — sezilarli): defis bilan bo'lingan so'zlar va har sahifadagi "MAXFIY..." sarlavhasi qidiruvni chalg'itardi. Chunking ni o'zgartirish retrieval ni (hit@3) keskin oshirdi (66.1% → 96.7%), lekin ekstraktiv javob kamroq o'sdi (+0.061, SE 0.031 — sezilarli emas): endi to'g'ri chunk kontekstda bor, lekin generator uning ichidan to'g'ri jumlani har doim ham tanlay olmaydi. Bu RAG ning muhim darsi: retrieval va generatsiyani alohida o'lchang (keyingi dars).

Quvur eng zaif bo'g'inicha kuchli: yuklash va tozalash — "zerikarli", lekin birinchi navbatdagi ish.

2.3. Chunking strategiyalari

text
QAT'IY UZUNLIK (n so'z yoki token):
  + oddiy, har qanday matnga
  - jumla o'rtasida kesadi -> javob ikki chunk orasida BO'LINADI
  2-misol: 30 so'z - javob jumlalarining 26.7% i bo'lingan

OVERLAP (qo'shni chunklar m so'z bilan ustma-ust):
  bo'linishni yo'qotadi (30 + overlap 10: 0.0%), lekin indeks kattalashadi
  (96 -> 138 chunk) va natijalarda takrorlar paydo bo'ladi

JUMLA / JUMLALAR OYNASI:
  tabiiy chegaralar, bo'linish yo'q
  - bitta jumla - kontekstsiz: "Bu holatda muddati 14 kun" - nimaning?
  2-misol: 'jumla' - yashirin mavzuli jumlalarda hit@3 2.0%

BO'LIM (sarlavha bo'yicha):
  + ma'no birligi; sarlavha kontekstni beradi
  - bo'limlar notekis: real hujjatda bir bo'lim bir necha sahifa bo'lishi mumkin

SARLAVHA + JUMLALAR OYNASI (kontekstli chunk):
  har chunk boshiga bo'lim sarlavhasini qo'shish: "Tibbiy ta'til. Bu holatda ..."
  kichik chunk + yo'qolmagan kontekst

2-MISOL (180 savol, 300 tokenli teng byudjet):
  strategiya             chunk  bo'lingan  hit@3   hit@300t   token (top-3)
  qat'iy 30                96    26.7%    67.8%     68.3%       157
  qat'iy 30 + overlap 10  138     0.0%    82.8%     93.9%       162
  qat'iy 120               24     6.1%    93.3%     82.8%       642
  jumla                   330     0.0%    72.2%     95.0%        47
  3 jumla, qadam 2        150     0.0%    93.9%     96.1%       137
  sarlavha + 3 jumla      150     0.0%    97.8%    100.0%       149
  bo'lim                   30     0.0%   100.0%    100.0%       505

Strategiyalarni solishtirishda teng kontekst byudjeti muhim: katta chunk top-3 da ko'proq matn olib keladi va hit@3 ni "sotib oladi". bo'lim hit@3 da 100%, lekin top-3 uchun 505 token; qat'iy 120 hit@3 da 93.3%, lekin 300 token byudjetga faqat bitta chunk sig'adi — 82.8%. Teng byudjetda sarlavha + 3 jumla va bo'lim ikkalasi 100%; qolganlari undan sezilarli yomon. Bizning bo'limlar qisqa (~170 token) — real hujjatlarda uzun bo'limlarni baribir bo'lish kerak, shuning uchun amaliy tavsiya — sarlavha kontekstli, jumla chegarasini hurmat qiladigan kichik chunklar.

Chunk — ma'no birligi: jumlani bo'lmang, sarlavha kontekstini saqlang va strategiyalarni teng token byudjetida taqqoslang.

2.4. Metadata va filtrlash

text
HAR CHUNK BILAN SAQLANADI:
  manba (hujjat nomi/ID), bo'lim sarlavhasi, sahifa
  versiya, kuchga kirgan sana, "amalda" belgisi
  bo'lim/departament, kirish huquqi (kim ko'ra oladi), til

FILTRLASH:
  OLDINDAN (pre-filter): faqat mos chunklar orasida qidirish
    -> top-k to'liq, hammasi mos
  KEYIN (post-filter): top-k ni topib, keyin mos kelmaganini tashlash
    -> kam natija yoki BO'SH natija (3-misol: k = 1 da 56.8% bo'sh)

3-MISOL (2024 va 2025 versiyalar, qiymati o'zgargan 74 fakt, top-3):
  usul              to'g'ri (2025)  eskirgan (2024)  chunklar
  filtrsiz              29.7%          37.8%           3.00
  keyin filtr           60.8%           0.0%           1.36
  oldindan filtr        67.6%           0.0%           3.00

NEGA FILTRSIZ YOMON:
  eski va yangi versiya matni deyarli bir xil -> embedding ularni ajrata olmaydi
  "qaysi biri amalda" - ma'no emas, METADATA savoli

KIRISH HUQUQI:
  filtr qidiruvdan OLDIN - aks holda ruxsatsiz hujjat promptga tushadi
  (LLM xavfsizligi darsida batafsil)

Versiya, sana va huquq — metadata, embedding emas; eskirgan va ruxsatsiz hujjatni qidiruvdan oldin chiqarib tashlang.

2.5. Indeksni yangilash

text
HUJJAT O'ZGARDI:
  1. o'zgarganini aniqlash: hujjat matni xeshi (sha256) yoki yangilanish sanasi
  2. shu hujjatning ESKI chunklarini o'chirish (hujjat_id bo'yicha)
  3. yangi chunklarni kodlab qo'shish
  -> faqat o'zgargan hujjat qayta kodlanadi (4-misol: 1/6 hujjat, 28 chunk)

XATOLAR (4-misol, 6 o'zgargan + 4 yangi fakt):
  yangilanmagan indeks     to'g'ri 0/10, eski qiymat 6/6
  faqat qo'shish           to'g'ri 5/10, eski qiymat 4/6  <- eski chunklar o'chirilmagan
  o'chirish + qo'shish     to'g'ri 9/10
  to'liq qayta qurish      to'g'ri 10/10

EMBEDDING MODELI O'ZGARMAYDI - SHART:
  neyron model: hujjat qo'shilganda eski vektorlar o'zgarmaydi -> inkremental to'g'ri
  TF-IDF: IDF butun korpusga bog'liq -> inkremental indeks sekin "eskiradi"
  4-misol: inkremental va to'liq qayta qurish top-3 mosligi 95.1%
  model/vektorizator almashsa -> TO'LIQ qayta qurish

AMALDA:
  hujjat_id + versiya + xesh ni har chunkda saqlang
  vaqti-vaqti bilan to'liq qayta qurish va solishtirish
  o'chirilgan hujjatlar ham indeksdan o'chsin

Yangilash = o'chirish + qo'shish; faqat qo'shish eskirgan javoblarni indeksda qoldiradi.

2.6. Kontekstni prompt ga sig'dirish va manba iqtiboslari

text
TOKEN BYUDJETI:
  kontekst oynasi katta bo'lsa ham: har token pul va kechikish (25.2, 25.6)
  ortiqcha chunklar - chalg'ituvchi (distraktor) matn

SIG'DIRISH ALGORITMI (4-misol):
  top-k (masalan 10) -> ball bo'yicha tartib
  ochko'z: byudjetga sig'sa - olamiz; sig'masa - keyingisiga o'tamiz
  deyarli takror chunklarni (overlap, Jaccard > 0.6) tashlab ketamiz

4-MISOL (180 savol, top-10 dan):
  byudjet  token  chunk  javob kontekstda  ekstraktiv to'g'ri
     60      50    1.0       80.0%            70.0%
    120     100    2.0       95.6%            78.9%
    240     220    4.5       99.4%            77.2%
    480     450    9.2      100.0%            75.6%
  kontekst ko'paysa - javob kontekstda bo'lish ehtimoli oshadi,
  lekin generator uchun chalg'ituvchilar ham ko'payadi

PROMPT TUZILISHI:
  ko'rsatma: "Faqat manbalarga tayan; har da'vodan keyin [n];
              manbada bo'lmasa - 'bilmayman' de"
  Manbalar:
    [1] (Ta'til nizomi, 2025, "O'quv ta'tili") ...
    [2] ...
  Savol: ...

TARTIB:
  eng muhim manbalarni boshiga (va/yoki oxiriga) - uzun kontekstning
  o'rtasidagi ma'lumot kamroq e'tibor olishi mumkin ("lost in the middle")
  manba raqami va metadata (nom, versiya) - iqtibos tekshiriladigan bo'lsin

Kontekst — byudjet ichidagi eng foydali, takrorlanmagan chunklar; har manba raqamlangan va metadata bilan, ko'rsatma esa "bilmayman" ga ruxsat beradi.

2.7. Tuzoqlar

Asosiy tuzoqlar: PDF matnini tozalamasdan indekslash (sahifa sarlavhalari, defisli bo'linish); tuzilmani (sarlavhalarni) yo'qotib yuborish; jumla o'rtasida kesadigan qat'iy chunklar; kontekstsiz bitta jumla chunklar ("Bu holatda ..."); strategiyalarni teng token byudjetisiz taqqoslash; metadata saqlamaslik; eskirgan versiyani filtrlamaslik yoki faqat keyin filtrlash (bo'sh natija); kirish huquqini qidiruvdan keyin tekshirish; hujjat yangilanganda eski chunklarni o'chirmaslik; embedding modelini almashtirib indeksni qayta qurmaslik; kontekstga "iloji boricha ko'p" chunk tiqish; takror (overlap) chunklarni promptga ikki marta qo'yish; manbalarni raqamlamaslik va "bilmayman" ga ruxsat bermaslik; retrieval va generatsiyani alohida o'lchamaslik.


3. Tez ma'lumotnoma

python
import hashlib
import re

# ---- tozalash ----
t = re.sub(r"([\w'])-\n(\w)", r"\1\2", xom)             # defisli bo'linish
qatorlar = [q for q in t.split("\n") if q.strip() != SAHIFA_SARLAVHASI]

# ---- kontekstli chunk: sarlavha + 3 jumla, qadam 2 ----
chunklar = [{"matn": f"{sarlavha}. " + " ".join(js[i:i + 3]),
             "hujjat_id": hujjat_id, "bolim": sarlavha, "versiya": "2025", "amalda": True}
            for sarlavha, js in bolimlar for i in range(0, len(js) - 1, 2)]

# ---- oldindan filtr ----
ball = np.where([c["amalda"] and c["bolim_turi"] in ruxsat for c in chunklar], ball, -np.inf)

# ---- yangilash ----
if hashlib.sha256(yangi_matn.encode()).hexdigest() != saqlangan_xesh[hujjat_id]:
    chunklar = [c for c in chunklar if c["hujjat_id"] != hujjat_id]     # o'chirish
    chunklar += chunkla(yangi_hujjat)                                    # qo'shish

# ---- prompt yig'ish (haqiqiy API bilan) ----
import anthropic

kontekst = "\n".join(f"[{i + 1}] ({c['hujjat_id']}, {c['versiya']}) {c['matn']}"
                     for i, c in enumerate(tanlangan))
client = anthropic.Anthropic()
javob = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1024,
    system=("Siz kompaniya qoidalari bo'yicha yordamchisiz. Faqat berilgan manbalarga tayaning. "
            "Har da'vodan keyin manba raqamini [n] ko'rinishida yozing. "
            "Javob manbalarda bo'lmasa, 'bilmayman' deng."),
    messages=[{"role": "user", "content": f"Manbalar:\n{kontekst}\n\nSavol: {savol}"}],
)

RAG: chunking va indeks xulosasi

RAG = retrieval + prompt; javob retrieval dan yaxshi bo'lolmaydi
yuklash va tozalash: sarlavha/footer, defis, tuzilmani saqlash
chunk: jumla chegarasi + sarlavha konteksti; strategiyalarni teng byudjetda solishtiring
metadata: manba, bo'lim, versiya, sana, huquq; filtr - qidiruvdan OLDIN
yangilash: xesh -> o'chirish + qo'shish; model almashsa - to'liq qayta qurish
kontekst: byudjet ichida, takrorsiz, raqamlangan manbalar, "bilmayman" ga ruxsat
retrieval va generatsiyani alohida o'lchang

4. Batafsil misollar

Misollar real numpy/sklearn bilan (Python 3.14). Korpus kod ichida yaratiladi. Retrieval — TF-IDF belgi n-gramlari (3-5); generator o'rnida — ekstraktiv jumla tanlovchi (LLM emas).

Misol 1 — To'liq quvur: yuklash, tozalash, chunking, retrieval, prompt

python
"""RAG quvuri: yuklash -> tozalash -> chunking -> embedding -> indeks -> retrieval -> prompt -> javob."""

import math
import random
import re
import textwrap
import warnings

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

HUJJATLAR = {
    "Ta'til nizomi": ["yillik ta'til", "o'quv ta'tili", "tibbiy ta'til", "haq to'lanmaydigan ta'til",
                      "ota-onalik ta'tili"],
    "Xizmat safari": ["mahalliy safar", "xorijiy safar", "safar avansi", "mehmonxona xarajati",
                      "safar hisoboti"],
    "Ish vaqti": ["kunlik ish vaqti", "tushlik tanaffusi", "qo'shimcha ish", "tungi smena",
                  "bayram navbatchiligi"],
    "Masofaviy ish": ["uydan ishlash", "gibrid jadval", "internet kompensatsiyasi",
                      "onlayn yig'ilish", "masofaviy jihozlar"],
    "Mukofotlash": ["choraklik mukofot", "yillik bonus", "tavsiya mukofoti", "loyiha mukofoti",
                    "stajyor mukofoti"],
    "IT xavfsizlik": ["parol siyosati", "noutbuk", "vpn ulanish", "elektron pochta", "zaxira nusxa"],
}
ATRIBUTLAR = [("muddati", "kun"), ("eng ko'p davomiyligi", "kun"), ("ariza berish muddati", "kun"),
              ("kompensatsiya miqdori", "ming so'm"), ("tasdiqlash muddati", "ish kuni"),
              ("oylik limiti", "soat"), ("ustama foizi", "foiz")]
TOLDIRUVCHI = ["Xodim ushbu qoidalar bilan tanishib chiqqanini imzo bilan tasdiqlaydi.",
               "Qo'shimcha savollar bo'yicha kadrlar bo'limiga murojaat qilinadi.",
               "Qoidalar barcha filiallar uchun bir xilda amal qiladi.",
               "Rahbar xodimning arizasini ko'rib chiqib, qaror qabul qiladi.",
               "Istisno holatlar alohida buyruq bilan belgilanadi.",
               "Hujjatlar elektron tizim orqali topshiriladi.",
               "Qoidani buzish intizomiy javobgarlikka olib keladi.",
               "Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga berilmaydi."]


def korpus_yarat(rng, yashirin=0.3):
    """Qaytaradi: hujjatlar [{nom, bolimlar: [(sarlavha, [jumlalar])]}] va savollar
    [{savol, qiymat, jumla, hujjat}]. yashirin - mavzu faqat sarlavhada bo'lgan jumlalar ulushi."""
    hujjatlar, savollar = [], []
    for nom, mavzular in HUJJATLAR.items():
        bolimlar = []
        for mavzu in mavzular:
            jumlalar = []
            for atr, birlik in rng.sample(ATRIBUTLAR, 6):
                qiymat = rng.randint(2, 90)
                if rng.random() < yashirin:
                    j = f"Bu holatda {atr} {qiymat} {birlik} etib belgilanadi."
                else:
                    j = f"{mavzu.capitalize()} uchun {atr} {qiymat} {birlik}ni tashkil etadi."
                jumlalar.append(j)
                savollar.append({"savol": f"{mavzu} bo'yicha {atr} qancha?", "qiymat": qiymat,
                                 "jumla": j, "hujjat": nom})
            jumlalar += rng.sample(TOLDIRUVCHI, 5)
            rng.shuffle(jumlalar)
            bolimlar.append((mavzu.capitalize(), jumlalar))
        hujjatlar.append({"nom": nom, "bolimlar": bolimlar})
    return hujjatlar, savollar


def matn(hujjat):
    return "\n\n".join(f"## {s}\n" + " ".join(j) for s, j in hujjat["bolimlar"])

SARLAVHA = "MAXFIY - FAQAT ICHKI FOYDALANISH UCHUN"


def pdf_dan_kelgandek(hujjat, rng):
    """Xom matn: 60 belgili qatorlar, so'z bo'linishi (defis), sahifa sarlavhasi va raqami."""
    qatorlar = []
    for blok in matn(hujjat).split("\n\n"):
        bosh, tana = blok.split("\n", 1)
        qatorlar.append(bosh)                                # bo'lim sarlavhasi alohida qator
        for q in textwrap.wrap(tana, 60):
            if rng.random() < 0.4 and qatorlar and " " in q:
                oldingi = qatorlar[-1].rsplit(" ", 1)
                soz, q = q.split(" ", 1)[0], q.split(" ", 1)[1]
                yarim = max(2, len(soz) // 2)
                qatorlar[-1] = " ".join(oldingi) + " " + soz[:yarim] + "-"
                q = soz[yarim:] + " " + q
            qatorlar.append(q)
        qatorlar.append("")
    xom, sahifa = [], 1
    for i, q in enumerate(qatorlar):
        if i % 14 == 0:
            xom.append(SARLAVHA)
        xom.append(q)
        if i % 14 == 13:
            xom.append(f"--- Sahifa {sahifa} ---")
            sahifa += 1
    return "\n".join(xom)


def tozala(xom):
    qatorlar = [q for q in xom.split("\n")
                if q.strip() != SARLAVHA and not re.fullmatch(r"--- Sahifa \d+ ---", q.strip())]
    t = re.sub(r"([\w'])-\n(\w)", r"\1\2", "\n".join(qatorlar))    # defis bilan bo'lingan so'zlar
    chiqish = []
    for q in t.split("\n"):
        if q.startswith("## "):
            chiqish.append("\n" + q + "\n")                    # sarlavha - alohida qator
        elif q.strip():
            chiqish.append(q.strip() + " ")                    # paragraf ichidagi uzilish -> bo'shliq
    return re.sub(r"[ \t]+", " ", "".join(chiqish)).strip()


def bolimlarga(toza):
    bolimlar = []
    for blok in re.split(r"\n(?=## )", toza.strip()):
        bosh, _, tana = blok.partition("\n")
        jumlalar = [j.strip() for j in re.split(r"(?<=\.)\s+", tana.strip()) if j.strip()]
        bolimlar.append((bosh.replace("## ", "").strip(), jumlalar))
    return bolimlar


def chunk_sarlavha(bolimlar, oyna=3, qadam=2):
    return [f"{s}. " + " ".join(js[i:i + oyna])
            for s, js in bolimlar for i in range(0, max(1, len(js) - oyna + qadam), qadam)]


def chunk_qatiy(matn_, n=30):
    s = matn_.split()
    return [" ".join(s[i:i + n]) for i in range(0, len(s), n)]


def harflar(t):
    return re.sub(r"[^a-z0-9]", "", t.lower())


def ildizlar(t):
    return {w[:5] for w in re.findall(r"[a-z']+", t.lower()) if len(w) > 3}


def ekstraktiv(savol, chunklar):
    """Stub generator emas - ekstraktiv: topilgan chunklardan eng mos raqamli jumlani tanlaydi."""
    q, eng = ildizlar(savol), (-1.0, None, None)
    for raqam, c in enumerate(chunklar):
        for j in re.split(r"(?<=\.)\s+", c):
            if re.search(r"\d", j):
                ball = len(q & ildizlar(j)) + 0.5 * len(q & ildizlar(c)) - 0.01 * raqam
                if ball > eng[0]:
                    eng = (ball, j, raqam + 1)
    son = re.search(r"\d+", eng[1]) if eng[1] else None
    return (int(son.group()) if son else None), eng[1], eng[2]


def prompt_yig(savol, chunklar, manbalar):
    kontekst = "\n".join(f"[{i + 1}] ({m}) {c}" for i, (c, m) in enumerate(zip(chunklar, manbalar)))
    return ("Faqat quyidagi manbalarga tayanib javob bering. Har da'vodan keyin manba raqamini "
            "[n] ko'rinishida yozing. Javob manbalarda bo'lmasa, 'bilmayman' deng.\n\n"
            f"Manbalar:\n{kontekst}\n\nSavol: {savol}")


def quvur(hujjatlar, savollar, rng, tozalash, strategiya):
    chunklar, manba = [], []
    for h in hujjatlar:
        xom = pdf_dan_kelgandek(h, rng)
        t = tozala(xom) if tozalash else xom
        cs = chunk_sarlavha(bolimlarga(t)) if strategiya == "sarlavha" else chunk_qatiy(t)
        chunklar += cs
        manba += [h["nom"]] * len(cs)
    tf = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), sublinear_tf=True).fit(chunklar)
    C = tf.transform(chunklar)
    top = np.argsort(-(tf.transform([s["savol"] for s in savollar]) @ C.T).toarray(), axis=1,
                     kind="stable")[:, :3]
    hit = np.array([any(harflar(s["jumla"]) in harflar(chunklar[j]) for j in t)
                    for s, t in zip(savollar, top)], float)
    togri = np.array([ekstraktiv(s["savol"], [chunklar[j] for j in t])[0] == s["qiymat"]
                      for s, t in zip(savollar, top)], float)
    return chunklar, manba, top, hit, togri


def main() -> None:
    hujjatlar, savollar = korpus_yarat(random.Random(0))

    print("=== 1. Yuklash: 'PDF dan olingan' xom matn (parcha) ===")
    xom = pdf_dan_kelgandek(hujjatlar[0], random.Random(5))
    for q in xom.split("\n")[:5]:
        print("  | " + q)
    print("\n=== 2. Tozalash ===")
    toza = tozala(xom)
    print(f"  belgilar {len(xom)} -> {len(toza)}; defisli bo'linishlar {len(re.findall(r'-' + chr(10), xom))} -> "
          f"{len(re.findall(r'-' + chr(10), toza))}")
    print("  | " + toza.split("\n")[0])
    print("  | " + textwrap.shorten(toza.split("\n")[1], 80))

    print(f"\n=== 3. Quvur variantlari ({len(savollar)} savol, top-3) ===")
    print("  variant                       chunklar   hit@3   javob to'g'ri")
    natija = {}
    for nom, t, s in [("xom + qat'iy 30", False, "qatiy"), ("tozalangan + qat'iy 30", True, "qatiy"),
                      ("tozalangan + sarlavha 3 jumla", True, "sarlavha")]:
        chunklar, manba, top, hit, togri = quvur(hujjatlar, savollar, random.Random(5), t, s)
        natija[nom] = (hit, togri, chunklar, manba, top)
        print(f"  {nom:<29} {len(chunklar):>8} {hit.mean():>7.1%} {togri.mean():>14.1%}")

    print("\n=== 4. Juftlashgan farq (javob to'g'riligi, SE) ===")
    for a, b in [("tozalangan + qat'iy 30", "xom + qat'iy 30"),
                 ("tozalangan + sarlavha 3 jumla", "tozalangan + qat'iy 30")]:
        f = natija[a][1] - natija[b][1]
        se = f.std(ddof=1) / math.sqrt(len(f))
        print(f"  {a} - {b}: {f.mean():+.3f}, SE {se:.3f} -> "
              f"{'sezilarli' if abs(f.mean()) > 2 * se else 'sezilarli emas'}")

    print("\n=== 5. Iqtiboslar bilan prompt va ekstraktiv javob (1 savol) ===")
    _, _, chunklar, manba, top = natija["tozalangan + sarlavha 3 jumla"]
    s = savollar[7]
    tanlangan = [chunklar[j] for j in top[7]]
    p = prompt_yig(s["savol"], [textwrap.shorten(c, 70) for c in tanlangan], [manba[j] for j in top[7]])
    for q in p.split("\n")[2:8]:
        print("  | " + q)
    qiymat, jumla, raqam = ekstraktiv(s["savol"], tanlangan)
    print(f"  javob: {jumla} [{raqam}]  (haqiqiy qiymat {s['qiymat']})")
    print("  \u2b50 RAG sifati quvurning eng zaif bo'g'iniga teng: tozalash va chunking - birinchi")


if __name__ == "__main__":
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        main()

Natijaning muhim qismi:

text
=== 1. Yuklash: 'PDF dan olingan' xom matn (parcha) ===
  | MAXFIY - FAQAT ICHKI FOYDALANISH UCHUN
  | ## Yillik ta'til
  | Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga
  | berilmaydi. Istisno holatlar alohida buyruq bilan
  | belgilanadi. Hujjatlar elektron tizim orqali topshiriladi.

=== 2. Tozalash ===
  belgilar 3832 -> 3458; defisli bo'linishlar 32 -> 0
  | ## Yillik ta'til
  | Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga berilmaydi. Istisno [...]

=== 3. Quvur variantlari (180 savol, top-3) ===
  variant                       chunklar   hit@3   javob to'g'ri
  xom + qat'iy 30                    110   57.8%          57.8%
  tozalangan + qat'iy 30              96   66.1%          70.6%
  tozalangan + sarlavha 3 jumla      149   96.7%          76.7%

=== 4. Juftlashgan farq (javob to'g'riligi, SE) ===
  tozalangan + qat'iy 30 - xom + qat'iy 30: +0.128, SE 0.039 -> sezilarli
  tozalangan + sarlavha 3 jumla - tozalangan + qat'iy 30: +0.061, SE 0.031 -> sezilarli emas

=== 5. Iqtiboslar bilan prompt va ekstraktiv javob (1 savol) ===
  | Manbalar:
  | [1] (Ta'til nizomi) O'quv ta'tili. O'quv ta'tili uchun ustama foizi 87 foizni [...]
  | [2] (Ta'til nizomi) O'quv ta'tili. O'quv ta'tili uchun tasdiqlash muddati 9 ish [...]
  | [3] (Ta'til nizomi) O'quv ta'tili. O'quv ta'tili uchun ariza berish muddati 80 kunni [...]
  |
  | Savol: o'quv ta'tili bo'yicha oylik limiti qancha?
  javob: O'quv ta'tili uchun oylik limiti 72 soatni tashkil etadi. [1]  (haqiqiy qiymat 72)
  ⭐ RAG sifati quvurning eng zaif bo'g'iniga teng: tozalash va chunking - birinchi

Nima ko'rsatdi: 1-bo'lim "PDF dan olingan" xom matnni ko'rsatadi: har 14 qatorda "MAXFIY..." sarlavhasi, sahifa raqamlari va qator oxirida defis bilan bo'lingan so'zlar (bizda 32 ta). Tozalash ularni olib tashladi (3832 → 3458 belgi, defisli bo'linishlar 32 → 0) va eng muhimi — ## sarlavha qatorlarini alohida saqladi, shunda keyin bo'limlarga ajratish mumkin. 3-bo'limda uchta quvur: xom matn + qat'iy 30 so'z — hit@3 57.8%, javob 57.8%; tozalangan + qat'iy 30 — 66.1% va 70.6%; tozalangan + sarlavha kontekstli chunklar — hit@3 96.7%, javob 76.7%. Tozalashning foydasi sezilarli (+0.128, SE 0.039). Chunking ni almashtirish retrieval ni 30 punktdan ko'proq oshirdi, lekin ekstraktiv javobni faqat +0.061 ga (SE 0.031 — sezilarli emas): to'g'ri chunk endi deyarli har doim kontekstda, lekin bitta bo'limning uchta chunki bir-biriga o'xshash va "muddati", "ariza berish muddati", "tasdiqlash muddati" kabi atributlar generatorni chalg'itadi. Demak, bu quvurda keyingi zaif bo'g'in — retrieval emas, generatsiya. 5-bo'lim prompt ko'rinishini ko'rsatadi: ko'rsatma, raqamlangan manbalar hujjat nomi bilan, savol; ekstraktiv javob 72 ni [1] iqtibos bilan qaytardi. Bog'liq bo'limlar: 2.1, 2.2, 2.6.

Misol 2 — Chunking strategiyalari va javobning bo'linishi

python
"""Chunking strategiyalari: qat'iy uzunlik, overlap, jumla va bo'lim - retrieval sifatiga ta'siri."""

import math
import random
import re
import warnings

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

HUJJATLAR = {
    "Ta'til nizomi": ["yillik ta'til", "o'quv ta'tili", "tibbiy ta'til", "haq to'lanmaydigan ta'til",
                      "ota-onalik ta'tili"],
    "Xizmat safari": ["mahalliy safar", "xorijiy safar", "safar avansi", "mehmonxona xarajati",
                      "safar hisoboti"],
    "Ish vaqti": ["kunlik ish vaqti", "tushlik tanaffusi", "qo'shimcha ish", "tungi smena",
                  "bayram navbatchiligi"],
    "Masofaviy ish": ["uydan ishlash", "gibrid jadval", "internet kompensatsiyasi",
                      "onlayn yig'ilish", "masofaviy jihozlar"],
    "Mukofotlash": ["choraklik mukofot", "yillik bonus", "tavsiya mukofoti", "loyiha mukofoti",
                    "stajyor mukofoti"],
    "IT xavfsizlik": ["parol siyosati", "noutbuk", "vpn ulanish", "elektron pochta", "zaxira nusxa"],
}
ATRIBUTLAR = [("muddati", "kun"), ("eng ko'p davomiyligi", "kun"), ("ariza berish muddati", "kun"),
              ("kompensatsiya miqdori", "ming so'm"), ("tasdiqlash muddati", "ish kuni"),
              ("oylik limiti", "soat"), ("ustama foizi", "foiz")]
TOLDIRUVCHI = ["Xodim ushbu qoidalar bilan tanishib chiqqanini imzo bilan tasdiqlaydi.",
               "Qo'shimcha savollar bo'yicha kadrlar bo'limiga murojaat qilinadi.",
               "Qoidalar barcha filiallar uchun bir xilda amal qiladi.",
               "Rahbar xodimning arizasini ko'rib chiqib, qaror qabul qiladi.",
               "Istisno holatlar alohida buyruq bilan belgilanadi.",
               "Hujjatlar elektron tizim orqali topshiriladi.",
               "Qoidani buzish intizomiy javobgarlikka olib keladi.",
               "Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga berilmaydi."]


def korpus_yarat(rng, yashirin=0.3):
    """Qaytaradi: hujjatlar [{nom, bolimlar: [(sarlavha, [jumlalar])]}] va savollar
    [{savol, qiymat, jumla, hujjat}]. yashirin - mavzu faqat sarlavhada bo'lgan jumlalar ulushi."""
    hujjatlar, savollar = [], []
    for nom, mavzular in HUJJATLAR.items():
        bolimlar = []
        for mavzu in mavzular:
            jumlalar = []
            for atr, birlik in rng.sample(ATRIBUTLAR, 6):
                qiymat = rng.randint(2, 90)
                if rng.random() < yashirin:
                    j = f"Bu holatda {atr} {qiymat} {birlik} etib belgilanadi."
                else:
                    j = f"{mavzu.capitalize()} uchun {atr} {qiymat} {birlik}ni tashkil etadi."
                jumlalar.append(j)
                savollar.append({"savol": f"{mavzu} bo'yicha {atr} qancha?", "qiymat": qiymat,
                                 "jumla": j, "hujjat": nom})
            jumlalar += rng.sample(TOLDIRUVCHI, 5)
            rng.shuffle(jumlalar)
            bolimlar.append((mavzu.capitalize(), jumlalar))
        hujjatlar.append({"nom": nom, "bolimlar": bolimlar})
    return hujjatlar, savollar


def matn(hujjat):
    return "\n\n".join(f"## {s}\n" + " ".join(j) for s, j in hujjat["bolimlar"])


def jumlalarga(matn_):
    return [j for j in re.split(r"(?<=\.)\s+", matn_.strip()) if j]


def qatiy(hujjat, n, overlap):
    """Hujjat matnini so'zlar bo'yicha n talik oynalarga bo'ladi (sarlavhalar ham oqimda)."""
    sozlar = matn(hujjat).replace("\n", " ").split()
    qadam = n - overlap
    return [" ".join(sozlar[i:i + n]) for i in range(0, max(1, len(sozlar) - overlap), qadam)]


def jumla_oyna(hujjat, oyna, qadam, sarlavha):
    chunklar = []
    for s, js in hujjat["bolimlar"]:
        for i in range(0, max(1, len(js) - oyna + qadam), qadam):
            bolak = " ".join(js[i:i + oyna])
            chunklar.append(f"{s}. {bolak}" if sarlavha else bolak)
    return chunklar


def bolim(hujjat):
    return [f"{s}. " + " ".join(js) for s, js in hujjat["bolimlar"]]


STRATEGIYALAR = {
    "qat'iy 30 so'z": lambda h: qatiy(h, 30, 0),
    "qat'iy 30 + overlap 10": lambda h: qatiy(h, 30, 10),
    "qat'iy 120 so'z": lambda h: qatiy(h, 120, 0),
    "jumla": lambda h: jumla_oyna(h, 1, 1, False),
    "3 jumla, qadam 2": lambda h: jumla_oyna(h, 3, 2, False),
    "sarlavha + 3 jumla": lambda h: jumla_oyna(h, 3, 2, True),
    "bo'lim": bolim,
}


def main() -> None:
    hujjatlar, savollar = korpus_yarat(random.Random(0))
    print("=== 1. Korpus ===")
    print(f"  hujjatlar {len(hujjatlar)}, bo'limlar {sum(len(h['bolimlar']) for h in hujjatlar)}, "
          f"savollar {len(savollar)}")
    yash = [s for s in savollar if s["jumla"].startswith("Bu holatda")]
    print(f"  mavzusi faqat sarlavhada bo'lgan javob jumlalari: {len(yash)}")
    print(f"  savol:  {savollar[0]['savol']}")
    print(f"  javob:  {savollar[0]['jumla']}")

    print("\n=== 2. Strategiyalar (top-3 chunk, TF-IDF belgi 3-5) ===")
    print("  strategiya              chunk  bo'lingan  hit@1  hit@3  hit@3 yashirin  token  hit@300t")
    natija = {}
    for nom, fn in STRATEGIYALAR.items():
        chunklar = [(c, h["nom"]) for h in hujjatlar for c in fn(h)]
        matnlar = [c for c, _ in chunklar]
        tf = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), sublinear_tf=True).fit(matnlar)
        C = tf.transform(matnlar)
        B = (tf.transform([s["savol"] for s in savollar]) @ C.T).toarray()
        top = np.argsort(-B, axis=1, kind="stable")[:, :3]
        hit = np.array([any(s["jumla"] in matnlar[j] for j in t) for s, t in zip(savollar, top)], float)
        hit1 = np.mean([s["jumla"] in matnlar[t[0]] for s, t in zip(savollar, top)])
        bolingan = np.mean([not any(s["jumla"] in m for m in matnlar) for s in savollar])
        yashirin = np.array([s["jumla"].startswith("Bu holatda") for s in savollar])
        token = np.mean([sum(len(matnlar[j]) for j in t) / 4 for t in top])
        byudjet = []                                     # teng kontekst byudjeti: ~300 token
        for sv, qator in zip(savollar, np.argsort(-B, axis=1, kind="stable")):
            olingan, jami = [], 0.0
            for j in qator:
                if olingan and jami + len(matnlar[j]) / 4 > 300:
                    break
                olingan.append(j)
                jami += len(matnlar[j]) / 4
            byudjet.append(any(sv["jumla"] in matnlar[j] for j in olingan))
        natija[nom] = np.array(byudjet, float)
        print(f"  {nom:<23} {len(matnlar):>5} {bolingan:>9.1%} {hit1:>6.1%} {hit.mean():>6.1%} "
              f"{hit[yashirin].mean():>14.1%} {token:>6.0f} {natija[nom].mean():>9.1%}")

    print("\n=== 3. Teng byudjetda (300 token) eng yaxshisi bilan juftlashgan farq ===")
    eng = max(natija, key=lambda n: natija[n].mean())
    for nom, hit in natija.items():
        if nom == eng:
            continue
        f = natija[eng] - hit
        se = f.std(ddof=1) / math.sqrt(len(f))
        print(f"  {eng} - {nom}: {f.mean():+.3f}, SE {se:.3f} -> "
              f"{'sezilarli' if f.mean() > 2 * se else 'sezilarli emas'}")

    print("\n=== 4. Xulosa (natijadan) ===")
    print(f"  300 tokenli byudjetda eng yaxshi: {eng} ({natija[eng].mean():.1%})")
    print("  \u2b50 Chunk - ma'no birligi: jumlani bo'lmang, sarlavha kontekstini saqlang")


if __name__ == "__main__":
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        main()

Natijaning muhim qismi:

text
=== 1. Korpus ===
  hujjatlar 6, bo'limlar 30, savollar 180
  mavzusi faqat sarlavhada bo'lgan javob jumlalari: 51
  savol:  yillik ta'til bo'yicha ustama foizi qancha?
  javob:  Yillik ta'til uchun ustama foizi 53 foizni tashkil etadi.

=== 2. Strategiyalar (top-3 chunk, TF-IDF belgi 3-5) ===
  strategiya              chunk  bo'lingan  hit@1  hit@3  hit@3 yashirin  token  hit@300t
  qat'iy 30 so'z             96     26.7%  39.4%  67.8%          56.9%    157     68.3%
  qat'iy 30 + overlap 10    138      0.0%  53.9%  82.8%          68.6%    162     93.9%
  qat'iy 120 so'z            24      6.1%  82.8%  93.3%          92.2%    642     82.8%
  jumla                     330      0.0%  71.7%  72.2%           2.0%     47     95.0%
  3 jumla, qadam 2          150      0.0%  67.8%  93.9%          82.4%    137     96.1%
  sarlavha + 3 jumla        150      0.0%  80.0%  97.8%          94.1%    149    100.0%
  bo'lim                     30      0.0% 100.0% 100.0%         100.0%    505    100.0%

=== 3. Teng byudjetda (300 token) eng yaxshisi bilan juftlashgan farq ===
  sarlavha + 3 jumla - qat'iy 30 so'z: +0.317, SE 0.035 -> sezilarli
  sarlavha + 3 jumla - qat'iy 30 + overlap 10: +0.061, SE 0.018 -> sezilarli
  sarlavha + 3 jumla - qat'iy 120 so'z: +0.172, SE 0.028 -> sezilarli
  sarlavha + 3 jumla - jumla: +0.050, SE 0.016 -> sezilarli
  sarlavha + 3 jumla - 3 jumla, qadam 2: +0.039, SE 0.014 -> sezilarli
  sarlavha + 3 jumla - bo'lim: +0.000, SE 0.000 -> sezilarli emas

=== 4. Xulosa (natijadan) ===
  300 tokenli byudjetda eng yaxshi: sarlavha + 3 jumla (100.0%)
  ⭐ Chunk - ma'no birligi: jumlani bo'lmang, sarlavha kontekstini saqlang

Nima ko'rsatdi: 180 savolning 51 tasida javob jumlasi mavzuni aytmaydi ("Bu holatda ...") — mavzu faqat bo'lim sarlavhasida. Yetti strategiya bir xil retrieval bilan solishtirildi. "Bo'lingan" ustuni — javob jumlasi hech bir chunk ichida to'liq yo'qligi: qat'iy 30 so'zda 26.7%, qat'iy 120 da 6.1% — bu savollarga retrieval qanchalik yaxshi bo'lmasin, to'liq javob topilmaydi. Overlap 10 bo'linishni 0.0% ga tushirdi va hit@3 ni 67.8% dan 82.8% ga oshirdi, lekin chunklar soni 96 dan 138 ga ko'paydi. Jumla darajasidagi chunklar hech narsani bo'lmaydi, lekin "Bu holatda ..." jumlalari sarlavhasiz qolgani uchun ularda hit@3 atigi 2.0%. Sarlavhani har chunk boshiga qo'shish buni tuzatdi: yashirin jumlalarda 94.1% (sarlavhasiz 3 jumla oynasida 82.4%). Katta chunklar (qat'iy 120, bo'lim) hit@3 da yaxshi ko'rinadi, lekin top-3 uchun 642 va 505 token olib keladi — shuning uchun oxirgi ustun teng byudjetda (300 token) baholaydi. Unda sarlavha + 3 jumla va bo'lim — 100.0%, qat'iy 120 esa 82.8% gacha tushdi (byudjetga bitta chunk sig'adi). 3-bo'lim: teng byudjetda sarlavha + 3 jumla qolgan barcha strategiyalardan sezilarli yaxshi, bo'lim bilan farq yo'q. Bizning bo'limlar qisqa (11 jumla); real hujjatda uzun bo'lim byudjetga sig'maydi, shuning uchun kontekstli kichik chunk — xavfsizroq tanlov. Bog'liq bo'limlar: 2.3.

Misol 3 — Metadata: versiyalar va filtrlash

python
"""Metadata va filtrlash: hujjat versiyalari, oldindan va keyin filtrlash, eskirgan javoblar."""

import copy
import math
import random
import re
import warnings

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

HUJJATLAR = {
    "Ta'til nizomi": ["yillik ta'til", "o'quv ta'tili", "tibbiy ta'til", "haq to'lanmaydigan ta'til",
                      "ota-onalik ta'tili"],
    "Xizmat safari": ["mahalliy safar", "xorijiy safar", "safar avansi", "mehmonxona xarajati",
                      "safar hisoboti"],
    "Ish vaqti": ["kunlik ish vaqti", "tushlik tanaffusi", "qo'shimcha ish", "tungi smena",
                  "bayram navbatchiligi"],
    "Masofaviy ish": ["uydan ishlash", "gibrid jadval", "internet kompensatsiyasi",
                      "onlayn yig'ilish", "masofaviy jihozlar"],
    "Mukofotlash": ["choraklik mukofot", "yillik bonus", "tavsiya mukofoti", "loyiha mukofoti",
                    "stajyor mukofoti"],
    "IT xavfsizlik": ["parol siyosati", "noutbuk", "vpn ulanish", "elektron pochta", "zaxira nusxa"],
}
ATRIBUTLAR = [("muddati", "kun"), ("eng ko'p davomiyligi", "kun"), ("ariza berish muddati", "kun"),
              ("kompensatsiya miqdori", "ming so'm"), ("tasdiqlash muddati", "ish kuni"),
              ("oylik limiti", "soat"), ("ustama foizi", "foiz")]
TOLDIRUVCHI = ["Xodim ushbu qoidalar bilan tanishib chiqqanini imzo bilan tasdiqlaydi.",
               "Qo'shimcha savollar bo'yicha kadrlar bo'limiga murojaat qilinadi.",
               "Qoidalar barcha filiallar uchun bir xilda amal qiladi.",
               "Rahbar xodimning arizasini ko'rib chiqib, qaror qabul qiladi.",
               "Istisno holatlar alohida buyruq bilan belgilanadi.",
               "Hujjatlar elektron tizim orqali topshiriladi.",
               "Qoidani buzish intizomiy javobgarlikka olib keladi.",
               "Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga berilmaydi."]


def korpus_yarat(rng, yashirin=0.3):
    """Qaytaradi: hujjatlar [{nom, bolimlar: [(sarlavha, [jumlalar])]}] va savollar
    [{savol, qiymat, jumla, hujjat}]. yashirin - mavzu faqat sarlavhada bo'lgan jumlalar ulushi."""
    hujjatlar, savollar = [], []
    for nom, mavzular in HUJJATLAR.items():
        bolimlar = []
        for mavzu in mavzular:
            jumlalar = []
            for atr, birlik in rng.sample(ATRIBUTLAR, 6):
                qiymat = rng.randint(2, 90)
                if rng.random() < yashirin:
                    j = f"Bu holatda {atr} {qiymat} {birlik} etib belgilanadi."
                else:
                    j = f"{mavzu.capitalize()} uchun {atr} {qiymat} {birlik}ni tashkil etadi."
                jumlalar.append(j)
                savollar.append({"savol": f"{mavzu} bo'yicha {atr} qancha?", "qiymat": qiymat,
                                 "jumla": j, "hujjat": nom})
            jumlalar += rng.sample(TOLDIRUVCHI, 5)
            rng.shuffle(jumlalar)
            bolimlar.append((mavzu.capitalize(), jumlalar))
        hujjatlar.append({"nom": nom, "bolimlar": bolimlar})
    return hujjatlar, savollar


def matn(hujjat):
    return "\n\n".join(f"## {s}\n" + " ".join(j) for s, j in hujjat["bolimlar"])


def yangi_versiya(hujjatlar, savollar, rng, ulush=0.4):
    """2025-versiya: faktlarning ~40% ida qiymat o'zgargan; qolgan matn aynan bir xil."""
    yangi, ozgargan = copy.deepcopy(hujjatlar), {}
    for s in savollar:
        if rng.random() < ulush:
            eski = s["qiymat"]
            yangi_q = eski + rng.choice([-1, 1]) * rng.randint(3, 20)
            yangi_q = yangi_q if yangi_q > 0 else eski + 25
            ozgargan[s["savol"]] = (eski, yangi_q)
            for h in yangi:
                for i, (sar, js) in enumerate(h["bolimlar"]):
                    h["bolimlar"][i] = (sar, [j.replace(f" {eski} ", f" {yangi_q} ") if j == s["jumla"]
                                              else j for j in js])
    return yangi, ozgargan


def chunklar_meta(hujjatlar, versiya, sana, amalda):
    natija = []
    for h in hujjatlar:
        for sar, js in h["bolimlar"]:
            for i in range(0, len(js) - 1, 2):
                natija.append({"matn": f"{sar}. " + " ".join(js[i:i + 3]), "manba": h["nom"],
                               "versiya": versiya, "sana": sana, "amalda": amalda,
                               "bolim": "IT" if h["nom"] == "IT xavfsizlik" else "HR"})
    return natija


def ildizlar(t):
    return {w[:5] for w in re.findall(r"[a-z']+", t.lower()) if len(w) > 3}


def ekstraktiv(savol, chunklar):
    q, eng = ildizlar(savol), (-1.0, None)
    for raqam, c in enumerate(chunklar):
        for j in re.split(r"(?<=\.)\s+", c["matn"]):
            if re.search(r"\d", j):
                ball = len(q & ildizlar(j)) + 0.5 * len(q & ildizlar(c["matn"])) - 0.01 * raqam
                if ball > eng[0]:
                    eng = (ball, j)
    return int(re.search(r"\d+", eng[1]).group()) if eng[1] else None


def main() -> None:
    rng = random.Random(0)
    eski, savollar = korpus_yarat(rng)
    yangi, ozgargan = yangi_versiya(eski, savollar, random.Random(1))
    indeks = (chunklar_meta(eski, "2024", "2024-01-15", False)
              + chunklar_meta(yangi, "2025", "2025-02-01", True))
    random.Random(2).shuffle(indeks)                  # indeks tartibi natijaga ta'sir qilmasin
    matnlar = [c["matn"] for c in indeks]
    tf = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), sublinear_tf=True).fit(matnlar)
    C = tf.transform(matnlar)

    print("=== 1. Indeks va metadata ===")
    print(f"  chunklar {len(indeks)} (2024: {sum(not c['amalda'] for c in indeks)}, "
          f"2025: {sum(c['amalda'] for c in indeks)}); qiymati o'zgargan faktlar {len(ozgargan)}")
    c = indeks[0]
    print(f"  chunk metadata: manba={c['manba']!r}, versiya={c['versiya']}, sana={c['sana']}, "
          f"amalda={c['amalda']}, bolim={c['bolim']}")

    def qidir(savol, usul, k=3):
        b = (tf.transform([savol]) @ C.T).toarray()[0]
        if usul == "oldindan filtr":
            b = np.where([c["amalda"] for c in indeks], b, -np.inf)
        tartib = np.argsort(-b, kind="stable")[:k]
        topildi = [indeks[j] for j in tartib if np.isfinite(b[j])]
        if usul == "keyin filtr":
            topildi = [c for c in topildi if c["amalda"]]
        return topildi

    print("\n=== 2. Qiymati o'zgargan faktlar bo'yicha savollar ===")
    print("  usul              to'g'ri (2025)  eskirgan (2024)  bo'sh natija  chunklar (o'rtacha)")
    tsav = [s for s in savollar if s["savol"] in ozgargan]
    natija = {}
    for usul in ["filtrsiz", "keyin filtr", "oldindan filtr"]:
        togri, eskirgan, bosh, eski_bor = [], 0, 0, 0
        for s in tsav:
            top = qidir(s["savol"], usul)
            eski_bor += len(top)
            if not top:
                bosh += 1
                togri.append(0.0)
                continue
            javob = ekstraktiv(s["savol"], top)
            togri.append(float(javob == ozgargan[s["savol"]][1]))
            eskirgan += javob == ozgargan[s["savol"]][0]
        natija[usul] = np.array(togri)
        n = len(tsav)
        print(f"  {usul:<16} {np.mean(togri):>15.1%} {eskirgan / n:>16.1%} {bosh / n:>13.1%} "
              f"{eski_bor / n:>17.2f}")

    print("\n=== 3. Juftlashgan farq (to'g'ri javob ulushi, SE) ===")
    for a, b in [("oldindan filtr", "filtrsiz"), ("oldindan filtr", "keyin filtr")]:
        f = natija[a] - natija[b]
        se = f.std(ddof=1) / math.sqrt(len(f))
        print(f"  {a} - {b}: {f.mean():+.3f}, SE {se:.3f} -> "
              f"{'sezilarli' if abs(f.mean()) > 2 * se else 'sezilarli emas'}")

    bosh1 = np.mean([not qidir(s["savol"], "keyin filtr", k=1) for s in tsav])
    print(f"  keyin filtr, k = 1: bo'sh natija {bosh1:.1%} - top-1 eski versiya bo'lsa, hech narsa qolmaydi")

    print("\n=== 4. Bo'lim bo'yicha filtr: qidiruv maydoni ===")
    it = sum(c["bolim"] == "IT" and c["amalda"] for c in indeks)
    print(f"  bolim == 'IT' va amalda: {it} chunk ({it / len(indeks):.0%} indeks) - "
          f"HR qoidalari IT savoliga aralashmaydi")

    print("\n=== 5. Xulosa (natijadan) ===")
    print(f"  filtrsiz {natija['filtrsiz'].mean():.1%} -> oldindan filtr {natija['oldindan filtr'].mean():.1%}")
    print("  \u2b50 Versiya va sana - metadata; eskirgan hujjatni qidiruvdan OLDIN chiqarib tashlang")


if __name__ == "__main__":
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        main()

Natijaning muhim qismi:

text
=== 1. Indeks va metadata ===
  chunklar 300 (2024: 150, 2025: 150); qiymati o'zgargan faktlar 74
  chunk metadata: manba='Mukofotlash', versiya=2024, sana=2024-01-15, amalda=False, bolim=HR

=== 2. Qiymati o'zgargan faktlar bo'yicha savollar ===
  usul              to'g'ri (2025)  eskirgan (2024)  bo'sh natija  chunklar (o'rtacha)
  filtrsiz                   29.7%            37.8%          0.0%              3.00
  keyin filtr                60.8%             0.0%          0.0%              1.36
  oldindan filtr             67.6%             0.0%          0.0%              3.00

=== 3. Juftlashgan farq (to'g'ri javob ulushi, SE) ===
  oldindan filtr - filtrsiz: +0.378, SE 0.060 -> sezilarli
  oldindan filtr - keyin filtr: +0.068, SE 0.035 -> sezilarli emas
  keyin filtr, k = 1: bo'sh natija 56.8% - top-1 eski versiya bo'lsa, hech narsa qolmaydi

=== 4. Bo'lim bo'yicha filtr: qidiruv maydoni ===
  bolim == 'IT' va amalda: 25 chunk (8% indeks) - HR qoidalari IT savoliga aralashmaydi

=== 5. Xulosa (natijadan) ===
  filtrsiz 29.7% -> oldindan filtr 67.6%
  ⭐ Versiya va sana - metadata; eskirgan hujjatni qidiruvdan OLDIN chiqarib tashlang

Nima ko'rsatdi: indeksda qoidalarning 2024 va 2025 versiyalari bor (har biri 150 chunk); 2025 da 74 ta faktning qiymati o'zgargan, qolgan matn aynan bir xil. Har chunk metadata bilan saqlanadi: manba, versiya, sana, "amalda" belgisi, bo'lim. Qiymati o'zgargan faktlar bo'yicha savollarda filtrsiz qidiruv faqat 29.7% to'g'ri (2025) javob berdi va 37.8% hollarda eskirgan 2024 qiymatini qaytardi — eski va yangi chunk matni deyarli bir xil, embedding ularni ajrata olmaydi. Keyin filtrlash eskirgan javoblarni yo'qotdi (0.0%), lekin kontekstda o'rtacha 1.36 chunk qoldi (top-3 ning yarmidan ko'pi eski versiya edi) va to'g'rilik 60.8%. Oldindan filtrlash to'liq top-3 ni amaldagi versiyadan oldi: 67.6%, filtrsizdan +0.378 (SE 0.060, sezilarli); keyin filtrdan farqi +0.068 (SE 0.035) — sezilarli emas, lekin keyin filtrning boshqa xavfi bor: k = 1 da savollarning 56.8% ida natija bo'sh qoladi. 67.6% ning o'zi ham 100% emas — qolgan xatolar ekstraktiv generatordan (1-misoldagi kabi). 4-bo'lim: bolim == 'IT' filtri qidiruvni indeksning 8% iga toraytiradi. Bog'liq bo'limlar: 2.4.

Misol 4 — Indeksni yangilash va kontekst byudjeti

python
"""Indeksni yangilash (hujjat o'zgarganda) va kontekstni token byudjetiga sig'dirib prompt yig'ish."""

import copy
import hashlib
import random
import re
import warnings

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer

HUJJATLAR = {
    "Ta'til nizomi": ["yillik ta'til", "o'quv ta'tili", "tibbiy ta'til", "haq to'lanmaydigan ta'til",
                      "ota-onalik ta'tili"],
    "Xizmat safari": ["mahalliy safar", "xorijiy safar", "safar avansi", "mehmonxona xarajati",
                      "safar hisoboti"],
    "Ish vaqti": ["kunlik ish vaqti", "tushlik tanaffusi", "qo'shimcha ish", "tungi smena",
                  "bayram navbatchiligi"],
    "Masofaviy ish": ["uydan ishlash", "gibrid jadval", "internet kompensatsiyasi",
                      "onlayn yig'ilish", "masofaviy jihozlar"],
    "Mukofotlash": ["choraklik mukofot", "yillik bonus", "tavsiya mukofoti", "loyiha mukofoti",
                    "stajyor mukofoti"],
    "IT xavfsizlik": ["parol siyosati", "noutbuk", "vpn ulanish", "elektron pochta", "zaxira nusxa"],
}
ATRIBUTLAR = [("muddati", "kun"), ("eng ko'p davomiyligi", "kun"), ("ariza berish muddati", "kun"),
              ("kompensatsiya miqdori", "ming so'm"), ("tasdiqlash muddati", "ish kuni"),
              ("oylik limiti", "soat"), ("ustama foizi", "foiz")]
TOLDIRUVCHI = ["Xodim ushbu qoidalar bilan tanishib chiqqanini imzo bilan tasdiqlaydi.",
               "Qo'shimcha savollar bo'yicha kadrlar bo'limiga murojaat qilinadi.",
               "Qoidalar barcha filiallar uchun bir xilda amal qiladi.",
               "Rahbar xodimning arizasini ko'rib chiqib, qaror qabul qiladi.",
               "Istisno holatlar alohida buyruq bilan belgilanadi.",
               "Hujjatlar elektron tizim orqali topshiriladi.",
               "Qoidani buzish intizomiy javobgarlikka olib keladi.",
               "Ma'lumotlar maxfiy saqlanadi va uchinchi shaxslarga berilmaydi."]


def korpus_yarat(rng, yashirin=0.3):
    """Qaytaradi: hujjatlar [{nom, bolimlar: [(sarlavha, [jumlalar])]}] va savollar
    [{savol, qiymat, jumla, hujjat}]. yashirin - mavzu faqat sarlavhada bo'lgan jumlalar ulushi."""
    hujjatlar, savollar = [], []
    for nom, mavzular in HUJJATLAR.items():
        bolimlar = []
        for mavzu in mavzular:
            jumlalar = []
            for atr, birlik in rng.sample(ATRIBUTLAR, 6):
                qiymat = rng.randint(2, 90)
                if rng.random() < yashirin:
                    j = f"Bu holatda {atr} {qiymat} {birlik} etib belgilanadi."
                else:
                    j = f"{mavzu.capitalize()} uchun {atr} {qiymat} {birlik}ni tashkil etadi."
                jumlalar.append(j)
                savollar.append({"savol": f"{mavzu} bo'yicha {atr} qancha?", "qiymat": qiymat,
                                 "jumla": j, "hujjat": nom})
            jumlalar += rng.sample(TOLDIRUVCHI, 5)
            rng.shuffle(jumlalar)
            bolimlar.append((mavzu.capitalize(), jumlalar))
        hujjatlar.append({"nom": nom, "bolimlar": bolimlar})
    return hujjatlar, savollar


def matn(hujjat):
    return "\n\n".join(f"## {s}\n" + " ".join(j) for s, j in hujjat["bolimlar"])


def chunkla(hujjat):
    return [{"matn": f"{sar}. " + " ".join(js[i:i + 3]), "hujjat_id": hujjat["nom"]}
            for sar, js in hujjat["bolimlar"] for i in range(0, len(js) - 1, 2)]


def xesh(hujjat):
    return hashlib.sha256(matn(hujjat).encode()).hexdigest()


class Indeks:
    def __init__(self, hujjatlar):
        self.chunklar = [c for h in hujjatlar for c in chunkla(h)]
        self.tf = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), sublinear_tf=True)
        self.tf.fit([c["matn"] for c in self.chunklar])
        self.xeshlar = {h["nom"]: xesh(h) for h in hujjatlar}
        self.hisoblangan = len(self.chunklar)
        self._qayta()

    def _qayta(self):
        self.C = self.tf.transform([c["matn"] for c in self.chunklar])

    def yangila(self, hujjatlar, ochirish=True):
        """Faqat xeshi o'zgargan hujjatlar: eski chunklarini o'chirish + yangilarini qo'shish."""
        ozgargan = [h for h in hujjatlar if self.xeshlar.get(h["nom"]) != xesh(h)]
        for h in ozgargan:
            if ochirish:
                self.chunklar = [c for c in self.chunklar if c["hujjat_id"] != h["nom"]]
            yangi = chunkla(h)
            self.chunklar += yangi
            self.hisoblangan += len(yangi)
            self.xeshlar[h["nom"]] = xesh(h)
        self._qayta()                     # vektorizator o'zgarmaydi - faqat yangi chunklar kodlanadi
        return len(ozgargan)

    def qidir(self, savol, k):
        b = (self.tf.transform([savol]) @ self.C.T).toarray()[0]
        t = np.argsort(-b, kind="stable")[:k]
        return [(self.chunklar[j]["matn"], float(b[j])) for j in t]


def ildizlar(t):
    return {w[:5] for w in re.findall(r"[a-z']+", t.lower()) if len(w) > 3}


def ekstraktiv(savol, chunklar):
    q, eng = ildizlar(savol), (-1.0, None)
    for raqam, c in enumerate(chunklar):
        for j in re.split(r"(?<=\.)\s+", c):
            if re.search(r"\d", j):
                ball = len(q & ildizlar(j)) + 0.5 * len(q & ildizlar(c)) - 0.01 * raqam
                if ball > eng[0]:
                    eng = (ball, j)
    return int(re.search(r"\d+", eng[1]).group()) if eng[1] else None


def byudjetga_sigdir(topilgan, byudjet):
    """Ball bo'yicha ochko'z tanlov; ko'p takrorlanadigan (overlap) chunkni tashlab ketish."""
    tanlandi, jami = [], 0
    for m, _ in topilgan:
        t = len(m) // 4
        if jami + t > byudjet:
            continue
        s = set(m.split())
        if any(len(s & set(x.split())) / len(s | set(x.split())) > 0.6 for x in tanlandi):
            continue
        tanlandi.append(m)
        jami += t
    return tanlandi, jami


def main() -> None:
    hujjatlar, savollar = korpus_yarat(random.Random(0))
    indeks = {"yangilanmagan": Indeks(hujjatlar), "faqat qo'shish": Indeks(hujjatlar),
              "o'chirish + qo'shish": Indeks(hujjatlar)}

    # "Masofaviy ish" yangilandi: 6 ta fakt qiymati o'zgardi va yangi bo'lim qo'shildi
    rng = random.Random(3)
    yangi = copy.deepcopy(hujjatlar)
    h = next(x for x in yangi if x["nom"] == "Masofaviy ish")
    tekshir = []
    for s in [s for s in savollar if s["hujjat"] == "Masofaviy ish" and not s["jumla"].startswith("Bu")][:6]:
        yq = s["qiymat"] + 11
        h["bolimlar"] = [(sar, [j.replace(f" {s['qiymat']} ", f" {yq} ") if j == s["jumla"] else j
                                for j in js]) for sar, js in h["bolimlar"]]
        tekshir.append((s["savol"], yq, s["qiymat"]))
    bolim = []
    for atr, birlik in rng.sample(ATRIBUTLAR, 4):
        q = rng.randint(2, 90)
        bolim.append(f"Kovorking markazi uchun {atr} {q} {birlik}ni tashkil etadi.")
        tekshir.append((f"kovorking markazi bo'yicha {atr} qancha?", q, None))
    h["bolimlar"].append(("Kovorking markazi", bolim + rng.sample(TOLDIRUVCHI, 3)))

    print("=== 1. Yangilash: o'zgargan hujjatni xesh bo'yicha topish ===")
    for nom, ind in indeks.items():
        n = 0 if nom == "yangilanmagan" else ind.yangila(yangi, ochirish=nom != "faqat qo'shish")
        print(f"  {nom:<22} o'zgargan hujjatlar {n}/6, chunklar {len(ind.chunklar)}, "
              f"jami kodlangan {ind.hisoblangan}")
    toliq = Indeks(yangi)
    indeks["to'liq qayta qurish"] = toliq
    print(f"  {'to' + chr(39) + 'liq qayta qurish':<22} chunklar {len(toliq.chunklar)}, "
          f"kodlangan {toliq.hisoblangan} (+ vektorizator qayta o'rgatildi)")

    print("\n=== 2. O'zgargan (6) va yangi (4) faktlar bo'yicha 10 savol (top-3, ekstraktiv) ===")
    for nom, ind in indeks.items():
        natija = [ekstraktiv(sv, [m for m, _ in ind.qidir(sv, 3)]) for sv, _, _ in tekshir]
        togri = sum(r == y for r, (_, y, _) in zip(natija, tekshir))
        eski = sum(r == e for r, (_, _, e) in zip(natija, tekshir) if e is not None)
        print(f"  {nom:<22} to'g'ri {togri}/10, eski qiymat qaytdi {eski}/6")

    print(f"\n=== 3. Inkremental va to'liq qayta qurish mosligi ({len(savollar)} + 4 savol, top-3) ===")
    savol_hammasi = [s["savol"] for s in savollar] + [t[0] for t in tekshir[6:]]
    a, b = indeks["o'chirish + qo'shish"], toliq
    mos = np.mean([{m for m, _ in a.qidir(s, 3)} == {m for m, _ in b.qidir(s, 3)} for s in savol_hammasi])
    print(f"  top-3 to'plami bir xil: {mos:.1%} (farq - IDF og'irliklari eskirganidan)")

    print(f"\n=== 4. Kontekst byudjeti: top-10 dan tokenlarga sig'dirish ({len(savollar)} savol) ===")
    asl = indeks["yangilanmagan"]                       # savollar asl hujjatlarga tegishli
    print("  byudjet  o'rtacha token  chunk soni  javob kontekstda  ekstraktiv to'g'ri")
    for byudjet in (60, 120, 240, 480):
        kont, token, soni, togri = [], [], [], []
        for s in savollar:
            tanlandi, jami = byudjetga_sigdir(asl.qidir(s["savol"], 10), byudjet)
            kont.append(any(s["jumla"] in m for m in tanlandi))
            token.append(jami)
            soni.append(len(tanlandi))
            togri.append(ekstraktiv(s["savol"], tanlandi) == s["qiymat"])
        print(f"  {byudjet:>7} {np.mean(token):>15.0f} {np.mean(soni):>11.1f} {np.mean(kont):>17.1%} "
              f"{np.mean(togri):>18.1%}")
    print("  \u2b50 Indeks - hujjatlar bilan sinxron; kontekst - byudjet ichida, eng foydali chunklar")


if __name__ == "__main__":
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        main()

Natijaning muhim qismi:

text
=== 1. Yangilash: o'zgargan hujjatni xesh bo'yicha topish ===
  yangilanmagan          o'zgargan hujjatlar 0/6, chunklar 150, jami kodlangan 150
  faqat qo'shish         o'zgargan hujjatlar 1/6, chunklar 178, jami kodlangan 178
  o'chirish + qo'shish   o'zgargan hujjatlar 1/6, chunklar 153, jami kodlangan 178
  to'liq qayta qurish    chunklar 153, kodlangan 153 (+ vektorizator qayta o'rgatildi)

=== 2. O'zgargan (6) va yangi (4) faktlar bo'yicha 10 savol (top-3, ekstraktiv) ===
  yangilanmagan          to'g'ri 0/10, eski qiymat qaytdi 6/6
  faqat qo'shish         to'g'ri 5/10, eski qiymat qaytdi 4/6
  o'chirish + qo'shish   to'g'ri 9/10, eski qiymat qaytdi 0/6
  to'liq qayta qurish    to'g'ri 10/10, eski qiymat qaytdi 0/6

=== 3. Inkremental va to'liq qayta qurish mosligi (180 + 4 savol, top-3) ===
  top-3 to'plami bir xil: 95.1% (farq - IDF og'irliklari eskirganidan)

=== 4. Kontekst byudjeti: top-10 dan tokenlarga sig'dirish (180 savol) ===
  byudjet  o'rtacha token  chunk soni  javob kontekstda  ekstraktiv to'g'ri
       60              50         1.0             80.0%              70.0%
      120             100         2.0             95.6%              78.9%
      240             220         4.5             99.4%              77.2%
      480             450         9.2            100.0%              75.6%
  ⭐ Indeks - hujjatlar bilan sinxron; kontekst - byudjet ichida, eng foydali chunklar

Nima ko'rsatdi: "Masofaviy ish" hujjatida 6 ta fakt o'zgardi va yangi "Kovorking markazi" bo'limi qo'shildi. Indeks hujjat xeshi bo'yicha faqat o'zgargan hujjatni topdi (1/6) va faqat uning chunklarini qayta kodladi (28 ta). To'rt holat solishtirildi. Yangilanmagan indeks o'zgargan 6 faktning hammasida eski qiymatni qaytardi va yangi bo'lim haqida hech narsa bilmadi (0/10). "Faqat qo'shish" (eski chunklar o'chirilmagan) — indeksda 178 chunk, yarim javoblar to'g'ri (5/10), 6 tadan 4 tasida eski qiymat qaytdi: eski va yangi chunklar raqobatlashadi. To'g'ri inkremental yangilash (o'chirish + qo'shish) — 9/10, to'liq qayta qurish — 10/10. 3-bo'lim: inkremental indeks vektorizatorni qayta o'rgatmagani uchun IDF og'irliklari biroz eskirgan — to'liq qayta qurilgan indeks bilan top-3 to'plami 95.1% savolda bir xil. Neyron embedding modelida bu farq bo'lmaydi (vektor boshqa hujjatlarga bog'liq emas); TF-IDF da esa vaqti-vaqti bilan to'liq qayta qurish kerak. 4-bo'lim kontekst byudjeti: 60 token byudjetda bitta chunk sig'adi va javob kontekstda 80.0% hollarda; 120 tokenda 95.6%, 480 da 100.0%. Lekin ekstraktiv javob 120 tokenda eng yuqori (78.9%), keyin pasaydi (77.2%, 75.6%) — ortiqcha chunklar generator uchun chalg'ituvchi. Bu ekstraktiv generatorning xulqi; LLM da ham uzun, shovqinli kontekst sifatni tushirishi mumkin, lekin buni o'z modelingizda o'lchash kerak. Bog'liq bo'limlar: 2.5, 2.6.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"RAG gallyutsinatsiyani yo'q qiladi" Kamaytiradi; retrieval topmagan yoki eskirgan narsa baribir xato beradi
"PDF dan matn olish — texnik mayda ish" 1-misol: tozalash javob to'g'riligini +0.128 ga oshirdi
"Chunk qancha katta bo'lsa, shuncha yaxshi" Teng byudjetda qat'iy 120 82.8%, sarlavha + 3 jumla 100.0%
"Qat'iy uzunlikdagi chunklar yetarli" 30 so'zda javoblarning 26.7% i ikki chunk orasida bo'lingan
"Bitta jumla — eng aniq chunk" Sarlavhasiz "Bu holatda ..." jumlalarida hit@3 2.0%
"Embedding eski va yangi versiyani ajratadi" 3-misol: filtrsiz 37.8% eskirgan javob; versiya — metadata
"Keyin filtrlash ham xuddi shunday" k = 1 da 56.8% bo'sh natija
"Yangi hujjatni qo'shish yetarli" Faqat qo'shishda 6 tadan 4 ta eski qiymat qaytdi
"Kontekstga qancha ko'p chunk, shuncha yaxshi" 4-misol: 120 tokendan keyin ekstraktiv javob pasaydi
"Retrieval yaxshilansa, javob ham shuncha yaxshilanadi" 1-misol: hit@3 +30 punkt, javob +6 punkt (sezilarli emas)

6. Keng tarqalgan xatolar va yechimlari

1. Tozalanmagan PDF matni

python
chunklar = chunkla(pdf_matn)                                        # ⚠️
chunklar = chunkla(bolimlarga(tozala(pdf_matn)))                    # ✅

2. Jumla o'rtasida kesish

python
chunklar = [" ".join(s[i:i + 30]) for i in range(0, len(s), 30)]    # ⚠️
chunklar = [f"{sar}. " + " ".join(js[i:i + 3])                     # ✅
            for sar, js in bolimlar for i in range(0, len(js) - 1, 2)]

3. Metadata siz chunk

python
indeks.add(matn)                                                    # ⚠️
indeks.add(matn, {"hujjat_id": h, "bolim": s, "versiya": v, "amalda": True})   # ✅

4. Keyin filtrlash

python
top = [c for c in qidir(savol, k=3) if c["amalda"]]                 # ⚠️ bo'sh bo'lishi mumkin
top = qidir(savol, k=3, filtr=lambda c: c["amalda"])                # ✅ oldindan

5. Yangilashda eski chunklar qoladi

python
chunklar += chunkla(yangi_hujjat)                                   # ⚠️
chunklar = [c for c in chunklar if c["hujjat_id"] != yangi_hujjat["nom"]]   # ✅
chunklar += chunkla(yangi_hujjat)

6. Byudjetsiz kontekst

python
kontekst = "\n".join(m for m, _ in qidir(savol, k=50))             # ⚠️
tanlandi, _ = byudjetga_sigdir(qidir(savol, k=10), byudjet=150)     # ✅

7. Iqtibossiz prompt

python
prompt = kontekst + "\n" + savol                                    # ⚠️
prompt = prompt_yig(savol, tanlandi, manbalar)                      # ✅ [n], "bilmayman"

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 25.2-dars (o'tilgan): kontekst oynasi va token xarajati — byudjet ichida kontekst yig'ish
  • 25.4-dars (o'tilgan): prompt engineering — RAG prompti: ko'rsatma, manbalar, "bilmayman"
  • 25.5-dars (o'tilgan): strukturali chiqish — javob + iqtiboslarni JSON ko'rinishida olish va tekshirish
  • 25.6-dars (o'tilgan): API — prefiks kesh (barqaror ko'rsatma boshida), xarajat hisobi
  • 25.7-dars (o'tilgan): embedding, gibrid qidiruv, ANN — retrieval bo'g'ini
  • Keyingi darslar: RAG: retrieval sifati va baholash — recall@k, qayta saralash, retrieval va generatsiyani alohida baholash; LLM ni baholash — javobning manbaga sodiqligi; Agentlar — qidiruv asbob sifatida; LLM xavfsizligi — hujjat ichidagi injeksiya va kirish huquqi

8. Eng yaxshi amaliyotlar

  1. Yuklashdan keyin tozalang va tuzilmani (sarlavhalar, bo'limlar) saqlang.

  2. Chunklarni jumla chegarasida kesing va har chunkka sarlavha kontekstini qo'shing.

  3. Chunking strategiyalarini teng token byudjetida, juftlashgan holda solishtiring.

  4. Har chunk bilan manba, bo'lim, versiya, sana va kirish huquqini saqlang.

  5. Eskirgan va ruxsatsiz hujjatlarni qidiruvdan oldin filtrlang.

  6. Indeksni xesh bo'yicha yangilang: o'chirish + qo'shish; model almashsa — to'liq qayta qurish.

  7. Kontekstni byudjet ichida, takrorsiz yig'ing; manbalarni raqamlang va "bilmayman" ga ruxsat bering.

  8. Retrieval (hit@k) va javob to'g'riligini alohida o'lchang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # RAG modelning o'zini o'zgartiradimi?
2.  # "ta'-\ntil" qidiruvga qanday ta'sir qiladi?
3.  # 30 so'zlik qat'iy chunkda javob jumlasi bo'linsa, top-k ni oshirish yordam beradimi?
4.  # overlap nimani tuzatadi va nimaning narxiga?
5.  # "Bu holatda muddati 14 kun" jumlasi alohida chunk bo'lsa, qaysi savolga topiladi?
6.  # nega hit@3 da katta chunklar yaxshi ko'rinadi?
7.  # eski va yangi versiyani embedding ajrata oladimi?
8.  # keyin filtrda k = 1 bo'lsa, eng yomon holat?
9.  # hujjat yangilanganda faqat yangi chunklarni qo'shsak?
10. # TF-IDF indeksini inkremental yangilashning kamchiligi?
11. # kontekstga 50 ta chunk qo'yishning ikki kamchiligi?
12. # nega promptda "bilmayman" ga ruxsat berish kerak?
Javoblar
  1. Yo'q — faqat promptga manbalar qo'shiladi; model og'irliklari o'zgarmaydi
  2. So'z ikki bo'lakka bo'linadi; so'z/n-gram mosligi yo'qoladi (1-misol: tozalash +0.128)
  3. Yo'q — hech bir chunkda to'liq jumla yo'q; bo'linish indeks qurilayotganda yuz bergan
  4. Chegaradagi bo'linishni; narxi — ko'proq chunk va natijalarda takrorlar (96 → 138)
  5. Deyarli hech biriga — mavzu yo'q (2-misol: 2.0%); sarlavha qo'shish kerak
  6. Ko'proq matn olib keladi — teng token byudjetida solishtirish kerak
  7. Deyarli yo'q — matn bir xil; versiya metadata bilan filtrlanadi
  8. Top-1 eski versiya bo'lsa, natija bo'sh (3-misol: 56.8%)
  9. Eski chunklar qoladi va raqobatlashadi (4-misol: 6 tadan 4 ta eski qiymat)
  10. IDF eskiradi; natijalar to'liq qayta qurishdan farq qiladi (95.1% moslik)
  11. Qimmat va sekin; chalg'ituvchi matn ko'payadi (4-misol: javob sifati pasaydi)
  12. Aks holda model manbada yo'q javobni o'ylab topishga undaladi

Vazifa 2: Xatolarni tuzating

python
1.  matn = pdf_dan_matn(fayl)
    chunklar = [matn[i:i + 200] for i in range(0, len(matn), 200)]

2.  top = qidir(savol, k=1)
    top = [c for c in top if c["versiya"] == "2025"]
    javob = llm(prompt_yig(savol, top))

3.  def yangila(indeks, hujjat):
        indeks.qosh(chunkla(hujjat))

4.  kontekst = "\n".join(c["matn"] for c in qidir(savol, k=40))
    prompt = kontekst + "\nSavol: " + savol

5.  chunklar = [{"matn": j} for sar, js in bolimlar for j in js]
Javoblar
python
1.  toza = tozala(pdf_dan_matn(fayl))                          # sarlavha/footer, defis
    chunklar = chunk_sarlavha(bolimlarga(toza))                # jumla chegarasi + sarlavha

2.  top = qidir(savol, k=3, filtr=lambda c: c["amalda"])       # oldindan filtr
    if not top:
        javob = "bilmayman"
    else:
        javob = llm(prompt_yig(savol, top))

3.  def yangila(indeks, hujjat):
        if xesh(hujjat) == indeks.xeshlar.get(hujjat["nom"]):
            return
        indeks.ochir(hujjat_id=hujjat["nom"])                  # eski chunklar
        indeks.qosh(chunkla(hujjat))
        indeks.xeshlar[hujjat["nom"]] = xesh(hujjat)

4.  tanlandi, _ = byudjetga_sigdir(qidir(savol, k=10), byudjet=150)
    prompt = prompt_yig(savol, tanlandi, manbalar)             # [n] va "bilmayman"

5.  chunklar = [{"matn": f"{sar}. " + " ".join(js[i:i + 3]), "bolim": sar}
                for sar, js in bolimlar for i in range(0, len(js) - 1, 2)]

Vazifa 3: Quvur

Modellang:

  1. "PDF" xom matn generatori (sarlavha, sahifa raqami, defis)
  2. Tozalash va bo'limlarga ajratish
  3. Uch quvur: xom / tozalangan / tozalangan + kontekstli chunk
  4. hit@3 va ekstraktiv javob to'g'riligi, juftlashgan SE

Vazifa 4: Chunking

Modellang:

  1. Yetti strategiya (qat'iy, overlap, jumla, oyna, sarlavha, bo'lim)
  2. Bo'lingan javoblar ulushi
  3. hit@1, hit@3, yashirin jumlalarda hit@3
  4. Teng token byudjetida solishtirish va qaror qoidasi; qo'shimcha: byudjetni 150 va 600 ga o'zgartiring

Vazifa 5: Metadata

Modellang:

  1. Ikki versiyali indeks va metadata
  2. Filtrsiz, keyin va oldindan filtrlash
  3. To'g'ri, eskirgan va bo'sh natija ulushlari
  4. Bo'lim va kirish huquqi filtri

Vazifa 6: Yangilash va kontekst

Modellang:

  1. Xesh bo'yicha o'zgarishni aniqlash
  2. Yangilanmagan / faqat qo'shish / o'chirish + qo'shish / to'liq qayta qurish
  3. Inkremental va to'liq indeks mosligi
  4. Byudjetga sig'dirish, takrorni tashlash, byudjet bo'yicha egri chiziq

Vazifa 7: O'ylash

Jamoa rahbari aytdi: "Chat-botimiz ba'zan eskirgan javob beradi va ba'zan javobni topmaydi. Keling, eng katta kontekstli modelga o'tamiz va barcha hujjatlarni to'liq promptga qo'yamiz — RAG ham, chunking ham kerak bo'lmaydi."

Javob

Qisqa javob: katta kontekst ba'zi hollarda qulay, lekin bu ikki muammoning sababi kontekst hajmida emas — ular indeks va metadata muammolari. Hamma narsani promptga qo'yish ularni hal qilmaydi va narxni keskin oshiradi.

1. Eskirgan javoblar. 3- va 4-misollar ko'rsatganidek, sabab — indeksda eski versiyaning qolishi yoki filtr yo'qligi. Barcha hujjatlarni promptga qo'ysak, ikkala versiya ham kontekstda bo'ladi — model qaysi biri amalda ekanini bilmaydi. Yechim: versiya metadata, oldindan filtr, yangilashda o'chirish + qo'shish.

2. Javobni topmaslik. Tekshirish kerak: javob indeksda bormi (tozalash, bo'lingan chunklar — 1- va 2-misollar), retrieval uni topyaptimi (hit@k), generator uni ishlatyaptimi. Bular alohida o'lchanadi; 1-misolda retrieval 96.7% bo'lsa ham javob 76.7% edi.

3. Hamma narsa promptda. Har so'rovda butun korpus uchun to'lash (25.6: kirish tokenlari har safar), kechikish, va uzun, shovqinli kontekstda kerakli ma'lumotning "yo'qolishi" (4-misolda ortiqcha kontekst javobni yaxshilamadi). Iqtibos ham qiyinlashadi. Kichik va barqaror korpus uchun (bir necha hujjat) bu variant bo'lishi mumkin — prefiks kesh bilan — lekin buni o'lchab qaror qilish kerak.

Tavsiya:

python
# 1. indeks auditi: har hujjatning versiyasi, eski chunklar bormi
# 2. metadata filtri: amalda == True (oldindan)
# 3. tozalash va kontekstli chunking
# 4. baholash to'plami: savol -> to'g'ri hujjat/jumla; hit@k va javob to'g'riligi alohida
# 5. kichik korpus bo'lsa - "hammasi promptda + kesh" variantini ham shu to'plamda solishtiring

Rahbarga javob: "Eskirgan javoblar — indeksda eski versiya qolgani uchun; buni metadata filtri va to'g'ri yangilash bilan tuzatamiz. Topilmayotgan javoblar uchun retrieval va generatsiyani alohida o'lchaymiz. Katta kontekst variantini ham sinab ko'ramiz, lekin xarajat va sifatni bir xil savollar to'plamida solishtirib qaror qilamiz."

Nimani mustahkamlaydi: 2.2, 2.3, 2.4, 2.5, 2.6-bo'limlar.


Xulosa

Bu darsda RAG quvurini boshidan oxirigacha qurdik: yuklash va tozalash, chunking strategiyalari, metadata va filtrlash, indeksni yangilash va kontekstni token byudjetiga sig'dirib, iqtiboslar bilan prompt yig'ish. Retrieval haqiqiy va o'lchandi; generator o'rnida ekstraktiv jumla tanlovchi ishlatildi.

Eng muhim uch fikr:

  1. Quvurning "oddiy" bo'g'inlari hal qiluvchi. 1-misolda tozalash javob to'g'riligini 57.8% dan 70.6% ga oshirdi (+0.128, SE 0.039); sarlavha kontekstli chunklar hit@3 ni 96.7% ga chiqardi. Lekin javob faqat 76.7% — retrieval va generatsiya alohida o'lchanishi kerak.

  2. Chunk — ma'no birligi, taqqoslash — teng byudjetda. 2-misolda 30 so'zlik qat'iy chunklarda javoblarning 26.7% i bo'lingan, sarlavhasiz jumla chunklarda "Bu holatda ..." jumlalari deyarli topilmadi (2.0%). 300 tokenli teng byudjetda sarlavha + 3 jumla 100.0%, qat'iy 120 so'z 82.8%.

  3. Metadata va yangilash eskirgan javoblarni to'xtatadi. 3-misolda filtrsiz qidiruv o'zgargan faktlarda 37.8% eskirgan javob berdi; oldindan filtr to'g'rilikni 29.7% dan 67.6% ga oshirdi, keyin filtr esa k = 1 da 56.8% bo'sh natija berdi. 4-misolda "faqat qo'shish" 6 tadan 4 ta eski qiymatni qaytardi, o'chirish + qo'shish 9/10 to'g'ri. Kontekst byudjetini oshirish javobni 120 tokendan keyin yaxshilamadi.

Keyingi darsda RAG: retrieval sifati va baholash: recall@k va MRR bilan retrieval ni baholash, qayta saralash (reranking), gibrid qidiruv, javobning manbaga sodiqligi va retrieval hamda generatsiya xatolarini ajratib o'lchashni ko'ramiz.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
25.8-dars: RAG — chunking va indeks — IlmHamroh