# วิธีจัดทำดัชนีโค้ดเบสขนาดใหญ่เพื่อค้นหาโค้ดเชิงความหมายแบบโลคัลล้วน

> คู่มือลงมือทำจริงสำหรับการชี้เอเจนต์ AI ไปยังโมโนรีโปขนาดมหึมาเมื่อ grep ไม่เพียงพออีกต่อไป Octocode แบ่งโค้ดเป็นชังก์ด้วย tree-sitter อย่างไร รันเอ็มเบดดิงแบบโลคัลโดยไม่ต้องใช้ GPU และไม่ต้องใช้คลาวด์ ผสานการค้นหาเชิงความหมายกับการค้นหาแบบตรงตัว และเปิดเผยทุกอย่างผ่าน MCP พร้อมทั้งกับดักที่เล่นงานเรา

รีโปมีไฟล์ราว 11,000 ไฟล์ เอเจนต์มีเครื่องมือ `bash` และคำสั่งคลุมเครือว่า "หาให้เจอว่าเราตรวจสอบลายเซ็นเวบฮุกตรงไหน" มันรัน `grep -rn "signature" .` ได้ผลลัพธ์ 600 รายการกระจายอยู่ในไฟล์ JS ของเวนเดอร์ ฟิกซ์เจอร์ทดสอบ ไฟล์ `CHANGELOG` และตัวช่วยคริปโตที่ไม่เกี่ยวข้องอีกสามตัว แล้วก็ชี้ไปยังไฟล์ที่ผิดอย่างมั่นใจ เราเฝ้าดูมันทำแบบนี้ติดต่อกันสี่ครั้งด้วยถ้อยคำต่างกัน ก่อนที่ใครบางคนจะพึมพำสิ่งที่เห็นได้ชัด: grep จับคู่สตริง แต่เอเจนต์กำลังถามคำถามเกี่ยวกับ _ความหมาย_

นั่นคือปัญหาทั้งหมดในประโยคเดียว `grep` และ `rg` ยอดเยี่ยมสำหรับ "โทเค็นนี้ปรากฏตรงไหนพอดี" แต่ไร้ประโยชน์สำหรับ "สิ่งที่ทำ X อยู่ที่ไหน" เพราะโค้ดที่ทำ X แทบไม่เคยมีคำที่คุณจะใช้บรรยาย X อยู่เลย ฟังก์ชันชื่อ `verify_hmac` คุณค้น "webhook signature" ทับซ้อนกันเป็นศูนย์ และเครื่องมือเดียวที่เอเจนต์เอื้อมหยิบเป็นอย่างแรกก็ไม่คืนอะไรที่มีประโยชน์เลย

ทางแก้คือการค้นหาเชิงความหมาย: เอ็มเบดโค้ด เอ็มเบดคำค้น แล้วหาชังก์ที่ความหมายใกล้เคียงที่สุด ส่วนที่ติดขัดคือคำแนะนำแบบ "ก็แค่ใช้เอ็มเบดดิงสิ" ส่วนใหญ่สันนิษฐานว่าคุณจะส่งซอร์สโค้ดไปยัง API คลาวด์และเช่า GPU สำหรับโมโนรีโปส่วนตัว ทั้งสองอย่างนั้นรับไม่ได้: ฝ่ายกฎหมายจะไม่อนุมัติการอัปโหลดโค้ดกรรมสิทธิ์ให้บุคคลที่สาม และคุณก็ไม่อยากได้บิลคิดต่อโทเค็นที่โตขึ้นตามทุกครั้งที่รีอินเด็กซ์

