GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

使い方

groove コマンドのリファレンス — index / status / serve / search / graph / validate / doctor / eval / tune / service。

English version: usage.md

検索インデックスの構築 / 再構築

groove index --kb-path /path/to/knowledge-base
groove index --kb-path /path/to/knowledge-base --force   # 完全再インデックス
groove index --kb-path /path/to/knowledge-base --model bge-m3 --force  # BGE-M3 (1024 dim、多言語) に切替

指定ディレクトリ配下のソースファイルを走査し、既定の exclude_dirs セット (.obsidian / .git / node_modules / target / .vscode / .idea — docs/behavior.ja.md の「ディレクトリ除外」参照) をスキップする。既定では .md のみ取り込み。groove.toml に [parsers].enabled = ["md", "txt"] を追加すると .txt もインデックス対象になる (タイトルはファイル名から派生: deep-dive-2026.txt → "deep dive 2026"、本文全体が 1 チャンク)。このキーは groove が信頼する groove.toml — --config で名指ししたもの、バイナリの隣にあるもの、groove service install が置いたもの — に書くこと。KB の隣で見つけただけの config は [parsers] が既定へ戻される (信頼する置き場所 / しない置き場所)。前回実行時と content hash が変わっていないファイルは --force を渡さない限りスキップされる。

[parsers].enabled = ["md", "rs"] (v1.2.0+) を指定すると Rust のソースも index 対象になる。単位は見出しではなく定義 1 つにつき 1 チャンク — 詳細は docs/behavior.ja.md のソースコードインデックスの補足を参照。他の言語は利用者が置く plugin として届き、"py" (v1.3.0+) は groove-grammar-python を、"php" (v1.5.0+) は groove-grammar-php を、先に grammar ディレクトリへ置く必要がある (grammar plugin の置き方)。[parsers.code].max_chunk_chars を変えると既存ファイルの切れ目も変わるが、index は内容が変わっていないファイルとそれを区別できない — 警告を出して --force を名指しするので、再チャンクはそれで行う。

--model が受け付ける値:

既存インデックスでのモデル切替には --force が必須 (DB の index_meta テーブルにモデル / 次元が記録されており、不一致時は起動が拒否される)。

外部の OpenAI 互換 embedding

既定は FastEmbed のままで、embedding の推論はプロセス内で行われる。代わりに HTTP の embedding サービスを使うには、groove が信頼する groove.toml に [embedding] セクションを書き、そのファイルがプロジェクトローカルなら --config で名指しする:

[embedding]
provider = "openai-compatible"
endpoint = "http://127.0.0.1:8001/v1/embeddings"
query_model = "query-model"
document_model = "document-model"
dimension = 768
request_dimensions = false
timeout_seconds = 60
max_input_chars = 8000
max_retries = 3

endpoint はインデックス時に document のチャンクを、検索時に検索テキストを受け取る。 groove がカレントディレクトリや Git の祖先で見つけただけの config では、この セクションは無視されて FastEmbed に戻る。信頼する置き場所 / しない置き場所 を参照。

各リクエストは OpenAI 互換の POST /v1/embeddings の JSON 形 (model と input) を使う。任意の dimensions 欄に対応した endpoint に限り、request_dimensions = true にする。どの role にも model alias が要る: query_model と document_model は それぞれ自分の role について共通の model より優先し、共通の model は書かれていない role を埋める。したがって model 1 つだけ、上の例のように role 別 alias を両方、 model と role 別 alias 1 つ、のどれでもよい。dimension は必須: groove は HTTP provider を作る前に この値でベクトル索引を開くか検証し、その後はベクトルの長さが異なるレスポンスを すべて拒否する。リクエストは 1 回あたり最大 64 入力のバッチで送る。groove は 索引を開く・検証する・配信するために endpoint を probe しない (次元を必須にし probe しない理由は ADR-0022)。 例外は強制再構築だけで、groove index --force と MCP ツール rebuild_index の force: true は、まず固定の文字列 GrooveSeek endpoint probe (知識ベースの中身は 含まない) を document_model で 1 件 embedding し、endpoint が断るか次元の違う 応答を返したら、索引から何も消していないと述べるエラーで止まる (ADR-0024)。 --force が無ければ probe はしない。任意の api_key は bearer token として送られる。GROOVE_EMBEDDING_API_KEY が設定されていればそちらが 優先され、token をファイルに書かずに済む。

provider、どちらかの model alias、または次元を変えると、実行時の設定が既存インデックスと 合わなくなり groove index --force が必要になる。endpoint、api_key、 request_dimensions、timeout_seconds、max_input_chars、max_retries を変えてもそうはならない。これらは記録される identity に含まれないので、alias の背後の model が変わっても groove には分からない。endpoint が同じ alias で別の model を返すように なったら、自分で groove index --force を実行すること。そうしないと、古い document の ベクトルと新しいクエリのベクトルがエラーも出ずに一緒に検索される。--model は従来どおりの意味を保ち、 その 1 回の実行に限って FastEmbed を選び、[embedding] を上書きする。トップレベルの model キーは FastEmbed 専用で、provider = "openai-compatible" と同時に書いた config は groove が拒否する。provider を切り替える時はこのキーを消すこと。FastEmbed (既定の provider) ではこのキーと [embedding].model が同じものを指すので、両方を書いた config も 拒否される。どちらか一方を残すこと。

