npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fastrag/pageindex

v0.3.1

Published

TypeScript SDK for PageIndex hierarchical document indexing and optional vector enhancement.

Readme

PageIndex TS SDK

English

VectifyAI/PageIndex 的 TypeScript 重写版

传统向量 RAG 依赖语义相似度而非真正的相关性 — 但相似 ≠ 相关。PageIndex 采用不同的思路:从文档中构建层级树索引(类似"目录"),然后利用 LLM 推理在树上导航检索。无需分块,无需向量数据库 — 像人类专家一样结构化地阅读文档。

本 SDK 是 PageIndex 框架的 TypeScript 实现,面向 Node.js/TypeScript RAG 场景,完全运行时解耦。

特性

  • 基于推理的检索 — 从文档构建层级树索引,通过 LLM 驱动的树搜索替代向量相似度匹配
  • LLM 驱动的 TOC 检测 — 自动检测目录并选择最优处理模式
  • 三种处理模式process_toc_with_page_numbersprocess_toc_no_page_numbersprocess_no_toc,支持自动降级
  • Markdown 转树mdToTree() 将 Markdown 转换为结构化树
  • 可选向量增强 — 将树索引结果向量化,支持单文档内的精确片段检索,也适用于多文档场景下快速定位相关节点、避免逐一 LLM 树搜索
  • 自托管检索层@fastrag/pageindex/retrieval 提供文档注册、检索工具、LLM 树搜索和多文档混合检索
  • 运行时解耦 — 你自己提供 LLM(LlmProvider)、PDF 解析器(DocumentParser)、向量数据库(VectorStore / Embedder

包结构

@fastrag/pageindex         ← 文档结构索引 SDK
  exports:
    .           → 核心流程
    ./types     → 共享类型 + 默认配置
    ./vector    → 向量增强
    ./retrieval → 自托管检索层
  runtime deps:
    - gpt-tokenizer

| 子路径 | 说明 | | --- | --- | | @fastrag/pageindex | 文档结构索引主流程 | | @fastrag/pageindex/types | 共享类型定义与接口契约 | | @fastrag/pageindex/vector | 向量增强:分块、索引、搜索 | | @fastrag/pageindex/retrieval | 文档注册、检索工具、树搜索、混合检索 |

环境要求

  • Node.js >= 20
  • pnpm >= 10

安装

作为依赖安装:

pnpm add @fastrag/pageindex

在本仓库本地开发:

pnpm install
pnpm build

examples/ 里的可运行示例会从 dist/ 读取构建产物,所以新克隆仓库后要先执行 pnpm build

先选你的第一条上手路径

  • 如果你想先做一个完全不依赖 LLM 的 smoke test,先看 功能手册索引 里的 mdToTree()
  • 如果你想先确认 pageIndex() 的接线没问题、再去接真实模型,先用下面的 mock provider smoke test,目标是先拿到非空 structure
  • 如果你要做向量检索或 retrieval,先把稳定的 PageIndexResult 跑出来,再继续看对应手册。
  • 如果你想直接跑端到端 retrieval 示例,在本仓库里执行 pnpm build && node examples/agentic-retrieval.mjs

快速开始

1) 构建文档树索引

import { pageIndex } from '@fastrag/pageindex';
import type { LlmProvider, PageContent } from '@fastrag/pageindex/types';

// pageIndex 是权威的物理页码
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 = createDemoProvider();

const result = await pageIndex(
  pages,
  {
    docName: 'example.pdf',
    addNodeId: true,
    addNodeSummary: false,
    addDocDescription: false,
  },
  provider,
);

console.log(result.metadata.processingMode);
console.log(result.structure);

function createDemoProvider(): LlmProvider {
  return {
    async chat(messages) {
      const prompt = messages[messages.length - 1]?.content ?? '';

      if (prompt.includes('detect if there is a table of content')) {
        return {
          content: '{"thinking":"short document","toc_detected":"no"}',
          finishReason: 'stop',
        };
      }

      if (prompt.includes('generate\nthe tree structure')) {
        return {
          content: JSON.stringify([
            {
              structure: '1',
              title: 'Introduction',
              physical_index: '<physical_index_1>',
            },
            {
              structure: '2',
              title: 'Methods',
              physical_index: '<physical_index_3>',
            },
          ]),
          finishReason: 'stop',
        };
      }

      if (prompt.includes('check if the given section appears')) {
        return {
          content: '{"thinking":"found","answer":"yes"}',
          finishReason: 'stop',
        };
      }

      if (prompt.includes('starts in the beginning')) {
        return {
          content: '{"thinking":"yes","start_begin":"yes"}',
          finishReason: 'stop',
        };
      }

      return { content: '{}', finishReason: 'stop' };
    },
  };
}

