B · Chunking
Günün sorusunun cevabı hr-leave’in 198 satırından tek bir tablo satırında: | 7 | 22 |. Dokümanı nasıl böldüğümüz, aramanın o satırı bulup bulamayacağını belirliyor.
Neden bölüyoruz?
Her soruda 8 dokümanın hepsini modele vermek pahalı; metnin çoğunun soruyla ilgisi yok. Dokümanları parçalara böleriz; arama yalnızca soruya en yakın birkaç parçayı seçer. Bu parçaların adı chunk.
Birinci yol: her 300 karakterde bir kes
fixedSize(doc, 300): içeriğe bakmadan her 300 karakterde bir makas; başlık, tablo, kelime tanımaz. Kod kısa bir fonksiyon. Herkes işe bununla başlar. 300 bizim seçimimiz, bir kural değil.src/steps/3-chunk.tsiçinde chunker’ıfixedyapın venpm run step -- 3çalıştırın.Makas tabloya denk geliyor
c06, başlıktaki “Annual” kelimesini ikiye bölüyor: “Ann” c06’da, “ual” c07’de. c07, 5. yılın satırını “| 5 | 2” diye bitiriyor; c08 “2 |” ile başlıyor.
data/3-chunks.jsoniçindehr-leave@2025#c07vehr-leave@2025#c08kayıtlarını bulun: tablo başlığı hangisinde?Başlıksız satır ne anlatır?
Cevap satırı
| 7 | 22 |c08’de, ama başlığı c07’de kaldı: 7 yıl mı, 22 gün mü, c08 söylemiyor; “annual” kelimesi de geçmiyor.Model yine de cevap veriyor
Prompt’a giden 3 kaynak arasında c08 yok. Model yine de şunu yazdı:
“An employee with 7 years of service gets at most 7 working days [1].”
Kaynak [1] c11 ve “at most 7 working days.” diye başlıyor. Cümlenin başı c10’da kaldı: “one block of leave can be” (tek bir izin bloğu … olabilir). Makas, 7’nin neyi saydığını gizledi. Kaynak var ama sayı yanlış.
npm run questionçalıştırın.İkinci yol: dokümanın kendi başlıklarından kes
bySection(doc): her## n.başlığında keser ve her parçanın önüne[Annual Leave Policy › 3. Annual Paid Leave]ekler. Tablo, onu açıklayan başlıkla aynı parçada kalır.Aynı soru, doğru cevap
Section chunk’larla cevap: “An employee with 7 years of service gets 22 working days of annual leave [1].”, kaynak
hr-leave@2025#3. Değişen tek şey makasın yeri.Chunker’ısection’a döndürün,npm run step -- 3ve yenidennpm run question.Bedeli: büyük parça, ortak ön ek
Section: 60 chunk, ortalama 695 karakter (fixed: 138, ortalama 294); büyük parça prompt’u daha çabuk doldurur.
[Doküman › Bölüm]ön eki A’daki sayfa başlığına benziyor ama bilgi taşıyor. Bedeli: aynı dokümanın chunk’ları birbirine daha çok benzer (E sayfası).
Log’da c08’e bakın. npm run question şimdilik bir kara kutuyla arıyor; vektörleri ve depoyu C ve D sayfalarında açacağız.
$ npm run step -- 3 (chunker: fixed)
━━ STEP 3/8 · CHUNK IN data/2-clean.json (8 documents) WHAT fixed: a cut every 300 characters, blind to headings and tables OUT 138 chunks · avg 294 chars · min 72 · max 300
the chunk holding "| 7 |" → hr-leave@2025#c08 (sections 3) │ 2 | │ | 6 | 22 | │ | 7 | 22 | │ | 8 | 22 | │ | 9 | 22 | │ | 10 | 25 | │ | 11 | 25 | │ | 12 | 25 | │ | 13 | 25 | │ | 14 | 25 | │ | 15 or more | 28 | │ │ Leave requests go to the manager on the HR portal at least 15 working days before the leave starts. For leave longer than 10 working days without a break, the department d TIME 3 ms → data/3-chunks.json$ npm run question
━━ QUESTION OF THE DAY How many working days of annual leave does an employee with 7 years of service get? HOW chunker fixed · in-memory search (black box) · rerank on · filter {"edition":"2025","access":["all"]}
An employee with 7 years of service gets at most 7 working days [1].
[1] hr-leave@2025#c11 Annual Leave Policy › §4 [2] hr-leave@2025#c20 Annual Leave Policy › §7 [3] hr-leave@2025#c23 Annual Leave Policy › §8Aynı adım, chunker section:
$ npm run step -- 3 (chunker: section)
━━ STEP 3/8 · CHUNK IN data/2-clean.json (8 documents) WHAT section: a cut at every '## n.' heading, title › heading kept in front OUT 60 chunks · avg 695 chars · min 103 · max 1514
the chunk holding "| 7 |" → hr-leave@2025#3 (sections 3) │ [Annual Leave Policy › 3. Annual Paid Leave] │ Part-time staff get the same number as full-time staff. The HR portal shows the balance for each person. Questions go to the unit's HR business partner. │ │ | Years of service (full years) | Annual paid leave (working days) | │ |---|---| │ | 1 | 16 | │ | 2 | 16 | │ | 3 | 18 | │ | 4 | 18 | │ | 5 | 22 | │ | 6 | 22 | │ | 7 | 22 | │ | 8 | 22 | │ | 9 | 22 | │ | 10 | 25 | │ | 11 | 25 | │ | 12 | 25 | │ | 13 | 25 | │ | 14 | 25 | │ | 15 or more | 28 | │ │ Leave requests go to the manager on the HR portal at least 15 working days before the leave starts. For leave longer than 10 working days without a break, the department director must also approve. │ │ Up to 5 unused working days can move to the next calendar year. For the conditions of this carry-over, see section 7. TIME 5 ms → data/3-chunks.json$ npm run question
━━ QUESTION OF THE DAY How many working days of annual leave does an employee with 7 years of service get? HOW chunker section · in-memory search (black box) · rerank on · filter {"edition":"2025","access":["all"]}
An employee with 7 years of service gets 22 working days of annual leave [1].
[1] hr-leave@2025#3 Annual Leave Policy › §3 [2] hr-leave@2025#7 Annual Leave Policy › §7 [3] hr-leave@2025#4 Annual Leave Policy › §4
LEDGER (.cache/question-ledger.jsonl) 12:52 Typically, an employee with 7 years of servic… bare model, no documents 12:52 An employee with 7 years of service gets at m… chunker fixed · in-memory search (black box) · rerank on · filter {"e… 12:53 An employee with 7 years of service gets 22 w… chunker section · in-memory search (black box) · rerank on · filter {…İki bölme yolu ve aralarındaki seçim, src/steps/3-chunk.ts içinde. Seçimde üçüncü bir satır da var: paragraph. Onu aşağıda siz yazıyorsunuz.
// TOGGLE chunker — keep exactly one line active, then: npm run step -- 3 (and npm run question)const CHUNKER: ChunkerName = "section"; // default// const CHUNKER: ChunkerName = "fixed"; // alternative// const CHUNKER: ChunkerName = "paragraph"; // alternative/** * What it does: cuts a document into equal-size chunks, blind to headings and tables. * * Cut every `size` characters, blind to what is there — mid-word, mid-table, mid-sentence. * The baseline everyone starts with. * @example fixedSize(doc, 300).length // ≈ doc.text.length / 300 */export function fixedSize(doc: CleanDoc, size = 300): Chunk[] { const chunks: Chunk[] = []; for (let at = 0, n = 0; at < doc.text.length; at += size, n++) { const text = doc.text.slice(at, at + size); if (!text.trim()) continue; chunks.push({ ...base(doc), id: `${doc.id}@${doc.edition}#c${String(n).padStart(2, "0")}`, sections: sectionsIn(doc.text, at, at + size), text, }); } return chunks;}/** * What it does: cuts a document at its numbered headings, with title and heading in front. * * Cut at the document's own headings ("## 3. Annual Paid Leave") and put the document title and * heading in front of every chunk. A table then stays with the heading that explains it, and even a * chunk that is mostly numbers says what it is about. * @example bySection(doc)[0].text // "[Annual Leave Policy › 1. Purpose and Scope]\n…" */export function bySection(doc: CleanDoc): Chunk[] { const parts = doc.text.split(/\n(?=## \d+\.)/); const chunks: Chunk[] = []; for (const part of parts) { const m = part.match(/^## ((\d+)\..*)\n([\s\S]*)$/); const [heading, n, body] = m ? [m[1]!.trim(), m[2]!, m[3]!.trim()] : ["", "0", part.replace(/^# .*\n?/, "").trim()]; if (!body) continue; const prefix = heading ? `[${doc.title} › ${heading}]` : `[${doc.title}]`; const pieces = body.length > MAX_SECTION_CHARS ? packParagraphs(body, MAX_SECTION_CHARS) : [body]; pieces.forEach((piece, i) => chunks.push({ ...base(doc), id: `${doc.id}@${doc.edition}#${n}${pieces.length > 1 ? `.${i + 1}` : ""}`, sections: [n], text: `${prefix}\n${piece}`, }), ); } return chunks;}| Bölme yolu | Nereden keser | Ne için iyi | Dikkat | Repoda |
|---|---|---|---|---|
| Sabit boyut | Her N karakterde bir | Hızlı başlangıç, her metin | Tabloyu, cümleyi, kelimeyi bile böler | Var: fixed |
| Bölüm (section) | Dokümanın kendi başlıklarından | Başlıkları belli dokümanlar | Uzun bölüm büyük bir parça olur | Var: section |
| Paragraf | Boş satırlardan; paragraflar bir sınıra kadar toplanır | Başlığı az olan metinler | Parça başlığını kaybedebilir | Var: paragraph, siz yazıyorsunuz |
| Recursive splitter | Önce büyük ayraçlar; parça uzunsa daha küçükleri | Karışık dokümanlar | Ayraçları ve boyutu siz seçersiniz | Yok |
| Cümle | Her cümlenin sonundan | Kısa bilgiler, SSS sayfaları | Tek cümle çoğu zaman az şey söyler | Yok |
| Semantic | Cümleler arasında anlamın değiştiği yerden | Yapısı olmayan uzun metinler | Her cümleye bir embedding; sonucu kestirmek zor | Yok |
| Parent-child | Küçük parçalarla ara, modele onları içeren büyük parçayı ver | Hem isabetli arama hem tam bağlam | İki boyut saklanır ve bağlanır; prompt uzar | Yok |
Kesmenin birçok yolu var
Fixed ve section, kesmenin iki yolu. Tablo yaygın olanları gösteriyor; üçü bizim repoda var. Üçüncüsü, paragraf, şimdilik boş. Onu bugün siz yazıyorsunuz.
Overlap bir yol değil, bir ayar
Overlap bir kesme yolu değil; tablodaki her yola eklenebilen bir ayar. Komşu parçalar birkaç cümleyi paylaşır. Böylece makasın denk geldiği fikrin bütün kalma şansı artar. Bedeli: daha çok parça, tekrar eden metin.
Recursive: önce büyük kesimler
Recursive splitter önce büyük ayraçları dener: bölümler, sonra paragraflar, sonra cümleler. Parça hâlâ uzunsa bir küçüğüne iner. Bizim
bySectionbunun küçük bir örneği: önce başlıklardan keser, 1500 karakteri aşan bölümü paragraflarından yeniden böler.Parent-child: küçükle bul, büyükle oku
Parent-child parçaları iki boyutta saklar. Arama küçükleri kullanır; böylece tek bir satırı bile bulabilir. Model onu içeren büyük parçayı alır ve başlığı da görür. Bugün bunu kurmuyoruz.
Hangi yol en iyisi?
Dokümanlarınıza ve sorularınıza bağlı. O yüzden tahmin etmeyin. Her yol aynı sınava girer:
npm run eval. Onu Kaliteyi ölçmek sayfasında koşacağız. Şimdi üçüncü yolu siz yazıyorsunuz ve günün sorusunda deniyorsunuz.src/steps/3-chunk.tsiçindebyParagraph’ı yazın. Chunker’ıparagraphyapıpnpm run step -- 3çalıştırın:OUTsatırı 85 chunk göstermeli.
Dosyanın en altında byParagraph hâlâ boş. Şimdi onu siz yazıyorsunuz:
// ── YOUR TURN ───────────────────────────────────────────────────────────────────────────────────// A third way to cut: by paragraph. It sits between fixed and section.// 1. Remove the "# title" line. Cut the rest at blank lines.// Pack the paragraphs into pieces of up to `max` characters. packParagraphs() does this.// 2. Put the title in front of each piece, like bySection does: "[title]\n" + piece.// 3. Give each chunk the sections it touches. fixedSize() above shows how:// it calls sectionsIn() on the text it cuts, with the start and the end of the piece.// 4. Give each chunk an id like "hr-leave@2025#p03". base() fills the other labels.// Check: switch the TOGGLE chunker to "paragraph" and run npm run step -- 3 → 85 chunks.// Then run npm run question.// Then switch the TOGGLE back to "section" and run npm run step -- 3 again.// We measure it on the Measuring quality page.
/** * What it does: cuts a document at blank lines, packs paragraphs up to a size limit, puts the title in front. * * Paragraph chunks with a title prefix, each at most `max` characters (plus the prefix). * Each chunk lists every section it touches. * @example byParagraph(doc, 600).every((c) => c.text.startsWith(`[${doc.title}]`)) // true */export function byParagraph(doc: CleanDoc, max = 600): Chunk[] { throw new Error("byParagraph() is not written yet — see the YOUR TURN note above it in src/steps/3-chunk.ts");}1 · Makası kendiniz görün. src/steps/3-chunk.ts içinde TOGGLE chunker bloğunda "section"; // default satırının başına // koyun, "fixed"; // alternative satırındaki // işaretini kaldırın. npm run step -- 3 çalıştırın: log, | 7 | satırını taşıyan parçayı (hr-leave@2025#c08) bulur ve parçanın tamamını gösterir.
2 · Parçaları açın. data/3-chunks.json içinde hr-leave@2025#c07 ve hr-leave@2025#c08 kayıtlarını bulun. Tablo başlığı hangisinde, | 7 | 22 | hangisinde? sections alanı ne diyor? Sonra yanlış cevabın kaynağı hr-leave@2025#c11 kaydını bulun ve ilk kelimelerini okuyun.
3 · Cevabı sorun. npm run question → “An employee with 7 years of service gets at most 7 working days [1].” Kaynak gösteriyor ama sayı yanlış. “at most 7 working days” sözleri sorudan değil, c11’den geliyor. İlk çalıştırmada biraz sürer: kara kutu chunk’ları o an embed ediyor.
4 · Geri alın. Toggle’ı section’a döndürün, npm run step -- 3 ve yeniden npm run question → 22 iş günü, kaynak [1] hr-leave@2025#3.
5 · Üçüncü yol: paragraf. src/steps/3-chunk.ts dosyasının altında byParagraph boş duruyor. Onu derste birlikte yazıyoruz. Üstündeki YOUR TURN notu dört adım veriyor:
# başlıksatırını çıkarın, kalanı boş satırlardan kesin.packParagraphs()paragrafları en fazlamaxkarakterlik parçalara toplar.- Her parçanın önüne
[${doc.title}]ekleyin;bySectionda böyle yapıyor. - Her chunk’a dokunduğu bölümleri yazın:
sectionsIn()’e kestiğiniz metni, parçanın başladığı ve bittiği yeri verin.fixedSizede böyle yapıyor. - Her chunk’a
hr-leave@2025#p03gibi bir id verin. Öteki alanlarıbase()doldurur.
Şimdi kontrol edin ve günün sorusunda deneyin:
TOGGLE chunkerbloğunda"section"; // defaultsatırının başına//koyun,"paragraph"; // alternativesatırındaki//işaretini kaldırın.npm run step -- 3→OUTsatırına bakın: 85 chunk.| 7 |satırıhr-leave@2025#p05chunk’ında.npm run question→ “An employee with 7 years of service gets 22 working days of annual leave [1].” Kaynak[1] hr-leave@2025#p05. Defterinize bir satır daha eklenir: paragraf.- Tek soru, paragrafın section kadar iyi olup olmadığını söyleyemez. Bunu sonra, Kaliteyi ölçmek sayfasında ölçüyoruz.
- Toggle’ı
section’a döndürün venpm run step -- 3’ü yeniden çalıştırın. Sonraki sayfalar section chunk’larının üstüne kuruluyor.
Geride kaldıysanız: npm run catchup -- 3.
3-chunk.ts, chunker paragraph: | 7 | satırını taşıyan chunk doğrudan tabloyla başlıyor; tablonun başlığı, 3. Annual Paid Leave, içinde yok:
$ npm run step -- 3 (chunker: paragraph)
━━ STEP 3/8 · CHUNK IN data/2-clean.json (8 documents) WHAT paragraph: a cut at blank lines, packed up to 600 characters, title kept in front OUT 85 chunks · avg 500 chars · min 94 · max 625
the chunk holding "| 7 |" → hr-leave@2025#p05 (sections 3) │ [Annual Leave Policy] │ | Years of service (full years) | Annual paid leave (working days) | │ |---|---| │ | 1 | 16 | │ | 2 | 16 | │ | 3 | 18 | │ | 4 | 18 | │ | 5 | 22 | │ | 6 | 22 | │ | 7 | 22 | │ | 8 | 22 | │ | 9 | 22 | │ | 10 | 25 | │ | 11 | 25 | │ | 12 | 25 | │ | 13 | 25 | │ | 14 | 25 | │ | 15 or more | 28 | │ │ Leave requests go to the manager on the HR portal at least 15 working days before the leave starts. For leave longer than 10 working days without a break, the department director must also approve. │ │ Up to 5 unused working days can move to the next calendar year. For the conditions of this carry-over, see section 7. TIME 9 ms → data/3-chunks.jsonSık sorulan
Bölüm başlığı “Sık sorulan”Neden 300? Başka bir sayı tabloyu kurtarmaz mıydı?
Bazen. Başka bir sayı makası başka bir yere düşürür. Bu dokümanda başka boyutları da denedik: 250, 350, 450, 500 ya da 1000 karakterde başlık satırı ve | 7 | satırı aynı parçada kalır. 300 tutmaz; 300’ü, tabloyu bölen bir boyut olduğu için kullanıyoruz. Sorun aynı kalır: makas içeriğe bakmıyor. Hangi sayı olursa olsun makas bir gün bir tablonun ya da bir cümlenin ortasına denk gelir.
Parçalar biraz örtüşse (overlap)?
Komşu parçaların birkaç yüz karakteri paylaşması yaygın bir yöntem: makasın düştüğü yerdeki başlık bazen komşu parçaya da düşer. Garantisi yoktur, parça sayısı ve depolama artar. Bu korpusta ölçmedik.
Dokümanın başlıkları yoksa ne yaparız?
Paragraflardan (SIRA SENDE 5), cümlelerden ya da anlamın değiştiği yerden (semantic chunking) bölebilirsiniz. Hangisinin kazandığını tahmin etmeyin, ölçün: Kaliteyi ölçmek sayfasındaki npm run eval iki chunker’ı aynı soru setinde karşılaştırıyor.
Büyük parça her zaman daha mı iyi?
Hayır. Büyük parça prompt’u uzatır (maliyet ve süre) ve farklı konuları tek vektörde karıştırır. Bu yüzden bySection, 1500 karakteri aşan bir bölümü paragraflarından yeniden böler (MAX_SECTION_CHARS, bizim seçtiğimiz sınır).
Fixed chunk'ta sections: 2,3 ne demek?
Makas bir bölüm başlığının üstünden geçtiğinde parça iki bölüme birden dokunur; sections alanı bunu kaydeder. Değerlendirme (Kaliteyi ölçmek) doğru cevabı bölüm düzeyinde kontrol ettiği için bu alan önemli.