endpoint が断った時・失敗した時。 endpoint が HTTP 400・413・422 で入力を断った ファイルは、そのファイルだけが skip される: groove index は warning: <file>: embedding endpoint rejected the input (HTTP <status>); skipped, the index keeps what it had for this file を出し、skipped に数え、そのファイルの既存の row を残して先へ進む。まだ row の無い ファイルは代わりに skipped, this file is not in the index と名指しされる。 --force のとき (MCP rebuild_index {force: true} も) は、再構築が先に索引を空に するので全ファイルがこの立場になる: 断られたファイルが 1 つでもあれば、他のファイルを すべて索引し run を最後まで済ませたうえで exit 非 0 になる (MCP は counter の隣に error)。 run が 1 つでも ファイルを (そのどのバッチも受け付けられないまま) 断られ、endpoint がどのバッチも受け付けなかった 場合は、run を最後まで (削除も含めて) 済ませた うえで exit 非 0 で終わる: この形はファイルではなく model / document_model か endpoint の誤りを指している。MCP rebuild_index は同じ run に error を返す。 HTTP 429・5xx は再試行し (max_retries)、401・403 とその他の 4xx はその場で run を 止める。status 行が届く前の timeout・接続失敗は再試行し、その他の通信エラーは run を 止める。status 行が届いたらその status で決まり、body が止まっても途中で切れても変わら ない (2xx で body が timeout したときは再試行する)。daemon は再試行の間 embedder を握ったままなので、検索もその間 待たされる。それが困る所では max_retries = 0 にする。入力は先に max_input_chars 文字で切られる。日本語は 1 文字が 1 token を超えることがあるので、上限 8192 token の サーバでも 8000 文字を断る場合がある。warning が多くのファイルを挙げるなら max_input_chars を下げる。groove-schema.toml の宣言キーが変わった後は、断られた Markdown ファイルは「読めていない」ものとして数えられるので、endpoint が断り続ける間は 新しい宣言フィールドの集合が記録されず、そのファイルを直すか、max_input_chars を下げるか、 groove index --force を打つまで、検索は fields / fields_not の filter を断る。詳細は ADR-0025。

--force は、SQLite として開けなくなった .groove.db の修復手段でもある (書き込み途中の切断や、migration 中に kill されたプロセスで起きる)。その状態では file を開くすべてのコマンドが、file の場所 (--kb-path の親ディレクトリにある) と 2 通りの直し方を message で示して失敗する: file を消して groove index を実行するか、groove index --force を実行する。後者は file と -wal / -shm の付随 file を置き換えて最初から作り直す。索引は corpus から完全に導出できるので失うものは無い。--force 無しでは file に触らない。

進捗出力フラグ (v0.7.8+)

groove index の進捗表示を切り替える 2 フラグ。相互排他 + フラグなしの既定動作は不変 (= 既存の per-file ` indexed: foo.md (N chunks)` 出力をそのまま維持)。

groove index --kb-path ./big-kb --quiet         # 完了まで silence
groove index --kb-path ./big-kb --progress      # TTY ではバー、pipe では定期行

Done in 行はその run が何をしたかを数える: updated (内容が変わって再 embedding したファイル)、renamed、deleted、skipped (disk にはあるが索引されなかった — read 不能 / size cap 超過 / 空)、そして frontmatter unparsed (v1.7.0+)。最後のものは、その run で YAML frontmatter が parse できなかった Markdown の件数。各ファイルは上の warning: 行で名指しされ、title / date / topic 空で索引され、frontmatter:unparsed の tag が付くので --tag-any frontmatter:unparsed で一覧できる。frontmatter だけの stub は本文が無いので skip されるが、名指しはされたので数には入る。前回の索引から変わっていないファイルは再 parse されないので再カウントもされない。--force なら全件 parse し直す。例外は、旧版が書いた index にこの版を初めて当てる run で、変わっていない Markdown 全件の frontmatter を 1 回だけ読み直し、見つけたものに tag を付けて数え (再 embedding はしない)、Tagged N unchanged Markdown document(s) ... と報告する。次の run からは fast path に戻る。groove.toml に [index].fail_on_frontmatter_error = true があると、run を完走して summary を出してから、この件数が 0 でなければ exit 1 で終わる。

.docx の切り方を変えた版 (見出しを word/styles.xml から読む、本文を持たない見出しを次の節に畳む、表の行を 1 行に書く) で最初の groove index / rebuild_index は、変わっていない .docx を 1 回だけ読み直す。.pptx の読み方を変えた版 (v1.19.0: 表の行を 1 行に書き、改行を保つ) も、変わっていない .pptx を同じく 1 回だけ読み直し、専用の告知 (Re-reading N unchanged .pptx document(s) once: table rows are now one line each, cells separated by tabs, and line breaks are kept; ...) を、両方が走る時は .docx の告知の後に出す。どちらの読み直しも自分の形式のファイルしか読まない。.docx の告知行は、追いつく変更が表の変更だけの時は table rows are now one line each, cells separated by tabs、それ以外は headings now come from word/styles.xml。v1.20.0 は .docx / .pptx の chunk に表の行の位置も記録する: 追いつくのがその記録だけの形式では、告知は table rows are now marked in each chunk になり (Re-reading N unchanged .docx document(s) once: table rows are now marked in each chunk; ...、.pptx も同じ)、run は変わっていない .docx / .pptx を 1 回だけ読み直してその記録だけを書き、再 embedding はせず updated にも数えない。分かれ方が変わった文書は再 embedding して updated に数え、前と同じに分かれる文書はそのまま、新しい読み方で索引するものが無くなった文書 (例: exclude_headings が全セクションを覆うようになった) は索引から外して、空のファイルと同じく skipped に数える。groove index は文書を処理する前に Re-reading N unchanged .docx document(s) once: ... と知らせる (--quiet を付けた時は出さない)。MCP rebuild_index は quiet で走るので、そこでの手がかりは updated に数えられた再分割の文書だけ。その run で読めなかった .docx / .pptx (サイズ上限超過や parse 失敗) は、変更されたファイルの parse 失敗と同じく、読めるまで以降の run ごとに試され Skipping ... 行が繰り返し出る。それ以外は次の run から fast path に戻る。再分割した .docx / .pptx を embedding endpoint が断った場合も同じく以降の run ごとに試され、その run で他に何も embed されなければ、変更されたファイルの時と同じく exit 非 0 で終わる (外部の OpenAI 互換 embedding を参照)。groove serve を使っているなら、先に daemon を新しい版で再起動し、その後で rebuild_index を呼ぶか groove index を 1 回打つこと: file watcher は変わったファイルしか読み直さないので、それまで変わっていない .docx / .pptx は前の版が切った chunk のまま。旧版の daemon が動いたままだと、読み直しの後に編集された .docx / .pptx を旧判定で書き、それは --force でしか戻らない。v1.19.0 以前の daemon が動いたままだと、v1.20.0 の読み直しの後に編集された .docx / .pptx は表の行の位置を持たずに書かれ、次の編集か --force までそのまま残る。

