
# 索引 AST 搜索

索引 AST 搜索是 [AST 搜索](https://greph.inline0.dev/zh/docs/modes/ast)的预热缓存版本。首次调用会将节点级事实（调用名称、实例化、类声明、方法声明）提取到磁盘存储中；后续调用使用该存储跳过显然不可能包含结构匹配的文件。随后，匹配器会根据源代码重新验证候选项。

事实索引通过 `greph-index ast-index` CLI 子命令以及 `Greph::buildAstIndex()` / `Greph::searchAstIndexed()` 门面方法提供。它也支持预热多索引和基于清单的集合工作流程。

## 构建索引

```bash
# Full build at the current directory
./vendor/bin/greph-index ast-index build .

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

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

# Build a mutable overlay that can refresh cheaply on demand
./vendor/bin/greph-index ast-index build . \
  --lifecycle opportunistic-refresh \
  --auto-refresh-max-files 32 \
  --auto-refresh-max-bytes 1048576
```

默认情况下，事实索引存储在 `<root>/.greph-ast-index/`。

构建成功后会输出一行摘要：

```text
Built AST index for 2547 files in .greph-ast-index (84931 fact rows, +2547 ~0 -0 =0)
```

这四项计数分别表示自上次构建以来新增、更新、删除和未更改的文件。

## 刷新索引

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

`refresh`会重新遍历索引根目录，重新解析已更改的文件，并且只重写发生变化的事实行。输出使用与文本索引相同的四项计数。

生命周期配置与预热文本索引的行为相同：

- `static`：从不自动检查新鲜度或修改数据
- `manual-refresh`：在 `stats` 中报告过期状态，但搜索时从不刷新
- `opportunistic-refresh`：仅当变更集仍足够小时，才在搜索时刷新
- `strict-stale-check`：拒绝过期搜索，而不执行刷新

## 查询索引

```bash
./vendor/bin/greph-index ast-index search 'new $CLASS()' src
./vendor/bin/greph-index ast-index search '$obj->$method($$$ARGS)' src
./vendor/bin/greph-index ast-index search --json 'array($$$ITEMS)' src
./vendor/bin/greph-index ast-index search -l 'array($$$ITEMS)' src
./vendor/bin/greph-index ast-index search --trace-plan 'new $CLASS()' src
```

元变量语法与原生 AST 搜索完全相同。完整语法请参阅 [模式 / AST 搜索](https://greph.inline0.dev/zh/docs/modes/ast)。

### 选项

| 标志 | 含义 |
|---|---|
| `--lang NAME` | AST 语言。默认值：`php` |
| `-j N`、`--jobs N` | 并行工作进程数 |
| `-l`、`--files-with-matches` | 仅列出匹配的文件 |
| `--glob GLOB` | 仅包含路径与 GLOB 匹配的文件 |
| `--type NAME` / `--type-not NAME` | 文件类型筛选器 |
| `--json` | 输出 JSON 格式 |
| `--no-ignore` | 忽略 `.gitignore` 和 `.grephignore` |
| `--hidden` | 包含隐藏文件 |
| `--strict-parse` | 遇到解析错误时失败，而不是跳过错误 |
| `--fallback MODE` | 索引缺失时的行为：`fail`（默认）或 `scan` |
| `--index-dir DIR` | 使用非默认索引目录 |
| `--trace-plan` | 将预热 AST 规划器诊断信息输出到 `stderr` |
| `--show-index-origin` | 在多索引或集合模式下，在输出前添加匹配的索引标签 |

### 索引缺失时的回退行为

默认情况下，如果请求位置不存在索引，`ast-index search` 会报错。传入 `--fallback scan` 可改为回退到原生 AST 扫描。对于希望在索引可用时进行索引搜索、但在尚未构建索引时仍需生成结果的工具，这是合适的设置。

```bash
./vendor/bin/greph-index ast-index search --fallback scan 'new $CLASS()' src
```

### 多索引搜索

重复指定 `--index-dir`，即可在一条命令中搜索多个预热 AST 索引。这适用于 WordPress 核心加插件或主题等目录布局。

```bash
./vendor/bin/greph-index ast-index search \
  --index-dir ../wordpress/.greph-ast-index \
  --index-dir ./.greph-ast-index \
  --show-index-origin \
  'new $CLASS()' .
```

启用 `--show-index-origin` 后，输出会添加匹配的预热索引条目名称前缀，便于查看命中结果来自哪个根目录。

### 基于清单的索引集合

对于稳定的预热配置，可以定义清单，并让 `greph-index set` 对分组索引进行操作：

```bash
./vendor/bin/greph-index set build
./vendor/bin/greph-index set stats --mode ast-index --dry-refresh
./vendor/bin/greph-index set search --mode ast-index --show-index-origin 'new $CLASS()' .
```

集合子命令与直接使用 `ast-index` 时，共享相同的生命周期处理、过期检查和规划器诊断功能。

### 规划器诊断

`--trace-plan`显示 Greph 在完整匹配器运行前如何缩小查询范围。对于 `ast-index`，通常包括：

- 从模式中提取的粗略事实签名
- 搜索的预热索引数量
- 被事实筛选器排除的文件数量
- 为精确结构验证而重新解析的候选文件数量

## 索引存储内容

对于每个已索引文件，AST 事实提取器都会为每个“有意义的”节点记录一行。事实类型涵盖 AST 模式最常针对的结构：

- 函数和方法调用（按调用名称）
- 类实例化（按类名）
- 类、接口、trait 和 enum 声明（按名称）
- 函数声明（按名称）
- 方法声明（按名称和可见性）
- use 语句

收到查询后，`Greph\Ast\AstFactQuery`会将搜索模式转换为粗略事实签名（例如“必须包含任意类的 `new`”），并与事实存储求交集。通过交集筛选的文件会重新解析，并根据完整模式进行匹配。

这意味着事实索引是一个*预筛选器*，而非完整保真度的匹配存储。匹配步骤仍会解析源代码，因此结构正确性与原生 AST 搜索相同；不同之处只在于候选集合更小。

## 适用场景

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

- 你会针对同一代码库反复运行相同的结构化查询。
- 模式针对可识别的结构（调用名称、类名、声明类型），而这些正是事实提取器存储的内容。
- 你希望降低延迟，无需每次重新解析所有文件。

对于区分特征不在事实集合中的模式（例如没有可识别头部的复杂嵌套表达式），[缓存 AST 模式](https://greph.inline0.dev/zh/docs/indexed/cache)通常更合适，因为它完全跳过解析步骤，无需依赖粗略签名。

## 编程方式使用

```php
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
use Greph\Index\IndexLifecycleProfile;

Greph::buildAstIndex('.', lifecycle: IndexLifecycleProfile::OpportunisticRefresh);

$matches = Greph::searchAstIndexed(
    'new $CLASS()',
    'src',
    new AstSearchOptions(jobs: 4, tracePlan: true),
);
```

`searchAstIndexed`接受与原生 AST 搜索相同的 `AstSearchOptions`，并返回相同的 `list<AstMatch>` 结构。对于多索引或基于清单的流程，请使用 `searchAstIndexedMany(...)` 和 `searchAstIndexedSet(...)`。