ดังนั้นนี่คือการตั้งค่าที่เราใช้งานจริง: [Octocode](https://github.com/Muvon/octocode) ที่จัดทำดัชนีรีโปขนาดใหญ่เพื่อค้นหาเชิงความหมาย **แบบโลคัลล้วน** — ไม่ใช้ GPU ไม่ใช้เอ็มเบดดิงคลาวด์ ไม่มีอะไรออกจากเครื่อง นี่คือวิธีการทำงานที่แท้จริง คอนฟิกที่สำคัญ และสี่สิ่งที่เล่นงานเรา

---

## ทำไม grep ถึงไม่พอ อย่างเป็นรูปธรรม

ก่อนจะเอื้อมไปหาเอ็มเบดดิง ควรชัดเจนเสียก่อนว่า _ทำไม_ การค้นข้อความถึงล้มเหลวสำหรับเอเจนต์ เพราะมันบอกว่าสิ่งที่มาแทนต้องทำอะไร

- **คำศัพท์ไม่ตรงกัน** คำค้นคือเจตนา ("rate limiting") แต่โค้ดคือกลไก (`Semaphore`, `token_bucket`, `RetryAfter`) ไม่มีโทเค็นร่วมกัน
- **ไม่มีการจัดอันดับ** `rg` ให้ทุกผลลัพธ์ด้วยน้ำหนักเท่ากัน 600 ผลลัพธ์ไม่ใช่คำตอบ แต่เป็นปัญหาการค้นหาอันที่สอง
- **ไม่มีโครงสร้าง** ผลลัพธ์ข้อความในคอมเมนต์ ในเทสต์ และในฟังก์ชันหลัก ดูเหมือนกันหมดสำหรับ `grep` มันบอกคุณไม่ได้ว่าอันที่สามคือนิยาม

สิ่งที่คุณต้องการแทนคือ: จัดอันดับตามความใกล้เชิง _ความหมาย_ แต่ไม่ทิ้งสิ่งที่ `grep` เก่งจริง ๆ — การจับคู่ตัวระบุ (identifier) แบบตรงตัว คำตอบของ Octocode คือทำทั้งสองอย่างแล้วผสานเข้าด้วยกัน ซึ่งผมจะกล่าวถึงต่อไป ก่อนอื่น มาดูว่าโค้ดถูกแปลงให้อยู่ในรูปที่ค้นหาได้อย่างไร

---

## Octocode แบ่งชังก์และจัดทำดัชนีโค้ดอย่างไร

RAG มาตรฐานหั่นไฟล์เป็นหน้าต่างข้อความขนาดคงที่ — ทุก N อักขระ เหลื่อมกันนิดหน่อย แล้วเอ็มเบด สำหรับร้อยแก้วก็โอเค แต่สำหรับโค้ดมันแย่อย่างจริงจัง: มันตัดฟังก์ชันออกเป็นสองชังก์ เอ็มเบดครึ่งหนึ่งของแขน `match` และสูญเสียข้อเท็จจริงที่ว่าเมธอดเป็นของไทป์

Octocode พาร์สแต่ละไฟล์ด้วย **tree-sitter** ก่อน เดินผ่าน AST และแบ่งชังก์ตามขอบเขตสัญลักษณ์จริง — ฟังก์ชัน เมธอด คลาส โมดูล — แทนที่จะใช้ตำแหน่งไบต์ที่กำหนดเอง สถาปัตยกรรมคือพาร์สเซอร์หนึ่งตัวต่อภาษาดึงฟังก์ชัน คลาส อิมพอร์ตและเอ็กซ์พอร์ตออกมา แล้วประมวลผลแบบแบ่งชังก์สำหรับอะไรก็ตามที่ใหญ่เกินกว่าจะเอ็มเบดเป็นชิ้นเดียว เมธอดที่ดึงมาจากภายในบล็อก `impl` หรือคลาสจะพกชื่อไทป์แม่ไว้ในรายการสัญลักษณ์ของมัน ดังนั้นการค้น `Suppression.mark_set` หรือ `Foo.bar` จึงเข้าชนชังก์เมธอดได้โดยตรง

การรองรับภาษาเป็นไวยากรณ์ tree-sitter จริง ไม่ใช่ฮิวริสติกแบบ regex ตามรีโป สิ่งที่จัดทำดัชนีเป็น **โค้ด** ด้วยการพาร์ส AST เต็มรูปแบบ:

| ภาษา                                   | นามสกุลไฟล์                                                                                                  |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Rust                                   | `.rs`                                                                                                        |
| Python                                 | `.py`                                                                                                        |
| TypeScript / JavaScript                | `.ts`, `.tsx`, `.js`, `.jsx`                                                                                 |
| Go                                     | `.go`                                                                                                        |
| PHP                                    | `.php`                                                                                                       |
| C / C++                                | `.c`, `.h`, `.cpp`, `.hpp`, `.cc`, `.cxx`, รวมถึงนามสกุลโมดูล C++20 `.cppm`, `.ixx`, `.mxx`, `.ccm`, `.cxxm` |
| Ruby                                   | `.rb`                                                                                                        |
| Java                                   | `.java`                                                                                                      |
| Swift                                  | `.swift`                                                                                                     |
| JSON, Bash, CSS, Lua, Svelte, Markdown | เส้นทาง tree-sitter เฉพาะ                                                                                    |

อย่างอื่นที่มีประโยชน์สำหรับเป็นบริบท (`yaml`, `toml`, `dockerfile`, `makefile`, `ini`, `conf`, `env`, `xml`, `html`, `sql`, `rst` และพวกพ้อง) จะจัดทำดัชนีเป็น **บล็อกข้อความ** จึงยังค้นหาได้ เพียงแต่ไม่มีการดึงสัญลักษณ์เชิงความหมาย

พารามิเตอร์การแบ่งชังก์อยู่ใต้ `[index]`:

```toml
[index]
chunk_size = 2000        # อักขระสูงสุดต่อชังก์โค้ด
chunk_overlap = 100      # การเหลื่อมระหว่างชังก์ที่ติดกัน
quantization = true      # การบีบอัดเวกเตอร์ RaBitQ ~32x สูญเสียคุณภาพน้อยมาก
require_git = true        # ค่าเริ่มต้นจัดทำดัชนีเฉพาะภายในรีโป git
```

`chunk_size` เป็นเพดาน ไม่ใช่เป้าหมาย — ฟังก์ชัน 40 บรรทัดที่เล็กกว่าขีดจำกัดจะอยู่ครบทั้งตัว ขีดจำกัดจะมีผลก็ต่อเมื่อบอดี้ใหญ่จริง ๆ และ Octocode ก็จัดทำดัชนีคลาสขนาดใหญ่ใน Python, TypeScript, C++ และ Ruby แบบทีละเมธอดอยู่แล้ว แทนที่จะเทคลาสทั้งคลาสลงในชังก์เดียว ประเด็นของทั้งหมดนี้: เมื่อคุณค้นหา "ฟังก์ชันที่จัดการการตั้งค่า remote pull" ในภายหลัง หน่วยที่ถูกเอ็มเบดคือ _ฟังก์ชันนั้น_ พร้อมชื่อและไทป์แม่ที่แนบมา ไม่ใช่หน้าต่างที่บังเอิญคร่อมมันอยู่

---

## เอ็มเบดดิงโลคัล: ไม่มี GPU ไม่มีคลาวด์ ไม่มี API key

ส่วนที่ผู้คนเหมาเอาว่าต้องใช้คลาวด์ — การแปลงโค้ดเป็นเวกเตอร์ — จริง ๆ แล้วไม่ต้อง

Octocode มาพร้อม [สแตกแบบ local-first](/blog/octocode-0-15-0-local-first-hybrid-search) ที่สร้างบน `fastembed` (มี ONNX Runtime อยู่เบื้องหลัง) คุณชี้คอนฟิกไปยังโมเดลโลคัลด้วยพรีฟิกซ์ `fastembed:` แล้วเอ็มเบดดิงจะรันบน CPU บนเครื่องของคุณ:

```toml
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code"   # 768 มิติ ออกแบบมาเพื่อโค้ดโดยเฉพาะ
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5"        # ข้อความและเอกสาร
```

สตริงโมเดลจะเป็น `provider:model` เสมอ พรีฟิกซ์ผู้ให้บริการคือการตัดสินใจเรื่องการกำหนดเส้นทางทั้งหมด: `fastembed:` และ `huggingface:` เป็นโลคัล (ไม่ต้องใช้คีย์ ไม่ต้องใช้เครือข่าย) ส่วน `voyage:`, `jina:`, `google:`, `openai:`, `together:`, `octohub:` เป็นคลาวด์และต้องการ API key ที่สอดคล้องในสภาพแวดล้อม จะผสมก็ได้ (เอ็มเบดโค้ดโลคัล เอ็มเบดข้อความคลาวด์) แต่สำหรับรีโปส่วนตัว เส้นทางโลคัลล้วนคือประเด็นสำคัญ

ที่นี่ไม่มีข้อกำหนดเรื่อง GPU และไม่มีเส้นทางโค้ดสำหรับ GPU: นี่คือการอนุมาน ONNX บน CPU บนเครื่องมัลติคอร์รุ่นใหม่ที่มี AVX2 มันเร็วพอที่ตัวเอ็มเบดเดอร์แทบไม่เคยเป็นคอขวด — โดยปกติคือ disk I/O และการพาร์ส tree-sitter ต่างหากที่เป็น บันทึกประสิทธิภาพของรีโปเองระบุว่าการจัดทำดัชนีด้วย FastEmbed แบบโลคัลอยู่ราว 45 วินาทีต่อ 1,000 ไฟล์ในเชิงลำดับขนาดคร่าว ๆ ผลลัพธ์ของคุณจะต่างกันไปตามขนาดไฟล์และจำนวนคอร์ จึงควรถือว่าเป็นตัวอย่างประกอบ ไม่ใช่คำสัญญา

ข้อควรระวังตรง ๆ สองข้อ:

1. **การรันครั้งแรกดาวน์โหลดโมเดล** `octocode index` ครั้งแรกหลังชี้ไปยังโมเดล `fastembed:` จะดึงน้ำหนัก ONNX (ไม่กี่ร้อย MB) ลงไดเรกทอรีแคช (`~/.local/share/octocode/fastembed/` บน Linux/macOS) เป็นต้นทุนครั้งเดียว หลังจากนั้นออฟไลน์ตลอดไป
2. **ผู้ให้บริการโลคัลถูกควบคุมด้วยฟีเจอร์แฟล็กและขึ้นกับแพลตฟอร์ม** FastEmbed และ HuggingFace ต้องการฟีเจอร์ build `fastembed`/`huggingface` บิลด์ค่าเริ่มต้นจะมีให้ในที่ที่รองรับดี ส่วนแพลตฟอร์มที่ไม่รองรับ คุณจะถอยกลับไปใช้ผู้ให้บริการคลาวด์ ตรวจ `octocode models list fastembed` เพื่อดูว่าไบนารีของคุณเปิดเผยอะไรจริง ๆ [งานเรื่องผู้ให้บริการเอ็มเบดดิงโลคัลใน 0.17.0](/blog/octocode-0-17-1-local-embedding-providers) เมื่อไม่นานนี้คือสิ่งที่ทำให้รายการนี้ซื่อตรงว่าอะไรพร้อมใช้ที่ไหน

ถ้าคุณยังไม่เคยตั้งค่าการค้นหาเชิงความหมายเหนือโค้ดมาก่อน [บทแนะนำการค้นหาโค้ดเชิงความหมายของ Octocode](/blog/octocode-semantic-code-search) ครอบคลุมพื้นฐานเชิงแนวคิด โพสต์นี้สันนิษฐานว่าคุณผ่านขั้นนั้นแล้วและต้องการการตั้งค่าสำหรับโปรดักชัน

---

## จัดทำดัชนีรีโป

เมื่อคอนฟิกพร้อม การจัดทำดัชนีคือคำสั่งเดียวจากรากของรีโป:

```bash
cd /path/to/big-monorepo
octocode index
# → Indexed 12,847 blocks across 342 files
```

สิ่งที่เกิดขึ้นจริงตามลำดับ: ค้นหาไฟล์ (เคารพกฎการเพิกเฉย) พาร์สแต่ละไฟล์ด้วย tree-sitter ดึงสัญลักษณ์ แบ่งชังก์ตามขอบเขต AST เอ็มเบดแต่ละชังก์แบบโลคัล แล้วเขียนทุกอย่างลงสโตร์แบบคอลัมน์ **LanceDB** ฐานข้อมูล _ไม่ได้_ อยู่ในรีโปของคุณ — มันถูกคีย์ตามโปรเจกต์ใต้ไดเรกทอรีข้อมูลของระบบ (`~/.local/share/octocode/<project-id>/storage` บน Linux/macOS) ซึ่งช่วยให้ working tree ของคุณสะอาด และหมายความว่า `git clean -fdx` จะไม่ล้างดัชนีของคุณ

แฟล็กที่มีประโยชน์:

```bash
octocode index --force      # เพิกเฉยแคชแบบ incremental สร้างใหม่จากศูนย์
octocode index --verbose    # ดูความคืบหน้าทีละไฟล์
octocode stats              # มีบล็อกโค้ด/ข้อความ/เอกสารกี่บล็อก โหนดกราฟ ความเก่าของดัชนี
```

`octocode stats` คือคำสั่งที่ควรรันเมื่อรู้สึกว่ามีอะไรผิดปกติ — มันบอกว่า `HEAD` ของคุณตรงกับคอมมิตที่จัดทำดัชนีล่าสุดหรือไม่ คุณจึงเห็นได้ในแวบเดียวว่าดัชนีเก่าแล้วหรือยัง

### Incremental กับการรีอินเด็กซ์เต็ม

การรัน `octocode index` ครั้งต่อ ๆ ไปเป็นแบบ incremental — เฉพาะไฟล์ที่เปลี่ยนแปลงเท่านั้นที่ถูกพาร์สและเอ็มเบดใหม่ ทุกรอบการจัดทำดัชนีจบด้วยรอบ optimize ตาราง เพื่อให้เส้นทางคิวรียังเร็วเมื่อฐานข้อมูลโต (การจัดทำดัชนีแบบ incremental ซ้ำ ๆ เคยทิ้งหางที่ยังไม่ถูกจัดทำดัชนีซึ่งการค้นหาต้องสแกนแบบ brute-force ตอนนี้จัดการอัตโนมัติแล้ว)

เมื่อคุณเปลี่ยน _วิธี_ การจัดทำดัชนี (กฎการแบ่งชังก์ใหม่ โมเดลเอ็มเบดดิงต่างออกไป) แบบ incremental จะไม่ย้อนกลับไปแบ่งชังก์ไฟล์ที่มีอยู่แล้วใหม่ การเปลี่ยนโมเดลเอ็มเบดดิงเป็นเรื่องใหญ่: เวกเตอร์จากโมเดลหนึ่งเทียบกับอีกโมเดลไม่ได้ หลังเปลี่ยน `code_model` หรือ `text_model` ให้สร้างใหม่แบบสะอาด:

```bash
octocode clear        # ทิ้งตารางที่จัดทำดัชนีไว้
octocode index        # สร้างใหม่ด้วยโมเดลใหม่
```

---

## โหมด watch: ทำให้ดัชนีสดอยู่เสมอ

ดัชนีที่ทำครั้งเดียวจะเก่าทันทีที่มีคนคอมมิต สำหรับเอเจนต์ที่ควรตอบคำถามเกี่ยวกับสถานะ _ปัจจุบัน_ ของรีโป ให้รัน watcher:

```bash
octocode watch              # รีอินเด็กซ์อัตโนมัติเมื่อไฟล์เปลี่ยน
octocode watch --debounce 5 # รอ 5 วินาทีหลังการเปลี่ยนแปลงครั้งสุดท้ายก่อนรีอินเด็กซ์
octocode watch --quiet
```

watcher ทำ debounce (ปรับได้ 1–30 วินาที) ดังนั้น `git checkout` ที่แตะ 400 ไฟล์จึงทริกเกอร์การรีอินเด็กซ์หนึ่งครั้ง ไม่ใช่ 400 ครั้ง มันเคารพกฎการเพิกเฉยแบบเดียวกับการจัดทำดัชนีเต็ม ดังนั้นการแก้ไฟล์ใน `node_modules` จึงไม่ปลุกมัน

ถ้าคุณรันเซิร์ฟเวอร์ MCP (หัวข้อถัดไป) ก็มีทางเลือกที่สะอาดกว่า: ตั้ง `mcp_index = true` ใต้ `[index]` แล้วเซิร์ฟเวอร์ MCP จะจัดทำดัชนีและเฝ้าดู **ภายในโปรเซสเดียว** — มีของที่รันอยู่ชิ้นเดียวแทนที่จะเป็นสองชิ้น ค่าเริ่มต้นคือ `false` ซึ่งให้บริการดัชนีที่มีอยู่แบบอ่านอย่างเดียวและสันนิษฐานว่าคุณจัดทำดัชนีแยกต่างหาก เปิดมันเมื่อคุณต้องการให้เซิร์ฟเวอร์เป็นเจ้าของวงจรชีวิตทั้งหมด

---

## การค้นหาแบบไฮบริด: เชิงความหมาย _และ_ แบบตรงตัว ผสานกัน

นี่คือส่วนที่แก้ความล้มเหลวของ grep แต่แรกโดยไม่ทิ้งจุดแข็งหนึ่งเดียวของ grep

การค้นหาด้วยเวกเตอร์ล้วนแพ้สำหรับคำค้นที่หนาแน่นด้วยตัวระบุ ค้น `parse_remote` แล้วเอ็มเบดดิงแบบ dense จะคืนสิ่งที่ _ใกล้เชิงความหมาย_ กับ `parse_remote` อย่างเริงร่าในขณะที่พลาดฟังก์ชันที่ชื่อ `parse_remote` ตรง ๆ การค้นหาคีย์เวิร์ด BM25 มีจุดอ่อนตรงกันข้าม: มันจับตัวระบุแบบตรงตัวได้แม่นแต่พลาดเจตนาที่ถูกเรียบเรียงใหม่อย่าง "ฟังก์ชันที่จัดการการตั้งค่า remote pull"

Octocode รันทั้งสองในทุกคำค้นและผสานด้วย Weighted Reciprocal Rank Fusion ภายใน LanceDB น้ำหนักถูกเปิดเผยให้ปรับ:

```toml
[search.hybrid]
enabled = true
default_vector_weight  = 0.7
default_keyword_weight = 0.3
```

สำหรับรีโปที่หนาแน่นด้วยโค้ดซึ่งตัวระบุแบบตรงตัวให้สัญญาณมาก ให้เอนไปทางคีย์เวิร์ด เบนช์มาร์กการดึงข้อมูลของรีโปเอง (คำค้นการค้นหาโค้ด 127 รายการพร้อม ground truth แบบช่วงบรรทัด รันแบบโลคัลล้วนด้วย `jina-embeddings-v2-base-code` และ _ไม่ใช้_ รีแรงเกอร์) แสดงผลชัดเจน: การเปลี่ยนจากสัดส่วนเริ่มต้น `0.7/0.3` ไปเป็น `0.3/0.7` ที่จูนเข้าหาคีย์เวิร์ด ยก Hit@5 จาก 0.598 เป็น 0.732 และ Recall@10 จาก 0.671 เป็น 0.807 โดยไม่มีต้นทุนเพิ่ม นั่นคือ Hit@5 ที่ +22% จากคอนฟิกบรรทัดเดียว สำหรับเอกสารเนื้อยาว ให้เอนไปอีกทาง (`0.8/0.2`) เพราะเจตนาเป็นใหญ่ที่นั่น

เหนือการผสาน รีแรงเกอร์ที่เป็นทางเลือกจะจัดอันดับผู้สมัครอันดับต้น ๆ ใหม่:

```toml
[search.reranker]
enabled = true
model   = "fastembed:jina-reranker-v2-base-multilingual"   # รีแรงเกอร์โลคัล ไม่ต้องใช้คีย์
top_k_candidates = 50    # ดึงจำนวนนี้จากการค้นหาเวกเตอร์
final_top_k = 10         # คืนจำนวนนี้หลังการรีแรงก์
```

รีแรงเกอร์ก็รันแบบโลคัลถ้าคุณชี้ไปยังโมเดล `fastembed:` มันเป็นพาส cross-encoder บนผู้สมัคร 50 อันดับแรก ซึ่งจับกรณีที่การจัดอันดับขั้นแรกเรียงผิด ต้นทุนมีจริงแต่จำกัด — มันเห็นแค่ `top_k_candidates` ไม่ใช่ดัชนีทั้งหมด

จาก CLI พื้นผิวการค้นหาตรงไปตรงมา:

```bash
octocode search "webhook signature verification"
octocode search "auth" "middleware" "session"          # มัลติคำค้น ได้ถึง 10
octocode search "database connection pool" --mode code  # จำกัดเฉพาะบล็อกโค้ด
octocode search "auth" --detail-level signatures        # เฉพาะลายเซ็น กระชับ
octocode search "authentication refactor" --mode commits # ค้นประวัติ git
```

เส้นทาง `--mode commits` ควรรู้ไว้: ประวัติคอมมิตถูกจัดทำดัชนีแบบ lazy เมื่อค้นคอมมิตครั้งแรก (ไม่ใช่ระหว่าง `octocode index`) คุณจึงถามได้ว่า "เราเปลี่ยนโฟลว์ auth เมื่อไหร่" แล้วได้คอมมิตที่จัดอันดับเชิงความหมาย ไม่ใช่แค่ `git log --grep`

---

## การค้นหาเชิงโครงสร้าง: เมื่อคุณต้องการแพตเทิร์น AST ไม่ใช่ความหมาย

บางครั้งคุณไม่ต้องการความหมายเลย — คุณต้องการ "ทุก `.unwrap()` ใน Rust" หรือ "ทุก `new Foo()` ใน JS" นั่นคือคำค้นเชิงโครงสร้าง และการค้นข้อความก็พลาดอยู่เรื่อย (คอมเมนต์ที่กล่าวถึง `.unwrap()` ไม่ใช่การเรียก) Octocode ครอบ ast-grep ไว้:

```bash
octocode grep '$FUNC.unwrap()' --lang rust
octocode grep 'new $CLASS($$$ARGS)' --lang javascript
octocode grep 'console.log($ARG)' --lang javascript --rewrite 'logger.info($ARG)' --update-all
```

`$VAR` จับคู่โหนด AST หนึ่งโหนด `$$$ARGS` จับคู่อาร์กิวเมนต์ชุดหนึ่ง และ `--rewrite` ทำรีแฟกเตอร์ที่รู้จัก AST ในที่ ส่วนที่มีประโยชน์จริงสำหรับเอเจนต์: LLM มักผิด _kind_ ของโหนดอย่างสม่ำเสมอ — มันจะเขียน `function_declaration` สำหรับ Python ทั้งที่จริงคือ `function_definition` เมื่อแพตเทิร์นไม่จับคู่กับอะไรเลย Octocode จะลองการตีความที่ผ่อนลงเรื่อย ๆ และรู้ kind ที่ถูกต้องของแต่ละภาษา ดังนั้นความพยายามที่เกือบถูกของเอเจนต์จึงยังคืนผลลัพธ์แทนที่จะเป็นศูนย์เงียบ ๆ

---

## GraphRAG: ความสัมพันธ์ ไม่ใช่แค่ความคล้าย

การค้นหาเชิงความหมายหาโค้ดที่ _คล้าย_ กัน มันบอกคุณไม่ได้ว่า `auth_middleware.rs` อิมพอร์ต `jwt.rs` เรียก `user_store.rs` และเชื่อมเข้ากับ `router.rs` นั่นเป็นคำถามเชิงกราฟ และ Octocode สร้างกราฟระหว่างการจัดทำดัชนี

```toml
[graphrag]
enabled = true
use_llm = true
```

มันดึงเอจ `imports`, `calls`, `extends` และ `implements` จาก AST ในเก้าภาษา และด้วย `use_llm = true` ก็เพิ่มความสัมพันธ์เชิงสถาปัตยกรรมระดับสูงกว่า (`configures`, `uses`, แพตเทิร์น factory/observer/strategy) ที่ค้นพบโดย LLM จาก CLI:

```bash
octocode graphrag get-relationships --node-id "src/auth/middleware.rs"
octocode graphrag find-path --source-id "src/auth/mod.rs" --target-id "src/database/mod.rs"
octocode graphrag overview
```

นี่คือความต่างระหว่าง "ขอโค้ดที่ดูเหมือน auth" กับ "ขอทุกอย่างที่ _ขึ้นอยู่กับ_ โมดูล auth" สำหรับเอเจนต์ที่กำลังรีแฟกเตอร์ คำถามที่สองคือคำถามที่กันไม่ให้อะไรพัง

มีลูกเล่นใหม่ที่เป็นทางเลือก: **การขยายการดึงข้อมูลที่รู้จักกราฟ** ด้วย `[search] graph_expansion = true` (และเปิด GraphRAG) การค้นหาจะดึงบล็อกโค้ดจากไฟล์ที่สัมพันธ์เชิงโครงสร้างกับผลลัพธ์อันดับต้นของคุณเข้ามา _ก่อน_ การรีแรงก์ ดังนั้นคำค้นที่ตกลงบน auth-middleware อาจดึงตัวช่วย JWT ที่มันเรียกขึ้นมาได้ แม้ตัวช่วยนั้นจะไม่จับคู่กับข้อความคำค้นก็ตาม มันปิดไว้ตามค่าเริ่มต้น และคอมเมนต์ในโค้ดก็ตรงไปตรงมา: ทำ A/B บนชุดประเมินของคุณเองก่อนจะไว้ใจ เพราะการขยายอาจเพิ่มสัญญาณรบกวนได้ง่ายพอ ๆ กับสัญญาณ

---

## ต่อเข้ากับเอเจนต์ผ่าน MCP

ทั้งหมดข้างต้นถูกเปิดเผยให้ผู้ช่วย AI ผ่าน **เซิร์ฟเวอร์ MCP** ในตัว นี่คือวิธีหลักในการใช้ Octocode — เอเจนต์ได้เครื่องมือ ไม่ใช่เชลล์

```bash
octocode mcp --path /path/to/your/project
```

หรือในคอนฟิกของไคลเอนต์ (Claude Desktop, Cursor, Windsurf, Claude Code):

```json
{
	"mcpServers": {
		"octocode": {
			"command": "octocode",
			"args": ["mcp", "--path", "/path/to/your/project"]
		}
	}
}
```

เครื่องมือที่เอเจนต์เห็น ตรวจสอบกับเซิร์ฟเวอร์แล้ว:

| เครื่องมือ MCP      | ทำอะไร                                                                                |
| ------------------- | ------------------------------------------------------------------------------------- |
| `semantic_search`   | การค้นหาไฮบริดเชิงความหมาย + คีย์เวิร์ด มัลติคำค้น ทุกโหมดรวมถึง `commits`            |
| `view_signatures`   | โครงสร้างไฟล์ (ลายเซ็น คลาส อิมพอร์ต) ตาม glob โดยไม่ต้องอ่านไฟล์ทั้งไฟล์             |
| `structural_search` | การจับคู่แพตเทิร์น AST ผ่าน ast-grep พร้อมการกู้คืนด้วย kind                          |
| `graphrag`          | คำค้นความสัมพันธ์: `search`, `get-node`, `get-relationships`, `find-path`, `overview` |

`view_signatures` คือฮีโร่ที่ไม่ได้รับการกล่าวถึง แทนที่เอเจนต์จะเผาบริบทด้วยการอ่านไฟล์ 800 บรรทัดสามไฟล์เพื่อหาลายเซ็นฟังก์ชัน มันขอลายเซ็นตาม glob แล้วได้โครงร่างมา (ในเวอร์ชันล่าสุดคุณส่งสตริง glob เดี่ยวได้ ไม่ใช่แค่อาร์เรย์ — เรื่องเล็ก ๆ ที่หยุดการเรียกเครื่องมือที่ผิดรูปแบบไปได้เยอะ)

สำหรับโมโนรีโปที่มีหลายโปรเจกต์ย่อย มีโหมดมัลติรีโป: `octocode mcp --multi --path /workspace` สแกนไดเรกทอรีย่อยชั้นแรกหารีโป git แล้วให้บริการทั้งหมดจากเอนด์พอยต์เดียว โดยแต่ละเครื่องมือจะได้อาร์กิวเมนต์ `project` ไว้เลือกเป้าหมาย เซิร์ฟเวอร์ MCP เดียว รีโปทุกตัวในเวิร์กสเปซ

---

## สี่สิ่งที่เล่นงานเรา

เป็นเรื่องจริง ไม่ใช่สมมติ — นี่คือสิ่งที่ทำให้เราเสียเวลา

**1. ดัชนีอยู่นอกรีโป และมันทำเอาทุกคนงง** ครั้งแรกที่มีคนรันเอเจนต์บนโคลนใหม่เอี่ยม มันไม่มีดัชนี: ฐานข้อมูลอยู่แบบต่อโปรเจกต์ใต้ `~/.local/share/octocode/` ไม่ใช่ใน working tree โคลน ≠ จัดทำดัชนี ทางแก้คือบรรทัดเดียวในการ onboarding: หลังโคลน ให้รัน `octocode index` หนึ่งครั้ง ชัดเจนเมื่อมองย้อนกลับ แต่ไม่ชัดเลยตอนตีสอง

**2. การเปลี่ยนโมเดลเอ็มเบดดิงทำให้ผลลัพธ์แย่ลงอย่างเงียบ ๆ** มีคนเปลี่ยน `code_model` เพื่อลองโมเดลโลคัลตัวอื่น แต่ _ไม่ได้_ ล้างดัชนี คำค้นใหม่ถูกเอ็มเบดด้วยโมเดลใหม่ ชังก์เก่าถูกเอ็มเบดด้วยตัวเก่า เวกเตอร์เทียบกันไม่ได้ การจัดอันดับเป็นขยะ เวกเตอร์จากต่างโมเดลไม่ได้อยู่ในปริภูมิเดียวกัน **เปลี่ยนโมเดลเอ็มเบดดิงเมื่อไหร่ ให้ `octocode clear && octocode index` เสมอ** ไม่มีคำเตือนใดมาช่วยคุณตรงนี้ — มันแค่ค่อย ๆ แย่ลงเงียบ ๆ

**3. สัดส่วนเวกเตอร์/คีย์เวิร์ดเริ่มต้นไม่เหมาะกับรีโปของเรา** เรารันด้วย `0.7/0.3` (เน้นเวกเตอร์) อยู่หนึ่งสัปดาห์และพลาดคำค้นตัวระบุแบบตรงตัวอยู่เรื่อย โค้ดหนาแน่นด้วยตัวระบุ เบนช์มาร์กข้างบนแสดงว่าน้ำหนักที่จูนเข้าหาคีย์เวิร์ดดีกว่ามากสำหรับการค้นหาโค้ด การเปลี่ยนไปใช้ `0.3/0.7` คือการปรับคอนฟิกที่ให้ผลคุ้มที่สุดที่เราทำ

**4. ไฟล์ไบนารีและไฟล์ที่ถูกเจเนอเรตไม่ถูกจัดทำดัชนี — ซึ่งถูกต้องแล้ว แต่ตรวจดูให้แน่** Octocode ข้ามไฟล์ไบนารี (มันตรวจ null byte และอัตราส่วนอักขระที่พิมพ์ได้ก่อนจะถือว่าเนื้อหาเป็นข้อความ) และเคารพ `.gitignore`, `.git/info/exclude` และไฟล์ `.noindex` นั่นคือสิ่งที่คุณต้องการพอดี — คุณไม่อยากได้เอ็มเบดดิงของบันเดิลเวนเดอร์ที่ถูกย่อ แต่ถ้าไดเรกทอรีที่ _คุณสนใจ_ อยู่ใน gitignore (บางทีมใส่ API client ที่ถูกเจเนอเรตไว้ใน gitignore) มันจะไม่ถูกจัดทำดัชนี แล้วคุณจะงงว่าทำไมค้นไม่เจอ วาง `.noindex` เพื่อ _ยกเว้น_ เส้นทางเพิ่มเติม ตรวจ `.gitignore` ถ้าบางอย่างที่คาดหวังไว้หายไป `octocode stats` จะแสดงจำนวนบล็อกให้คุณตรวจสอบความครอบคลุม

---

## การตั้งค่าแบบ end-to-end ขั้นต่ำ

ประกอบรวมกัน นี่คือทั้งหมดสำหรับโมโนรีโปส่วนตัว แบบโลคัลล้วน:

```toml
# ~/.local/share/octocode/config.toml (หรือ override แบบโลคัลของโปรเจกต์)
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code"
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5"

[search.hybrid]
enabled = true
default_vector_weight  = 0.3   # โค้ดหนาแน่นด้วยตัวระบุ → เอนเข้าหาคีย์เวิร์ด
default_keyword_weight = 0.7

[search.reranker]
enabled = true
model   = "fastembed:jina-reranker-v2-base-multilingual"

[graphrag]
enabled = true
use_llm = false   # ความสัมพันธ์จาก AST อย่างเดียว ไม่เรียก LLM ออฟไลน์ล้วน
```

```bash
cd /path/to/monorepo
octocode index                          # การรันครั้งแรกดาวน์โหลดโมเดล แล้วจัดทำดัชนี
octocode search "webhook signature verification"   # ตรวจความสมเหตุสมผล
octocode watch --quiet &                # ทำให้สดอยู่เสมอ
claude mcp add octocode -- octocode mcp --path .   # ต่อเข้ากับเอเจนต์
```

ในโฟลว์นั้นไม่มีอะไรแตะเครือข่ายเลยหลังการดาวน์โหลดโมเดลครั้งเดียว ไม่มี GPU ไม่มีบิลคิดต่อโทเค็น เอเจนต์ที่เคย grep หา "signature" แล้วชี้ผิดไฟล์ ตอนนี้เรียก `semantic_search` ได้ `verify_hmac` ขึ้นอันดับหนึ่ง และใช้ `view_signatures` ยืนยันก่อนจะแตะอะไร

---

## คำถามที่พบบ่อย

**ต้องใช้ GPU ไหม?** ไม่ เอ็มเบดดิงโลคัลรันบน CPU ผ่าน ONNX Runtime CPU มัลติคอร์รุ่นใหม่ก็เพียงพอ ตัวเอ็มเบดเดอร์แทบไม่เคยเป็นคอขวด

**มีโค้ดอะไรออกจากเครื่องของผมไหม?** ด้วยโมเดล `fastembed:`/`huggingface:` และ `graphrag.use_llm = false` ไม่มี — การจัดทำดัชนี การค้นหา และการรีแรงก์ ล้วนเป็นโลคัล ผู้ให้บริการเอ็มเบดดิงคลาวด์และฟีเจอร์ GraphRAG/ข้อความคอมมิตที่ขับเคลื่อนด้วย LLM เป็นแบบ opt-in และต้องใช้คีย์อย่างชัดเจน

**ดัชนีอยู่ที่ไหน และอยู่ในรีโปของผมไหม?** อยู่ใต้ไดเรกทอรีข้อมูลของระบบ คีย์ตามโปรเจกต์ (`~/.local/share/octocode/<project-id>/` บน Linux/macOS) ไม่ใช่ใน working tree `git clean` จะไม่แตะมัน โคลนใหม่จะไม่มีมัน

**ฐานข้อมูลใหญ่แค่ไหน?** ราว ๆ ~10KB ต่อไฟล์ โดยมีการควอนไทซ์ RaBitQ ให้การบีบอัดเวกเตอร์ ~32x รีโป 10k ไฟล์อยู่ที่หลักสิบถึงหลักร้อย MB ไม่ใช่กิกะไบต์

**Incremental หรือรีอินเด็กซ์เต็ม?** Incremental ตามค่าเริ่มต้น — เฉพาะไฟล์ที่เปลี่ยนถูกเอ็มเบดใหม่ บังคับสร้างใหม่เต็มด้วย `octocode index --force` หรือ `octocode clear && octocode index` หลังเปลี่ยนโมเดลเอ็มเบดดิง

**ค้นประวัติ git ได้ไหม?** ได้ `octocode search "..." --mode commits` คอมมิตถูกจัดทำดัชนีแบบ lazy เมื่อค้นคอมมิตครั้งแรก `octocode index` แรกจึงยังเร็วอยู่

---

— Don

---

_[Octocode](https://github.com/Muvon/octocode) เป็นโอเพนซอร์สภายใต้ Apache-2.0 สแตกแบบ local-first มาถึงใน [0.15.0](/blog/octocode-0-15-0-local-first-hybrid-search) และรายการผู้ให้บริการเอ็มเบดดิงโลคัลใน [0.17.x](/blog/octocode-0-17-1-local-embedding-providers) ถ้าการตั้งค่าของคุณเจอกับดักที่โพสต์นี้ไม่ได้ครอบคลุม [เปิด issue](https://github.com/Muvon/octocode/issues) — นั่นคือวิธีที่เคล็ดลับเหล่านี้ส่วนใหญ่ถูกบันทึกไว้ตั้งแต่แรก_
