# Octocode 0.18.0: Retrieval Benchmark ที่ทำซ้ำได้ และ GraphRAG File-Level Expansion

> Octocode 0.18.0 มาพร้อม semantic code search benchmark ที่ทำซ้ำได้ — 127 queries ที่คัดมาอย่างดีพร้อม ground truth ระดับช่วงบรรทัด รวมถึง harness ของ Loc-Bench บน 560 GitHub issue จริง — และเพิ่ม GraphRAG file-level expansion ที่ดึงไฟล์ที่เกี่ยวข้องเชิงโครงสร้างเข้ามาใน search ก่อน reranking โอเพนซอร์ส, local-first, Apache-2.0

[0.17.1 ให้คุณรันทุก embedding ที่ Octocode สร้างบนฮาร์ดแวร์ที่คุณควบคุมเอง](/blog/octocode-0-17-1-local-embedding-providers) — OpenAI-compatible server ใด ๆ, โมเดลใด ๆ และโค้ดของคุณไม่เคยออกจากเครือข่ายของคุณ รีลีสนั้นเกี่ยวกับว่า search รัน _ที่ไหน_

**0.18.0 เกี่ยวกับว่า search ดีจริงแค่ไหน — และพิสูจน์ด้วยตัวเลขที่คุณทำซ้ำได้เอง**

