Перейти к содержимому

Индексированный текстовый поиск

Индексированный текстовый поиск — это версия текстового поиска с прогретым кэшем. При первом вызове создается хранилище триграмм и вхождений идентификаторов на диске; при последующих вызовах оно позволяет пропускать файлы, в которых заведомо не может быть шаблона. В результате повторные запросы к крупным репозиториям выполняются с гораздо меньшей задержкой, без запуска демона или процесса наблюдения за файлами.

Теперь прогретый текстовый поиск включает:

  • поиск по одному индексу
  • повторный поиск --index-dir по нескольким индексам
  • поиск по набору на основе манифеста
  • обработку устаревших данных с учетом жизненного цикла
  • диагностику планировщика через --trace-plan

Индекс доступен через greph-index CLI и методы фасада 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/.

После успешного создания выводится сводка в одну строку:

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

Четыре счетчика — это количество добавленных, обновленных, удаленных и неизмененных файлов. При первой сборке счетчик добавленных равен количеству файлов, а все остальные равны нулю.

Обновление индекса

Командная оболочка
./vendor/bin/greph-index refresh .

refresh повторно обходит индексируемый корневой каталог, хэширует каждый файл и перезаписывает только записи с изменившимися метаданными. Результат содержит те же четыре счетчика, чтобы вы точно видели, что изменилось:

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

Запустите refresh после редактирования отслеживаемых файлов, если используете manual-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 извлекает:

  • Набор триграмм: набор трехбайтовых последовательностей, присутствующих в содержимом файла. Используется как грубый предварительный фильтр для буквальных запросов и запросов с регулярными выражениями.
  • Вхождения идентификаторов: набор токенов в форме идентификаторов ([A-Za-z_][A-Za-z0-9_]*), присутствующих в файле. Используется для ускорения поиска по целым словам.
  • Метаданные жизненного цикла: сохраненные пороговые значения и поведение при проверке актуальности.
  • Метаданные содержимого: размер и время изменения для принятия решения об обновлении и проверки устаревших данных.

Получив запрос, Greph извлекает из шаблона те же данные (триграммы из буквальных подстрок, токены-идентификаторы из шаблонов поиска по целым словам), пересекает наборы вхождений и перечитывает только совпавшие файлы. Файлы, оставшиеся после пересечения, проверяются тем же механизмом, что и в нативном текстовом режиме.

Некоторые прогретые запросы на сводку Greph может обработать непосредственно по данным о вхождениях:

  • целое слово -l
  • целое слово -L
  • целое слово -q

Так можно вообще не открывать файлы-кандидаты, если данные о вхождениях уже дают однозначный ответ.

Это та же идея, что и необязательная предварительная индексация в ripgrep и индекс триграмм в codesearch, но здесь она адаптирована к формам данных на диске, которые прогретый поиск PHP обрабатывает быстрее всего.

Диагностика планировщика

--trace-plan выводит решение планировщика прогретого текстового поиска в stderr, не затрагивая обычный поток результатов.

Трассировка включает:

  • профиль жизненного цикла
  • количество выбранных файлов
  • источник кандидатов (full-scan, trigram-postings, word-postings или invert-scan)
  • количество термов вхождений
  • количество файлов-кандидатов и проверенных файлов
  • возможность выдачи сводки напрямую
  • возможность использования кэша запросов и решение о его заполнении

Когда использовать

Индексированный режим подходит, когда:

  • Вы многократно подряд ищете в одном и том же репозитории (задания CI, циклы агентов, интеграции с IDE).
  • Репозиторий достаточно велик, чтобы нативное сканирование стало узким местом.
  • Шаблоны содержат буквальные подстроки или токены-идентификаторы, по которым индекс может предварительно отфильтровать файлы.

Нативное сканирование по-прежнему подходит, когда:

  • Вы ищете в небольшом репозитории, где затраты на построение индекса превышают выгоду.
  • Вы ищете по пути, который больше проверять не будете.
  • Вам нужны все совпадения в одном разовом запросе, и вы не хотите поддерживать индекс.

Опубликованные результаты тестирования в README показывают, что прогретый индексированный режим превосходит rg и grep при поиске по регулярным выражениям, а также в запросах на подсчет, поиск файлов с совпадениями и поиск файлов без совпадений по корпусу WordPress. При обычном поиске по буквальному тексту он быстрее 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(...).