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

@weifuwujs/rag

v1.3.0

Published

weifuwu 向量数据面:Postgres/pgvector-first 的知识灌入与相似检索(headless,增量嵌入,元数据过滤)

Readme

向量数据面(@weifuwujs/rag)

状态:已发布(@weifuwujs/[email protected],tag rag-v1.2.0)。路线与边界见 plan/rag.md。

声明一次知识库,在**既有 Postgres(pgvector)**上得到增量灌入 + 相似检索;零外部向量库、零硬依赖。

import { defineRag } from '@weifuwujs/rag'
import postgres from 'postgres'

const rag = defineRag({
  sql: postgres(process.env.DATABASE_URL!),   // 或 @weifuwujs/db 的 postgresExecutor(...)
  dim: 1536,
  embed: async (texts) => yourEmbeddingApi(texts),   // 可替换;测试可直传向量
})

await rag.migrate()                             // create extension vector + _rag_* + HNSW 索引(幂等)
await rag.ingest([{ id: 'policy-1', text: '……', metadata: { tenantId: 'acme' } }])

const hits = await rag.search('退款政策是什么', {
  k: 5,
  filter: { tenantId: { eq: 'acme' } },         // 键值全部参数化,租户隔离
  minScore: 0.3,
})
// hits: [{ score, documentId, chunkIndex, text, metadata }]

最小可跑示例(零依赖,node 直接运行;test/batteries/readme-examples.test.ts 会真跑它):

import assert from 'node:assert/strict'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { loadFiles } from '@weifuwujs/rag'

const dir = await mkdtemp(join(tmpdir(), 'rag-readme-'))
try {
  const path = join(dir, 'policy.md')
  await writeFile(path, '---\ntenant: acme\n---\n# 退款政策\n\n7 天内可退。\n')

  const [document] = await loadFiles([path], { root: dir })
  assert.ok(document, 'loadFiles 必须返回文档')
  assert.equal(document.id, 'policy.md')                  // id = 相对 root 的 POSIX 路径
  assert.deepEqual(document.metadata?.['headings'], ['退款政策'])
  assert.deepEqual(document.metadata?.['frontMatter'], { tenant: 'acme' })
  assert.match(document.text, /7 天内可退/)
} finally {
  await rm(dir, { recursive: true, force: true })
}

示例标注:ts exec = 由测试真跑;ts fragment = 片段(依赖前后文,不独立运行);自足示例带包标签(如 ts tasks),由测试编译。

运行期 API

| 方法 | 语义 | | --- | --- | | defineRag({ sql, dim, embed?, chunk?, prefix? }) | 构造期校验维度(1..2000,HNSW 上限)与前缀白名单 | | plan() | 返回待执行 DDL(零副作用) | | migrate({ actor?, dryRun? }) | 事务 + 独立 advisory lock → 建扩展/表/HNSW → 审计 _rag_migrations;二次为空 | | ingest(docs, { embed?, force? }) | 文档级内容 hash 增量:未变 0 次嵌入、变更仅重嵌该篇;事务内删旧插新 | | search(query, { k?, mode?, filter?, minScore?, withDocument?, rerank?, embed? }) | 字符串或向量;k 默认 5 / 上限 50;score = 1 - 余弦距离;mode:'hybrid' 见下节;withDocument:true 时命中额外带 documentText(父文档当前全文,逐字不截断;整页只增 1 条 id = any($1::text[]) 批量查询,零 N+1,且不改排序/分数/条数) | | delete(id) | 级联清块;返回是否删除 | | stats() | { documents, chunks } |

混合检索(L1.5)