เครื่องมือ code-search ส่วนใหญ่อ้างเรื่องคุณภาพ แต่น้อยมากที่เผยแพร่ benchmark ที่คุณรันเองได้ รีลีสนี้ให้มาสองตัว: retrieval benchmark ที่คัดมาอย่างดีรันบน source code ของ Octocode เอง และ harness ของ [Loc-Bench](https://github.com/czlll/Loc-Bench_V1) บน 560 GitHub issue จริง นอกจากนี้ยังเพิ่ม **GraphRAG file-level expansion** — ขั้นตอนของ search ที่ดึงไฟล์ที่เกี่ยวข้องเชิงโครงสร้างเข้ามาก่อนที่ reranker จะให้คะแนน

นี่คือสิ่งที่เปลี่ยนไป และสิ่งที่ตัวเลขบอก — รวมถึงส่วนที่ไม่ได้ผลด้วย

---

## ทำไมต้อง benchmark code search ตั้งแต่แรก?

เมื่อคุณเปลี่ยน chunking heuristic หรือ fusion weight คุณอยากรู้ว่า retrieval ดีขึ้นหรือแย่ลง การดูด้วยตาไม่กี่ query บอกไม่ได้ — มันแค่ยืนยันสิ่งที่คุณเชื่ออยู่แล้ว คำตอบที่ซื่อสัตย์อย่างเดียวคือ ชุด query ที่ตายตัว, ground truth ที่ตายตัว, และตัวเลขที่ขยับได้

ดังนั้น 0.18.0 จึงมาพร้อม benchmark ของจริง มันอยู่ใน [`benchmark/`](https://github.com/Muvon/octocode/tree/master/benchmark) ในรีโป และวัด pipeline ทั้งหมดของ Octocode แบบ end to end: chunking → embedding → vector search → hybrid fusion → reranking

### Ground truth

`code.csv` มี **127 queries** แต่ละ query annotate ด้วย code location ที่ถูกต้อง 1–3 ตำแหน่งในรูปของช่วงบรรทัด:

```
query,result1,result2,result3
```

แต่ละ result คือ `src/path/file.rs:start-end:relevance` โดย relevance `2` คือ primary (ตอบ query โดยตรง) และ `1` คือ secondary (context ที่มีประโยชน์) ผลการ search จะ match ด้วย **การซ้อนทับของช่วงบรรทัด (line-range overlap)** — hit ที่บรรทัด 40–90 match กับ ground truth 45–92 เพราะช่วงตัดกัน

Corpus ถูก **pin ไว้ที่ commit เดียว** (`b1771ba`) ground truth annotate เทียบกับเลขบรรทัดเป๊ะ ๆ เหล่านั้น ดังนั้นการ index checkout อื่นจะทำให้ช่วงบรรทัดเลื่อนและทำให้คะแนนพังเงียบ ๆ pin ก่อน แล้วค่อย index

วิธีสร้าง ground truth สำคัญ เพราะ answer key ที่ลวก ๆ ทำให้ benchmark ไหน ๆ ก็โกหก:

1. **Candidate ที่สร้างโดย AI** ครอบคลุมทุก module หลักและไฟล์เอกสาร
2. **Source verification** — ทุกไฟล์และช่วงบรรทัดที่อ้างถึงถูกอ่านและตรวจสอบเทียบกับโค้ดจริง
3. **Multi-agent validation** — agent หลายตัวขนานกันตรวจซ้ำช่วงบรรทัด, path และระดับ relevance อย่างอิสระ
4. **การแก้ไขที่อิงผล search** — ถ้า search เจอ logic เดียวกันในตำแหน่งทางเลือกที่ถูกต้อง ตำแหน่งนั้นจะถูก _เพิ่ม_ เป็น secondary result ไม่ใช่ลบทิ้ง

### Hard queries

27 query สุดท้ายจาก 127 เป็น adversarial โดยตั้งใจ: **ภาษาธรรมชาติที่ไม่สะท้อนชื่อ function หรือ comment ใด ๆ** มันทดสอบความเข้าใจเชิงความหมาย ไม่ใช่ดวงเรื่อง keyword ตัวอย่าง:

| เจตนาของ query                            | ทำไมถึงยาก                                                     |
| ----------------------------------------- | -------------------------------------------------------------- |
| ป้องกัน infinite loop ใน chunk ที่ซ้อนทับ | guard clause 5 บรรทัด ไม่มี keyword โค้ดใน query               |
| ข้าม indexing เมื่อรีโปไม่เปลี่ยน         | ฝังลึก 100+ บรรทัดในฟังก์ชัน 900 บรรทัด                        |
| ประมาณ token แบบหยาบโดยไม่มี tokenizer    | คำตอบคือบรรทัดเดียว: `s.len() / 4`                             |
| ป้องกัน reindexing พร้อมกันใน MCP server  | `AtomicBool` + `compare_exchange` ซึ่งเป็น concurrency pattern |

ถ้า standard queries ได้คะแนนใกล้ 1.0 แต่ hard queries ตามหลัง แสดงว่า benchmark กำลังทำหน้าที่: มันเปิดเผยจุดอ่อนของ retrieval จริง ๆ แทนที่จะประจบเครื่องมือ

### Metrics

IR metric มาตรฐานสี่ตัว ทั้งหมด "ยิ่งสูงยิ่งดี":

- **Hit@k** — มีผลที่ถูกต้อง _สักตัว_ อยู่ใน top k ไหม? (coverage)
- **MRR** — reciprocal rank ของผลที่ถูกต้อง _ตัวแรก_ (hit ตัวแรกอยู่สูงแค่ไหน?)
- **NDCG@10** — ให้รางวัลกับการวางผลที่ relevant _ที่สุด_ ไว้บนสุด โดยถ่วงน้ำหนักทั้งระดับ relevance และตำแหน่ง (คุณภาพการจัดอันดับ)
- **Recall@k** — เจอ ground-truth block _ทั้งหมด_ คิดเป็นสัดส่วนเท่าไร? (ความครบถ้วน)

นิยามและตัวอย่างที่คำนวณให้ดูอยู่ใน [`benchmark/README.md`](https://github.com/Muvon/octocode/tree/master/benchmark)

---

## ตัวเลข: พื้น (floor) แบบ local ไม่ใช้ key

ผลเหล่านี้ใช้ stack **local เต็มรูปแบบ ไม่ใช้ API key** — `jina-embeddings-v2-base-code` ผ่าน `fastembed`, **ไม่มี reranker** ทำให้มันเป็น _floor_ ไม่ใช่ ceiling: reranker แบบเสียเงินที่เข้าใจโค้ดจะดันให้สูงขึ้น รันเทียบกับ source code ของ Octocode เอง, 127 code queries:

| Config                                    | Hit@5     | Hit@10    | MRR       | NDCG@10   | Recall@10 |
| ----------------------------------------- | --------- | --------- | --------- | --------- | --------- |
| Dense vector อย่างเดียว                   | 0.598     | 0.717     | 0.485     | 0.528     | 0.671     |
| Hybrid, RRF weights ค่าเริ่มต้น (0.7/0.3) | 0.598     | 0.717     | 0.485     | 0.528     | 0.671     |
| **Hybrid, ปรับเอียงไป keyword (0.3/0.7)** | **0.732** | **0.835** | **0.572** | **0.620** | **0.807** |

มีสองอย่างที่สะดุดตา

**RRF weights ค่าเริ่มต้นทำตัวเหมือน vector อย่างเดียว** เมื่อ fusion เอียง 0.7 ไปทาง dense signal ผล vector จะครอบงำ และ keyword signal แทบไม่นับ — สองแถวบนเหมือนกันเป๊ะ

**การเอียงไปทาง keyword คือชัยชนะก้อนใหญ่และฟรีสำหรับโค้ด** พลิก weights เป็น 0.3/0.7 — ให้ BM25/keyword signal นำ — แล้ว Hit@5 พุ่ง **+22%** และ Recall@10 **+20%** โดยไม่มีต้นทุนเพิ่ม ทำไม? โค้ดเต็มไปด้วย identifier ที่ตรงเป๊ะ — `compare_exchange`, `get_code_blocks_by_path`, `RelationshipDirection` — และการ match token แบบตรงตัวมีน้ำหนักมากเป็นพิเศษเมื่อสิ่งที่คุณค้นหา _คือ_ token พอดี ความคล้ายเชิงความหมายล้วน ๆ ทำให้มันเบลอ แต่ keyword fusion ทำให้มันคม

นี่คือผลแบบที่คุณจะเจอก็ต่อเมื่อวัดเท่านั้น มันไม่ได้ตรงสัญชาตญาณเลยที่สำหรับโค้ดคุณจะอยากให้ keyword _มีน้ำหนักมากกว่า_ embedding

---

## สิ่งที่ไม่ช่วย (และทำไมมันอยู่ใน benchmark)

benchmark ที่รายงานแต่ชัยชนะคือการตลาด ตัวนี้รายงาน regression ด้วย — matrix ครบ 6 variant อยู่ใน [`benchmark/RESULTS.md`](https://github.com/Muvon/octocode/tree/master/benchmark/RESULTS.md)

**reranker ทั่วไปทำให้แย่ลง** การใส่ local cross-encoder แบบ off-the-shelf (`bge-reranker-base`) ทับบน config ที่ดีที่สุดไม่ได้ช่วย — มัน _regress_ Hit@5 จาก **0.732 กลับลงมา 0.598** ลบล้างชัยชนะจากการปรับ keyword ทั้งหมด และยังช้ากว่าราว ๆ **4 เท่า** (≈1900s เทียบกับ ≈490s ต่อรอบ)

บทเรียนไม่ใช่ว่า "reranker ไม่ดี" แต่คือ **code retrieval ต้องการ reranker ที่เข้าใจโค้ด** reranker ที่เทรนบนงานเขียนทั่วไปให้คะแนน `fn verify_hmac` เทียบกับ "webhook signature validation" แย่กว่าที่ hybrid fusion ทำได้อยู่แล้วเสียอีก reranker ที่ปรับมาเพื่อโค้ดอย่าง `voyage:rerank-2.5` เป็นอีกเรื่อง — แต่คุณต้อง verify บน eval ของคุณเอง ไม่ใช่เดาเอา benchmark นี่แหละที่ทำให้คุณทำได้

---

## GraphRAG File-Level Expansion

ฟีเจอร์เด่นเคียงคู่กับ benchmark ทั้ง dense และ keyword search ให้คะแนน block แบบแยกเดี่ยว — มันไม่รู้ว่าไฟล์ที่คุณต้องการ _import_ ไฟล์ที่มันเจอ หรือ _call_ ฟังก์ชันที่นิยามไว้ข้าง ๆ **GraphRAG file-level expansion** ปิดช่องว่างนั้น

หลังจาก hybrid search รอบแรกผลิต candidate ที่จัดอันดับแล้ว Octocode จะหยิบตัวที่แข็งแกร่งสุดไม่กี่ตัว เดิน GraphRAG graph เพื่อหาไฟล์ที่ **เกี่ยวข้องเชิงโครงสร้างผ่าน imports, calls หรือ extends** ดึง block จำนวนหนึ่งจากไฟล์เพื่อนบ้านเหล่านั้น แล้วส่ง candidate set ที่ _ขยายแล้ว_ ให้ reranker:

```rust
// Seeds are already RRF-ranked; only expand from the strongest few.
const SEED_LIMIT: usize = 10;        // expand from top 10 hits
const MAX_NEIGHBOR_FILES: usize = 8; // pull at most 8 related files
const BLOCKS_PER_FILE: usize = 3;    // 3 blocks per neighbor
```

เพื่อนบ้านถูกให้คะแนนด้วย `weight × confidence` ข้ามทุก seed file โดยลิงก์ที่แข็งแกร่งสุดมาก่อน มันเป็นระดับไฟล์เพราะ graph เป็นระดับไฟล์ (`CodeNode.id == relative path`) และมันเป็น **best-effort**: error ใด ๆ ของ graph จะ degrade ไปเป็นการส่งผลเดิมผ่านตรง ๆ — ไม่เคยทำให้ search พัง

เปิดใช้ใน `config.toml`:

```toml
[search]
graph_expansion = false  # default; requires [graphrag] enabled
```

มันมาแบบ **ปิดเป็นค่าเริ่มต้น** โดยตั้งใจ นี่คือส่วนที่ซื่อสัตย์จาก benchmark:

> `+graph` = GraphRAG file-level expansion ซึ่ง **ขยับผลก็ต่อเมื่อ reranker ให้คะแนน set ที่ขยายแล้วใหม่เท่านั้น**

หากไม่มี reranker การ expansion จะเพิ่ม candidate แต่ไม่เปลี่ยนการจัดอันดับ — นี่คือเหตุที่ `hybrid_30_70` และ `hybrid_30_70+graph` ได้คะแนนเท่ากันใน matrix แบบ local ไม่ใช้ key ข้างบน ผลตอบแทนของ expansion มาเมื่อ reranker (ที่เข้าใจโค้ด) ให้คะแนน pool ที่ใหญ่ขึ้นใหม่และดันไฟล์ที่เกี่ยวข้องเชิงโครงสร้างซึ่ง similarity ดิบจัดอันดับไว้ต่ำเกินไปขึ้นมา การ expansion แบบไร้เดียงสายังอาจ _เจือจาง_ ความแม่นยำได้ด้วย ดังนั้นคำแนะนำที่ฝังใน comment ของ config จึงตรงไปตรงมา: **A/B บน eval ของคุณเองก่อนจะไว้ใจมัน** benchmark มีอยู่ก็เพื่อสิ่งนี้พอดี

---

## Loc-Bench: Issue → Code โดยไม่มีอะไร self-annotated

benchmark ที่คัดมานั้นเรา annotate เอง คำถามที่ยุติธรรม: query มัน _เอื้อ_ กับเครื่องมือของเราเองเกินไปไหม? 0.18.0 มาพร้อม harness ตัวที่สองที่เอาเราออกจากสมการไปเลย

[`benchmark/locbench.py`](https://github.com/Muvon/octocode/tree/master/benchmark) ประเมิน Octocode บน **Loc-Bench V1** (`czlll/Loc-Bench_V1`): **560 GitHub issue จริง** จากรีโป Python ยอดนิยม query คือ _ข้อความ issue ต้นฉบับ_ ground truth คือชุดไฟล์และฟังก์ชันที่ _fix จริงที่ merge แล้ว_ แก้ไขจริง ๆ (field `edit_functions` ที่ dataset คัดมา) ไม่มีอะไร self-annotated — เป็นงาน industry-standard แบบ "ให้ bug report มา หาโค้ดที่ต้องแก้"

ต่อหนึ่ง instance harness จะ clone รีโป (blobless, cached) checkout base commit รัน `octocode index` search ด้วยข้อความ issue และให้คะแนนไฟล์ที่ดึงมาได้เทียบกับ fix จริง — ทั้งระดับ **file level** (hit@k, recall@k, MRR, accuracy) และ **hunk level** (ผลที่ดึงมาซ้อนทับกับบรรทัดที่ patch แตะจริงไหม?) แยกตามหมวดของ issue:

```bash
python3 benchmark/locbench.py --limit 10 --verbose   # smoke test
python3 benchmark/locbench.py                         # full 560
python3 benchmark/locbench.py --category "Bug Report"
```

นี่คือเครื่องมือวัด ไม่ใช่การอ้าง leaderboard — มันมีไว้ให้คุณรัน issue localization แบบโลกจริงบนฮาร์ดแวร์และโมเดลของคุณเอง และเพื่อให้ทุกการเปลี่ยนแปลงของ Octocode ในอนาคตวัดได้กับ benchmark ที่เราไม่ได้เขียนเอง

---

## Variant Matrix

`score.py` วัดได้ทีละ config [`run_matrix.py`](https://github.com/Muvon/octocode/tree/master/benchmark) sweep หลาย config บน **index ที่แชร์ร่วมกันตัวเดียว** — ดังนั้นสิ่งเดียวที่เปลี่ยนระหว่างแถวคือ search-time parameter ไม่ใช่ corpus หรือ embedding มันเขียนตารางเปรียบเทียบลง `RESULTS.md` (และ `RESULTS.json` ดิบ) และสะสมข้ามรอบรัน ดังนั้นการรันซ้ำแบบเฉพาะจุดจะ merge เข้าตารางเดิม

```bash
# pin corpus ไปที่ commit ของ ground truth ก่อน — ไม่งั้นช่วงบรรทัดจะเลื่อน
git worktree add /tmp/corpus b1771ba

# matrix แบบ local เต็ม (fastembed, ไม่ใช้ API key): vector-only เทียบกับ sweep ของ hybrid weight
CORPUS=/tmp/corpus python3 benchmark/run_matrix.py

# เพิ่ม local cross-encoder reranker (ยังไม่ใช้ key) — ใช้ index เดิมซ้ำ
CORPUS=/tmp/corpus SKIP_INDEX=1 ONLY=rerank \
  RERANK_MODEL=fastembed:bge-reranker-base python3 benchmark/run_matrix.py
```

Variant: `vector_only`, `hybrid_70_30` (weight ค่าเริ่มต้น), `hybrid_30_70` (เอียงไป keyword), `+graph` (GraphRAG expansion) และ `+rerank` (cross-encoder) ถ้าคุณตั้ง `VOYAGE_API_KEY` variant ของ reranker แบบเสียเงินที่เข้าใจโค้ดจะเปิดใช้อัตโนมัติ — นั่นคือวิธีที่คุณหาเพดาน (ceiling) จริงเหนือ floor แบบ local

---

## อื่น ๆ ทั้งหมด

**อัปเดต MCP registry metadata และ server schema** ทำให้ descriptor ของ MCP server ที่เผยแพร่ของ Octocode สอดคล้องกับ tool surface จริง เพื่อให้ registry และ client เห็น schema ที่ถูกต้อง บวกกับงานดูแล dependency และ tooling ตามปกติ — ไม่มี behavior change

---

## อัปเกรด

```bash
# Homebrew
brew upgrade muvon/tap/octocode

# Universal installer
curl -fsSL https://raw.githubusercontent.com/Muvon/octocode/master/install.sh | sh

# Cargo
cargo install octocode --version 0.18.0
```

ไม่มีอะไรในรีลีสนี้ที่เปลี่ยน index หรือ config ของคุณ — อัปเกรดได้อย่างปลอดภัย และ setup เดิมของคุณยังทำงานต่อไป สองอย่างที่ควรลอง:

1. **เอียง fusion weight ของคุณไปทาง keyword** ถ้าคุณ search โค้ดมากกว่างานเขียนทั่วไป benchmark บอกว่า RRF ที่นำด้วย keyword คือชัยชนะด้านความแม่นยำแบบฟรี วัดมันบนรีโปของคุณ
2. **รัน benchmark** clone Octocode, pin corpus และรัน `run_matrix.py` เทียบกับ embedding model และ reranker ของคุณเอง ตัวเลขข้างบนคือ floor แบบ local — ไปหาเพดานของคุณเอง

หากต้องการลอง GraphRAG expansion ให้เปิด `[graphrag]` ตั้ง `search.graph_expansion = true` จับคู่กับ reranker ที่เข้าใจโค้ด และ A/B เทียบกับ baseline ของคุณก่อนจะเปิดค้างไว้

---

Octocode เป็นโอเพนซอร์ส (Apache 2.0) ที่ [github.com/Muvon/octocode](https://github.com/Muvon/octocode) — มีทั้ง benchmark, ground truth และผลดิบให้ครบ มันคือ code-search engine เบื้องหลัง [Octomind](https://octomind.run) และตอนนี้คุณภาพ retrieval ของมันเป็นสิ่งที่คุณวัดได้เอง ไม่ใช่แค่เชื่อคำเรา