モデル選択のトレードオフ

観点 BGE-small-en-v1.5 BGE-M3
初回 DL 約 130 MB 約 2.3 GB
埋め込み次元 384 1024 (index ファイルが約 2.6 倍)
実行時 RAM 約 500 MB 約 2 GB
index ビルド時間 baseline CPU 推論で約 3–10 倍遅い
日本語精度 低い (英語中心語彙) 強い (多言語 tokenizer + 訓練)
英語精度 強い 同等

モデル切替コスト (既存 index → 新モデル):

  1. groove index --kb-path ... --model <new> --force で完全再 embedding (増分更新不可: documents/chunks/vec_chunks を全削除してやり直す)
  2. 以降の serve / index はすべて同じ --model を渡す (または groove.toml に書く)。不一致は index_meta チェックで起動拒否

実務的な推奨: 最初に KB の主要言語に合うモデルを選び、具体的な精度問題が無い限りモデル間でブレない — 完全再 embedding が最も重いステップだから。

MCP サーバの起動

groove serve --kb-path /path/to/knowledge-base
groove serve --kb-path /path/to/knowledge-base --model bge-m3   # index 時と一致必須
groove serve --kb-path ... --model bge-m3 --reranker bge-v2-m3  # + cross-encoder 再ランク
groove serve --kb-path ... --transport http --port 3100         # HTTP、複数クライアント
groove serve --kb-path ... --no-watch                           # ライブ同期無効

既定では stdio トランスポート (1 クライアント / サーバ) で MCP サーバを起動する。複数クライアントを同時接続するには --transport http --port <PORT> (または --bind <SOCKETADDR>) を渡し Streamable HTTP に切り替える — 詳細は HTTP トランスポート (複数クライアント同時接続) 参照。loopback 外の --bind は、groove が認証を持たないため追加で --i-know が必要。Linux で LISTEN_FDS を渡す service manager (systemd、または同じ protocol を話すもの) がある環境では、--systemd-socket でその service manager が既に bind した待ち受け socket をそのまま受け取れる。--bind / --port とは同時に指定できず、socket が渡されていない場合は TCP に落ちずに起動に失敗する。abstract socket — 名前が @ で始まる ListenStream= — も拒否する。SocketMode= を効かせるファイルが無いためである。他の OS 向けビルドはこのフラグ自体を拒否する。trusted config の [transport.http].systemd_socket も同じ扱いで、実効 transport が socket を受け取れない stdio に解決されたときは、キーを無視せず起動を拒否する。ただし自分で --transport stdio を指定した場合は例外で、その 1 回に限りその config を stdio で動かせる。

サーバは 6 つの MCP ツール (docs/mcp-tools.ja.md) を公開し、インデックスをプロセス内に保持して低レイテンシでクエリに答える。--model が現在の index を作ったモデルと一致しない場合、実行可能なエラーメッセージで起動を拒否する。ファイルウォッチャ (既定有効) が --kb-path 配下のコンテンツ変更を検知して再インデックスする — ライブ同期 (file watcher) 参照。

--reranker (任意、既定 none) はハイブリッド検索の上位候補に cross-encoder 再ランクをかける:

再ランクは CPU では高い。しかも効いているのはモデルのロードではなく cross-encoder の推論そのものである。v1.0.0 に対して 1 台の Windows マシンで実測 (CPU のみ、埋め込み bge-m3、reranker bge-v2-m3、141 文書 / 1,855 chunk の KB、limit = 5 なので候補プールは 50 ペア): 1 クエリが groove search で 74〜87 秒、常駐 daemon の /mcp 経由で 74〜79 秒。同じクエリを再ランク無しで投げると 3.1〜3.6 秒 と 約 0.1 秒である。常駐させても救われない: reranker は run_server が起動時に構築するので、daemon の 2 本目以降にロードすべきモデルは残っていない。それでも 74〜79 秒かかる。効いているのは候補プールに対する cross-encoder の推論そのものである。有効にする前に自分のハードウェアで測ること: groove search "<query>" --reranker bge-v2-m3 と --reranker none を同条件で繰り返し、プロセスの外側から時間を計る。--rerank-by-default <BOOL> (--reranker 指定時は既定 on) はすべての search 呼び出しで再ランクするかを制御する。値を取るフラグなので、無効化は --rerank-by-default=false と書く。MCP ツール側は rerank: Option<bool> で per-query 上書き可能。reranker の切替に再インデックスは不要 (index 非依存)。