const hits = await rag.search('退款政策', { mode: 'hybrid', k: 5 })
// hits[i]: { score, vectorScore?, lexicalScore?, documentId, chunkIndex, text, metadata }
  • mode: 'vector'(默认,与 v1 行为一致);mode: 'hybrid' = 向量 + pg_trgm lexical 双路各取 4k 候选, JS 侧 RRF(RRF_K = 60)融合:score = Σ 1/(60+rank),并暴露 vectorScore / lexicalScore 便于解释与调参。
  • 解决「精确词/编号/专名/中文短查询」被语义近邻挤掉的问题:lexical 通道保证关键词命中进入融合。
  • 迁移自动创建 pg_trgm 扩展与 gin ("text" gin_trgm_ops) 索引(幂等,无需手工 DDL)。
  • 约束:hybrid 只接受字符串查询;暂不支持 minScore(RRF 与相似度不同量纲,明确拒绝而非静默忽略)。

自定义重排(切片 C)

const hits = await rag.search('退款政策', {
  k: 5,
  rerank: async (query, candidates) => rerankModel(query, candidates), // 收到原始 query 与 ≤4k 候选
})
  • 只能返回输入候选的子集(可重排、可过滤),最终条数仍 ≤ k;无 rerank 时行为不变。
  • 伪造命中 / 重复元素 / 非数组 / 抛异常 → 一律 RERANK_FAILED(不泄漏底层异常)。
  • withDocument 在重排与截断之后挂载,只对最终 k 条取父文档全文。

离线评测(切片 D)

const report = await rag.evaluate({
  k: 5,
  queries: [
    { query: '退款政策', expected: ['policy-1'], mode: 'hybrid' },
    { query: queryVector, expected: ['doc-a'] }, // 向量查询同样离线可用
  ],
})
// report: { k, queries, recallAtK, mrr, misses }
  • 口径:chunk 级 top-k 检索 → 按 documentId 去重;recall@k = 每条查询「期望命中数 / 期望总数」均值; MRR = 首个期望命中排名倒数均值(未命中记 0)。
  • misses 只含未完全召回的查询(期望与实收 id),用来区分「没召回到」与「排名太靠后」。
  • 纯离线:只调用现有 search,零新依赖、无额外网络/事务;可直接作为 CI 回归门(如断言 recallAtK >= 0.9)。

HTTP 投射(L2)

import { createApp } from '@weifuwujs/weifuwu'

const app = createApp().mount(
  rag.api({
    basePath: '/rag',
    authorize: (ctx, { action }) =>
      action === 'search'
        ? { where: { tenantId: ctx.user.tenantId } }   // 行级策略:只能召回自己租户
        : ctx.user.role === 'admin',                    // 写默认拒绝,除非显式放行
  }),
  { openapi: '/openapi.json' },
)

| 端点 | 形态 | | --- | --- | | POST /rag/ingest | { documents, force? } → 201 灌入报告 | | POST /rag/search | { query, k?, mode?, filter?, minScore?, withDocument? } → { hits } | | GET /rag/stats | { documents, chunks } | | DELETE /rag/documents/:id | 删除(未命中 404)|

  • 无 authorize → 全端点 403(不允许裸奔检索);被拒请求零嵌入、零 SQL(授权先于一切成本)。
  • search 的 { where } 与客户端 filter 同通道 AND 合并(全参数化),客户端 or 注入无法绕过。
  • { where } 对写操作非法(403)——不猜行级写入语义。
  • 错误码(兼容契约):INVALID_*/DIMENSION_MISMATCH → 400;未授权 → 403;EMBEDDING_FAILED/RERANK_FAILED → 502。
  • OpenAPI 3.1 文档可直接喂 openapi-typescript(门控 e2e:OPENAPI_CODEGEN_E2E=1)。

CLI(L2)

weifuwu-rag migrate --dry-run        # 预览(零执行)
weifuwu-rag migrate --actor deploy   # 建扩展/表/索引 + 审计
weifuwu-rag ingest --file docs.json  # { documents: [...] } 或文档数组
weifuwu-rag search --vector '[1,0,0]' --k 5 --filter '{"tenantId":{"eq":"acme"}}'
weifuwu-rag search '退款政策' --mode hybrid --with-document
weifuwu-rag stats
  • --config <file> 默认 ./weifuwu-rag.config.ts,默认导出 defineRag(...) 的产物(也支持导出工厂函数)。
  • 退出码 0 成功 / 1 错误;结果 JSON 只写 stdout(可管道给 jq),错误只写 stderr 且带稳定 CODE: 前缀。
  • 只编排库的公开方法(零新 SQL 通道、零新依赖);migrate --dry-run 零副作用;输出后显式退出,不依赖 config 关闭连接。

Loader:文件 → 文档(1.3)

零依赖的 loader 协议:Loader(kind / extensions / load({ buffer, path })), 产物 LoadedFile 就是 RagDocument({ id, text, metadata? }),一行进 ingest:

import { loadFiles } from '@weifuwujs/rag'

const docs = await loadFiles(['docs/a.md', 'docs/b.html'], { root: process.cwd() })
await rag.ingest(docs) // id = 相对路径(POSIX)→ 同文件重复 ingest 幂等更新

内置三个零依赖实现(保守清洗,不是 HTML/Markdown 解析器):

| loader | 扩展名 | 做什么(规则可测) | | --- | --- | --- | | textLoader | .txt .log | UTF-8(fatal,非法编码 → LOADER_INVALID)+ 去 BOM + 换行归一 | | markdownLoader | .md .markdown | front matter(--- 块内 key: value,值一律字符串不猜类型)→ metadata.frontMatter;# 标题 → metadata.headings;正文保留 Markdown 语法 | | htmlLoader | .html .htm | 去 <script>/<style>/注释、抽 <title>、去标签、解码 &amp; &lt; &gt; &quot; &#39;、压空行 |

PDF 走插槽(重依赖不进包):pdfLoader(parse) 传入你的解析器;不传 → LOADER_UNAVAILABLE(不是空文本), 解析器抛错 → LOADER_INVALID。未知扩展名 → LOADER_UNSUPPORTED 并列出可用扩展名(不静默回退 text)。

语义与安全契约

  • 增量嵌入:文档 hash = 文本 + 分块参数;仅内容变化触发重嵌。元数据更新不触发嵌入。
  • 分块:段落优先 → 长段落按句 → 超长句硬切;相邻块携带 overlap 尾部(默认 800/100)。
  • 过滤:针对文档与块 metadata 的合并视图(文档值优先),支持 eq/neq/in/isNull/and/or; 字符串/数值/布尔按 JS 类型选择 text/numeric/boolean 转换;键与值全部 $n 参数化。
  • 维度:配置维度非法 → INVALID_DIMENSION;嵌入返回维度不符 → DIMENSION_MISMATCH 且零写入。
  • 错误码:INVALID_ARGUMENT / INVALID_DIMENSION / INVALID_PREFIX / INVALID_FILTER / DIMENSION_MISMATCH / EMBEDDING_FAILED / LOADER_UNSUPPORTED / LOADER_UNAVAILABLE / LOADER_INVALID, 以 CODE: detail 前缀稳定暴露。
  • 不猜:不存在「向量维度自动截断/补齐」;k 越界直接拒绝。

与 @weifuwujs/db 的关系

  • sql 接受 db 的 postgresExecutor(...)(执行器结构兼容);迁移锁独立(weifuwujs/rag),互不阻塞。
  • _rag_* 表带 vector 列,db 的 adopt 会整表跳过并给原因——两边互不干扰。
  • 行级权限({ where })在 HTTP 投影层:rag.api({ authorize }),与客户端 filter AND 合并;库内直用时用 filter 做隔离。
  • 四包对照:docs/authorization.md——四包默认拒绝矩阵(rag 无公开检索开关)与组合边界(可执行契约)。

真库测试

docker compose up -d            # 仓库根目录(pgvector/pgvector:pg18)
npm test -w @weifuwujs/rag      # 服务不可达时显式 skip,绝不假绿