@fastrag/pageindex
v0.2.0
Published
TypeScript SDK for PageIndex hierarchical document indexing and optional vector enhancement.
Maintainers
Readme
PageIndex TS SDK
VectifyAI/PageIndex 的 TypeScript 重写版
传统向量 RAG 依赖语义相似度而非真正的相关性 — 但相似 ≠ 相关。PageIndex 采用不同的思路:从文档中构建层级树索引(类似"目录"),然后利用 LLM 推理在树上导航检索。无需分块,无需向量数据库 — 像人类专家一样结构化地阅读文档。
本 SDK 是 PageIndex 框架的 TypeScript 实现,面向 Node.js/TypeScript RAG 场景,完全运行时解耦。
特性
- 基于推理的检索 — 从文档构建层级树索引,通过 LLM 驱动的树搜索替代向量相似度匹配
- LLM 驱动的 TOC 检测 — 自动检测目录并选择最优处理模式
- 三种处理模式 —
toc_with_page_numbers、toc_no_page_numbers、no_toc,支持自动降级 - Markdown 转树 —
mdToTree()将 Markdown 转换为结构化树 - 可选向量增强 — 将树索引结果向量化,支持单文档内的精确片段检索,也适用于多文档场景下快速定位相关节点、避免逐一 LLM 树搜索
- 运行时解耦 — 自带 LLM(
LlmProvider)、PDF 解析器(DocumentParser)、向量数据库(VectorStore/Embedder)
包结构
@fastrag/pageindex ← 文档结构索引 SDK
exports:
. → 核心流程
./types → 共享类型 + 默认配置
./vector → 向量增强
runtime deps:
- gpt-tokenizer| 子路径 | 说明 |
| --- | --- |
| @fastrag/pageindex | 文档结构索引主流程 |
| @fastrag/pageindex/types | 共享类型定义与接口契约 |
| @fastrag/pageindex/vector | 向量增强:分块、索引、搜索 |
环境要求
- Node.js >= 20
- pnpm >= 10
安装
作为依赖安装:
pnpm add @fastrag/pageindex在本仓库本地开发:
pnpm install
pnpm build工程日志(问题/优化/注意点/下一步计划):docs/review/engineering-log.md
快速开始
1) 构建文档树索引
import { pageIndex } from '@fastrag/pageindex';
import type { LlmProvider, PageContent } from '@fastrag/pageindex/types';
// pageIndex 为 1-based
const pages: PageContent[] = [
{ pageIndex: 1, text: 'Table of Contents\n1 Introduction ... 1' },
{ pageIndex: 2, text: '1 Introduction\nThis document ...' },
{ pageIndex: 3, text: '2 Methods\nWe propose ...' },
];
const provider: LlmProvider = {
async chat(messages) {
// 接入 OpenAI / Claude / 本地模型
return { content: '{}', finishReason: 'stop' };
},
};
const result = await pageIndex(
pages,
{
docName: 'example.pdf',
addNodeId: true,
addNodeSummary: true,
addDocDescription: true,
},
provider,
);
console.log(result.metadata.processingMode);
console.log(result.structure);2) Markdown 转树
import { mdToTree } from '@fastrag/pageindex';
const tree = mdToTree('# Intro\nhello\n## Background\nmore context', {
thinning: true,
minTokenThreshold: 500,
});3) 向量增强
import { VectorEnhancer, InMemoryAdapter, treeChunker } from '@fastrag/pageindex/vector';
import type { Embedder } from '@fastrag/pageindex/vector';
const store = new InMemoryAdapter();
const embedder: Embedder = {
dimension: 3,
async embed(texts: string[]) {
// 替换为真实 embedding 服务
return texts.map((t) => [t.length, t.length / 2, 1]);
},
};
const enhancer = new VectorEnhancer(
store,
embedder,
treeChunker, // 也可以替换为自定义分片策略
{ chunkMaxTokens: 1000 },
);
await enhancer.index(result); // 来自 pageIndex() 的结果
const hits = await enhancer.search('introduction section');4) 混合检索
import { HybridSearch } from '@fastrag/pageindex/vector';
const hybrid = new HybridSearch(enhancer, {
vectorTopK: 20,
rerankTopK: 5,
});
const results = await hybrid.search('what is the main contribution?');核心流程
PageContent[] → TOC 检测 → 模式选择 → 验证/修复/降级 → TreeNode[] → 后处理 → PageIndexResult- 扫描前 N 页检测目录
- 提取候选目录页并抽取规范化 TOC 文本
- 根据 TOC 有无和页码信息选择处理模式
toc_with_page_numbers、toc_no_page_numbers或no_tocno_toc模式下,首个编号章节前的 front matter(如标题块、摘要类标题)会尽量保留为顶层条目
- 验证 TOC 准确性,自动修复或降级到更简单的模式
- 包含重试/修复/降级链,准确率不足时自动触发
- 检查各标题是否出现在对应页面开头
- 结果用于
endIndex区间边界判断 - 含确定性抖动抑制:页首强匹配可提升为 yes,页尾晚出现可降级为 no
- 结果用于
- 构建
TreeNode[]树结构- 将扁平 TOC 条目转换为带页码范围的层级节点
- 递归拆分大节点(包括已有的深层子节点,不仅限于新拆分的父节点)
- 对超阈值节点重新解析以提升粒度
- 可选后处理:
nodeId、nodeText、summary、docDescription
向量增强
原版 PageIndex 使用 LLM 树搜索进行检索 — 将整个树结构作为 prompt 发送给 LLM,由 LLM 推理判断哪些节点相关。这种方式效果好,但每次查询都需要调用 LLM。
@fastrag/pageindex/vector 子路径导出提供了另一种方案:将树结构转换为向量 embedding,检索变为相似度搜索 — 查询时无需 LLM,延迟在毫秒级。
| | 索引阶段 | 检索阶段 | | --- | --- | --- | | 树搜索(原版) | LLM 构建树 | 每次查询 LLM 推理遍历树 | | 向量增强(本 SDK) | LLM 构建树 → embedding 入库 | 向量相似度搜索(无需 LLM) |
覆盖两种场景:
- 单文档内 — 将树节点分块为 embedding,无需 LLM 调用即可快速精确定位相关片段
- 跨多文档 — 将多个文档索引到同一个向量库,一次搜索覆盖所有文档;避免对每个文档逐一执行 LLM 树搜索
流程:PageIndexResult → Chunker(默认实现为 treeChunker)→ Embedder(生成向量)→ VectorStore(存储/检索)。treeChunker 当前采用“按段落切分 + 字符近似阈值”(1 token ≈ 4 chars)。每个 chunk 保留树结构元数据(docName、nodeId、title、页码范围),检索结果可追溯到原始文档结构。
接口抽象
LlmProvider
interface LlmProvider {
chat(messages: LlmMessage[], options?: LlmOptions): Promise<LlmResponse>;
chatWithSchema?(
messages: LlmMessage[],
schema: JsonSchema,
options?: LlmOptions,
): Promise<LlmResponse>;
}DocumentParser
interface DocumentParser {
parse(input: string | ArrayBuffer | Uint8Array): Promise<PageContent[]>;
}Embedder
interface Embedder {
embed(texts: string[]): Promise<number[][]>;
readonly dimension: number;
}Chunker
type Chunker = (result: PageIndexResult, config?: VectorConfig) => Chunk[];treeChunker 是可直接使用的默认策略实现,但 VectorEnhancer 需要显式注入 chunker,因此你可以按模型分词规则替换为自定义分片策略。
配置项
| 字段 | 默认值 | 说明 |
| --- | ---: | --- |
| tocCheckPageNum | 20 | TOC 检测最多扫描页数 |
| maxPageNumEachNode | 10 | 大节点递归拆分页数阈值 |
| maxTokenNumEachNode | 20000 | 大节点递归拆分 token 阈值 |
| maxConcurrency | 10 | LlmClient 内部最大并发 LLM 请求数 |
| tocMarkerTitles | ['contents','content','table of contents','目录'] | TOC 标记标题默认词表(多语言),用于过滤标记行 |
| isTocMarkerTitle | 基于 tocMarkerTitles 的匹配器 | 可选自定义回调,用于过滤 TOC 标记行;设置后优先于 tocMarkerTitles |
| addNodeId | true | 生成 4 位节点 ID |
| addNodeSummary | true | 通过 LLM 生成节点摘要 |
| addDocDescription | false | 生成文档一句话描述 |
| addNodeText | false | 在输出中保留节点原文 |
| retryConfig | 指数退避 | LLM 重试策略(maxRetries: 10、initialDelayMs: 1000、maxDelayMs: 30000、backoffMultiplier: 2) |
| onDegradation | undefined | 处理模式降级时的回调 |
| onProcessingSnapshot | undefined | 可选的处理阶段快照回调(用于诊断/artifacts) |
| logger | 静默 | 自定义日志注入 |
输出结构
interface PageIndexResult {
docName: string;
docDescription?: string;
structure: TreeNode[];
metadata: {
processingMode: ProcessingMode;
degradations: DegradationEvent[];
};
}延伸阅读
- 检索策略探讨 — LLM 树搜索、MCTS、向量检索三种方案的对比,以及面向大规模文档集合的混合架构设计
