Semantic search over a Markdown knowledge base, served over MCP.
コードの chunker が持っていた上限は 1 つだけだった — 1 MiB を超えるソースファイルは 拒む (ADR-0012 が それ以外をどう扱うかを書いている)。この上限が数えるのはバイト数であり、効く入力は 大きくない。
定義がどのスコープに属するかを求めるには、そのノードから構文木の root まで登る必要が
あり、tree-sitter の Node::parent は保持されたポインタではなく探索である。この
walk は定義ごとに繰り返されるので、定義が深く入れ子になったファイルは深さを二重に払い、
合計は深さの三乗で伸びる。mod a{ を 1000 回並べて閉じ括弧を付けた 1 行のファイルは
10 KB 未満で、index に 64 秒かかった。500 段なら 8 秒、125 段なら 0.3 秒。何もこれを
拒まなかった — 上限がバイト数しか見ておらず、このファイルは 1 MiB に対して誤差だからである。
これは「遅いファイル」より悪い。rebuild_index は embedder と DB を実行の間ずっと
保持するので、こういうファイルが 1 本ある KB は、その実行が終わるまでサーバへの
全リクエストが止まる。KB は必ずしも検索する本人が書いたものではない。
問い: コストがサイズの関数でない chunker は、何で縛るのか。
ParseOptions の progress callback、TagsContext::generate_tags の
cancellation flag)選択肢 3 を採用し、超過時は拒否 (4) ではなく degrade する。定義は祖先 64 個までの
位置に居てよい。walk は歩数を数え、上限を超えた最初の定義でそのファイルの定義単位
chunk 化を終える。以降そのファイルは行単位で chunk 化され (定義が覆わない範囲が
既にそうなっているのと同じ形)、parse:too-deep を付ける。
なぜ時計ではないのか (選択肢 1)。秒数の予算は索引を「それを作ったマシンの関数」に してしまう。同じファイルが速いマシンでは定義 chunk に、負荷の高いマシンでは行 chunk に なり、KB 側は何も変わっていないのに再 index で答えが変わる。groove は同じ理由で connection graph の壁時計予算を一度退けている。口は使えるが、出てくるものが再現しない。
なぜ木ではなくスコープ walk なのか (選択肢 2)。どちらも入力の性質なので再現性は 同じ。違うのは効かないときのコストである。木の深さを測るには全ファイルの全ノードを 歩く必要があり、このリポジトリ自身の 62 ファイルで 174 ms かかった (parse 424 ms / tags query 882 ms に対して)。既に走っている walk の中で歩数を数えるならインクリメント 1 回で済む。加えて scope walk こそが実際に爆発する当人なので、「どの代理指標なら 十分近いか」という議論が要らない。
なぜ 64 なのか。選んだのではなく測った: このリポジトリの全ソースをパースして最長の
walk を記録すると 8 (main.rs)。同じファイル群の構文木の最大の深さは 32 で、
これは選択肢 2 が超えなければならなかった数 — 2 つの量は交換可能ではないという
証拠でもある。64 は実在コードに 8 倍の余地を残す。
なぜ拒否ではなく degrade なのか (選択肢 4)。拒否は 1 行で済み、その後の per-file skip は既にある。しかし ADR-0012 は「ファイルは持っているバイトをすべて出す」と約束しており、 深く入れ子なだけのファイルは読める — 構造を解く価値が無くても、本文はクエリに答える。 プレーンテキスト parser への fallback も選べない: あれは 1 ファイル 1 chunk を作るもので、 コード chunker はまさにその形を避けるために存在する。
symbol_kind も見出しも
スコープ context も無い)。document に付く parse:too-deep が、それを黙らせずに
検索から見えるようにするparse:too-deep は parse:degraded とは別のタグである。後者は「grammar が
ファイルの一部を読めなかった」、前者は「読めたが groove が chunk 化を降りた」groove index --force で作り直すchunker は grooveseek/src/parser/code/ にある。上記の計測は release ビルドで、
2 本のバイナリを 1 台のマシンで交互に回して取った (生成した fixture への
groove index --force)。そうやって取らないと、ビルドが違うだけで数字が 3 割動く。