コンテンツへスキップ

インデックス付きテキスト検索

インデックス付きテキスト検索は、テキスト検索のキャッシュをウォームアップした状態です。最初の呼び出しでトライグラムと識別子の出現位置をディスク上に保存し、以降の呼び出しでは、そのパターンが含まれないことが明らかなファイルをストアで除外します。デーモンや監視プロセスを導入せずに、大規模なリポジトリでの反復クエリのレイテンシーを大幅に下げられます。

ウォームアップ済みのテキスト検索では、次の機能を利用できます。

  • 単一インデックス検索
  • 繰り返し実行する--index-dir複数インデックス検索
  • マニフェストを使ったセット検索
  • ライフサイクルを考慮した古いインデックスの処理
  • --trace-planによるプランナー診断

インデックスはgreph-indexCLIと、Greph::buildTextIndex() / Greph::searchTextIndexed()のファサードメソッドから利用できます。

インデックスの作成

シェル
# Full build at the current directory
./vendor/bin/greph-index build .

# Full build at an explicit root
./vendor/bin/greph-index build path/to/repo

# Use a non-default index directory
./vendor/bin/greph-index build . --index-dir /tmp/greph-index

# Build with an explicit lifecycle policy
./vendor/bin/greph-index build . \
  --lifecycle opportunistic-refresh \
  --auto-refresh-max-files 32 \
  --auto-refresh-max-bytes 1048576

デフォルトでは、インデックスは<root>/.greph-index/に保存されます。

作成に成功すると、概要が1行で表示されます:

Text
Built index for 2547 files in .greph-index (193428 trigrams, +2547 ~0 -0 =0)

4つのカウンターは、追加、更新、削除、変更なしのファイル数を表します。新規構築時は、追加カウンターがファイル数と等しく、それ以外はすべて0です。

インデックスの更新

シェル
./vendor/bin/greph-index refresh .

refreshはインデックス対象のルートを再走査し、すべてのファイルをハッシュ化して、メタデータが変わったエントリーだけを書き換えます。出力には同じ4つのカウンターが表示され、何が変わったかを正確に確認できます。

Text
Refreshed index for 2547 files in .greph-index (193428 trigrams, +3 ~7 -1 =2536)

manual-refreshを使っている場合は、追跡対象のファイルを編集した後にrefreshを実行してください。opportunistic-refreshインデックスでは、変更されたファイルの数が十分少なければ、検索中にGrephが自動でインデックスを更新できます。

ライフサイクルプロファイル

Grephはデーモンを使いません。ライフサイクルプロファイルでは、ソースツリーが変わったときのウォームアップ済みインデックスの動作を制御します。

  • static: 新しさの確認も自動変更も行わない
  • manual-refresh: statsに古い情報を表示するが、検索中の更新は行わない
  • opportunistic-refresh: 変更ファイル数が設定済みのしきい値を下回る場合に限り、検索中に更新する
  • strict-stale-check: 更新せず、古いウォーム検索を拒否する

ウォーム検索を実行する前に、greph-index stats --dry-refreshでその動作を確認できます。

シェル
./vendor/bin/greph-index stats . --dry-refresh

インデックスの検索

シェル
# Fixed-string search
./vendor/bin/greph-index search -F "function" .

# Case-insensitive whole-word
./vendor/bin/greph-index search -F -i -w "function" .

# Counts
./vendor/bin/greph-index search -F -c "function" .

# Files only
./vendor/bin/greph-index search -F -l "function" .

# Multi-index search
./vendor/bin/greph-index search -F "apply_filters" . \
  --index-dir wordpress/.greph-index \
  --index-dir wp-content/plugins/my-plugin/.greph-index \
  --show-index-origin

# Regex search
./vendor/bin/greph-index search "function\s+\w+" .

# JSON output
./vendor/bin/greph-index search --json -F "function" .

# Planner trace to stderr
./vendor/bin/greph-index search --trace-plan -F "function" .

フラグはネイティブのテキスト検索と同じです。全一覧はCLIリファレンスを参照してください。

指定された場所にインデックスがない場合、greph-index searchはエラーを発生させます。このコマンドでは、インデックスを使わないスキャンへの自動フォールバックはありません(ASTコマンドにはオプトインの--fallback scanがありますが、テキストコマンドにはありません)。まずインデックスを構築してください。

名前付きインデックスセット

同じウォームアップ済みの構成を繰り返し検索する場合は、--index-dirフラグを毎回指定する代わりにマニフェストを使ってください。