这个 mock provider 只适合第一轮 smoke test,用来确认接线和返回结构没问题。只要你已经能拿到非空 result.structure,就应该把它替换成真实的 OpenAI / Anthropic / 本地模型适配层。

接真实模型时,稳定默认值本身就是 structure-first:addNodeSummary: falseaddDocDescription: false。先保持这个默认姿态,只调通树构建本身;真正需要摘要时再显式打开。当前 summary 仍属于显式开启、但不承诺稳定质量的能力,不要把 addNodeSummary: true 当成稳定版契约的一部分。addDocDescription 只会在摘要后处理路径里生成;真正需要 docDescription 时,要同时打开 addNodeSummary: trueaddDocDescription: true

页码语义说明:

  • pageIndex() 会把 PageContent.pageIndex 当作权威物理页码使用。
  • pageIndex 必须严格递增且唯一;中间允许有缺口。
  • 如果你只有按顺序排列的页面文本,也请先组装成 PageContent[],用 1..N 这样的连续 pageIndexpageIndex()

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 },
);

// treeChunker 依赖 node.text,因此为默认 chunker 准备结果时要打开 addNodeText: true
const vectorReadyResult = await pageIndex(
  pages,
  {
    docName: 'example.pdf',
    addNodeText: true,
    addNodeSummary: false,
    addDocDescription: false,
  },
  provider,
);

await enhancer.index(vectorReadyResult);
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?');

5) 自托管检索

import { pageIndex, LlmClient, createSilentLogger } from '@fastrag/pageindex';
import { DEFAULT_RETRY_CONFIG } from '@fastrag/pageindex/types';
import {
  PageIndexClient,
  LlmTreeSearcher,
  HybridPipeline,
  getPageContent,
} from '@fastrag/pageindex/retrieval';
import {
  InMemoryAdapter,
  VectorEnhancer,
  treeChunker,
  type Embedder,
} from '@fastrag/pageindex/vector';

const client = new PageIndexClient();
const result = await pageIndex(
  pages,
  {
    docName: 'example.pdf',
    addNodeText: true,
    addNodeSummary: false,
    addDocDescription: false,
  },
  provider,
);

const docId = await client.addResult(result, { pages });

const embedder: Embedder = {
  dimension: 3,
  async embed(texts) {
    return texts.map((text) => [text.length, text.length / 2, 1]);
  },
};

const enhancer = new VectorEnhancer(
  new InMemoryAdapter(),
  embedder,
  treeChunker,
);
await enhancer.index(result, { docId });

const treeSearcher = new LlmTreeSearcher(
  new LlmClient(provider, DEFAULT_RETRY_CONFIG, createSilentLogger()),
);
const pipeline = new HybridPipeline(client, enhancer, treeSearcher, {
  vectorTopK: 20,
  maxCandidateDocs: 3,
});

const retrievalResults = await pipeline.search('introduction');
if (retrievalResults[0]) {
  const supportPages = await getPageContent(client, retrievalResults[0].docId, '1-2');
  console.log(supportPages);
}

核心流程

PageContent[] → TOC 检测 → 模式选择 → 验证/修复/降级 → TreeNode[] → 后处理 → PageIndexResult
  1. 扫描前 N 页检测目录
  2. 根据 TOC 有无和页码信息选择处理模式
  3. 验证 TOC 准确性,自动修复或降级到更简单的模式
  4. 构建 TreeNode[] 树结构
  5. 可选后处理:nodeIdnodeTextsummarydocDescription

向量增强

原版 PageIndex 使用 LLM 树搜索进行检索 — 将整个树结构作为 prompt 发送给 LLM,由 LLM 推理判断哪些节点相关。这种方式效果好,但每次查询都需要调用 LLM。