v0.27.0 以降、rerank_by_default は groove search も読む。同じ groove.toml で「CLI は再ランクするがサーバはしない」という食い違いは起きない。コマンドライン側の per-query 上書きは --reranker そのもので、モデルを明示すればファイルが false でもそのクエリだけ再ランクし、--reranker none ならそのクエリだけ切れる (groove eval は意図的にこのキーを読まない — 測るのは --reranker が選んだパイプラインで、run fingerprint もそのモデルを記録するため、黙って別物を測ってはいけない)。

再ランクを有効にすべきケース

再ランクは精度とレイテンシのトレードオフ。使用パターン次第:

シナリオ 推奨
対話的エージェントフロー (LLM が 1 ターンで 2–5 回 search を呼ぶ) 切っておく。1 回 1 分超では「レイテンシ税」ではなく別の対話形式になる。BGE-M3 + 見出し加重 bm25 の検索品質で大抵十分
精度重視の単発クエリ (調査・定義的回答) 1 クエリ 1 分を許容できるなら有効化。支払いは 1 ターンに 1 回で、cross-encoder が意味的に関連する候補を明確に前に出す。ただし CPU ではその 1 分が回答時間そのものなので、既定 on より per-query の opt-in (rerank: true) を勧める
混在 rerank_by_default = false で始め、呼び出し側が個別に選べるようにする — MCP ツールの rerank: true、コマンドラインなら --reranker <model>

再ランクを入れるべきサイン:

再ランクは index 非依存なので、1 週間試して品質差を測り、見えなければ無効化してよい — 再インデックス不要。

groove を OS サービスとして登録 (v0.8.0+)

groove service install で daemon を OS のユーザレベルサービスとして登録し、ログイン時の auto-start を設定できる (admin / sudo 不要)。

# デフォルト: service name 'groove'、bind 127.0.0.1:3100、auto-start ON
groove service install --kb-path /path/to/your-kb

# Multi-instance (= 複数 KB を別サービスとして実行)
groove service install --service-name work --kb-path /path/to/work-kb --bind 127.0.0.1:3100
groove service install --service-name personal --kb-path /path/to/personal-kb --bind 127.0.0.1:3101

# 確認 / 管理
groove service status                              # default 'groove'
groove service status --service-name personal      # 名前付き instance
groove service list                                # 全 instance
groove service uninstall --service-name personal               # unit のみ削除、config + DB 残す
groove service uninstall --service-name personal --purge --yes # config + DB も削除

OS 別バックエンド:

Installer は config home を <dirs::config_dir()>/groove/<service-name>/ に作成し、groove.toml (kb_path / bind 含む) を配置。base directory は GROOVE_CONFIG_HOME env var で override 可能。登録される launch line はこのファイルを --config で名指しする (v0.20.0+) ので、daemon は working directory から探し当てたものではなく installer が書いた config を読む。詳細は 信頼する置き場所 / しない置き場所 を参照。

非 loopback の bind (例: 0.0.0.0:3100) は groove が認証機構を持たないため --i-know 明示が必要。

v0.7.x personal-http レシピからの移行: grooveseek/examples/deployments/personal-http/ のテンプレートは v0.8.0 で削除。手動 install 済の unit を groove service install 実行前に削除すること:

  • Linux: systemctl --user disable groove.service && rm ~/.config/systemd/user/groove.service
  • macOS: launchctl bootout gui/<uid>/com.groove.groove && rm ~/Library/LaunchAgents/com.groove.groove.plist
  • Windows: schtasks /End /TN '\groove' ; schtasks /Delete /TN '\groove' /F (= \groove は旧 task 名に置換)

旧 groove.toml の設定 (model = "bge-m3" / exclude_dirs / best_practice / fastembed_cache_dir 等) を持ち越したい場合は、install 後に 新 config (<dirs::config_dir()>/groove/<service-name>/groove.toml) を編集。kb_path は必ず絶対パスで記述すること — 新 daemon の WorkingDirectory は config_home なので、相対パス kb_path = "./knowledge-base" は <config_home>/knowledge-base に解決され実 KB を見失う。Windows path の backslash escape を避けるには TOML literal 文字列 (single quote) が便利: kb_path = 'C:\Users\you\your-kb'。

Tray monitor (Windows only、v0.9.0+)

groove-tray.exe は Windows system tray に常駐する daemon 監視 binary。v0.14.0 以降は専用 archive groove-tray-x86_64-pc-windows-msvc.zip として配布される (groove の archive の中ではない)。groove.exe と同じディレクトリに展開すること — groove service install --with-tray はそこを探す。(v0.14.0 より前のリリースにはそもそも含まれていなかった: Windows 用の companion binary 2 本はビルドされていたが release に添付されていなかった。v0.14.0 以降を使うこと)

daemon と一緒に install:

groove service install --kb-path C:\path\to\kb --with-tray

次回 logon で tray icon が表示され、color dot で daemon 状態を示す:

right-click で 6 menu items: Status (read-only) / Open Web UI / Start / Stop / Restart / Quit Tray。Start は scheduled task を実行、Stop は /api/admin/status が報告する pid のプロセスを終了させ (v0.14.0+)、daemon の bind アドレスを bind できることで停止を確認する — Stop-ScheduledTask が止めていたのは即座に終了する launcher だけで、実質何もしていなかった。

Tray log は %LOCALAPPDATA%\groove\logs\tray.YYYY-MM-DD (= 日次 rotation)。verbose 出力には GROOVE_TRAY_LOG=debug を設定、--debug flag で console attach して stdout/stderr を直接見る。

