
# 索引文本搜索

索引文本搜索是 [文本搜索](https://greph.inline0.dev/zh/docs/modes/text)的预热缓存版本。首次调用会在磁盘上构建三元组和标识符倒排表存储；后续调用使用该存储跳过显然不可能包含模式的文件。这样一来，在大型代码库中重复查询时，延迟会大幅降低，而且无需引入守护进程或监视进程。

预热文本功能现在包括：

- 单索引搜索
- 通过重复指定 `--index-dir` 进行多索引搜索
- 基于清单的集合搜索
- 感知生命周期的过期处理
- 通过 `--trace-plan` 提供规划器诊断信息

索引通过 `greph-index` CLI 和 `Greph::buildTextIndex()` / `Greph::searchTextIndexed()` 门面方法提供。

## 构建索引

```bash
# 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)
```

四项计数分别表示新增、更新、删除和未更改的文件数量。首次构建时，新增计数等于文件数量，其余计数均为零。

## 刷新索引

```bash
./vendor/bin/greph-index refresh .
```

`refresh`会重新遍历索引根目录，对每个文件进行哈希计算，并且只重写元数据发生变化的条目。输出使用相同的四项计数，让你准确了解哪些内容发生了变化：

```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` 检查预热搜索将执行的操作：

```bash
./vendor/bin/greph-index stats . --dry-refresh
```

## 查询索引

```bash
# 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参考](https://greph.inline0.dev/zh/docs/cli#greph-index)。

如果请求位置不存在索引，`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`，然后：

```bash
./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_]*`）集合。用于加速整词查询。
- **生命周期元数据**：存储的阈值和过期检查行为。
- **内容元数据**：用于刷新决策和过期检查的大小和 mtime。

收到查询后，Greph会从模式中提取相同形态的内容（从字面子串中提取三元组，从整词模式中提取标识符词元），求倒排表的交集，并且只重新读取匹配的文件。通过交集筛选的文件会使用与原生文本模式相同的引擎进行搜索。

对于某些已预热的摘要查询，Greph可以直接从倒排表中作答：

- 整词 `-l`
- 整词 `-L`
- 整词 `-q`

这样一来，如果倒排表已经能确定结果，就完全无需重新打开候选文件。

这与ripgrep的可选预索引和 codesearch 的三元组索引思路相同，但专门针对磁盘上的形态构建，让已预热的 PHP能够以最快速度命中。

## 规划器诊断信息

`--trace-plan`会将已预热的文本规划器决策输出到 `stderr`，同时保持正常结果流不变。

跟踪信息包括：

- 生命周期配置
- 选中的文件数
- 候选来源（`full-scan`、`trigram-postings`、`word-postings`或 `invert-scan`）
- 倒排表词项数
- 候选文件数和已验证文件数
- 是否符合直接生成摘要的条件
- 是否符合查询缓存条件，以及缓存填充决策

## 适用场景

以下情况适合使用索引模式：

- 你会连续多次查询同一代码仓库（CI 作业、代理循环、IDE 集成）。
- 代码仓库足够大，原生扫描已成为瓶颈。
- 模式包含索引可以预筛选的字面子串或标识符词元。

以下情况仍适合使用原生扫描：

- 查询的是小型代码仓库，构建索引的成本占主导。
- 只搜索一次之后不会再查询的路径。
- 需要在一次临时调用中获取所有匹配项，而且不想维护索引。

[README](https://github.com/inline0/greph#performance)中公布的基准测试数据显示，在针对 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(...)`。
