Skip to content

fix(search): 限制 FTS5 查询 token 数并统一去重#483

Open
Qiyuanqiii wants to merge 1 commit into
TencentCloud:mainfrom
Qiyuanqiii:fix/issue-160-bounded-fts-query
Open

fix(search): 限制 FTS5 查询 token 数并统一去重#483
Qiyuanqiii wants to merge 1 commit into
TencentCloud:mainfrom
Qiyuanqiii:fix/issue-160-bounded-fts-query

Conversation

@Qiyuanqiii

@Qiyuanqiii Qiyuanqiii commented Jul 12, 2026

Copy link
Copy Markdown

背景

#178 已被维护者确认为 #160 的标准运算符净化修复,但当前
buildFtsQuery() 仍允许超长或重复输入生成大量 OR 子句:

  • fallback 分词路径没有去重;
  • jieba 与 fallback 路径都没有 token 数量上限;
  • 重复内容或超长会话可能显著放大 SQLite FTS5 解析开销。

SQLite 官方也建议对外部可控输入限制资源复杂度:
https://www.sqlite.org/limits.html

修改内容

  • 对两条分词路径的最终输出统一执行 first-seen 稳定去重。
  • 最多保留前 64 个唯一 token。
  • 保持现有 quoted-token 与 OR 查询语义不变。
  • 新增独立测试,覆盖 fallback 去重、fallback 上限、jieba 上限及去重顺序。

与现有 PR 的边界

本地复现结果

指示性测试:

  • 50,000 个唯一词:MATCH 表达式由约 739 KB 降至 754 字符;
  • 空 FTS 表查询中位耗时由约 1.22 秒降至亚毫秒级(本次为 0.039 毫秒,具体耗时随环境波动);
  • 2,000 个重复词由 2,000 个子句缩减为 1 个子句。

性能复现命令:

可选:FTS 查询预算性能复现命令

下面的 oldQuery() 保留 PR 修改前 fallback 路径的查询构造行为,
用于与当前分支的 buildFtsQuery() 做同机对比。耗时会随机器环境波动。

gh pr checkout 483
npm install

NODE_NO_WARNINGS=1 npm exec -- tsx <<'TS'
import { DatabaseSync } from 'node:sqlite';
import { performance } from 'node:perf_hooks';
import {
  _setJiebaForTest,
  buildFtsQuery,
} from './src/core/store/sqlite.ts';

function oldQuery(raw) {
  const tokens =
    raw.match(/[\p{L}\p{N}_]+/gu)?.filter(Boolean) ?? [];

  return tokens
    .map(token => `"${token.replaceAll('"', '')}"`)
    .join(' OR ');
}

function medianMs(db, query) {
  const stmt = db.prepare(
    'SELECT count(*) FROM docs WHERE docs MATCH ?',
  );

  const times = Array.from({ length: 3 }, () => {
    const start = performance.now();
    stmt.get(query);
    return performance.now() - start;
  }).sort((a, b) => a - b);

  return times[1];
}

_setJiebaForTest(null);

const unique = Array.from(
  { length: 50000 },
  (_, index) => `term${index}`,
).join(' ');

const repeated = 'alpha '.repeat(2000);

const oldUnique = oldQuery(unique);
const newUnique = buildFtsQuery(unique);
const oldRepeated = oldQuery(repeated);
const newRepeated = buildFtsQuery(repeated);

const db = new DatabaseSync(':memory:');
db.exec('CREATE VIRTUAL TABLE docs USING fts5(content)');

console.log('== 50,000 unique tokens ==');
console.log({
  beforeClauses: oldUnique.split(' OR ').length,
  beforeChars: oldUnique.length,
  beforeMedianMs: medianMs(db, oldUnique),
  afterClauses: newUnique.split(' OR ').length,
  afterChars: newUnique.length,
  afterMedianMs: medianMs(db, newUnique),
});

console.log('\n== 2,000 repeated tokens ==');
console.log({
  beforeClauses: oldRepeated.split(' OR ').length,
  beforeChars: oldRepeated.length,
  afterClauses: newRepeated.split(' OR ').length,
  afterChars: newRepeated.length,
});

db.close();
TS

本机参考结果:

50,000 unique:
before clauses=50000, chars=738886, median≈1219.7ms
after  clauses=64, chars=754, median≈0.039ms

2,000 repeated:
before clauses=2000, chars=21996
after  clauses=1, chars=7

测试截图:
image

验证结果

  • 定向测试:4/4 通过
  • 全量测试:71/71 通过
  • 完整构建:通过
  • git diff --check:通过

Related Issue

Related to #160

说明:#178 仍是 #160 的标准窄范围修复,本 PR 是独立的查询资源与稳定性补充。

截图

image

@Maxwell-Code07

Copy link
Copy Markdown
Collaborator

hank you for submitting this PR and participating in Tencent Rhino-bird Open-source Training Program!
We have successfully received your submission. The program is currently in full swing, and we will complete the Code Review for you as soon as possible. Please keep an eye on the status notifications for this PR so you can follow up promptly once the review feedback is provided.
Thanks again for your contribution and open-source spirit! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants