@customize-agent/knowledge 4.1.0 → 4.1.2
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.
|
@@ -180,7 +180,7 @@ export class IndexStateStore {
|
|
|
180
180
|
const rowRange = this.metadataString(chunk.metadata.rowRange) ?? null;
|
|
181
181
|
const searchContent = this.buildChunkSearchContent(relativePath, file, chunk, titlePath);
|
|
182
182
|
insert.run(chunkId, relativePath, chunk.index, chunk.text, searchContent, file.category, file.format, file.collectionName, chunk.tokenCount, chunk.sectionTitle ?? null, titlePath || null, parentId, chunkKind, rowRange, Number(chunk.metadata.startChar ?? chunk.startChar), Number(chunk.metadata.endChar ?? chunk.endChar), JSON.stringify(chunk.metadata), now);
|
|
183
|
-
insertFts?.run(chunkId, relativePath, file.category, file.format, chunk.sectionTitle ?? '', titlePath, chunkKind ?? '', searchContent);
|
|
183
|
+
insertFts?.run(chunkId, relativePath, file.category, file.format, chunk.sectionTitle ?? '', titlePath, chunkKind ?? '', `${searchContent} ${chunk.text}`);
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
186
|
transaction();
|
|
@@ -357,12 +357,20 @@ export class IndexStateStore {
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
searchChunksLike(terms, limit, filePaths) {
|
|
360
|
+
// 兜底拆两组:小列(路径/分类/标题等,行均几十字节)对所有词全表 LIKE,成本毫秒级;
|
|
361
|
+
// 大列(search_content/content)仅对 <3 字符短词扫描——trigram FTS 无法索引短词
|
|
362
|
+
// (中文 2 字词如“验收”),3+ 字符词已由 FTS 覆盖,不再全表扫大列
|
|
363
|
+
// (历史缺陷:40 term × 7 列全表扫描阻塞 Node 事件循环数分钟)
|
|
364
|
+
const shortTerms = [...new Set(terms.filter(term => term.length < 3))].slice(0, 6);
|
|
365
|
+
const smallColumns = terms.map(() => '(LOWER(relative_path) LIKE ? OR LOWER(category) LIKE ? OR LOWER(format) LIKE ? OR LOWER(COALESCE(title_path, \'\')) LIKE ? OR LOWER(COALESCE(chunk_kind, \'\')) LIKE ? OR LOWER(COALESCE(section_title, \'\')) LIKE ?)').join(' OR ');
|
|
366
|
+
const largeColumns = shortTerms.map(() => '(LOWER(search_content) LIKE ? OR LOWER(content) LIKE ?)').join(' OR ');
|
|
367
|
+
const condition = [smallColumns, largeColumns].filter(Boolean).join(' OR ');
|
|
360
368
|
const rows = this.db.prepare(`
|
|
361
369
|
SELECT rowid, * FROM kb_chunks
|
|
362
|
-
WHERE (${
|
|
370
|
+
WHERE (${condition})${this.filePathFilterClause(filePaths)}
|
|
363
371
|
ORDER BY created_at DESC
|
|
364
372
|
LIMIT ?
|
|
365
|
-
`).all(...terms.flatMap(term => [`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`]), ...filePaths, limit * 6);
|
|
373
|
+
`).all(...terms.flatMap(term => [`%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`, `%${term}%`]), ...shortTerms.flatMap(term => [`%${term}%`, `%${term}%`]), ...filePaths, limit * 6);
|
|
366
374
|
return rows
|
|
367
375
|
.map(row => {
|
|
368
376
|
const keyword = this.scoreChunkDetailed(this.searchableRowText(row), terms);
|
|
@@ -733,21 +741,48 @@ export class IndexStateStore {
|
|
|
733
741
|
}
|
|
734
742
|
initFts() {
|
|
735
743
|
try {
|
|
736
|
-
this.db.
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
744
|
+
const existing = this.db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'kb_chunks_fts'").get();
|
|
745
|
+
// unicode61 分词下中文子串(如“混凝土”)几乎无法命中 FTS,检索频繁落入
|
|
746
|
+
// LOWER(x) LIKE '%term%' 全表兜底(最多 40 term × 7 列),在大库上同步扫描会
|
|
747
|
+
// 阻塞 Node 事件循环数分钟(页面表现为转圈)。trigram 可对任意 >=3 字符子串建索引。
|
|
748
|
+
if (existing?.sql && !/tokenize\s*=\s*'trigram/u.test(existing.sql)) {
|
|
749
|
+
this.db.exec('DROP TABLE IF EXISTS kb_chunks_fts');
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
this.db.exec(`
|
|
753
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS kb_chunks_fts USING fts5(
|
|
754
|
+
id UNINDEXED,
|
|
755
|
+
relative_path,
|
|
756
|
+
category,
|
|
757
|
+
format,
|
|
758
|
+
section_title,
|
|
759
|
+
title_path,
|
|
760
|
+
chunk_kind,
|
|
761
|
+
content,
|
|
762
|
+
tokenize = 'trigram case_sensitive 0'
|
|
763
|
+
);
|
|
764
|
+
`);
|
|
765
|
+
this.ftsEnabled = true;
|
|
766
|
+
this.rebuildFtsIfNeeded();
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
// 旧版 SQLite 无 trigram tokenizer 时回退 unicode61,保持原有行为
|
|
770
|
+
this.db.exec(`
|
|
771
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS kb_chunks_fts USING fts5(
|
|
772
|
+
id UNINDEXED,
|
|
773
|
+
relative_path,
|
|
774
|
+
category,
|
|
775
|
+
format,
|
|
776
|
+
section_title,
|
|
777
|
+
title_path,
|
|
778
|
+
chunk_kind,
|
|
779
|
+
content,
|
|
780
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
781
|
+
);
|
|
782
|
+
`);
|
|
783
|
+
this.ftsEnabled = true;
|
|
784
|
+
this.rebuildFtsIfNeeded();
|
|
785
|
+
}
|
|
751
786
|
}
|
|
752
787
|
catch {
|
|
753
788
|
this.ftsEnabled = false;
|
|
@@ -759,9 +794,11 @@ export class IndexStateStore {
|
|
|
759
794
|
const row = this.db.prepare('SELECT COUNT(*) as count FROM kb_chunks_fts').get();
|
|
760
795
|
if (Number(row.count ?? 0) > 0)
|
|
761
796
|
return;
|
|
797
|
+
// content 列同时灌入 search_content 与正文:trigram 需要索引正文才能对正文内
|
|
798
|
+
// 中文子串召回,避免检索再次落入 LIKE 全表兜底
|
|
762
799
|
this.db.prepare(`
|
|
763
800
|
INSERT INTO kb_chunks_fts (id, relative_path, category, format, section_title, title_path, chunk_kind, content)
|
|
764
|
-
SELECT id, relative_path, category, format, COALESCE(section_title, ''), COALESCE(title_path, ''), COALESCE(chunk_kind, ''), COALESCE(search_content, content) FROM kb_chunks
|
|
801
|
+
SELECT id, relative_path, category, format, COALESCE(section_title, ''), COALESCE(title_path, ''), COALESCE(chunk_kind, ''), TRIM(COALESCE(search_content, '') || ' ' || COALESCE(content, '')) FROM kb_chunks
|
|
765
802
|
`).run();
|
|
766
803
|
}
|
|
767
804
|
rowToMinHash(row) {
|
|
@@ -924,9 +961,10 @@ export class IndexStateStore {
|
|
|
924
961
|
return [...terms].filter(term => term.length > 0).slice(0, 40);
|
|
925
962
|
}
|
|
926
963
|
toFtsQuery(terms) {
|
|
927
|
-
|
|
964
|
+
// trigram tokenizer 只支持 >=3 字符的查询 token,短词由 LIKE 小列兜底补齐
|
|
965
|
+
const normalized = terms.map(term => term.replace(/["*^:(){}\]\\[]/gu, ' ').trim()).filter(term => term.length >= 3);
|
|
928
966
|
const exact = normalized[0];
|
|
929
|
-
const weak = normalized.slice(1).filter(term => term.length >=
|
|
967
|
+
const weak = normalized.slice(1).filter(term => term.length >= 3).slice(0, 12);
|
|
930
968
|
return [exact ? `"${exact}"` : '', ...weak.map(term => `"${term}"`)].filter(Boolean).join(' OR ');
|
|
931
969
|
}
|
|
932
970
|
mergeKeywordResults(results, limit) {
|
|
@@ -26,10 +26,24 @@ export class HNSWVectorStore {
|
|
|
26
26
|
this.loadDocuments();
|
|
27
27
|
const mod = require('hnswlib-node');
|
|
28
28
|
this.index = new mod.HierarchicalNSW('cosine', this.dimensions);
|
|
29
|
-
|
|
29
|
+
const indexExists = fs.existsSync(this.indexPath);
|
|
30
|
+
if (indexExists) {
|
|
30
31
|
this.index.readIndexSync(this.indexPath, true);
|
|
31
|
-
|
|
32
|
+
// 空壳索引防护:hnswlib v3.0.0 的 loadIndex 对空索引(cur_element_count=0)会把
|
|
33
|
+
// max_elements_ 覆盖为 0,此后任何 addPoint 都抛 "The number of elements exceeds
|
|
34
|
+
// the specified limit"(真实生成中向量化中断于空壳 kb_other 索引,状态永久 error)
|
|
35
|
+
if (this.index.getMaxElements() < 1 || this.index.getMaxElements() < this.index.getCurrentCount()) {
|
|
36
|
+
this.index.initIndex(this.maxElements, 16, 200, 100, true);
|
|
37
|
+
}
|
|
38
|
+
// 旧索引防护:documents sidecar 丢失/为空但索引文件仍有数据时,旧 label 与当前 sqlite
|
|
39
|
+
// rowid 脱节,语义检索按 label 回查 documents 全部落空(召回静默为零)→ 重建索引
|
|
40
|
+
if (this.documents.size === 0 && this.index.getCurrentCount() > 0) {
|
|
41
|
+
this.index.initIndex(this.maxElements, 16, 200, 100, true);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
32
45
|
this.index.initIndex(this.maxElements, 16, 200, 100, true);
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
48
|
async upsert(documents, options = {}) {
|
|
35
49
|
await this.ensureCollection();
|