mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-28 01:57:25 +08:00
95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
/**
|
|
* FTS5 专用分词器
|
|
*
|
|
* 与 NLP 词频分析不同,FTS 分词不做词性过滤和停用词过滤,
|
|
* 搜索场景需要保留所有词以保证召回率。
|
|
*
|
|
* 使用 jieba 处理中文(天然兼容中英混合文本),
|
|
* Intl.Segmenter 处理纯英文/日文。
|
|
*
|
|
* 复用 segmenter 模块的 jieba 实例池,默认使用 zh-CN 词库。
|
|
*/
|
|
|
|
import { getJieba } from './segmenter'
|
|
|
|
/**
|
|
* 对文本进行 FTS 分词,返回空格分隔的 token 字符串。
|
|
* 用于写入 FTS5 索引。
|
|
*/
|
|
export function tokenizeForFts(text: string | null | undefined): string {
|
|
if (!text || text.trim().length === 0) return ''
|
|
|
|
try {
|
|
const jieba = getJieba()
|
|
const tokens = jieba.cut(text, false)
|
|
return tokens
|
|
.map((t) => t.trim().toLowerCase())
|
|
.filter((t) => t.length > 0)
|
|
.join(' ')
|
|
} catch {
|
|
return fallbackTokenize(text)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 降级分词:jieba 不可用时按字符/空格切分
|
|
*/
|
|
function fallbackTokenize(text: string): string {
|
|
return text
|
|
.toLowerCase()
|
|
.split(/\s+/)
|
|
.filter((t) => t.length > 0)
|
|
.join(' ')
|
|
}
|
|
|
|
/**
|
|
* 转义单个 token 使其在 FTS5 MATCH 中安全使用。
|
|
* 用双引号包裹并转义内部双引号。
|
|
*/
|
|
function escapeToken(token: string): string {
|
|
return `"${token.replace(/"/g, '""')}"`
|
|
}
|
|
|
|
/**
|
|
* 将用户搜索关键词列表转换为 FTS5 MATCH 表达式。
|
|
*
|
|
* 语义:
|
|
* - 单个关键词先分词,分词结果之间为 AND(都要出现)
|
|
* - 多个关键词之间为 OR(任一匹配即可)
|
|
*
|
|
* 示例:
|
|
* - ["今天开心"] -> '"今天" "开心"' (AND)
|
|
* - ["今天开心", "难过"] -> '("今天" "开心") OR "难过"' (OR)
|
|
* - ["hello"] -> '"hello"'
|
|
*/
|
|
export function tokenizeQueryForFts(keywords: string[]): string {
|
|
if (keywords.length === 0) return ''
|
|
|
|
const groups = keywords
|
|
.map((kw) => {
|
|
const trimmed = kw.trim()
|
|
if (!trimmed) return ''
|
|
|
|
try {
|
|
const jieba = getJieba()
|
|
const tokens = jieba
|
|
.cut(trimmed, false)
|
|
.map((t) => t.trim().toLowerCase())
|
|
.filter((t) => t.length > 0)
|
|
|
|
if (tokens.length === 0) return ''
|
|
if (tokens.length === 1) return escapeToken(tokens[0])
|
|
return tokens.map(escapeToken).join(' ')
|
|
} catch {
|
|
const simple = trimmed.toLowerCase().trim()
|
|
return simple ? escapeToken(simple) : ''
|
|
}
|
|
})
|
|
.filter((g) => g.length > 0)
|
|
|
|
if (groups.length === 0) return ''
|
|
if (groups.length === 1) return groups[0]
|
|
|
|
return groups.map((g) => (g.includes(' ') ? `(${g})` : g)).join(' OR ')
|
|
}
|