Files
Clipboard/lib/search-plan.js
2026-09-12 13:59:12 +08:00

59 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// trigram 分词器按 3 字符滑窗建索引,因此**任何**短于 3 字符的词都匹配不到。
const TRIGRAM_MIN = 3;
/**
* 转义 LIKE 元字符(% _ \),供 `LIKE ? ESCAPE '\'` 使用。
* 用户输入里的 % / _ 不转义会被当通配符:搜 "100%" 变成匹配一切以 100 开头的串。
*/
function escapeLike(s) {
return String(s).replace(/[\\%_]/g, (c) => '\\' + c);
}
/**
* 把查询拆成「每个 token 一个 LIKE 模式」的数组(已转义)。
* 多 token 是 AND 关系 —— 与 FTS MATCH 多短语的语义对齐,
* 且各 token 独立匹配,不要求原文里连空格一起逐字出现。
*
* 空查询返回空数组(调用方按"列出全部"处理)。
*/
function likePatterns(query) {
const trimmed = String(query == null ? '' : query).trim();
if (!trimmed) return [];
return trimmed.split(/\s+/).filter(Boolean).map((tok) => `%${escapeLike(tok)}%`);
}
/**
* 决定一个搜索词该走 FTS 还是退回 LIKE并给出安全转义后的 FTS 查询串。
*
* 背景clips_fts 用的是 trigram 分词器(见 lib/migrate.js 的 v2->v3
* 选它是因为默认的 unicode61 会把一整串中文当成一个 token导致
* "公园" 搜不到 "今天去公园散步" —— 中文场景下搜索基本全废。
* trigram 解决了子串匹配,但代价是 ≤2 字符的词一条也匹配不到,
* 所以这类查询必须退回 LIKE"公园""会议""复制" 这种两字词非常常见)。
*
* 另外,用户输入直接塞进 MATCH 会因 FTS5 保留字符(- " ( ) * ^ :)报语法错误,
* 所以每个 token 都要包成 FTS5 字符串字面量。
*
* @param {string} query 用户原始输入
* @returns {{mode:'all'} | {mode:'like', patterns:string[]} | {mode:'fts', match:string}}
*/
function planSearch(query) {
const patterns = likePatterns(query);
if (patterns.length === 0) return { mode: 'all' };
// 任何一个 token 太短 → 整个 MATCH 都会返回空(多 token 是 AND 关系),
// 所以只要有一个短 token 就整体退回 LIKE。
// 注意用 [...tok] 按码点算长度,别用 .length代理对会算成 2
const trimmed = String(query == null ? '' : query).trim();
const tokens = trimmed.split(/\s+/).filter(Boolean);
const tooShort = tokens.some((tok) => [...tok].length < TRIGRAM_MIN);
if (tooShort) return { mode: 'like', patterns };
const match = tokens.map((tok) => `"${tok.replace(/"/g, '""')}"`).join(' ');
return { mode: 'fts', match };
}
module.exports = { planSearch, likePatterns, escapeLike, TRIGRAM_MIN };