JSON
{
  "name": "wordpress-local",
  "indexes": [
    {
      "name": "core-text",
      "root": "wordpress",
      "mode": "text",
      "lifecycle": "static",
      "priority": 100
    },
    {
      "name": "plugin-text",
      "root": "wp-content/plugins/my-plugin",
      "mode": "text",
      "lifecycle": "opportunistic-refresh",
      "max_changed_files": 16,
      "max_changed_bytes": 262144,
      "priority": 200
    }
  ]
}

.greph-index-set.jsonとして保存し、次のように実行します。

シェル
./vendor/bin/greph-index set build
./vendor/bin/greph-index set stats --dry-refresh
./vendor/bin/greph-index set search --show-index-origin -F "apply_filters" .

インデックスに保存される情報

テキストインデックスは、トライグラムと識別子の出現位置を保存するストアです。インデックス対象の各ファイルについて、Grephは次の情報を抽出します。

  • トライグラムセット: ファイル内容に含まれる3バイトの並びの集合。リテラル検索と正規表現検索の両方で、大まかな事前フィルターとして使います。
  • 識別子の出現位置: ファイルに含まれる識別子形式のトークン([A-Za-z_][A-Za-z0-9_]*)の集合。単語全体の検索を高速化します。
  • ライフサイクルメタデータ: 保存されたしきい値と古いインデックスの確認動作。
  • コンテンツメタデータ: 更新判断と古さの確認に使うサイズとmtime。

クエリを受け取ると、Grephはパターンから同じ形式の情報(リテラル部分文字列からトライグラム、単語全体のパターンから識別子トークン)を抽出し、出現位置を照合して、一致したファイルだけを再読み込みします。照合を通過したファイルは、ネイティブテキストモードと同じエンジンで検索します。

ウォームアップ済みの一部の集計クエリでは、Grephは出現位置の情報だけから直接結果を返せます。

  • 単語全体の-l
  • 単語全体の-L
  • 単語全体の-q

出現位置だけで答えが確定する場合は、候補ファイルを開き直す必要がありません。

これはripgrepのオプションの事前インデックス化やcodesearchのトライグラムインデックスと同じ考え方ですが、ウォームアップ済みのPHPが最も速く検索できる、ディスク上のデータ形式に合わせて構築されています。

プランナー診断

--trace-planは通常の結果ストリームには手を加えず、ウォームアップ済みテキスト検索のプランナー判断をstderrに出力します。

トレースには次の情報が含まれます。

  • ライフサイクルプロファイル
  • 選択されたファイル数
  • 候補の出所(full-scan、trigram-postings、word-postings、またはinvert-scan)
  • 出現位置にある用語の数
  • 候補ファイル数と検証済みファイル数
  • 結果の直接集計が可能かどうか
  • クエリキャッシュが利用可能かどうか、およびキャッシュへの格納判断

使用する場面

インデックスモードが適しているのは、次の場合です。

  • 同じリポジトリを続けて何度も検索する(CIジョブ、エージェントのループ、IDE連携など)。
  • リポジトリが大きく、ネイティブスキャンがボトルネックになっている。
  • インデックスで事前に絞り込めるリテラル部分文字列や識別子トークンをパターンに含む。

ネイティブスキャンが適しているのは、次の場合です。

  • リポジトリが小さく、インデックス構築コストの方が大きい。
  • 一度しか検索しないパスを調べる。
  • 1回限りの呼び出しですべての一致を取得したく、インデックスを維持したくない。

READMEに掲載されたベンチマークでは、WordPressコーパスを対象にした正規表現検索と、件数、マッチを含むファイル、マッチを含まないファイルの各クエリで、ウォームアップ済みインデックスモードがrgとgrepの両方を上回っています。単純なリテラル検索ではgrepを上回る一方、rgにはわずかに及びません。

プログラムからの利用

PHP
use Greph\Greph;
use Greph\Text\TextSearchOptions;
use Greph\Index\IndexLifecycleProfile;

// Build once
Greph::buildTextIndex('.', lifecycle: IndexLifecycleProfile::OpportunisticRefresh);

// Refresh after edits
Greph::refreshTextIndex('.');

// Query
$results = Greph::searchTextIndexed(
    'function',
    'src',
    new TextSearchOptions(fixedString: true),
);

searchTextIndexedはネイティブのテキスト検索と同じTextSearchOptionsを受け取り、同じ形式のlist<TextFileResult>を返します。複数インデックスやマニフェストを使うフローでは、searchTextIndexedMany(...)とsearchTextIndexedSet(...)を使ってください。