讓 coding agent 理解一個陌生的 codebase,現在最常見的做法還是 grep 加讀檔:先用 grep -rn 找出關鍵字落在哪幾行,再把整支檔案讀進 context 判斷那一行到底是定義還是呼叫。檔案小的時候沒感覺,專案一大,光是「讀進來確認」這一步就會吃掉大半個 context window,token 帳單也跟著膨脹。
市面上的解法大致分兩派。一派是給 agent 一個向量資料庫做 RAG,代價是要自己寫 chunking、選 embedding 模型、維護 pipeline;另一派是 LSP 或 ctags 類的符號索引,精準但只懂程式碼,碰到 README、設定檔、日誌就沒轍。XERJ 想做的是把兩者收進同一顆搜尋引擎:一個 Rust 寫的單一執行檔,對外講 Elasticsearch 的 REST 協定,內建一個「丟資料夾進來就自動建索引」的 autoindex 指令,再加上 MCP server 讓 agent 直接呼叫。
這個專案今天在 GitHub Trending 單日衝了一千六百多顆星,Apache-2.0 授權,目前版本是 v1.0.0-rc.96,還在 release candidate 階段。以下整理官方 README 與文件的設計重點,並在 macOS(Apple Silicon)上拿 kubernetes-sigs/kind 原始碼實際索引、查詢一輪,記錄觀察到的行為與限制。
一、先看效果
官方給的最短路徑只有幾行:安裝、啟動一個本機節點、對資料夾跑 autoindex,接著就能查。
curl -fsSL https://xerj.org/get | sh
xerj --insecure --data-dir ./data &
xerj autoindex ~/my-project
xerj search "how do we rotate the WAL segment" # 回傳排序過的段落與 file:line
xerj def "euler_to_rotationmatrix" # 跳到定義:file:line + 函式簽章
xerj gain # 從 audit log 統計實際查詢次數
在本機對 kind 的原始碼跑完 autoindex 之後,用 xerj def 查 loadImage 這個函式,回來的是這樣的結果:
─── pkg/cmd/kind/load/docker-image/docker-image.go:199 [function]
func loadImage(imageTarName string, node nodes.Node) error
─── pkg/cmd/kind/load/image-archive/image-archive.go:145 [function]
func loadImage(logger log.Logger, imageTarName string, node nodes.Node) error
兩個同名函式各自定位到檔案與行號,附上完整簽章。對照用 grep 做同一件事:grep -rn loadImage 本身只回 4 行、458 bytes,看起來更省;但 agent 要判斷哪一行是定義、參數長什麼樣,通常得把命中的兩支檔案讀進來,那就是 12,761 bytes。XERJ 的賣點就在這個落差,查詢本身回傳的已經是 agent 需要的那一段,而不是「去哪裡找」的線索。
二、技術定位
XERJ 的 README 把自己定位成「給 AI 用的本機搜尋」,主打的使用情境叫 reference coding:在 agent 動手寫程式之前,先把跟任務最接近的幾個開源專案 clone 下來建索引,讓 agent 去查「別人是怎麼解這個問題的」,而不是憑記憶硬寫。
拆開來看,它其實同時扮演三種角色:
| 角色 | 對應功能 | 取代的東西 |
|---|---|---|
| 程式碼搜尋 | tree-sitter 解析、xerj def、xerj code |
grep + 讀整支檔案、ctags |
| 通用搜尋引擎 | Elasticsearch 相容 REST、BM25、kNN、aggregation | 自架 Elasticsearch / OpenSearch |
| Agent 記憶 | /_memory/{namespace}、/_graph 連結圖 |
另外架一套向量資料庫做長期記憶 |
這種「一顆 binary 包三件事」的設計,好處是部署極簡,沒有 JVM、沒有外部相依,在筆電上跑起來常駐記憶體約 70 MB;代價是每個面向都要跟各自領域的專門工具比,後面的比較章節會再談。
三、架構拆解