daemon を uninstall すると tray shortcut も一緒に削除:

groove service uninstall --service-name groove

daemon と独立に tray shortcut だけ管理する subcommand:

groove service tray-install --service-name groove     # shortcut のみ追加
groove service tray-install --service-name groove --force   # 既存 shortcut を上書き
groove service tray-uninstall --service-name groove   # shortcut のみ削除

tray は 127.0.0.1:<port>/api/admin/status を polling するので、daemon は loopback (127.0.0.1) または wildcard (0.0.0.0) で listen している必要あり。192.168.1.5:3100 のような特定 NIC bind は loopback で listen しないため tray polling が fail (= 起動時に warning log)。

インデックスの状態確認

groove status --kb-path /path/to/knowledge-base

既存 index の状態を stdout に 表示するので groove status | … が使える: document / chunk 数、tags frontmatter の parse に失敗した件数、index が構築された context mode (static / off)、そして index が記録している宣言 key の集合 (v1.11.0+) — --field で filter できる key と、document_fields が持つ値の行数。pending は「完走した groove index がまだ集合を記録していない」の意味で、いま --field 付きの検索を打つと拒否される (docs/filters.ja.md)。品質フィルタを通過するチャンク数はもう 1 行で出るが、実効閾値が 0 より大きいときだけなので、[quality_filter] enabled = false や threshold = 0.0 では出力されない。

索引がまだ無い場合、”No index found” の案内は stderr に出て stdout は空のままになる — 答えられなかったので結果を出していない、ということ。上の各行の文面は凍結していない (docs/stability.ja.md)。2 つの件数を機械可読に取りたい場合は groove doctor --format json を使う。

コマンドラインからの一発検索

シェルスクリプトや skill bin が「KB をこの文字列で検索したい」だけの目的で使う用途 — MCP 接続を立ち上げずに:

groove search "RAG server comparison" --limit 3 --format text
groove search "E0382" --category deep-dive --format json | jq '.results[] | .path'
groove search "クエリ最適化" --reranker bge-v2-m3        # 呼び出し単位の再ランクも可

--format は json (既定、後述「検索フィルタと引用」の通り { results, low_confidence, filter_applied } ラッパ) か text (--- 区切りの LLM フレンドリなブロック)。他のフラグは serve と同じ: --kb-path / --model / --reranker / --category / --topic / --limit。品質フィルタは既定有効 — 単発クエリで フィルタ無効状態に戻すには --include-low-quality または --min-quality 0 を渡す。groove.toml の既定値は serve / index と同じく適用される。

クエリがどうマッチするか (v0.16.0+): ハイブリッドの FTS 側はクエリを逐語で探すわけではない。クエリを Separator と文字種境界 (漢字 / ひらがな / カタカナ / それ以外の語構成文字) で割り、trigram 下限の 3 文字に満たない断片は隣接断片と連結し、そうしてできた phrase 群を OR で結んで検索する — つまり 再ランキングの評価について は 再ランキング / ランキング / の評価 / について を探すので、自然文の質問がそのままの形で出現していなくてもマッチする。1 個の逐語 phrase として固めたい部分は "..." で囲む (groove search '"Foundry Local" の設定')。クエリ全体を囲めば v0.16.0 以前の部分文字列検索がそのまま再現される。search MCP ツールも同じコードパスを通るので挙動は変わらない。この変更に再 index は不要。詳細は docs/retrieval-pipeline.ja.md を参照。

語を除外する (v1.1.0+): whitespace 区切りの group の先頭に - を付けると、検索するのではなく両脚から除外する。例: groove search 'rust -async'。コマンドラインでは positive の語を先に置くこと — 先頭が - のクエリは引数パーサに flag と解釈される — 先頭の除外は groove search -- '-async rust' で escape する。先頭ハイフンを逐語検索したいなら quote する ("-foo")。詳細: ADR-0011。

典型的な skill-bin 用途: Claude Code の skill が bin/ に groove.exe + groove.toml を同梱し、groove search "<user_query>" --format text --limit 3 のようなコマンドで LLM が引用するための参照抜粋を返す。

検索フィルタと引用 (v0.3.0+)

v0.3.0 から search MCP ツールの戻り値が単なるヒット配列ではなくラッパオブジェクトになる。これは破壊的変更で、Vec<SearchHit> を直接 parse しているクライアントは更新が必要:

{
  "results":        [{ "score": 0.83, "path": "...", "match_spans": [...], "tags": [...], ... }],
  "low_confidence": false,
  "filter_applied": { /* echo 対象のフィルタのうち指定されたものだけ。1 つも無ければ `{}`。min_quality / include_low_quality は適用されるが echo されない */ }
}

results[].match_spans はクエリを分割した term がすべて ASCII の場合に content 内のバイトオフセットを返すため、MCP クライアント側で原文の正確な引用を作れる。span は昇順かつ互いに重ならない。100 span の予算は検索した term 間で分け合うので、ある term が数百回一致しても 1 回しか出ない term はハイライトされる。32 phrase 上限に当たらない限り、クエリの語順を入れ替えても同じ配列が返る (v0.18.0+、完全な契約とこの但し書きは docs/citations.ja.md)。low_confidence は順位ベースの flag (top1.score / mean(top-N.score) < min_confidence_ratio) で、閾値の既定は 1.5。groove.toml の [search].min_confidence_ratio で全体調整、--min-confidence-ratio で per-query 上書き可能。

