chunker.py — matnni bo'laklash
Buni o'zingiz yozasiz. RAG ning eng muhim qismi.
Nega bo'laklash kerak
- Embedding modelining chegarasi bor — cheksiz matn qabul qilmaydi
- Uzun matn = suyuq vektor — 50 sahifa bitta vektorda hech narsa anglatmaydi
- LLM ga kontekst beramiz — 50 sahifa sig'maydi, kerakli 3 ta bo'lak sig'adi
Ikkita strategiya
Bu yerda tanlov bor va uni tushunish muhim.
A. Sliding window (overlap bilan)
[==========]
[==========]
[==========]
^^^^^ ustma-ust
Matnni belgilar bo'yicha teng kesadi, chegaralarda ustma-ust qoldiradi.
B. Paragraf bo'yicha (BIZNING TANLOV)
Paragraf 1 \ Paragraf 2 > 1-chunk (limitga sig'guncha) Paragraf 3 / Paragraf 4 \ Paragraf 5 > 2-chunk
Matnni \n\n bo'yicha bo'ladi, paragraflarni limitga sig'guncha
yig'adi.
Biz IKKALASINI birga ishlatamiz: paragraf bo'yicha bo'lamiz VA overlap qo'shamiz.
Nega paragraf: paragraf — muallif yaratgan tabiiy ma'no birligi. Uni buzmasak, gap o'rtasidan kesilmaydi.
Nega overlap ham kerak: paragraf chegarasi GAPNI buzmaydi, lekin KONTEKSTNI buzadi. Misol:
Paragraf 5: "Nihol Premium tarifi eng qimmat variant." --- chunk chegarasi --- Paragraf 6: "Narxi oyiga 450 000 so'm."
6-paragraf yolg'iz qolsa — "narxi" nimaning narxi? Chunk o'zi javob bermaydi. Savol "Premium qancha?" bo'lsa, bu chunk topilsa ham foydasiz.
Overlap bilan 6-chunk 5-paragrafni ham o'z ichiga oladi va ma'no butun qoladi.
Kod
from app.config import settings MAX_TOKENS = 2000 CHARS_PER_TOKEN = 3 def _estimate_tokens(text: str) -> int: """Token sonini TAXMINIY hisoblaydi.""" return len(text) // CHARS_PER_TOKEN def _overlap_parts(parts: list[str], overlap_tokens: int) -> list[str]: """Yopilgan chunk oxiridan keyingi chunk uchun paragraf tanlaydi.""" if overlap_tokens <= 0 or not parts: return [] tanlangan, jami = [], 0 for p in reversed(parts): # oxiridan orqaga t = _estimate_tokens(p) if jami + t > overlap_tokens and tanlangan: break tanlangan.insert(0, p) jami += t # HAMMASINI qaytarmaymiz — aks holda sikl oldinga siljimaydi if len(tanlangan) >= len(parts): tanlangan = tanlangan[1:] return tanlangan def chunk_text(text: str, title: str = "", max_tokens: int = MAX_TOKENS, overlap: int | None = None) -> list[str]: if overlap is None: overlap = settings.chunk_overlap # .env dagi CHUNK_OVERLAP # Overlap chunk dan katta bo'lsa — CHEKSIZ SIKL. Chegaralaymiz. overlap = max(0, min(overlap, max_tokens // 2)) if not text or not text.strip(): return [] prefix = title + "\n\n" if title else "" full_text = prefix + text if _estimate_tokens(full_text) <= max_tokens: return [full_text] paragraphs = text.split("\n\n") chunk_limit = max_tokens - _estimate_tokens(prefix) chunks = [] current_parts = [] current_tokens = 0 for paragraph in paragraphs: paragraph = paragraph.strip() if not paragraph: continue para_tokens = _estimate_tokens(paragraph) would_exceed = (current_tokens + para_tokens) > chunk_limit if would_exceed and current_parts: chunks.append(prefix + "\n\n".join(current_parts)) # OVERLAP: oxirgi paragraf(lar)ni keyingi chunk ga ko'chiramiz current_parts = _overlap_parts(current_parts, overlap) current_tokens = sum(_estimate_tokens(p) for p in current_parts) current_parts.append(paragraph) current_tokens += para_tokens if current_parts: chunks.append(prefix + "\n\n".join(current_parts)) return chunks
Qator-qator
_estimate_tokens — nega taxminiy?
return len(text) // CHARS_PER_TOKEN # 3 ga bo'lamiz
Aniq token sanash uchun modelning tokenizeri kerak — sekin va qo'shimcha kutubxona. Taxmin yetarli.
Nega 3, 4 emas? Ingliz tili uchun odatda ~4 belgi = 1 token. O'zbek va rus tillari uchun ko'proq token chiqadi (kamroq belgi bir tokenga). 3 ni tanlash — xavfsizlik chekinishi: haqiqiy token soni taxmindan kam bo'ladi, limitdan oshmaymiz.
prefix — sarlavha har bir chunkda
prefix = title + "\n\n" if title else ""
Har bir chunk sarlavha bilan boshlanadi. Nega?
Chunk bazadan yolg'iz chiqadi — konteksti yo'q. "Narxi 250 000 som" degan chunk nima haqida? Sarlavha bo'lsa — "Nihol tariflari" — ma'no tiklanadi. Embedding ham aniqroq bo'ladi.
chunk_limit = max_tokens - prefix_tokens
Prefix ham joy egallaydi. Uni hisobga olmasak, sarlavha uzun bo'lganda chunk limitdan oshadi.
would_exceed and current_parts
# parcha if would_exceed and current_parts:
current_parts sharti muhim. Busiz: birinchi paragrafning o'zi
limitdan katta bo'lsa, bo'sh chunk qo'shiladi va paragraf
yo'qoladi.
Bu shart bilan: katta paragraf yolg'iz chunk bo'ladi. Limitdan oshadi, lekin yo'qolmaydi. Ma'lumot yo'qotgandan ko'ra limitdan oshgan yaxshiroq.
Oxirgi qism
if current_parts: chunks.append(prefix + "\n\n".join(current_parts))
Sikl tugagach, yig'ilgan paragraflar qoladi. Bu qatorsiz oxirgi chunk yo'qoladi. Eng ko'p unutiladigan xato.
Overlap ishlayotganini KO'RING
matn = "\n\n".join(f"{i}-paragraf. Matn {i} haqida. " * 8 for i in range(20)) for ov in (0, 50, 200): c = chunk_text(matn, title="Sinov", max_tokens=400, overlap=ov) ust = 0 for a, b in zip(c, c[1:]): pa = set(a.split("\n\n")[1:]); pb = set(b.split("\n\n")[1:]) ust += len(pa & pb) print(f"overlap={ov:<4} chunk={len(c):<3} ustma-ust paragraf={ust}")
Natija:
overlap=0 chunk=4 ustma-ust paragraf=0 overlap=50 chunk=5 ustma-ust paragraf=4 overlap=200 chunk=6 ustma-ust paragraf=10
Overlap = 0 bo'lsa 4 chunk, 50 bo'lsa 5 chunk. Ko'proq chunk — chunki ma'lumot takrorlanadi. Bu NORMAL va shu narsa uchun to'laymiz: biroz ko'proq joy, lekin ancha yaxshi kontekst.
overlap oshgan sari ustma-ust tushgan paragraflar ham ko'payadi:
4 dan 10 ga. Bu to'g'ridan-to'g'ri sozlamaga bog'liq.
Chegaraviy holatlarni sinang
# overlap chunk dan KATTA — cheksiz sikl bo'lmasligi kerak c = chunk_text(matn, title="T", max_tokens=100, overlap=999) print(f"{len(c)} chunk — sikl tugadi") print(chunk_text("")) # [] print(len(chunk_text("Salom", title="T"))) # 1 # matn yo'qolmadimi asl = set(matn.split()) yig = set(" ".join(chunk_text(matn, max_tokens=400, overlap=50)).split()) print("yo'qolgan:", asl - yig or "yo'q")
Sinash (asosiy)
matn = "\n\n".join(f"Bu {i}-paragraf. " * 20 for i in range(30)) b = chunk_text(matn, title="Sinov hujjati", max_tokens=500) print(f"chunk soni : {len(b)}") print(f"o'lchamlar : {[len(x) for x in b]}") print(f"tokenlar : {[_estimate_tokens(x) for x in b]}") print(f"sarlavha bormi: {all(x.startswith('Sinov hujjati') for x in b)}")
Barcha matn saqlanganini tekshiring:
asl = set(matn.split()) yigilgan = set(" ".join(b).split()) print("yo'qolgan so'zlar:", asl - yigilgan)
Bo'sh to'plam chiqishi kerak.
MAX_TOKENS ni qanday tanlash
| max_tokens | Qachon |
|---|---|
| 200-500 | FAQ, qisqa javoblar, aniq qidiruv |
| 1000-2000 | umumiy hujjatlar (BIZNING TANLOV: 2000) |
| 4000+ | ilmiy maqola, uzun kontekst kerak bo'lsa |
2000 token ≈ 6000 belgi ≈ 1.5 sahifa.
Katta chunk — ko'proq kontekst, lekin qidiruv aniqligi pasayadi (vektor "suyuqlashadi"). Kichik chunk — aniq, lekin kontekst yetishmaydi.
Buni o'lchash kerak, taxmin qilish emas.
Amalda ko'ring
python -c " from scraper.chunker import chunk_text, _estimate_tokens m = open('hujjat.txt').read() b = chunk_text(m, title='Hujjat') print(f'{len(m)} belgi -> {len(b)} chunk') for i, c in enumerate(b[:3], 1): print(f' {i}: {_estimate_tokens(c)} token, {len(c)} belgi') "
Kutilmagan natija normal: kichik sahifadan bitta chunk chiqadi. 1000 belgi ≈ 333 token — bu 2000 limitdan ancha kam, shuning uchun bo'linmaydi. Bu to'g'ri xatti-harakat, xato emas.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.