@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 (${terms.map(() => '(LOWER(search_content) LIKE ? OR LOWER(content) LIKE ? OR LOWER(relative_path) LIKE ? OR LOWER(category) LIKE ? OR LOWER(format) LIKE ? OR LOWER(COALESCE(title_path, \'\')) LIKE ? OR LOWER(COALESCE(chunk_kind, \'\')) LIKE ?)').join(' OR ')})${this.filePathFilterClause(filePaths)}
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.exec(`
737
- CREATE VIRTUAL TABLE IF NOT EXISTS kb_chunks_fts USING fts5(
738
- id UNINDEXED,
739
- relative_path,
740
- category,
741
- format,
742
- section_title,
743
- title_path,
744
- chunk_kind,
745
- content,
746
- tokenize = 'unicode61 remove_diacritics 2'
747
- );
748
- `);
749
- this.ftsEnabled = true;
750
- this.rebuildFtsIfNeeded();
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
- const normalized = terms.map(term => term.replace(/["*^:(){}\]\\[]/gu, ' ').trim()).filter(term => term.length > 0);
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 >= 2).slice(0, 12);
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
- if (fs.existsSync(this.indexPath))
29
+ const indexExists = fs.existsSync(this.indexPath);
30
+ if (indexExists) {
30
31
  this.index.readIndexSync(this.indexPath, true);
31
- else
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customize-agent/knowledge",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "Local knowledge base infrastructure for customize-agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",