インデックス検索のしくみ¶
このセクションは技術的な詳細に興味がある方向けです。操作や設定の理解には不要です。
使用ライブラリとバージョン¶
| ライブラリ | バージョン | 用途 |
|---|---|---|
| Lucene.Net | 4.8.0-beta00017 | 全文検索エンジン本体 |
| Lucene.Net.Analysis.Kuromoji | 4.8.0-beta00017 | 日本語形態素解析(トークナイズ) |
| Lucene.Net.QueryParser | 4.8.0-beta00017 | クエリ文字列のパース |
| sqlite-net-pcl | 1.9.172 | SQLite 非同期データベースアクセス |
| SQLitePCLRaw.bundle_green | 2.1.11 | SQLite ネイティブバインディング |
| .NET 8 / WPF | — | アプリケーション基盤 |
| CommunityToolkit.Mvvm | 8.2.2 | MVVM フレームワーク |
インデックスの物理構造¶
インデックスはアプリ実行フォルダの data フォルダ内の index/ ディレクトリに保存されます。Lucene の FSDirectory(ファイルシステムベース)を使用し、CREATE_OR_APPEND モードで開きます。
ディレクトリ内のファイル構成:
| ファイル | 説明 |
|---|---|
segments_N |
Lucene セグメントマニフェスト(N はジェネレーション番号) |
_N.cfs |
Compound File Storage(セグメントデータ本体) |
write.lock |
Lucene ライターの排他ロックファイル |
indexed_roots.json |
インデックス済みルートの永続化データ |
last_full_rebuild.txt |
最終フルリビルドの ISO8601 タイムスタンプ |
zenith.db |
SQLite データベース(履歴・検索履歴・利用統計等) |
indexed_roots.json のフォーマット:
{
"C:\\Users\\example\\Documents": "2026-03-07T10:30:00|locked|5428"
}
値は | 区切りで、ISO8601 タイムスタンプ(最終インデックス日時)、locked フラグ(アーカイブロック時のみ)、ドキュメント数スナップショットの 3 要素で構成されます。
ドキュメントスキーマ(Lucene フィールド定義)¶
各ファイル・フォルダは 1 つの Lucene ドキュメントとして登録されます。フィールド構成は以下の通りです。
| フィールド名 | 型 | トークン化 | 格納 | 用途 |
|---|---|---|---|---|
path |
StringField | No | Yes | ファイル/フォルダのフルパス。一意識別子として UpdateDocument の Term に使用。PrefixQuery によるパススコープ絞り込みにも利用 |
name |
TextField | Yes(JapaneseAnalyzer) | Yes | ファイル名/フォルダ名。メイン検索対象。Kuromoji 形態素解析でトークン化される |
name_raw |
StringField | No | Yes | ファイル名の小文字化コピー(トークン化なし)。WildcardQuery による部分一致検索のフォールバック用 |
size |
Int64Field | — | Yes | ファイルサイズ(バイト単位)。フォルダは 0。NumericRangeQuery によるサイズフィルタで使用 |
modified |
StringField | No | Yes | 最終更新日時。DateTools.DateToString で SECOND 精度の文字列に変換して格納。TermRangeQuery による日付フィルタで使用 |
is_dir |
Int32Field | — | Yes | ディレクトリフラグ(0=ファイル、1=フォルダ) |
日本語トークナイズ¶
検索の name フィールドには JapaneseAnalyzer(Kuromoji 形態素解析エンジン)を使用しています。これにより、日本語テキストを意味のある単語単位に分割し、「議事録」で「第3回議事録.docx」がヒットするといった自然な検索が可能です。
トークナイズの流れ:
JapaneseAnalyzerでGetTokenStream("name", keyword)を呼び出すICharTermAttributeを取得し、IncrementToken()でトークンを順次読み出す- 得られたトークン列から
PhraseQuery(複数トークン時)またはTermQuery(単一トークン時)を構築する
この処理は QueryParser.Parse() が ParseException を投げた場合のフォールバックとして BuildQueryFromTokenized() メソッドで実行されます。ワイルドカード文字(*、?)やブーリアン演算子を含むクエリがパースエラーになった場合でも、トークナイズベースの検索に自動的に切り替わります。
クエリ構築パイプライン¶
検索キーワードからLuceneクエリが構築されるまでの全過程を解説します。
ステップ 1: クエリパース
QueryParser(フィールド: name、アナライザ: JapaneseAnalyzer)でキーワードをパースします。初期設定は以下の通りです。
DefaultOperator = Operator.AND(すべてのキーワードを含む結果のみ)AllowLeadingWildcard = true(*報告書のような先頭ワイルドカードを許可)
ステップ 2: パースエラー時のフォールバック
ParseException が発生した場合、BuildQueryFromTokenized() でキーワードを形態素解析し、トークンベースのクエリを構築します(日本語トークナイズ 参照)。
ステップ 3: ハイブリッドクエリの構築
パース結果に加え、name_raw フィールドへの WildcardQuery(*keyword*)を SHOULD 条件で組み合わせます。
BooleanQuery {
SHOULD: parsedQuery ← name フィールド(形態素解析済み)
SHOULD: WildcardQuery ← name_raw フィールド(部分一致)
}
これにより、形態素解析で分割できない英数字やカタカナの部分一致も確実にヒットします。
ステップ 4: パススコープの適用
- 通常検索: 現在のフォルダパスを
PrefixQuery("path", "C:\\current\\path\\")として MUST 条件で追加。そのフォルダ配下のみに限定 - インデックス検索:
GetScopePathsForSearch()で取得した複数のインデックスルートを、各ルートのPrefixQueryを SHOULD で結合したBooleanQueryとして MUST 条件で追加
ステップ 5: フィルタの適用(ApplySearchFilter)
検索フィルタが設定されている場合、以下のクエリが MUST 条件で追加されます。
- サイズフィルタ:
NumericRangeQuery.NewInt64Range("size", min, max, true, true) - 日付フィルタ:
TermRangeQuery("modified", minStr, maxStr, true, true)— 日付はDateTools.DateToStringで SECOND 精度文字列に変換
ステップ 6: AND→OR フォールバック
AND 検索で結果が 0 件かつキーワードにスペースが含まれる場合、DefaultOperator を Operator.OR に切り替えて自動的に再検索します。「会議 報告」で AND ヒットなしでも、「会議」または「報告」を含むファイルが表示されます。
検索結果の上限とソート:
- 通常検索: 最大 500 件
- 検証用(キーワードなし): 最大 1,000 件
- CSV エクスポート:
maxResultsOverrideパラメータで上限を指定可能 - ソート順: 更新日時の降順(
SortField("modified", SortFieldType.STRING, true))
具体例:
| 入力 | 構築されるクエリ | 説明 |
|---|---|---|
議事録 |
name:議事録 OR name_raw:*議事録* |
形態素解析 + 部分一致の OR |
会議 資料 |
(name:会議 AND name:資料) OR name_raw:*会議 資料* |
AND で検索、0 件なら OR に切り替え |
*.xlsx(フォルダ C:\Work 内) |
name_raw:*.xlsx* AND path:C:\Work\* |
ワイルドカード + パススコープ |
| サイズ 1MB 以上 + 今月 | メインクエリ AND size:[1048576 TO *] AND modified:[20260301… TO 20260307…] |
Lucene レベルでフィルタ適用 |
通常検索とインデックス検索の実装差異¶
| 項目 | 通常検索 | インデックス検索 |
|---|---|---|
| スコープ | 現在のフォルダ 1 つ(PrefixQuery で限定) |
登録済み全ルート(複数 PrefixQuery の OR 結合) |
| データソース | Lucene インデックス(同一エンジン) | Lucene インデックス(同一エンジン) |
| フィルタ方式 | サイズ・日付: Lucene クエリレベル | サイズ・日付: Lucene クエリレベル |
| 拡張子フィルタ | UI レベル(ICollectionView フィルタ、11 カテゴリ) |
UI レベル(ICollectionView フィルタ、11 カテゴリ) |
| 最大件数 | 500 件 | 500 件 |
通常検索・インデックス検索ともに同じ Lucene エンジンを使用しますが、スコープの広さが異なります。通常検索は rootPath = CurrentPath で単一フォルダ配下に限定されるのに対し、インデックス検索は登録された全ルートを横断して検索します。
インデクシングパイプライン¶
フォルダをインデックスに登録してから完了するまでの処理フローです。
- 登録: フォルダを
_pendingRootsキューに追加 - 排他制御:
SemaphoreSlim(1,1)で 1 タスクのみ実行。取得後_inProgressRootsに移動 - Box Drive 暖気運転: Box Drive パスの場合、
WarmUpBoxDirectoryAsync()で全ディレクトリを事前に再帰列挙。スタブファイルの実体化を促し、本スキャン時の「アクセス不能」エラーを防止 - ツリー走査: スタックベースの非再帰走査でフォルダツリーを深さ優先で探索
- 列挙オプション:
EnumerationOptionsでIgnoreInaccessible = true、AttributesToSkip = FileAttributes.System | FileAttributes.Temporaryを指定 - ドキュメント登録:
UpdateDocument(upsert)でファイル/フォルダを Lucene ドキュメントとして追加・更新 - コミット: 一定件数ごとに
IndexWriter.Commit()を実行。コミット間隔はドライブ種別で異なる: - ローカルドライブ: 500 件ごと
- ネットワークドライブ: 100 件ごと
- Box Drive: 50 件ごと
- スロットリング: バッチ処理後に待機時間を挿入し、CPU・ディスク・ネットワークへの負荷を制御:
- ローカル: 15ms(省エネモード時 50ms)
- ネットワーク: 50ms(省エネモード時 100ms)
- Box Drive: 200ms(ネットワーク低優先モード時 +150ms、省エネモード時 +50ms、最大 400ms)
- スレッド優先度: スキャン中は
Thread.CurrentThread.Priority = ThreadPriority.BelowNormalに下げ、完了後に元に戻す - 完了処理:
_indexedRootsに追加し、indexed_roots.jsonに永続化。タイムスタンプとドキュメント数を記録
除外対象(自動スキップ):
以下のフォルダ・ファイルはインデックス対象から自動的に除外されます。
- フォルダ:
.git、.svn、.hg、node_modules、bower_components、.vs、obj、bin、.nuget、__pycache__、.mypy_cache、venv、.venv、.next、.nuxt、.gradle、$Recycle.Bin、System Volume Information、Recovery、PerfLogs、$で始まるフォルダ全般 - ファイル:
desktop.ini、Thumbs.db、.DS_Store、NTUSER.DAT、~$で始まる Office 一時ファイル、拡張子.tmp/.temp/.bak/.swp/.swo、拡張子なしのファイル
同時実行制御とスレッド管理¶
- SemaphoreSlim(1, 1): インデックス作成タスクを同時 1 件に制限。ステータスバーの進捗表示の安定とサーバ負荷の軽減を実現
- lock(_lockObj):
IndexWriterやコレクション(_indexedRoots、_inProgressRoots等)への同時アクセスを保護 - Thread.Priority = BelowNormal: スキャン中はスレッド優先度を下げ、UI の応答性を維持
- CancellationToken の連鎖:
CancellationTokenSource.CreateLinkedTokenSourceでグローバルキャンセルトークンとユーザー操作のキャンセルトークンを結合。どちらからでもキャンセル可能 - CpuIdleService 連携: Interval モードで
IdleOnlyExecutionが有効な場合、CPU 使用率が閾値(既定 20%)以下になるまでインデックス実行を待機 - fire-and-forget の例外処理: Auto モードの差分更新タスクには
ContinueWith(OnlyOnFaulted)で例外を観測し、UnobservedTaskExceptionを防止
SQLite データベース(zenith.db)¶
アプリの各種履歴・統計データは data/index/zenith.db(SQLite)に保存されます。
| テーブル名 | 用途 | 主なカラム |
|---|---|---|
HistoryRecord |
フォルダ参照履歴 | Path(PK)、LastAccessed、SourceType(Local/Server/Box/SPO)、AccessCount |
SearchHistoryRecord |
検索履歴 | Key(PK)、Keyword、IsIndexSearch、IsGrepSearch、LastSearched、PresetName、MinSizeText、MaxSizeText、StartDateText、EndDateText |
RenameHistory |
リネーム履歴 | Name、LastUsed |
CustomRenameButton |
カスタムリネームボタン定義 | ユーザー定義のリネームテンプレート |
UsageRecord |
ライセンス使用記録 | Id(PK, AutoIncrement)、FeatureKey(indexed)、UsedAt(ISO8601) |
ActionStat |
利用統計 | ActionKey、Count、LastUsedAt |
SearchHistoryRecord の Key は keyword + "\u0001" + (isIndexSearch ? "1" : "0") の複合キーで、同じキーワードでも通常検索とインデックス検索を区別して保存します。V2 マイグレーションでフィルタ条件カラム(PresetName、サイズ・日付テキスト)が追加されています。
FileSystemWatcher 連携(フォルダ監視)¶
各タブは FileSystemWatcher で現在のフォルダを監視し、外部からのファイル変更をリアルタイムに反映します。
監視設定:
NotifyFilter:FileName、DirectoryName、LastWrite、SizeInternalBufferSize: 64KB(65,536 バイト)- 監視イベント:
Created、Deleted、Changed、Renamed
Auto モード時のインデックス連携:
インデックスの更新モードが Auto の場合、ファイル変更イベントに応じてインデックスを即時更新します。
Created/Changed→AddFileToIndex(fullPath)Deleted→RemoveFileFromIndex(fullPath)Renamed→RemoveFileFromIndex(oldPath)+AddFileToIndex(newPath)
Interval / Manual モードでは、定例スケジュールまたは手動トリガーのみでインデックスが更新されます。Interval の定例は前回の実行時刻を index/last_interval_update.txt に残し、起動時にそこから数えて最初の待ち時間を決めます(間隔を過ぎていれば最短 2 分後)。
アプリ自身の移動・コピーの反映:
モードにかかわらず、移動・コピー・削除・名前の変更が終わると、IndexSync が反映を 1 本の列に積み、IndexService が無くなった場所の文書を配下ごと削除し、新しい場所を配下ごと追加します(対象は、登録フォルダの配下で、ロック中でない場所だけ)。列に積むのは、一括リネームの「A → 仮の名前 → B」を順番どおりに反映するためです。転送のあとは一覧のフォルダサイズを引き直します。インデックスに文書の無いフォルダのサイズは 0 ではなく不明(空欄)として扱います。
開いているフォルダの監視で見えた変更は、Auto と Interval のときに反映します(Manual は反映しない。フォルダの Changed は見送る)。監視から届く分と名前の変更は、コミットを 1 秒まとめます。
エラー時の自動再接続:
監視エラー発生時は指数バックオフで自動再接続を試みます。
- 待機時間: 1 秒 → 2 秒 → 4 秒 → 8 秒 → … → 最大 30 秒
- 最大リトライ回数: 10 回
- 全リトライ失敗後は監視を停止し、タブがアクティブになった際にリフレッシュで補完
アイドル実行(CPU 負荷回避)¶
ユーザー操作中にインデックス作成が走って他アプリを巻き添えにしないよう、アイドル時のみ実行する負荷制御を備えています。
- 設定: Control Deck → インデックス → 「省エネモードでインデックスを更新する」(
WindowSettings.IdleOnlyExecution)+「CPU 使用率しきい値」(IdleCpuThreshold、デフォルト 30%)。 - CPU 監視:
CpuIdleServiceがPerformanceCounter("Processor", "% Processor Time", "_Total")を一定間隔でサンプリングし、直近の平均値を保持。 - 判定フロー:
- インデクサは 1 バッチ処理前に
CpuIdleService.IsIdleを参照。 CpuIdleService.IsIdle == false(しきい値を超えている)なら指数バックオフで待機し、再判定。- 連続待機中もユーザー操作による検索要求はブロックしない(検索は別キュー)。
- ネットワークドライブ軽量処理:
NetworkDriveSlowIndexが ON のとき UNC / マップドドライブに対して追加のスロットリングを適用し、LAN・クラウド帯域の圧迫を抑制。 - フル再構築最短間隔:
FullRebuildMinInterval(6h / 12h / 24h)により、手動のフル再構築要求をしきい値時間内は差分更新に置換。
この仕組みは起動直後にもアイドル評価が入るため、起動フリーズに寄与しません(起動時のインデクシングは StartupInitTask に乗らず、IndexService 自体の初期化後の遅延キューで実行)。