入力境界 (防御的、v0.6.0+): query は 1 KiB 上限、超過時は ErrorResponse で reject。match_spans は 256 KiB 以下の chunk にのみ計算、上限 100 span/chunk。乱用防止が目的で正常用途には影響しない — 通常 chunk は十分上限以下。

v0.3.0 で search ツール / CLI に追加されたフィルタ:

groove search "tokio spawn" \
  --path-glob "docs/**" --path-glob "!docs/draft/**" \
  --tag-any rust,async \
  --field status=active --field-not environment=staging \
  --date-from 2026-01-01 \
  --min-confidence-ratio 1.5

CLI groove search --format json のラッパ (results / low_confidence / filter_applied) は MCP と同じで、hit のフィールドも 1 点を除いて同じ: MCP の hit はサーバが引き渡せる文書のとき uri を持つが、CLI の hit は持たない。uri が付く条件は docs/mcp-tools.ja.md 参照。match_spans / byte offset の詳細は docs/citations.ja.md、フィルタの完全リファレンスは docs/filters.ja.md 参照。

多様性 (MMR) と parent retriever (v0.7.0+)

retrieval 品質を上げるための任意の knob を 2 つ追加。両者は独立しており、片方だけ on / 両方 on / 両方 off いずれでも動く。既定は両方 off なので既存パイプラインの挙動は変わらない。

# MMR (多様性再ランク)
groove search "tokio runtime" --mmr true --mmr-lambda 0.7

# Parent retriever (短い chunk を隣接 sibling や全文に展開)
groove search "k=60 in RRF" --parent-retriever true

# 両方同時
groove search "context management" --mmr true --parent-retriever true

CLI フラグ (groove eval も同じものを受け付ける):

MCP search ツールも同名の per-call params (mmr / mmr_lambda / mmr_same_doc_penalty / parent_retriever) を受ける。toml 既定値は [search.mmr] / [search.parent_retriever] (docs/configuration.ja.md)。優先順位は per-call > toml > built-in defaults。

パイプライン順序は RRF → reranker → MMR → parent retriever → match_spans。MMR は reranker score を保ったまま並べ替え、parent retriever は最後に走るので展開 content が relevance signal を汚さない。完全な解説とチューニング指針は docs/retrieval-pipeline.ja.md 参照。

Contextual Retrieval (v0.12.0+、opt-in)

各チャンクの先頭に短い context breadcrumb ―― ドキュメントタイトルと見出しの祖先パンくず (ドキュメントタイトル > セクション > サブセクション、 > 区切り) ―― を静的に生成して付与し、それを embedding の入力、FTS5 index (専用の第 3 列、Contextual BM25 の重み付きでスコアリング)、reranker の入力に注入する機能。Anthropic 原典の Contextual Retrieval 手法と異なり、この context は index 時にドキュメント構造だけから決定論的に生成される ―― LLM 呼び出しも追加の実行時依存も無く、通常の再 index で対応できる範囲を超える staleness も生じない。

有効化するには:

[contextual]
enabled = true

既定は off で、これは慎重さのためではなく実測された悪化が根拠になっている: 574 doc の dogfood knowledge base (bge-m3 embedding) で A/B 評価したところ、groove の実際の default パイプライン (reranker なし) では static context 注入によって retrieval がむしろ悪化した ―― recall@5 は 0.707 から 0.627 に低下し (-0.080)、MRR も -0.041 悪化した。短いチャンク本文のベクトル信号が、前置された breadcrumb テキストによって希釈され、かつそれを補正する後段の再スコアリングが無いためと見られる。

reranker を併用する場合 (--reranker bge-v2-m3) は様相が反転する: context 注入により recall@10 のわずかな低下を除く全指標が改善した ―― recall@5 は 0.760 から 0.807、MRR は 0.848 から 0.950、nDCG@10 は 0.814 から 0.858 へ向上。cross-encoder reranker は、生の embedding/BM25 段だけでは活かしきれない追加の構造的シグナルを利用できる。

推奨: reranker (--reranker bge-v2-m3 等 / groove.toml の reranker = "bge-v2-m3") を併用する場合に限り [contextual] enabled = true を有効化すること。素の default パイプラインでは off のままにする。

補足:

起点ドキュメントからの Connection Graph

単一ドキュメントではなく「その近傍 (さらにその近傍)」を意味的に探索したいときは graph サブコマンド:

groove graph --start deep-dive/mcp/overview.md --depth 2 --fan-out 5
groove graph --start notes/rag.md --dedup-by-path --format text
groove graph --start a.md --exclude-paths junk1.md,junk2.md --min-similarity 0.5

フラグ:

形を見る (--format dot / --format svg)

json も text も同じノードを並べるだけで、どこで枝分かれしたかを見せない。見る価値があるのはそこなので、図として出す形式を 2 つ用意した:

groove graph --start notes/rag.md --format dot > graph.dot   # dot -Tsvg graph.dot に渡す
groove graph --start notes/rag.md --format svg > graph.svg   # ブラウザでそのまま開ける

dot は Graphviz のプログラム。dot -Tsvg / -Tpng / -Tpdf に渡すも、DOT ビューアで開くも、web のビューアに貼るも自由。svg は何も入れていない環境でそのまま開ける完成品で、groove が自前でレイアウトしている — 描画用の依存を取っていないのは、このグラフが木であり、木なら 1 パスで配置できるから。

どちらも BFS の深さで配色し、edge に類似度を書き、上限で探索が打ち切られた場合はその旨を図に載せる (図を全体像と誤解させないため)。横に広いグラフでは Graphviz 経由の方が紙面が締まる。組み込み SVG は葉 1 つにつき 1 行積むので、読める大きさに保つ調整は --max-nodes で行う。