@fastrag/pageindex/vector 子路径导出提供了另一种方案:将树结构转换为向量 embedding,检索变为相似度搜索 — 查询时无需 LLM,延迟在毫秒级。

| | 索引阶段 | 检索阶段 | | --- | --- | --- | | 树搜索(原版) | LLM 构建树 | 每次查询 LLM 推理遍历树 | | 向量增强(本 SDK) | LLM 构建树 → embedding 入库 | 向量相似度搜索(无需 LLM) |

覆盖两种场景:

  • 单文档内 — 将树节点分块为 embedding,无需 LLM 调用即可快速精确定位相关片段
  • 跨多文档 — 将多个文档索引到同一个向量库,一次搜索覆盖所有文档;避免对每个文档逐一执行 LLM 树搜索

流程:PageIndexResultChunker(默认实现为 treeChunker)→ Embedder(生成向量)→ VectorStore(存储/检索)。treeChunker 当前采用“按段落切分 + 字符近似阈值”(1 token ≈ 4 chars),长段落或没有空行的文本会继续按字符上限切分,并直接读取 node.text,所以如果你准备用默认 chunker 做向量化,应该在 pageIndex() 时打开 addNodeText: true。每个 chunk 保留树结构元数据(docName、可选 docIdnodeIdtitle、页码范围),检索结果可追溯到原始文档结构。如果你要把向量召回接到 HybridPipeline,请在 enhancer.index(result, { docId }) 时显式传入稳定文档 ID。

自托管检索层

@fastrag/pageindex/retrieval 子路径把索引内核扩展成可复用的本地检索底座:

  • PageIndexClient 负责文档注册和 workspace 持久化
  • getDocumentgetDocumentStructuregetPageContent 提供 agent 友好的工具面
  • LlmTreeSearcher 负责单文档、只基于结构树的树搜索
  • HybridPipeline 负责“向量粗筛 + 树搜索精排”

这层能力刻意保持在自托管范围内,不引入托管 HTTP API、OCR、MCP 或 chat 产品层能力。

接口抽象

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,
  context?: VectorIndexContext,
) => Chunk[];

treeChunker 是可直接使用的默认策略实现,但 VectorEnhancer 需要显式注入 chunker,因此你可以按模型分词规则替换为自定义分片策略。

配置项

| 字段 | 默认值 | 说明 | | --- | ---: | --- | | tocCheckPageNum | 20 | TOC 检测最多扫描页数 | | maxPageNumEachNode | 10 | 大节点递归拆分页数阈值 | | maxTokenNumEachNode | 20000 | 大节点递归拆分 token 阈值 | | addNodeId | true | 生成 4 位节点 ID | | addNodeSummary | false | 只有在显式开启时才通过 LLM 生成节点摘要;当前不属于稳定质量承诺 | | addDocDescription | false | 当 addNodeSummary 同时开启时,生成文档一句话描述 | | addNodeText | false | 在输出中保留节点原文;这是本地文本挂载,不新增 LLM 调用 | | retryConfig | 指数退避 | LLM 重试策略(maxRetries: 10initialDelayMs: 1000maxDelayMs: 30000backoffMultiplier: 2) | | onDegradation | undefined | 处理模式降级时的回调 | | logger | 静默 | 自定义日志注入 |

输出结构

interface PageIndexResult {
  docName: string;
  docDescription?: string;
  structure: TreeNode[];
  metadata: {
    processingMode: ProcessingMode;
    degradations: DegradationEvent[];
  };
}

Manuals

当前已经实现的功能手册位于 docs/manuals/README.md

npm tarball 里也会附带这些手册文件(位于 docs/manuals/)以及可运行示例 examples/agentic-retrieval.mjs,这样第三方即使脱离仓库页面也能直接查阅。

延伸阅读

  • 功能手册索引 — 面向当前 SDK 已实现能力的操作说明
  • 可运行 retrieval 示例PageIndexClient + VectorEnhancer + HybridPipeline 端到端 smoke test;本仓库运行前先执行 pnpm build
  • 检索策略探讨 — LLM 树搜索、MCTS、向量检索三种方案的对比,以及面向大规模文档集合的混合架构设计

许可证

MIT