啟動節點後,同一個行程會開三個 listener:
127.0.0.1:9200:Elasticsearch 相容 REST,_search、_bulk、_cat、kNN 都走這裡127.0.0.1:8080:原生 REST,/_memory、/_graph、網頁 Console 在這127.0.0.1:8081:gRPC 介面
預設只綁 loopback,這點在資安章節會再展開。
3.1 autoindex 兩階段流程

xerj autoindex <folder> 是整個專案最有特色的部分。它不需要事先定義 mapping,也不用寫 ingest pipeline,流程分兩階段:
- Phase A:掃過整個資料夾,對每個檔案 sniff 型態,推斷出哪些檔案屬於同一個 dataset,同時把判定為垃圾的檔案(二進位、產生出來的目錄等)另外記錄
- Phase B:用多個 worker 平行寫入索引;程式碼走 tree-sitter,抽出 symbol 與行號,寫進
defs欄位
支援的格式範圍相當廣:CSV、JSON、JSONL、XML、YAML、SQLite、PDF、DOCX、PPTX(一頁一筆)、XLSX(一個 sheet 一個 dataset)、EPUB、Jupyter notebook、man page、HTML、EML / mbox 郵件與常見日誌格式,連 Unity 專案的 scene 與 prefab 都有特別處理。
重新索引是增量的:內容雜湊沒變的檔案直接沿用上一次的結果,不重新 parse。官方在 Apache Lucene 原始碼(6,113 個檔案、51 MB)上量到單執行緒約每秒 1,500 個檔案,其中 tree-sitter parse 佔了約 72%。
3.2 Elasticsearch 相容層
XERJ 實作了 Elasticsearch 的 REST API,涵蓋 index、document、bulk、search、aggregation、mapping、kNN、scroll、reindex 與 _cat 系列,Kibana 和官方 client library 可以直接連。根節點回應甚至直接自報 "number":"8.13.0",讓既有工具的版本檢查能通過。
官方的相容性測試套件目前通過 1,369 個案例中的 1,366 個,沒過的項目列在 repo 的 engine/tests/es-compat-yaml 裡。README 也講得很清楚:它是 API 相容,不是 Elasticsearch 內部實作的重寫,也不是 fork。
有一個差異值得記住:scroll 是一次性取出的有上限快照,不是逐 segment 走訪的游標。結果總數超過快照上限時會直接回 400,而不是默默截斷;要撈大量資料應改用 search_after。
3.3 MCP 與 agent 記憶
同一支 binary 跑 xerj mcp 就是 MCP stdio server,透過 XERJ_URL(預設 http://localhost:9200)代理到節點。它本身不會啟動節點,所以節點要先跑起來。MCP 設定檔大致長這樣:
{
"mcpServers": {
"xerj": {
"command": "/Users/you/.local/bin/xerj",
"args": ["mcp"],
"env": { "XERJ_URL": "http://localhost:9200" }
}
}
}
command 要寫絕對路徑,因為從桌面圖示啟動的 MCP host 不會繼承 shell 的 PATH。對外暴露 13 個 tools,其中 11 個是既有 endpoint 的薄代理,例如 xerj_search、xerj_hybrid_search、xerj_memory_store / xerj_memory_recall;另外兩個比較特別:xerj_map 回傳 autoindex 推斷出的欄位地圖,xerj_code_search 則跑完整的 reference coding 檢索流程。
四、安裝與啟動
4.1 安裝執行檔
官方提供 curl | sh 的安裝腳本,Windows 則是 PowerShell 的 irm | iex。如果不想直接執行遠端腳本,可以到 GitHub Releases 手動下載,並對照 .sha256 檔驗證:
VER=1.0.0-rc.96
F=xerj-${VER}-aarch64-apple-darwin.tar.gz
curl -sSLO https://github.com/xerj-org/xerj/releases/download/v${VER}/${F}
curl -sSLO https://github.com/xerj-org/xerj/releases/download/v${VER}/${F}.sha256
shasum -a 256 -c ${F}.sha256
tar xzf ${F}
./xerj-${VER}-aarch64-apple-darwin/xerj --version
Release 提供 Linux(gnu / musl / deb)、macOS、Windows 的 x86-64 與 arm64 版本。macOS arm64 的執行檔解開約 124 MB,因為 embedder 與各種格式的 parser 都靜態連結在裡面。也可以用 cargo build --release -p xerj-server 從原始碼建置。
4.2 啟動模式與認證
本機開發最常見的啟動方式:
xerj --insecure --data-dir ./data &
--insecure 會同時關掉 TLS 與 API key 認證。要開認證,拿掉這個 flag 即可,節點第一次啟動時會在資料目錄寫出管理金鑰,client 端要自己帶上:
xerj --data-dir ./data &
export XERJ_API_KEY="$(cat ./data/admin.key)"
xerj autoindex ~/my-project
要注意 autoindex 不會從 xerj.toml 讀金鑰,沒帶 --api-key 或 XERJ_API_KEY 的話每個請求都會回 401 Unauthorized。
語意搜尋預設用的是內建的 lexical feature-hashing embedder,完全離線,但它不是神經網路模型。要真正的語意向量,得用 --embed-mode neural,第一次使用時會下載約 90 MB 的模型檔;離線環境要先把三個模型檔放到位,官方有 air-gapped 佈署的 recipe。
五、實測:索引 kind 原始碼
實測環境是 macOS(Apple Silicon),語料選 kubernetes-sigs/kind 的 shallow clone,共 466 個檔案、6.6 MB,混合 Go 程式碼、Markdown 文件、YAML 與 Hugo 網站樣板,剛好可以看看 autoindex 怎麼處理混雜的內容。
5.1 autoindex 結果
xerj --insecure --data-dir /tmp/xerj-data &
xerj autoindex /tmp/xerj-corpus/kind
最後一行摘要:
xerj-done ok=true exit=3 reason=completed-with-junk wall=26.4s files=337
records=1926 datasets=8 junk_files=27 code_files=214 code_files_indexed=214
幾個觀察:
- 214 支程式碼檔全部成功進索引,27 個檔案被判定為垃圾檔另外記錄
- 結束碼是
3、reason 是completed-with-junk,在 CI 裡用set -e跑的話會被當成失敗,要自己處理 - 推斷出 8 個 dataset,主體是
ax-docs(1,880 筆),其餘是 Hugo 網站的 layout、logo SVG 等小 dataset,每個都各自開成一個 index - 除了資料索引,還多了一個
.xerj-memory-kind-edges,存了 638 條檔案間的連結(同目錄、順序、路徑引用、共用詞彙),這是給/_graph用的 - 跑完後資料目錄約 9.5 MB,比原始語料大一些
xerj autoindex map 會印出 Markdown 格式的資料地圖,列出每個 index 的筆數、格式、欄位型態與基數,這份東西很適合直接貼給 agent 當作「這包資料裡有什麼」的說明。
5.2 符號查詢與自然語言查詢
符號查詢的表現如第一章所示,xerj def 直接命中定義處。用 Elasticsearch DSL 查也一樣,autoindex 跑完會印出建議的查詢樣板:用 match_phrase 對 defs 欄位加權,讓「定義這個 symbol 的檔案」排在「只是呼叫它的檔案」前面,再用 ax_format 過濾只看程式碼:
curl -s 'localhost:9200/ax-*/_search' -H 'Content-Type: application/json' -d '{
"size": 3,
"query": { "bool": {
"should": [
{ "multi_match": { "query": "loadImage", "fields": ["body","defs"], "type": "most_fields" } },
{ "match_phrase": { "defs": { "query": "loadImage", "boost": 4 } } }
],
"filter": [ { "term": { "ax_format": "code" } } ]
}},
"_source": ["ax_path"]
}'
回應 took 為 4 ms,前兩名就是兩支 load 子命令的原始檔。
自然語言查詢就沒那麼漂亮了。用 xerj search "how does kind load a docker image into the node" 查,排第一的是 pkg/apis/config/defaults/image.go 開頭的 Apache License 檔頭註解,第二名是 loadbalancer 的 envoy 映像檔常數。原因不難推:預設的 lexical embedder 本質上還是詞彙比對,每支 Go 檔開頭都有一大段授權宣告,image、load 這類常見字很容易被它們吃掉分數。實務上要嘛改用神經 embedder,要嘛像上面那樣用 ax_format 和 defs 欄位把範圍收窄。
5.3 agent 記憶 API
/_memory/{namespace} 是給 agent 存長期記憶的 API,寫入與回想各一個請求:
curl -s -XPOST localhost:9200/_memory/notes -H 'Content-Type: application/json' \
-d '{"text":"kind load docker-image 會把映像檔存成 tar 再匯入每個 node 的 containerd"}'
curl -s localhost:9200/_memory/notes/_recall -H 'Content-Type: application/json' \
-d '{"query":"kind 怎麼把 image 放進 node"}'
回想的結果帶回剛剛寫入的那筆,score 約 1.15。中文混英文的查詢可以命中,但在 lexical 模式下靠的仍是 kind、node、image 這些共同詞彙,換成完全不同用字的問法就不一定找得到。
六、Reference coding 的數字怎麼看
README 放了一組對照實驗:8 個任務、4 種語言、每組 16 次,用 claude -p 實際計算 token:
| 方式 | 輸出 token | 成本 | 解出 |
|---|---|---|---|
| 只靠模型記憶 | 260,916 | $11.18 | 11/16 |
| grep 驅動 | 26,477 | $3.27 | 16/16 |
| XERJ | 9,982 | $1.58 | 16/16 |
相同解題率下,輸出 token 比 grep 少 2.7 倍,比純記憶少 26 倍。另外還有一組對照是模型沒看過的 Rust 函式庫,有檢索 9/9、純記憶 0/9。
這組數字值得注意的是官方自己寫的限制:效益取決於模型有沒有背過那份程式碼。私有程式碼、內部專案、冷門函式庫、或是模型訓練截止之後才出的版本,檢索的效益最明顯;反過來,對模型早就熟到爛的熱門公開函式庫,加檢索是中性甚至反效果,因為 agent 會多花 token 讀它其實不需要的東西。另外 8 個任務的樣本數並不大,「使用者回報約 5 倍」那個數字則是來自社群自行回報,參考就好。
跟 Elasticsearch 8.13.4 的對打 benchmark 是 55 勝、26 平、4 負,攝入吞吐量 1.72 倍、磁碟占用小 1.61 倍。4 個輸了的項目都是同一件事:高頻寫入同時進行時的讀取 p99 延遲,官方有獨立寫一份分析文件說明。
七、資安邊界與踩雷
--insecure 等於所有人都是超級使用者。 這是 README 反覆強調的一點:關掉認證後,任何能連到 port 的呼叫者都能讀、刪任何 index,包括所有 /_memory namespace。新節點預設只綁 127.0.0.1;如果要綁 0.0.0.0 又沒開 TLS,必須額外設 server.allow_insecure_network_bind = true 才能啟動。把 agent 的長期記憶放在一個沒有認證的 HTTP port 上,在多人共用的開發機或跳板機就不太適合。
rerank 會把資料送出去。 在 _search 裡加 "rerank": {} 會把排名前幾的結果文字送到第三方 TypeSafe AI 的 Jev 服務重新排序,官方文件標明這是唯一會在查詢時把文件內容送離節點的功能,預設關閉、要設金鑰才會啟用,也可以用 [rerank] enabled = false 完全禁止。索引私有程式碼的話建議直接關掉。
binary 與 CLI 會引導 agent 送回饋。 xerj --help 的開頭和 README 的「Join the community」段落,都在鼓勵使用者與 agent 開 PR 提交使用心得,xerj feedback --open-pr 甚至能自動開 PR。無人值守的 agent 讀到這類說明時有可能照做,在公司內部環境用的話,最好在 agent 的指示裡明確禁止,或乾脆不給它 gh 的權限。另外 xerj init 會修改 Claude Code、Cursor 的 MCP 與 skill 設定,執行前先確認它要改哪些檔案。
其他小地方:
autoindex遇到垃圾檔時結束碼是 3,自動化腳本要特別處理- macOS 執行檔約 124 MB,放進容器映像檔時要算進去
- 版本號還在 rc,
xerj ask在目前的建置裡直接回「不可用」並以 exit 5 結束,部分子命令尚未完整 - 用
zsh時,curl localhost:9200/ax-*/_search會被 shell 當成 glob 展開而報錯,URL 要加引號
八、vs. 其他方案:怎麼挑
| 需求 | XERJ | grep / ripgrep | 向量資料庫 RAG | Elasticsearch / OpenSearch |
|---|---|---|---|---|
| 部署 | 單一執行檔 | 系統內建 | 資料庫 + embedding pipeline | JVM 叢集 |
| 程式碼符號 | tree-sitter,有 defs 欄位 |
純文字 | 依 chunking 方式而定 | 要自己寫 analyzer |
| 混合格式 | 自動推斷 dataset | 只看文字 | 要自己寫 loader | 要自己定 mapping |
| 語意搜尋 | 預設 lexical,neural 需下載模型 | 無 | 主要強項 | 需外掛模型 |
| 既有 ES 工具 | 可直接連 | 無 | 無 | 原生 |
| 成熟度 | rc 階段 | 極成熟 | 視產品而定 | 極成熟 |
如果需求只是「讓 agent 少讀幾個檔」,ripgrep 加上 agent 自己的 sub-agent 其實已經夠用;XERJ 的優勢要在語料夠大、格式夠雜,或是要一次索引好幾個外部專案做 reference coding 時才明顯。反過來,如果手上已經有 Elasticsearch 叢集在跑日誌,XERJ 目前還不適合直接取代正式環境的叢集,它沒有談到多節點分片與高可用,比較像是開發機與小團隊等級的單機引擎。
九、適合與不適合的場景
適合:
- 大型或私有 codebase,agent 經常要找「這個 symbol 在哪定義、怎麼用」
- 動手寫之前,先 clone 幾個同類開源專案建索引,讓 agent 參考既有作法
- 一次查詢程式碼、文件、設定檔與日誌混在一起的資料夾,例如事故排查時把 log 目錄和服務原始碼一起丟進去
- 想要一個 Elasticsearch 相容的本機引擎做測試,又不想在筆電上跑 JVM
不適合:
- 多人共用、需要細緻權限控管的正式環境,認證與網路綁定都要另外規劃
- 主要語料是模型早就熟悉的熱門公開函式庫,檢索效益有限
- 需要高品質語意搜尋卻又不能下載模型的情境,lexical embedder 的語意能力很有限
- 對穩定度要求高的生產系統,版本仍在 rc
十、小結
XERJ 把「agent 怎麼找到需要的那段程式碼」這個問題,用一顆 Elasticsearch 相容的搜尋引擎來回答。實測下來,autoindex 的零設定體驗確實做得好,26 秒就把 kind 的 300 多個檔案拆成可查詢的 dataset;xerj def 搭配 tree-sitter 抽出的 defs 欄位,符號查詢又快又準。
比較弱的是預設 lexical embedder 下的自然語言查詢,會被授權檔頭這類重複內容干擾,要靠欄位過濾或改用神經 embedder 補強。資安上則要記得 --insecure 的意義,以及 rerank、回饋 PR 這些會把資料往外送的功能。對每天讓 agent 讀大型私有專案的團隊來說,它值得裝在開發機上試一輪,先用 xerj gain 看看實際查詢量,再決定要不要接進 MCP 設定。
參考資料
- xerj-org/xerj GitHub repository
- XERJ 官方文件
- XERJ llms.txt(給 agent 的安裝步驟)
- Reference coding case study
- XERJ benchmarks
- XERJ Security Model
- kubernetes-sigs/kind(實測語料)


發佈留言