dsh-data-domain-knowledge 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +360 -0
- package/cordis.patch.yml +410 -0
- package/eval/README.md +76 -0
- package/eval/evaluate-retrieval.mjs +121 -0
- package/eval/queries.example.jsonl +2 -0
- package/lib/client.js +111415 -0
- package/lib/types/config-schema.d.ts +35 -0
- package/lib/types/config-schema.d.ts.map +1 -0
- package/lib/types/config-schema.js +34 -0
- package/lib/types/config-schema.js.map +1 -0
- package/lib/types/convert/core-loader.d.ts +32 -0
- package/lib/types/convert/core-loader.d.ts.map +1 -0
- package/lib/types/convert/core-loader.js +60 -0
- package/lib/types/convert/core-loader.js.map +1 -0
- package/lib/types/convert/host.d.ts +88 -0
- package/lib/types/convert/host.d.ts.map +1 -0
- package/lib/types/convert/host.js +479 -0
- package/lib/types/convert/host.js.map +1 -0
- package/lib/types/convert/ingest.d.ts +125 -0
- package/lib/types/convert/ingest.d.ts.map +1 -0
- package/lib/types/convert/ingest.js +228 -0
- package/lib/types/convert/ingest.js.map +1 -0
- package/lib/types/convert/settings-schema.d.ts +18 -0
- package/lib/types/convert/settings-schema.d.ts.map +1 -0
- package/lib/types/convert/settings-schema.js +26 -0
- package/lib/types/convert/settings-schema.js.map +1 -0
- package/lib/types/convert/types.d.ts +46 -0
- package/lib/types/convert/types.d.ts.map +1 -0
- package/lib/types/convert/types.js +41 -0
- package/lib/types/convert/types.js.map +1 -0
- package/lib/types/diagnosis/cards.d.ts +71 -0
- package/lib/types/diagnosis/cards.d.ts.map +1 -0
- package/lib/types/diagnosis/cards.js +482 -0
- package/lib/types/diagnosis/cards.js.map +1 -0
- package/lib/types/diagnosis/problem-classes.d.ts +51 -0
- package/lib/types/diagnosis/problem-classes.d.ts.map +1 -0
- package/lib/types/diagnosis/problem-classes.js +66 -0
- package/lib/types/diagnosis/problem-classes.js.map +1 -0
- package/lib/types/diagnosis/schema.d.ts +65 -0
- package/lib/types/diagnosis/schema.d.ts.map +1 -0
- package/lib/types/diagnosis/schema.js +208 -0
- package/lib/types/diagnosis/schema.js.map +1 -0
- package/lib/types/diagnosis/seed-source.d.ts +20 -0
- package/lib/types/diagnosis/seed-source.d.ts.map +1 -0
- package/lib/types/diagnosis/seed-source.js +96 -0
- package/lib/types/diagnosis/seed-source.js.map +1 -0
- package/lib/types/diagnosis/seed.d.ts +50 -0
- package/lib/types/diagnosis/seed.d.ts.map +1 -0
- package/lib/types/diagnosis/seed.js +242 -0
- package/lib/types/diagnosis/seed.js.map +1 -0
- package/lib/types/diagnosis/store.d.ts +81 -0
- package/lib/types/diagnosis/store.d.ts.map +1 -0
- package/lib/types/diagnosis/store.js +409 -0
- package/lib/types/diagnosis/store.js.map +1 -0
- package/lib/types/diagnosis/types.d.ts +124 -0
- package/lib/types/diagnosis/types.d.ts.map +1 -0
- package/lib/types/diagnosis/types.js +13 -0
- package/lib/types/diagnosis/types.js.map +1 -0
- package/lib/types/directory-picker.d.ts +94 -0
- package/lib/types/directory-picker.d.ts.map +1 -0
- package/lib/types/directory-picker.js +110 -0
- package/lib/types/directory-picker.js.map +1 -0
- package/lib/types/extract-model.d.ts +76 -0
- package/lib/types/extract-model.d.ts.map +1 -0
- package/lib/types/extract-model.js +193 -0
- package/lib/types/extract-model.js.map +1 -0
- package/lib/types/extract.d.ts +121 -0
- package/lib/types/extract.d.ts.map +1 -0
- package/lib/types/extract.js +549 -0
- package/lib/types/extract.js.map +1 -0
- package/lib/types/fts.d.ts +96 -0
- package/lib/types/fts.d.ts.map +1 -0
- package/lib/types/fts.js +184 -0
- package/lib/types/fts.js.map +1 -0
- package/lib/types/index.d.ts +39 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +1112 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/intent/engine.d.ts +108 -0
- package/lib/types/intent/engine.d.ts.map +1 -0
- package/lib/types/intent/engine.js +501 -0
- package/lib/types/intent/engine.js.map +1 -0
- package/lib/types/intent/host.d.ts +88 -0
- package/lib/types/intent/host.d.ts.map +1 -0
- package/lib/types/intent/host.js +389 -0
- package/lib/types/intent/host.js.map +1 -0
- package/lib/types/intent/preset-tools.d.ts +37 -0
- package/lib/types/intent/preset-tools.d.ts.map +1 -0
- package/lib/types/intent/preset-tools.js +45 -0
- package/lib/types/intent/preset-tools.js.map +1 -0
- package/lib/types/intent/settings-schema.d.ts +59 -0
- package/lib/types/intent/settings-schema.d.ts.map +1 -0
- package/lib/types/intent/settings-schema.js +57 -0
- package/lib/types/intent/settings-schema.js.map +1 -0
- package/lib/types/intent/store.d.ts +75 -0
- package/lib/types/intent/store.d.ts.map +1 -0
- package/lib/types/intent/store.js +139 -0
- package/lib/types/intent/store.js.map +1 -0
- package/lib/types/intent/tools.d.ts +63 -0
- package/lib/types/intent/tools.d.ts.map +1 -0
- package/lib/types/intent/tools.js +108 -0
- package/lib/types/intent/tools.js.map +1 -0
- package/lib/types/intent/types.d.ts +189 -0
- package/lib/types/intent/types.d.ts.map +1 -0
- package/lib/types/intent/types.js +62 -0
- package/lib/types/intent/types.js.map +1 -0
- package/lib/types/knowledge-bases.d.ts +266 -0
- package/lib/types/knowledge-bases.d.ts.map +1 -0
- package/lib/types/knowledge-bases.js +1107 -0
- package/lib/types/knowledge-bases.js.map +1 -0
- package/lib/types/layout.d.ts +262 -0
- package/lib/types/layout.d.ts.map +1 -0
- package/lib/types/layout.js +312 -0
- package/lib/types/layout.js.map +1 -0
- package/lib/types/migrate.d.ts +110 -0
- package/lib/types/migrate.d.ts.map +1 -0
- package/lib/types/migrate.js +280 -0
- package/lib/types/migrate.js.map +1 -0
- package/lib/types/recall-rank.d.ts +158 -0
- package/lib/types/recall-rank.d.ts.map +1 -0
- package/lib/types/recall-rank.js +261 -0
- package/lib/types/recall-rank.js.map +1 -0
- package/lib/types/runtime.d.ts +568 -0
- package/lib/types/runtime.d.ts.map +1 -0
- package/lib/types/runtime.js +3045 -0
- package/lib/types/runtime.js.map +1 -0
- package/lib/types/settings-schema.d.ts +117 -0
- package/lib/types/settings-schema.d.ts.map +1 -0
- package/lib/types/settings-schema.js +92 -0
- package/lib/types/settings-schema.js.map +1 -0
- package/lib/types/settings-volatile.d.ts +102 -0
- package/lib/types/settings-volatile.d.ts.map +1 -0
- package/lib/types/settings-volatile.js +114 -0
- package/lib/types/settings-volatile.js.map +1 -0
- package/lib/types/types.d.ts +799 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/types.js +387 -0
- package/lib/types/types.js.map +1 -0
- package/lib/types/wiki-types.d.ts +68 -0
- package/lib/types/wiki-types.d.ts.map +1 -0
- package/lib/types/wiki-types.js +322 -0
- package/lib/types/wiki-types.js.map +1 -0
- package/package.json +101 -0
- package/preset/tita-data-agent/agent.cordis.yml +376 -0
- package/preset/tita-data-agent/preset.yml +3 -0
- package/python/requirements.txt +8 -0
- package/scripts/doctor.mjs +70 -0
- package/scripts/e2e-pipeline.mjs +355 -0
- package/scripts/install-agent-preset.mjs +92 -0
- package/scripts/setup-embedding.sh +34 -0
- package/scripts/sync-agent-preset.mjs +188 -0
- package/src/client/AgentPresetCard.tsx +247 -0
- package/src/client/DirectoryPicker.tsx +357 -0
- package/src/client/InfoHint.tsx +65 -0
- package/src/client/IntentLogicTab.tsx +185 -0
- package/src/client/IntentTab.tsx +715 -0
- package/src/client/KnowledgeBaseTab.tsx +1454 -0
- package/src/client/KnowledgeGraphView.tsx +306 -0
- package/src/client/KnowledgePanel.tsx +381 -0
- package/src/client/KnowledgeStepRail.tsx +60 -0
- package/src/client/NodeSettings.tsx +392 -0
- package/src/client/PreviewSections.tsx +116 -0
- package/src/client/RecallPanel.tsx +399 -0
- package/src/client/TypeDefinitionDrawer.tsx +215 -0
- package/src/client/WikiFlowCard.tsx +226 -0
- package/src/client/WikiTypeEditor.tsx +332 -0
- package/src/client/agent-preset.ts +121 -0
- package/src/client/arch/AgentArchitectureGraph.tsx +219 -0
- package/src/client/arch/NodeLogicPanel.tsx +133 -0
- package/src/client/arch/ProblemDetailPanel.tsx +88 -0
- package/src/client/arch/model.ts +691 -0
- package/src/client/architecture.css +191 -0
- package/src/client/architecture.tsx +530 -0
- package/src/client/attribution/AttributionGraph.tsx +104 -0
- package/src/client/attribution/AttributionTab.tsx +332 -0
- package/src/client/attribution/CaseView.tsx +102 -0
- package/src/client/attribution/GraphStage.tsx +87 -0
- package/src/client/attribution/RunView.tsx +184 -0
- package/src/client/attribution/StageChrome.tsx +403 -0
- package/src/client/attribution/ViewSwitch.tsx +107 -0
- package/src/client/attribution/graph-options.ts +381 -0
- package/src/client/attribution/model.ts +462 -0
- package/src/client/config-scope.ts +119 -0
- package/src/client/convert/ConvertPanel.tsx +594 -0
- package/src/client/convert/styles.css +138 -0
- package/src/client/convert/styles.ts +49 -0
- package/src/client/format.ts +39 -0
- package/src/client/graph/layout.ts +181 -0
- package/src/client/graph.css +113 -0
- package/src/client/index.ts +98 -0
- package/src/client/problem-domains.ts +114 -0
- package/src/client/styles.css +940 -0
- package/src/client/styles.ts +11 -0
- package/src/client/useFitScale.ts +68 -0
- package/src/config-schema.ts +45 -0
- package/src/convert/core-loader.ts +68 -0
- package/src/convert/host.ts +579 -0
- package/src/convert/ingest.ts +367 -0
- package/src/convert/settings-schema.ts +27 -0
- package/src/convert/types.ts +65 -0
- package/src/diagnosis/cards.ts +498 -0
- package/src/diagnosis/problem-classes.ts +117 -0
- package/src/diagnosis/schema.ts +260 -0
- package/src/diagnosis/seed-source.ts +107 -0
- package/src/diagnosis/seed.ts +291 -0
- package/src/diagnosis/store.ts +432 -0
- package/src/diagnosis/types.ts +152 -0
- package/src/directory-picker.ts +164 -0
- package/src/extract-model.ts +227 -0
- package/src/extract.ts +632 -0
- package/src/fts.ts +203 -0
- package/src/index.ts +1091 -0
- package/src/intent/README.md +63 -0
- package/src/intent/engine.ts +527 -0
- package/src/intent/host.ts +462 -0
- package/src/intent/preset-tools.ts +48 -0
- package/src/intent/settings-schema.ts +63 -0
- package/src/intent/store.ts +143 -0
- package/src/intent/tools.ts +139 -0
- package/src/intent/types.ts +245 -0
- package/src/knowledge-bases.ts +1160 -0
- package/src/layout.ts +350 -0
- package/src/migrate.ts +262 -0
- package/src/python/embedding_worker.py +471 -0
- package/src/recall-rank.ts +299 -0
- package/src/runtime.ts +3069 -0
- package/src/settings-schema.ts +94 -0
- package/src/settings-volatile.ts +122 -0
- package/src/types.ts +1140 -0
- package/src/wiki-types.ts +357 -0
package/src/fts.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 关键词路的索引(0031):**SQLite FTS5 + BM25**,中文靠我们自己的 CJK 分词喂进去。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是这个组合
|
|
5
|
+
*
|
|
6
|
+
* 0030 之前的关键词路是 `haystack.includes(token)` 的计数打分,而 `tokens()` 只按
|
|
7
|
+
* 非字母数字切分 —— 中文没有空格,于是一句自然语言问题被切**成一个 token**:
|
|
8
|
+
* 实测 `排查`→6 命中、`指标`→8、`口径`→10,但 `怎么排查指标异常`→**0**、
|
|
9
|
+
* `销售额如何计算`→**0**。产品的主查询形态就是整句中文,所以那条路等于不存在。
|
|
10
|
+
*
|
|
11
|
+
* 修它需要三样东西,而它们必须一起来:**中文能切**、**BM25 的词权重**、**零新服务**。
|
|
12
|
+
*
|
|
13
|
+
* - FTS5 自带 BM25(`bm25()`),是 SQLite 内建的,Node 22.5+ 的 `node:sqlite` 直接能用
|
|
14
|
+
* —— 不引入 `better-sqlite3` 这类原生模块,也不引入外部检索服务(0014 的非目标)。
|
|
15
|
+
* - FTS5 的两个内置 tokenizer 都不够用:`unicode61` 不切中文(`支付订单`→0 命中),
|
|
16
|
+
* `trigram` 要求查询至少 3 个字符(`排查`→0、`支付`→0),而且会把整句查询拆成
|
|
17
|
+
* **互相 AND 的三字窗口**,等于要求原文逐字包含整句。
|
|
18
|
+
* - 所以分词由我们做,**不用词典**:CJK 串切成 单字 + 相邻双字,ASCII 串整体小写成一个词。
|
|
19
|
+
* 查询走同一套切分、用 `OR` 连接、交给 BM25 排序。实测 `怎么排查指标异常` 能召回含
|
|
20
|
+
* `排查` 的文档,`排查`(2 字)也能召回 —— 这正是 trigram 做不到的两件事。
|
|
21
|
+
*
|
|
22
|
+
* ## 分数的量纲(重要)
|
|
23
|
+
*
|
|
24
|
+
* `bm25()` 是无界的、依赖语料的,所以本模块把它按**本次查询的最高分**归一化到 0..1。
|
|
25
|
+
* 这意味着 `keywordThreshold` 是**相对**阈值:它挡得住「弱命中」,挡不住「整批都弱」
|
|
26
|
+
* (只有弱命中时,最弱的那条也会被归一成 1.0)。真正的**绝对**闸门是向量路的余弦阈值
|
|
27
|
+
* 与重排阈值 —— 这一点在 `diagnostics.abstention` 里如实标注,不假装关键词阈值能判弃答。
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { mkdirSync, rmSync, existsSync } from 'node:fs'
|
|
33
|
+
import { dirname } from 'node:path'
|
|
34
|
+
|
|
35
|
+
/** `03_compiled/fts.sqlite`:关键词索引与向量索引同级,可重建、不进备份语义。 */
|
|
36
|
+
export const FTS_FILE = 'fts.sqlite'
|
|
37
|
+
|
|
38
|
+
/** 一条可被索引的召回单元。只取索引需要的字段,便于单测直接构造。 */
|
|
39
|
+
export interface FtsDocument {
|
|
40
|
+
unitId: string
|
|
41
|
+
title: string
|
|
42
|
+
content: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FtsHit {
|
|
46
|
+
unitId: string
|
|
47
|
+
/** 归一化到 0..1 的相关性(本次查询的最高分 = 1)。 */
|
|
48
|
+
score: number
|
|
49
|
+
/** 原始 `bm25()` 值(负数,越小越相关)。留给诊断与调参看。 */
|
|
50
|
+
raw: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SqliteStatement {
|
|
54
|
+
run(...params: unknown[]): unknown
|
|
55
|
+
all(...params: unknown[]): unknown[]
|
|
56
|
+
get(...params: unknown[]): unknown
|
|
57
|
+
}
|
|
58
|
+
interface SqliteDatabase {
|
|
59
|
+
exec(sql: string): void
|
|
60
|
+
prepare(sql: string): SqliteStatement
|
|
61
|
+
close(): void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `node:sqlite` 是否可用。
|
|
66
|
+
*
|
|
67
|
+
* 它是 Node 22.5+ 的实验特性,而这个仓库声明支持 `node >= 20`。不可用时**不报错**:
|
|
68
|
+
* 关键词路退化成 `degraded` 并在诊断里写明原因(`ftsUnavailable`),整条链路照常跑。
|
|
69
|
+
* 这比「装不上就起不来」诚实,也比「静默用回旧打分」安全。
|
|
70
|
+
*/
|
|
71
|
+
let sqliteModule: { DatabaseSync: new (path: string) => SqliteDatabase } | null | undefined
|
|
72
|
+
export async function loadSqlite(): Promise<{ DatabaseSync: new (path: string) => SqliteDatabase } | null> {
|
|
73
|
+
if (sqliteModule !== undefined) return sqliteModule
|
|
74
|
+
try {
|
|
75
|
+
sqliteModule = await import('node:sqlite') as unknown as { DatabaseSync: new (path: string) => SqliteDatabase }
|
|
76
|
+
} catch {
|
|
77
|
+
sqliteModule = null
|
|
78
|
+
}
|
|
79
|
+
return sqliteModule
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 供同步路径查询的「上一次探测结论」;`undefined` 表示还没探过。 */
|
|
83
|
+
export function sqliteProbed(): boolean | undefined {
|
|
84
|
+
return sqliteModule === undefined ? undefined : sqliteModule !== null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 分词用的切分模式。
|
|
89
|
+
*
|
|
90
|
+
* ASCII 只收 `[A-Za-z0-9]+`,**故意不把 `._:/-` 留在词里** —— 因为 FTS5 的
|
|
91
|
+
* `unicode61` 就是按它们切的(实测:索引里的 `document:ledger` 变成 `document` +
|
|
92
|
+
* `ledger`,`external_id` 变成 `external` + `id`)。这个函数有两个消费者:喂索引,
|
|
93
|
+
* 以及**在 JS 侧算查询词覆盖率**。两边切法不一致时,覆盖率会算错并悄悄把好命中丢掉
|
|
94
|
+
* (实测:查 `ledger` 时索引命中了、覆盖率却是 0,于是整条路 0 命中)。
|
|
95
|
+
*/
|
|
96
|
+
const CJK_RUN = /[\p{Script=Han}]+|[A-Za-z0-9]+/gu
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 把一段文本切成索引词。
|
|
100
|
+
*
|
|
101
|
+
* - CJK 串 → 每个字(单字)+ 每对相邻字(双字)。单字让 1 字查询也能命中,双字承担
|
|
102
|
+
* 真正的区分度;BM25 的 IDF 会把「的」「了」这类高频单字自动压下去。
|
|
103
|
+
* - ASCII / 数字串 → 小写,并按 `unicode61` 的口径切开(`external_id` → `external` + `id`)。
|
|
104
|
+
* 与索引保持一致比「保留整串」更重要:`T-1` 这类写法在索引里本来就是两个 token。
|
|
105
|
+
*/
|
|
106
|
+
export function segment(text: string): string[] {
|
|
107
|
+
const out: string[] = []
|
|
108
|
+
for (const match of text.matchAll(CJK_RUN)) {
|
|
109
|
+
const chunk = match[0]
|
|
110
|
+
if (/^\p{Script=Han}/u.test(chunk)) {
|
|
111
|
+
const chars = [...chunk]
|
|
112
|
+
for (const char of chars) out.push(char)
|
|
113
|
+
for (let index = 0; index + 1 < chars.length; index += 1) out.push(chars[index]! + chars[index + 1]!)
|
|
114
|
+
} else {
|
|
115
|
+
out.push(chunk.toLowerCase())
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 查询 → FTS5 的 MATCH 表达式。
|
|
123
|
+
*
|
|
124
|
+
* 词之间用 `OR`:召回要的是「命中任意一个词、由 BM25 排序」,而不是「逐字包含整句」。
|
|
125
|
+
* 每个词加双引号,于是 FTS5 的查询语法(`-` 被当成列过滤、`*` 被当成前缀)不会把
|
|
126
|
+
* 一个普通问题变成语法错误 —— `T-1` 就是实测踩到的例子。
|
|
127
|
+
*/
|
|
128
|
+
export function matchExpression(query: string): string | null {
|
|
129
|
+
const terms = [...new Set(segment(query))].filter(term => term.length > 0)
|
|
130
|
+
if (terms.length === 0) return null
|
|
131
|
+
return terms.map(term => `"${term.replace(/"/g, '""')}"`).join(' OR ')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 关键词索引。构造即打开;`null` 表示不可用(调用方降级)。 */
|
|
135
|
+
export class FtsIndex {
|
|
136
|
+
private constructor(private readonly db: SqliteDatabase) {}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 重建索引。**整体替换**:先写临时文件再改名,于是构建失败不会留下半个索引
|
|
140
|
+
* (读路径只认改名后的那个文件)。
|
|
141
|
+
*/
|
|
142
|
+
static async build(path: string, documents: FtsDocument[], DatabaseSync: new (path: string) => SqliteDatabase): Promise<void> {
|
|
143
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
144
|
+
const temporary = `${path}.tmp`
|
|
145
|
+
rmSync(temporary, { force: true })
|
|
146
|
+
const db = new DatabaseSync(temporary)
|
|
147
|
+
try {
|
|
148
|
+
db.exec('PRAGMA journal_mode = OFF')
|
|
149
|
+
db.exec("CREATE VIRTUAL TABLE notes USING fts5(unit_id UNINDEXED, tokens, tokenize='unicode61 remove_diacritics 2')")
|
|
150
|
+
const insert = db.prepare('INSERT INTO notes(unit_id, tokens) VALUES (?, ?)')
|
|
151
|
+
db.exec('BEGIN')
|
|
152
|
+
for (const document of documents) {
|
|
153
|
+
// 标题重复一次:FTS5 没有字段权重,重复等于给它加权(标题命中比正文命中更值钱)。
|
|
154
|
+
const tokens = [...segment(document.title), ...segment(document.title), ...segment(document.content)]
|
|
155
|
+
insert.run(document.unitId, tokens.join(' '))
|
|
156
|
+
}
|
|
157
|
+
db.exec('COMMIT')
|
|
158
|
+
db.exec('INSERT INTO notes(notes) VALUES (\'optimize\')')
|
|
159
|
+
} finally {
|
|
160
|
+
db.close()
|
|
161
|
+
}
|
|
162
|
+
rmSync(path, { force: true })
|
|
163
|
+
const { renameSync } = await import('node:fs')
|
|
164
|
+
renameSync(temporary, path)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 打开已存在的索引;不存在或打不开返回 `null`(调用方降级)。 */
|
|
168
|
+
static async open(path: string, DatabaseSync: new (path: string) => SqliteDatabase): Promise<FtsIndex | null> {
|
|
169
|
+
if (!existsSync(path)) return null
|
|
170
|
+
try {
|
|
171
|
+
return new FtsIndex(new DatabaseSync(path))
|
|
172
|
+
} catch {
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 一次 BM25 查询。分数按本次结果的最高分归一化(见模块头的「分数的量纲」)。
|
|
179
|
+
*/
|
|
180
|
+
search(query: string, limit: number): FtsHit[] {
|
|
181
|
+
const expression = matchExpression(query)
|
|
182
|
+
if (!expression) return []
|
|
183
|
+
let rows: Array<{ unit_id?: unknown; rank?: unknown }>
|
|
184
|
+
try {
|
|
185
|
+
rows = this.db.prepare('SELECT unit_id, bm25(notes) AS rank FROM notes WHERE notes MATCH ? ORDER BY rank LIMIT ?').all(expression, limit) as Array<{ unit_id?: unknown; rank?: unknown }>
|
|
186
|
+
} catch {
|
|
187
|
+
// 查询语法或索引损坏:如实返回空,由调用方的诊断说明这一路没结果。
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
const parsed = rows
|
|
191
|
+
.map(row => ({ unitId: String(row.unit_id ?? ''), raw: Number(row.rank ?? 0) }))
|
|
192
|
+
.filter(row => row.unitId !== '' && Number.isFinite(row.raw))
|
|
193
|
+
if (parsed.length === 0) return []
|
|
194
|
+
// `bm25()` 越小越相关,所以取负号变成「越大越好」,再按最高分归一。
|
|
195
|
+
const best = Math.max(...parsed.map(row => -row.raw))
|
|
196
|
+
const scale = best > 0 ? best : 1
|
|
197
|
+
return parsed.map(row => ({ unitId: row.unitId, raw: row.raw, score: Math.max(0, Math.min(1, -row.raw / scale)) }))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
close(): void {
|
|
201
|
+
try { this.db.close() } catch { /* already closed */ }
|
|
202
|
+
}
|
|
203
|
+
}
|