İçeriğe geç

B · Chunking

bir kere (ingest)her sorudaYükleTemizleChunk'laEmbedVector DBSoruEmbedAraRerankPromptCevap
7 yıllık bir çalışanın yıllık izni kaç iş günü?
İngilizce, logların yazdığı gibi: “How many working days of annual leave does an employee with 7 years of service get?”
→ fixed → “An employee with 7 years of service gets at most 7 working days [1].” ✗ · section → “An employee with 7 years of service gets 22 working days of annual leave [1].” ✓
chunker fixed → section · in-memory search (black box) · rerank on

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.

hr-leave198 satırböl| 7 | 22 |chunk’laren iyi 3’üpromptsoru + 3 chunk8 dokümanın tamamı değil, yalnızca seçilen parçalar modele gider
fixed · her 300 karakterde bir kes… ## 3. Annc06c07ual Paid LeavePart-time staff get the … HR business partner.| Years of service (full years) | Annual paid … ||—|—|| 1 | 16 |⋮| 4 | 18 || 5 | 2başlık c07’de kaldı: c08’deki “7” ve “22” neyin sayısı?c082 || 6 | 22 || 7 | 22 || 8 | 22 |⋮| 15 or more | 28 |Leave requests … a break, the department director …c09✂ 300 (bizim seçimimiz), 5. yılın “22”sini böldüsection · başlıklardan kes[Annual Leave Policy › 2. Probation and …]#2başlık ve tablo aynı chunk’ta#3[Annual Leave Policy › 3. Annual Paid Leave]Part-time staff get the … HR business partner.| Years of service (full years) | Annual paid … ||—|—|| 1 | 16 |⋮| 6 | 22 || 7 | 22 || 8 | 22 |⋮| 15 or more | 28 |Leave requests … must also approve.Up to 5 … see section 7.[Annual Leave Policy › 4. Planning and …]#4fixed: 138 chunk · ortalama 294 karaktersection: 60 chunk · ortalama 695 karakter
  1. 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.

  2. 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.ts içinde chunker’ı fixed yapın ve npm run step -- 3 çalıştırın.
  3. 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.json içinde hr-leave@2025#c07 ve hr-leave@2025#c08 kayıtlarını bulun: tablo başlığı hangisinde?
  4. 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.

  5. 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.
  6. İ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.

  7. 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 -- 3 ve yeniden npm run question.
  8. 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)
$ 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
$ 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 › §8

Aynı adım, chunker section:

npm run step -- 3 (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
$ 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.

src/steps/3-chunk.ts
// 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
src/steps/3-chunk.ts
/**
* 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;
}
src/steps/3-chunk.ts
/**
* 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
  1. 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.

  2. 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.

  3. 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 bySection bunun küçük bir örneği: önce başlıklardan keser, 1500 karakteri aşan bölümü paragraflarından yeniden böler.

  4. 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.

  5. 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.ts içinde byParagraph’ı yazın. Chunker’ı paragraph yapıp npm run step -- 3 çalıştırın: OUT satırı 85 chunk göstermeli.

Dosyanın en altında byParagraph hâlâ boş. Şimdi onu siz yazıyorsunuz:

src/steps/3-chunk.ts
// ── 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ık satırını çıkarın, kalanı boş satırlardan kesin. packParagraphs() paragrafları en fazla max karakterlik parçalara toplar.
  • Her parçanın önüne [${doc.title}] ekleyin; bySection da 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. fixedSize de böyle yapıyor.
  • Her chunk’a hr-leave@2025#p03 gibi bir id verin. Öteki alanları base() doldurur.

Şimdi kontrol edin ve günün sorusunda deneyin:

  • TOGGLE chunker bloğunda "section"; // default satırının başına // koyun, "paragraph"; // alternative satırındaki // işaretini kaldırın.
  • npm run step -- 3 → OUT satırına bakın: 85 chunk. | 7 | satırı hr-leave@2025#p05 chunk’ı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 ve npm 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)
$ 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.json
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.

Bu farkı model değil, chunker yarattı.