概要
commandindex search コマンドを実装する。tantivy インデックスを使った全文検索と各種フィルタリングを提供する。
背景・動機
Phase 1 のユーザー向け主機能。インデックスされた Markdown 知識を高速に検索・取得できるようにする。
提案する解決策
CLI オプション(Phase 1 対象)
commandindex search "認証の流れ" # 全文検索
commandindex search "認証" --tag auth # タグ絞り込み
commandindex search "認証" --path docs/ # パス絞り込み
commandindex search "認証" --type markdown # 種別絞り込み
commandindex search "認証" --heading "認証フロー" # 見出し絞り込み
commandindex search "認証" --tag auth --path src/ --format json # 組み合わせ
commandindex search "認証" --limit 10 # 件数制限
| フラグ |
型 |
デフォルト |
説明 |
| (なし) |
String(位置引数) |
必須 |
全文検索(body + heading + tags) |
--tag <tag> |
Option<String> |
None |
frontmatter タグで絞り込み(AND 条件) |
--path <path> |
Option<String> |
None |
パスプレフィックスで絞り込み |
--type <type> |
Option<String> |
None |
ファイル種別で絞り込み(markdown 等) |
--heading <text> |
Option<String> |
None |
見出しフィールド限定の絞り込み(query との AND 条件) |
--format <fmt> |
OutputFormat |
human |
出力形式(human / json / path) |
--limit <n> |
usize |
20 |
結果件数の上限 |
注: query は必須。--tag/--heading は query と組み合わせてフィルタとして使用する。
検索ロジック
- tantivy の BM25 スコアリングを使用
- 複数フラグは AND 条件で結合
フィルタ実装方式
| フィルタ |
実装方式 |
詳細 |
--tag |
tantivy クエリ(QueryParser 経由、tags フィールド限定検索) |
ユーザー入力を QueryParser で tags フィールドに対してパースし、lindera トークナイザと整合性のあるクエリを構築 |
--path |
ポストフィルタ(path.starts_with(prefix)) |
path は STRING 型のためポストフィルタで実現 |
--type |
ポストフィルタ(path の拡張子から種別判定) |
Phase 1 では markdown (.md) のみ対応 |
--heading |
tantivy クエリ(QueryParser 経由、heading フィールド限定検索、query との AND 結合) |
heading は ja_text でインデックス済み |
tantivy クエリ構築の詳細
query, --tag, --heading を BooleanQuery で AND 結合:
use tantivy::query::{BooleanQuery, Occur};
let mut sub_queries = vec![];
// 1. メインクエリ: heading + body + tags の3フィールド
let main_parser = QueryParser::for_index(&index, vec![heading, body, tags]);
sub_queries.push((Occur::Must, main_parser.parse_query(&options.query)?));
// 2. --tag: tags フィールド限定
if let Some(ref tag) = options.tag {
let tag_parser = QueryParser::for_index(&index, vec![tags]);
sub_queries.push((Occur::Must, tag_parser.parse_query(tag)?));
}
// 3. --heading: heading フィールド限定
if let Some(ref heading_query) = options.heading {
let heading_parser = QueryParser::for_index(&index, vec![heading]);
sub_queries.push((Occur::Must, heading_parser.parse_query(heading_query)?));
}
let combined = BooleanQuery::new(sub_queries);
ポストフィルタと limit の戦略
ポストフィルタ(--path, --type)が指定されている場合:
- tantivy から
limit * 5 件を1回で取得(リトライなし、シンプルに)
- Rust 側で path.starts_with / 拡張子判定のフィルタを適用
- フィルタ後の結果を limit 件まで切り詰め
- ポストフィルタなしの場合は tantivy の limit をそのまま使用
Phase 1 ではシンプルな1回取得方式を採用。件数不足が問題になる場合は Phase 2 で段階取得を検討。
検索 API 設計
IndexReaderWrapper に search_with_options() メソッドを新規追加(既存の search() は後方互換として維持):
pub struct SearchOptions {
pub query: String,
pub tag: Option<String>,
pub heading: Option<String>,
pub limit: usize,
}
pub struct SearchFilters {
pub path_prefix: Option<String>,
pub file_type: Option<String>,
}
impl IndexReaderWrapper {
/// 既存メソッド(後方互換)
pub fn search(&self, query_str: &str, limit: usize) -> Result<Vec<SearchResult>, ReaderError> { ... }
/// フィルタ付き検索(新規)
pub fn search_with_options(
&self,
options: &SearchOptions,
filters: &SearchFilters,
) -> Result<Vec<SearchResult>, ReaderError> { ... }
}
SearchOptions: tantivy BooleanQuery として構築
SearchFilters: 検索結果取得後にRust側で適用するポストフィルタ
- 戻り値は
Result<Vec<SearchResult>, ReaderError>(cli 側で SearchError に変換)
ファイル種別マッピング
| --type 値 |
対象拡張子 |
| markdown |
.md |
Phase 1 では markdown (.md) のみ対応。Phase 3 以降で code 等を追加予定。
実装対象ファイル
変更ファイル
| ファイル |
変更内容 |
src/main.rs |
Commands::Search にオプション追加(tag, path, file_type, heading, limit)、ハンドラで cli::search::run 呼び出し |
src/indexer/reader.rs |
SearchOptions/SearchFilters 構造体、search_with_options メソッド追加、BooleanQuery import |
src/cli/mod.rs |
pub mod search 追加 |
tests/cli_args.rs |
search_subcommand_exits_with_not_implemented → search_without_index_shows_error + search_with_all_options_accepted に置き換え |
新規ファイル
| ファイル |
内容 |
src/cli/search.rs |
search コマンド実行ロジック(run 関数、SearchError 型) |
エラー型設計
pub enum SearchError {
IndexNotFound,
Reader(ReaderError),
Output(OutputError),
}
impl fmt::Display for SearchError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SearchError::IndexNotFound => write!(f, "Index not found. Run `commandindex index` first."),
SearchError::Reader(e) => write!(f, "Search error: {e}"),
SearchError::Output(e) => write!(f, "Output error: {e}"),
}
}
}
CLI --type フィールド名
/// Search the index
Search {
query: String,
#[arg(long, value_enum, default_value_t = OutputFormat::Human)]
format: OutputFormat,
#[arg(long)]
tag: Option<String>,
#[arg(long)]
path: Option<String>,
#[arg(long = "type")]
file_type: Option<String>,
#[arg(long)]
heading: Option<String>,
#[arg(long, default_value_t = 20)]
limit: usize,
}
インデックス存在確認
search ハンドラの先頭で .commandindex/tantivy ディレクトリの存在を確認し、未作成時はエラー表示。
検索結果0件時の出力
stdout への format_results 呼び出しは省略し、stderr に上記メッセージを表示。
tags 格納形式の統一(関連修正)
現在 cli/index.rs で fm.tags.join(", ")(カンマ+スペース区切り)で格納しているが、output/mod.rs の parse_tags() は split_whitespace() を使用。本 Issue の範囲で以下を統一:
- 格納形式:
fm.tags.join(" ")(スペース区切り)に変更
- parse_tags: 変更なし(split_whitespace で整合性あり)
受け入れ基準
依存 Issue
概要
commandindex searchコマンドを実装する。tantivy インデックスを使った全文検索と各種フィルタリングを提供する。背景・動機
Phase 1 のユーザー向け主機能。インデックスされた Markdown 知識を高速に検索・取得できるようにする。
提案する解決策
CLI オプション(Phase 1 対象)
--tag <tag>--path <path>--type <type>--heading <text>--format <fmt>--limit <n>検索ロジック
フィルタ実装方式
--tag--pathpath.starts_with(prefix))--type--headingtantivy クエリ構築の詳細
query, --tag, --heading を
BooleanQueryで AND 結合:ポストフィルタと limit の戦略
ポストフィルタ(--path, --type)が指定されている場合:
limit * 5件を1回で取得(リトライなし、シンプルに)検索 API 設計
IndexReaderWrapperにsearch_with_options()メソッドを新規追加(既存のsearch()は後方互換として維持):SearchOptions: tantivy BooleanQuery として構築SearchFilters: 検索結果取得後にRust側で適用するポストフィルタResult<Vec<SearchResult>, ReaderError>(cli 側で SearchError に変換)ファイル種別マッピング
実装対象ファイル
変更ファイル
src/main.rssrc/indexer/reader.rssrc/cli/mod.rspub mod search追加tests/cli_args.rs新規ファイル
src/cli/search.rsエラー型設計
CLI --type フィールド名
インデックス存在確認
search ハンドラの先頭で
.commandindex/tantivyディレクトリの存在を確認し、未作成時はエラー表示。検索結果0件時の出力
stdout への format_results 呼び出しは省略し、stderr に上記メッセージを表示。
tags 格納形式の統一(関連修正)
現在
cli/index.rsでfm.tags.join(", ")(カンマ+スペース区切り)で格納しているが、output/mod.rsのparse_tags()はsplit_whitespace()を使用。本 Issue の範囲で以下を統一:fm.tags.join(" ")(スペース区切り)に変更受け入れ基準
--tagによるタグ絞り込みが動作する--pathによるパス絞り込みが動作する--typeによる種別絞り込みが動作する--headingによる見出し検索が動作する--formatで出力形式を切り替えられる--limitで結果件数を制限できる依存 Issue