出力は parent_id / depth / score 付きのノードのフラット配列で、消費側で木を再構築できる。典型ユース: 「この note の周りの関連コンテキストを 30 チャンク LLM に読ませたい」「この overview から 2 ホップ辿ってどのトピックに触れているか見たい」。

TOML スキーマによる frontmatter 検証

ナレッジベースで frontmatter の規約を運用しているなら、groove validate がすべての .md を TOML スキーマに対して検証し違反を報告する。スキーマ書式は Frontmatter スキーマ検証 節参照。コマンド自体は:

groove validate --kb-path /path/to/knowledge-base
groove validate --kb-path ... --format json | jq '.files[]'
groove validate --kb-path ... --format github         # CI 用 ::error annotation

フラグ:

終了コード: 0 (違反なし) / 1 (違反あり) / 2 (スキーマロードエラー)。frontmatter ブロックが YAML として不正なファイルは違反 1 件 (frontmatter_unparsed、v1.8.0+) として数えるので、他の違反と同じく exit 1 になる。--kb-path 直下に groove-schema.toml が無いときは短い “no schema found” メッセージと共に exit 0 となるため、既存ワークフローへの groove validate 追加は実際にスキーマを書くまで非破壊。

groove index も同じ <kb-path>/groove-schema.toml を読む (v1.9.0+): 宣言された key は文書ごとに保存され、groove search --field がそれを filter に使う。スキーマの読み込みに失敗すると groove.toml の読み込み失敗がバイナリを止めるのと同様に groove index が止まる。

スキーマファイルは .grooveignore と同じ検査を通して読む: hardlink・通常ファイルでないもの・1 MiB 超、そして Unix では symlink を拒否し、「存在しない」以外の理由で開けないファイルも拒否する。スキーマの場合、拒否は「スキーマ無し」ではなくロードエラーになる — validate は exit 2、index・doctor・MCP rebuild_index は止まる。スキーマは索引がどのフィールドを持つかを決めるため。--schema で渡したパスにも同じ検査がかかる。詳細は 補足 (挙動リファレンス)。

索引そのものを検査する (v0.23.0+)

groove validate が検査するのは文書。groove doctor が検査するのは索引:

groove doctor --kb-path /path/to/knowledge-base
groove doctor --kb-path ... --format json | jq '.findings[]'

検索は 1 つの chunk について 3 つのテーブルが一致していることを前提にしている — 本文・embedding・全文検索行。ずれてもエラーにはならない: embedding の無い chunk は単にベクトル検索に出ず、全文検索行の無い chunk はキーワード検索に出ないだけ。これまでは full index を回して修復されるのを見るまで気付けなかった。doctor は直接それを問う。あわせて、MCP の resource 面がどの索引済み文書を提示していないか、なぜかも報告する — 現在の [parsers].enabled に無い拡張子 / resource read が返せるサイズを超える文書 / [index].max_decompressed_size を組み込みの既定より上げている間のバイナリ文書 (binary-uris-withheld、warning。検索には残り、キーを既定に戻せば URI も戻る) / 以前のバージョンで索引されたため size が未記録の文書。

さらに、定義単位ではなく行単位で chunk 化されたソースファイルも名指しする — 定義が入れ子の上限より深かったか、ファイルが 1 ファイルあたりの chunk 数の上限を超える chunk を要求したか、のいずれか。これらのファイルは欠けなく索引されて検索にも出るが、chunk が定義の symbol kind / 見出し / スコープを持たないので、定義の形をしたクエリでは辿り着けない。直し方はコマンドではなくファイルの側にある — index を回し直しても同じ上限に当たって同じ判断になる。

宣言 key の集合も見る (v1.11.0+): 索引に集合が記録されていない — 実行中の run がある / 中断された / 作成後または 1.8.0 からの更新後に groove index が一度も完走していない — 場合は declared-fields-pending として報告する。run が集合を記録するまで --field 付きの検索は拒否されるからで、groove-schema.toml の有無は問わず、document がまだ 1 つも無い索引でも同じ。記録済みの集合が schema の宣言と食い違っていれば declared-fields-stale — 次の run が行を書き直すまで --field は記録済みの集合で答える。どちらも warning で、groove index 1 回で (再 embedding なしに) 消える。読めない schema file は groove index / groove validate と同じ扱いで、DB を開く前にコマンドが止まる (終了コード 2)。

Windows では、以前の版が「Windows が書いたとおりには開けない名前」— CON.md のような予約デバイス名、末尾がドットか空白の区間を含むパス、Windows が名前に使わせない文字を含むパス — で索引した文書も name-not-spellable-on-windows (warning) として名指しする。そういう名前は今は索引しないので、次の groove index で行が消える。残したいならファイル名を変える。ADR-0023 を参照。

v1.6.0 より前に作られた索引には、その前に別の答えが出る。 そのリリースまで、上限を超えたファイルは切り捨てられていた。内容が変わらないファイルは再 chunk 化されないので、そういう索引は今も末尾の欠けたファイルを抱えている可能性があり、しかもそれを見つける手掛かりが document 側に無い。doctor はそこで「異常なし」と答えるのではなく、どの chunk 化ポリシーで作られた索引かが記録されているかを見て、記録が無くソースファイルを含む索引については「まだ答えられない」と報告する。groove index --force で作り直せば消える。

