GrooveSeek

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

View the Project on GitHub alphabet-h/grooveseek

検索フィルタ

search MCP ツールは複数のフィルタを受け付ける。フィルタは AND セマンティクスで合成される (すべての条件が一致した chunk のみ results に現れる)。

English version: filters.md

クイックリファレンス

パラメータ 効果
category string "deep-dive" documents.category と完全一致
topic string "mcp" documents.topic と完全一致
path_globs string[] ["docs/**", "!docs/draft/**"] glob include / exclude
tags_any string[] ["rust", "wasm"] OR — いずれかの tag が一致
tags_all string[] ["draft"] AND — 全 tag が一致
date_from string "2026-01-01" hit.date >= from (lex 比較)
date_to string "2026-12-31" hit.date <= to (lex 比較)
min_quality number 0.5 quality filter の閾値 ([quality_filter].threshold) をこの呼び出しだけ上書き
include_low_quality bool true この呼び出しでは quality filter を無効化 (min_quality: 0.0 と等価、意図が明示的)
min_confidence_ratio number 1.5 low_confidence フラグの閾値

path_globs

{
  "path_globs": ["docs/**", "!docs/draft/**"]
  // "docs/a.md" は通る、"docs/draft/b.md" は除外、"notes/c.md" は除外
}

tags_anytags_all

これらは documents.tags (YAML frontmatter の tags: 配列) を対象にする。

{
  "tags_all": ["rust"],
  "tags_any": ["async", "concurrency"]
  // "rust" タグかつ ("async" or "concurrency") を持つ docs にマッチ
}

ソースコード (v1.2.0+) のタグは frontmatter ではなく parser が付ける: ソースファイル由来の chunk はすべて codelang:<name> を持つ。結果は既定でコードと散文が混ざり、それを 分けるのがこの filter である — そのために search の parameter を足してはいない。

{ "tags_any": ["code"] }        // 定義だけ
{ "path_globs": ["!**/*.rs"] }  // 散文だけ。exclude だけの list は有効

date_from / date_to

{
  "date_from": "2026-01-01",
  "date_to":   "2026-04-30"
}

date 形式が混在するとき ("2026-04-26 12:00:00 +0900""2026-04-26T12:00:00+09:00" など) は lex 順序が崩れる。KB 内で形式を 統一すること。

low_confidencemin_confidence_ratio

レスポンス wrapper のトップレベルに low_confidence: bool が付く。top hit の score が他と比べて 目立って高くない ときに true になる:

low_confidence ⇔ (results.len() >= 2)
                 AND (mean(scores) > 0.0)
                 AND (max(scores) / mean(scores) < min_confidence_ratio)

low_confidence: true の意味は「マッチがダンゴ状態 — Claude は引用を 権威として扱うのを慎重に」。results 自体はそのまま返ってくる。フラグは あくまで助言。

何を検出し、何を検出しないか

このフラグはヒューリスティックであり、実測したので、式から推測させるのでは なくここに書いておく。どれだけ重みを置くかを決める上で効く限界が 2 つある。

rerank はこのフラグを丸ごと無効にし得る。 cross-encoder はロジットで スコアを付けるので無関係な chunk は強く負になり、結果集合の平均はしばしば負に なる。そうなると上の mean(scores) > 0.0 が、散らばりに関係なく false を返す。 bge-v2-m3 で 25 クエリ (各 10 件) を測った結果、コーパスに答が無いクエリを 含めて全部 false だった。

手元のクエリでそうなるかは、モデル・件数・返った chunk の関連度で決まる — 本当に良いマッチだけが少数返れば平均は正になり、比は通常どおり比較される。 つまり rerank 下でフラグが必ず消えるわけではない。信用できないのが正確な 言い方で、rerank が走ったときの false は何も意味しない (符号チェックが出す false と区別が付かない)。true の方は式が言うとおりの意味を持つ。

rerank 無しで反応しているのは、融合後スコア分布の形であって正解性ではない。 RRF は hit が現れた脚ごとに 1 / (rrf_k + rank + 1) を与えるので、 top1 を割る平均は ①各 hit を何本の脚が拾ったか ②各脚が何位に置いたか ③rrf_k ④要求件数で動く。どれも「top hit が正しいか」ではない。

実際の代償は 2 つの測定が示している。20 文書のコーパスで 25 クエリ全部が rank 1 で正解しているのに 14 件で発火した。そして同じクエリが別コーパスでは まったく違う位置に来る: 答が無い 12 クエリの median が、そのコーパスでは 1.08、121 文書のコーパスでは 1.40 — 後者は前者で正解していたクエリが 居た位置である。

測定した標本では、比を押し上げていたのは片方の脚しか拾わなかった hit だった。 「重なりを測っている」という読み方はここから来ている。ただしそれは規則ではなく そのコーパスについての観測として扱うこと — 上の要因は、重なりが変わらないままでも 比を動かし得る。

ノイズではない。コーパスを固定すれば、答が無いクエリの方が低く出る。だが 2 つのナレッジベースで同じ意味を持つ閾値が存在しない。既定値を特定の コーパスに合わせて調整せず、そのままにしてあるのはこのため。

どちらの限界も、意図した挙動ではなく未処理の課題として記録してある。 フィールド自体は 1.0 で凍結するが、式と既定値は明示的に凍結しない (docs/stability.ja.md)。

categorytags_any の違い (検索軸が別)

これらは index 上で 別のフィールド:

deep-dive/mcp/x.md にある tags: ["mcp", "rust"] を持つドキュメントは、 フィルタ category: "deep-dive"マッチするが、 tags_any: ["deep-dive"] では マッチしない。これらは別軸。

フィルタの組み合わせ

すべて AND で合成される:

{
  "path_globs": ["docs/**"],
  "tags_all":   ["rust"],
  "date_from":  "2026-01-01"
  // = docs/ 配下、"rust" タグ、2026 年以降
}

関連