Semantic search over a Markdown knowledge base, served over MCP.
ADR-0019 は、
frontmatter の key をパーサにすべて保持させ、groove-schema.toml がそのどれでも名指せる
ようにして、そこで止まった。保持された key が届く先は groove validate だけで、それ以外は
何も無かった。それを求めたのと同じユーザ (#289) が、残りの代償がどこにあるかを示した。
4 つのメタデータ軸を持つ 74 文書のコーパスにおいて、1,036 件のタグの出現のうち 434 件は、
status / environment / team / source を tags へ写すためだけに存在している。
検索フィルタから見える frontmatter が tags しか無いからである。その写しを正しく保つには
外部バリデータが要り、topic は文書の種別に費やされてしまう。ユーザが求めたのは 1 つだけ
である: 保持された key でのフィルタ、完全一致の文字列比較、演算子は無し、schema が宣言した
key に限る。
本決定が答える問い: 宣言された key を索引へ持ち込むのは誰で、それはいつか — schema を 読む indexer か、それとも schema を読むクエリか。
search も serve も
これを読んでおらず、読む必要も無いはずである: クエリは索引から答えるfrontmatter_policy で踏んだのと同じ罠であるfrontmatter_unparsed は CI が判定に使う数である。 新しい理由で健全な文書を読み直す
パスが、それらを壊れているものとして数えてはならないdocs/stability.md): フィールドの追加は minor
リリース、意味の変更はそうではない。データベース内部のスキーマは契約ではないsearch は
そのテーブルでフィルタし、schema を一切読まない。 採用serve と groove search の両方が schema を読み込む必要が生じ、
schema を持たない知識ベースには別の規則が要り、索引は誰も宣言していない値を抱えることに
なるdocuments 上の JSON カラムに持ち、tags と同じくクエリの後に Rust 側でフィルタする。
却下: テーブルを分ければクエリ自身が絞り込め、リストの値は 2 つ目のエンコーディングでは
なく 2 行目になるstatus!=deprecated)。 却下: 報告者が求めたのは完全一致であり、
演算子は求めていない。2 つ目のフラグ --field-not / fields_not は、除外に完全一致と
同じ形を与え、値の文法には手を触れないgroove index は毎回 <kb_path>/groove-schema.toml を読む — groove validate が読むのと
同じファイルである。そこで宣言された key から、パーサが自分のフィールドに格納する 5 つを
除いたものが宣言された集合である。読み込めない schema は、そのエラーとともに索引を
止める。読み込めない設定がバイナリを止めるのと同じである: それ無しで索引してしまえば、
すべてのフィールドフィルタが空を答え、しかも何も言わないことになるdocument_fields へ格納する。 スカラは 1 行、リストは
要素ごとに 1 行で、パーサが保持していた文字列そのままである。不透明な形と null は値を
持たないので何も書かない。宣言されていない key は索引の関知するところではない。この行を
持つのは Markdown だけであるindex_meta.declared_fields が、完了した実行の使った
集合を記録する。schema が宣言する集合がそれと異なるとき、その実行は変更されていない
Markdown 文書をもう一度だけ読み、再埋め込みはせずに行を書き直す。このとき健全な文書を
frontmatter が壊れたものとして数えることはないので、[index].fail_on_frontmatter_error は
schema の変更の影響を受けない。このキーより前に作られた索引は、集合が変わったものとして
扱われるsearch はそのテーブルでフィルタする。 fields は、与えられたすべての key に
ついてその値のいずれかを持つ文書を残す。fields_not は、与えられた key のいずれかに
ついてその値のいずれかを持つ文書を落とし、その key を持たない文書は残す。比較は完全一致の
バイト単位である。索引に無い key は何にも一致せず、エラーにもならない。判定の門は索引
だからである。コマンドラインでの綴りは --field key=value / --field-not key=value で、
繰り返し指定でき、最初の = で分割する。ツールは fields / fields_not を
key → 文字列のリスト として受け取る — 綴りを 1 つにしたのは、ツールスキーマにおける
string | list の union こそ schema_compat が避けるために存在する形だからである (#75) —
そして filter_applied がそれを返すgraph は変更しない。 フィルタを get_connection_graph へ広げるのは後の決定であるgroove-schema.toml は今やバリデーションだけでなく索引の入力でもある。 schema を編集
すれば、次の groove index がそれに追随する。フラグは要らないfilter_applied に現れ得る行が 2 つ、ツールのパラメータが 2 つ、フラグが 2 つ増えた:
docs/stability.md の下で minor リリースであるdocument_fields は 1.8.0 が作った索引には無いテーブルである。 1.9.0 で開けばテーブルが
作られ、最初の groove index がそれを埋めるdoctor はまだこのテーブルを見ない。status もこれを数えない
(2026-09-10、v1.11.0) 解消: doctor は declared-fields-pending (集合が未記録 =
field filter 付きの検索が拒否されるすべての状態) と
declared-fields-stale (記録済みの集合が disk 上の schema と食い違う) を報告し、
status は記録済みの集合と行数を出す。どちらも検索が見るのと同じ index_meta の
key を読み、indexer と同じ declared_field_names を呼ぶ — 規則を作り直してはいないgroove index が読むのは <kb_path>/groove-schema.toml だけである。 別の
場所に置いて groove validate --schema <path> に渡している schema は索引から
見えないので、それに対する --field / --field-not フィルタは空を返すgrooveseek/src/indexer.rs (declared_field_names / declared_field_rows)、
grooveseek/src/db/search.rs (field_predicates)、docs/filters.md