v1.13.0 以前で作った索引には、見える文字を 1 つも持たない chunk を抱えたソースファイルが残っている可能性もある。行単位で chunk 化したファイルの末尾が空行だけの piece になることがあり、それが他の chunk と同じく embedding され索引されていた。内容が変わらないファイルは再 chunk 化されないという同じ理由で残り続けるので、doctor は保存済みの chunk を読んで、そういうファイルを blank-code-chunks (warning) として名指しする。groove index --force で作り直せば消える。

終了コード: 0 (報告なし) / 1 (検出あり) / 2 (実行できない — 大抵は索引が無いか、開けない。stderr の message が file と修復コマンドを示す)。報告するだけで修復はしない。各検出には直し方が併記される (構造的なものはすべて groove index か groove index --force、コマンドでは直せないものは文書そのものへの変更)。

search / eval と同様、本コマンドは DB を開くので、未適用の schema migration があれば走る。検出結果については read-only だが、ファイルについてはそうではない。

Golden query セットに対する retrieval 品質評価

任意のパワーユーザ機能。groove eval は「想定される正解がわかっている質問」の小さなファイルを、search ツールと同じハイブリッド検索にかけ、recall@k / MRR / nDCG@k + 前回実行との差分を出す。モデル比較や [quality_filter] / RRF パラメータのチューニング時に便利。

groove index + groove serve で普通に使う一般ユーザは触る必要なし — golden ファイルが無ければ eval は hint 付きエラーで終了するだけで他の挙動には影響しない。

# 1) Golden YAML を <kb_path>/.groove-eval.yml に配置
cat > knowledge-base/.groove-eval.yml <<'EOF'
queries:
  - query: "RRF の k パラメータの意味は?"
    expected:
      - { path: "docs/ARCHITECTURE.md", heading: "Data flow" }
      - { path: "src/db.rs" }   # heading 省略 = ファイル一致で正解
EOF

# 2) index 済み DB に対して実行
groove eval --kb-path knowledge-base

# 3) 設定やモデルを変えて再実行、diff で変化を見る
groove eval --kb-path knowledge-base --reranker bge-v2-m3

出力: 集計指標 + 劣化 / ミスのあるクエリ行のみ。--format json で全クエリの詳細を取得可能。履歴は <kb_path>/.groove-eval-history.json に保存され、直近 10 件を diff 表示用に保持する。

評価対象のナレッジベースの中に評価についてのノートを置いていると、そのノートが本来の正解と競合する。v0.24.0 からは各 run がコーパスを走査し、2 件以上の golden query を逐語で含む文書 (= golden セット について 書いたノートの形) を stderr に警告する (--format json では findings)。報告のみで exit code は不変。1 件では報告しない理由を含めた詳細は docs/eval.ja.md。

CI 用途には --fail-on-regression (v0.6.0+) を渡す。直前の fingerprint-compatible run から recall@k / MRR / ndcg@k のいずれかが regression_threshold (既定 0.05) を超えて退化していたら exit code 1 を返す。golden YAML を更新すると hash が変わるので次回 run は比較対象外 = false positive にならない。

Golden YAML のリファレンス、指標の詳細説明、diff 出力の読み方、トラブルシューティングは docs/eval.ja.md 参照。

fusion パラメータを測る (groove tune、v0.13.0+)

[search.fusion] で RRF 定数と bm25 列重みを公開しているが、既定値は業界慣例値 であり、RRF は公式に「チューニング不要」とされている。自分の KB について当て推量 ではなく根拠が欲しい場合は:

groove tune --kb-path knowledge-base

golden query セットに対して固定グリッドを掃引し、leave-one-query-out 交差検証で 結果をガードした上で、貼り付け可能なスニペットか「既定値を維持すべき」という結論 を出力する。tune 自身は何も適用しない。なお、このパラメータが動かせるのは そもそも bm25 段に到達する クエリだけなので、tune はまず pre-flight で実効 N を報告し、0 なら exit 2 で終わる。詳細は docs/eval.ja.md を参照。

ログを詳しく見る

詳細度を上げるフラグは無い。詳細度は環境変数 RUST_LOG で決まる。 全サブコマンドが読み、未設定なら info として振る舞う。

RUST_LOG=grooveseek=debug groove serve --kb-path ./knowledge-base
RUST_LOG=grooveseek=debug groove search "query" --kb-path ./knowledge-base
RUST_LOG=debug groove index --kb-path ./knowledge-base   # 依存クレートも含む。かなり煩い

実用上の設定は grooveseek=debug。本プロジェクト自身の target だけを上げ、 HTTP スタックと ONNX runtime は info のまま残す。これで増えるもの:

RUST_LOG の管轄外のものが 2 つある。どの groove.toml が勝ったかは info で出るので、そもそも上げる必要がない (loaded config source=… path=… trust=…。docs/configuration.ja.md 参照)。index の進捗 (Indexing … / ` indexed: … / Done in …) と各ファイルの warning: 行は logger を 通さず直接書いているので、**どのレベルでも出る** — 進捗を制御するのは –quiet / –progress` の方で、warning を止めるものは無い。

ログは全サブコマンドで stderr に出るので、レベルを上げても stdout から取っている 出力を乱さない。とくに serve で効く — 既定の stdio transport では stdout が MCP プロトコルそのものなので、ログの行き先は stderr しかない。なおログの 文面は安定面ではない (docs/stability.ja.md)。読むためのもので あって parse するものではない。

groove service install で登録した daemon の場合、レベルはシェルではなく サービス定義側で決まる。systemd unit は Environment=RUST_LOG=info を、 launchd plist は同等のエントリを持つので、編集して再起動する。Windows の スケジュールタスクは RUST_LOG を一切設定しないので同じ既定 info に落ちる。 上げたい場合はタスクが動く環境側に変数を設定する。