opencode-mem 2.11.8 → 2.11.10

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.
Files changed (35) hide show
  1. package/README.md +8 -12
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -0
  5. package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -1
  6. package/dist/services/ai/providers/anthropic-messages.js +1 -0
  7. package/dist/services/ai/providers/base-provider.d.ts +1 -0
  8. package/dist/services/ai/providers/base-provider.d.ts.map +1 -1
  9. package/dist/services/api-handlers.js +6 -6
  10. package/dist/services/cleanup-service.js +1 -1
  11. package/dist/services/client.js +1 -1
  12. package/dist/services/deduplication-service.js +1 -1
  13. package/dist/services/migration-service.js +3 -3
  14. package/dist/services/sqlite/shard-manager.d.ts +1 -1
  15. package/dist/services/sqlite/shard-manager.d.ts.map +1 -1
  16. package/dist/services/sqlite/shard-manager.js +12 -1
  17. package/dist/services/sqlite/vector-search.d.ts +8 -4
  18. package/dist/services/sqlite/vector-search.d.ts.map +1 -1
  19. package/dist/services/sqlite/vector-search.js +107 -44
  20. package/dist/services/vector-backends/backend-factory.d.ts +3 -0
  21. package/dist/services/vector-backends/backend-factory.d.ts.map +1 -0
  22. package/dist/services/vector-backends/backend-factory.js +104 -0
  23. package/dist/services/vector-backends/exact-scan-backend.d.ts +39 -0
  24. package/dist/services/vector-backends/exact-scan-backend.d.ts.map +1 -0
  25. package/dist/services/vector-backends/exact-scan-backend.js +63 -0
  26. package/dist/services/vector-backends/types.d.ts +51 -0
  27. package/dist/services/vector-backends/types.d.ts.map +1 -0
  28. package/dist/services/vector-backends/types.js +1 -0
  29. package/dist/services/vector-backends/usearch-backend.d.ts +47 -0
  30. package/dist/services/vector-backends/usearch-backend.d.ts.map +1 -0
  31. package/dist/services/vector-backends/usearch-backend.js +174 -0
  32. package/package.json +3 -3
  33. package/dist/services/sqlite/hnsw-index.d.ts +0 -37
  34. package/dist/services/sqlite/hnsw-index.d.ts.map +0 -1
  35. package/dist/services/sqlite/hnsw-index.js +0 -235
package/README.md CHANGED
@@ -20,25 +20,21 @@ A persistent memory system for AI coding agents that enables long-term context r
20
20
 
21
21
  ## Core Features
22
22
 
23
- Local vector database with SQLite + HNSW (hnswlib-wasm), persistent project memories, automatic user profile learning, unified memory-prompt timeline, full-featured web UI, intelligent prompt-based memory extraction, multi-provider AI support (OpenAI, Anthropic), 12+ local embedding models, smart deduplication, and built-in privacy protection.
23
+ Local vector database with SQLite + USearch-first vector indexing and ExactScan fallback, persistent project memories, automatic user profile learning, unified memory-prompt timeline, full-featured web UI, intelligent prompt-based memory extraction, multi-provider AI support (OpenAI, Anthropic), 12+ local embedding models, smart deduplication, and built-in privacy protection.
24
24
 
25
25
  ## Prerequisites
26
26
 
27
- This plugin uses `hnswlib-node` for fast vector similarity search, which requires native compilation. Ensure you have:
27
+ This plugin uses `USearch` for preferred in-memory vector indexing with automatic ExactScan fallback. No custom SQLite build or browser runtime shim is required.
28
28
 
29
- **All platforms:**
29
+ **Recommended runtime:**
30
30
 
31
- - Python 3.x
32
- - A C++ compiler (gcc, clang, or MSVC)
33
- - `make` or CMake
31
+ - Bun
32
+ - Standard OpenCode plugin environment
34
33
 
35
- **Platform-specific setup:**
34
+ **Notes:**
36
35
 
37
- | Platform | Requirements |
38
- | ----------- | ------------------------------------------------------------------------------------------------------------------------- |
39
- | **macOS** | Xcode Command Line Tools: `xcode-select --install` |
40
- | **Linux** | Build essentials: `sudo apt install build-essential python3` (Debian/Ubuntu) or `sudo pacman -S base-devel python` (Arch) |
41
- | **Windows** | Visual Studio Build Tools with C++ workload, or Windows Build Tools: `npm install -g windows-build-tools` |
36
+ - If `USearch` is unavailable or fails at runtime, the plugin automatically falls back to exact vector scanning.
37
+ - SQLite remains the source of truth; search indexes are rebuilt from SQLite data when needed.
42
38
 
43
39
  ## Getting Started
44
40
 
package/dist/config.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare const CONFIG: {
20
20
  memoryApiUrl: string | undefined;
21
21
  memoryApiKey: string | undefined;
22
22
  memoryTemperature: number | false | undefined;
23
+ vectorBackend: "usearch-first" | "usearch" | "exact-scan";
23
24
  aiSessionRetentionDays: number;
24
25
  webServerEnabled: boolean;
25
26
  webServerPort: number;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAiaA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;oBAwBb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwCT,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoaA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;oBAwBb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;mBAMX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoCV,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
package/dist/config.js CHANGED
@@ -27,6 +27,7 @@ const DEFAULTS = {
27
27
  autoCaptureEnabled: true,
28
28
  autoCaptureMaxIterations: 5,
29
29
  autoCaptureIterationTimeout: 30000,
30
+ vectorBackend: "usearch-first",
30
31
  aiSessionRetentionDays: 7,
31
32
  webServerEnabled: true,
32
33
  webServerPort: 4747,
@@ -347,6 +348,7 @@ export const CONFIG = {
347
348
  memoryApiUrl: fileConfig.memoryApiUrl,
348
349
  memoryApiKey: resolveSecretValue(fileConfig.memoryApiKey),
349
350
  memoryTemperature: fileConfig.memoryTemperature,
351
+ vectorBackend: (fileConfig.vectorBackend ?? "usearch-first"),
350
352
  aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
351
353
  webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
352
354
  webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../../../src/services/ai/providers/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AA4BvF,qBAAa,yBAA0B,SAAQ,cAAc;IAC3D,OAAO,CAAC,gBAAgB,CAAmB;gBAE/B,MAAM,EAAE,GAAG,EAAE,gBAAgB,EAAE,gBAAgB;IAK3D,eAAe,IAAI,MAAM;IAIzB,eAAe,IAAI,OAAO;IAIpB,eAAe,CACnB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,kBAAkB,EAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAmL1B,OAAO,CAAC,cAAc;CAavB"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../../../src/services/ai/providers/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACpE,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AA4BvF,qBAAa,yBAA0B,SAAQ,cAAc;IAC3D,OAAO,CAAC,gBAAgB,CAAmB;gBAE/B,MAAM,EAAE,GAAG,EAAE,gBAAgB,EAAE,gBAAgB;IAK3D,eAAe,IAAI,MAAM;IAIzB,eAAe,IAAI,OAAO;IAIpB,eAAe,CACnB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,kBAAkB,EAC9B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAoL1B,OAAO,CAAC,cAAc;CAavB"}
@@ -54,6 +54,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
54
54
  const tool = ToolSchemaConverter.toAnthropic(toolSchema);
55
55
  const requestBody = {
56
56
  model: this.config.model,
57
+ max_tokens: this.config.maxTokens ?? 4096,
57
58
  system: systemPrompt,
58
59
  messages,
59
60
  tools: [tool],
@@ -10,6 +10,7 @@ export interface ProviderConfig {
10
10
  apiKey?: string;
11
11
  maxIterations?: number;
12
12
  iterationTimeout?: number;
13
+ maxTokens?: number;
13
14
  memoryTemperature?: number | false;
14
15
  }
15
16
  export declare abstract class BaseAIProvider {
@@ -1 +1 @@
1
- {"version":3,"file":"base-provider.d.ts","sourceRoot":"","sources":["../../../../src/services/ai/providers/base-provider.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACpC;AAED,8BAAsB,cAAc;IAClC,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC;gBAErB,MAAM,EAAE,cAAc;IAIlC,QAAQ,CAAC,eAAe,CACtB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,GAAG,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAE1B,QAAQ,CAAC,eAAe,IAAI,MAAM;IAElC,QAAQ,CAAC,eAAe,IAAI,OAAO;CACpC"}
1
+ {"version":3,"file":"base-provider.d.ts","sourceRoot":"","sources":["../../../../src/services/ai/providers/base-provider.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;CACpC;AAED,8BAAsB,cAAc;IAClC,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC;gBAErB,MAAM,EAAE,cAAc;IAIlC,QAAQ,CAAC,eAAe,CACtB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,GAAG,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,CAAC;IAE1B,QAAQ,CAAC,eAAe,IAAI,MAAM;IAElC,QAAQ,CAAC,eAAe,IAAI,OAAO;CACpC"}
@@ -258,7 +258,7 @@ export async function handleAddMemory(data) {
258
258
  metadata: JSON.stringify({ source: "api" }),
259
259
  };
260
260
  const db = connectionManager.getConnection(shard.dbPath);
261
- vectorSearch.insertVector(db, record, shard);
261
+ await vectorSearch.insertVector(db, record, shard);
262
262
  shardManager.incrementVectorCount(shard.id);
263
263
  return { success: true, data: { id } };
264
264
  }
@@ -360,7 +360,7 @@ export async function handleUpdateMemory(id, data) {
360
360
  projectName: existingMemory.project_name,
361
361
  gitRepoUrl: existingMemory.git_repo_url,
362
362
  };
363
- vectorSearch.insertVector(db, updatedRecord, foundShard);
363
+ await vectorSearch.insertVector(db, updatedRecord, foundShard);
364
364
  shardManager.incrementVectorCount(foundShard.id);
365
365
  return { success: true };
366
366
  }
@@ -870,12 +870,12 @@ export async function handleRunTagMigrationBatch(batchSize = 5) {
870
870
  }
871
871
  }
872
872
  const vector = await embeddingService.embedWithTimeout(m.content);
873
+ const tagsVector = currentTags.length
874
+ ? await embeddingService.embedWithTimeout(currentTags.join(", "))
875
+ : undefined;
873
876
  const vectorBuffer = new Uint8Array(vector.buffer);
874
877
  db.prepare("UPDATE memories SET vector = ?, updated_at = ? WHERE id = ?").run(vectorBuffer, Date.now(), m.id);
875
- const index = vectorSearch
876
- .getIndexManager()
877
- .getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
878
- await index.insert(m.id, vector);
878
+ await vectorSearch.updateVector(db, m.id, vector, shard, tagsVector);
879
879
  migrationProgress.processed++;
880
880
  batchProcessed++;
881
881
  }
@@ -61,7 +61,7 @@ export class CleanupService {
61
61
  if (protectedMemoryIds.has(memory.id)) {
62
62
  continue;
63
63
  }
64
- vectorSearch.deleteVector(db, memory.id);
64
+ await vectorSearch.deleteVector(db, memory.id, shard);
65
65
  shardManager.decrementVectorCount(shard.id);
66
66
  totalDeleted++;
67
67
  if (memory.container_tag?.includes("_user_")) {
@@ -128,7 +128,7 @@ export class LocalMemoryClient {
128
128
  metadata: Object.keys(dynamicMetadata).length > 0 ? JSON.stringify(dynamicMetadata) : undefined,
129
129
  };
130
130
  const db = connectionManager.getConnection(shard.dbPath);
131
- vectorSearch.insertVector(db, record, shard);
131
+ await vectorSearch.insertVector(db, record, shard);
132
132
  shardManager.incrementVectorCount(shard.id);
133
133
  return { success: true, id };
134
134
  }
@@ -36,7 +36,7 @@ export class DeduplicationService {
36
36
  const toDelete = duplicates.slice(1);
37
37
  for (const dup of toDelete) {
38
38
  try {
39
- vectorSearch.deleteVector(db, dup.id);
39
+ await vectorSearch.deleteVector(db, dup.id, shard);
40
40
  shardManager.decrementVectorCount(shard.id);
41
41
  exactDeleted++;
42
42
  }
@@ -105,7 +105,7 @@ export class MigrationService {
105
105
  total: mismatch.shardMismatches.length,
106
106
  currentShard: String(shardInfo.shardId),
107
107
  });
108
- shardManager.deleteShard(shardInfo.shardId);
108
+ await shardManager.deleteShard(shardInfo.shardId);
109
109
  deletedShards++;
110
110
  }
111
111
  catch (error) {
@@ -168,7 +168,7 @@ export class MigrationService {
168
168
  isPinned: memory.is_pinned || 0,
169
169
  });
170
170
  }
171
- shardManager.deleteShard(shardInfo.shardId);
171
+ await shardManager.deleteShard(shardInfo.shardId);
172
172
  for (const memory of tempMemories) {
173
173
  try {
174
174
  const vector = await embeddingService.embedWithTimeout(memory.content);
@@ -176,7 +176,7 @@ export class MigrationService {
176
176
  const hash = memory.containerTag.split("_").slice(2).join("_");
177
177
  const newShard = shardManager.getWriteShard(scope, hash);
178
178
  const newDb = connectionManager.getConnection(newShard.dbPath);
179
- vectorSearch.insertVector(newDb, {
179
+ await vectorSearch.insertVector(newDb, {
180
180
  id: memory.id,
181
181
  content: memory.content,
182
182
  vector,
@@ -17,7 +17,7 @@ export declare class ShardManager {
17
17
  incrementVectorCount(shardId: number): void;
18
18
  decrementVectorCount(shardId: number): void;
19
19
  getShardByPath(dbPath: string): ShardInfo | null;
20
- deleteShard(shardId: number): void;
20
+ deleteShard(shardId: number): Promise<void>;
21
21
  }
22
22
  export declare const shardManager: ShardManager;
23
23
  //# sourceMappingURL=shard-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAO5C,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,YAAY,CAAS;;IAQ7B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,iBAAiB;IAKzB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAsB9E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IAgCvE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IA2BxF,OAAO,CAAC,WAAW;IA8CnB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,iBAAiB;IAYzB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;IA+BtE,OAAO,CAAC,iBAAiB;IAOzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAkBhD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAwBnC;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
1
+ {"version":3,"file":"shard-manager.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/shard-manager.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAO5C,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,YAAY,CAAS;;IAQ7B,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,iBAAiB;IAKzB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAsB9E,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE;IAgCvE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS;IA2BxF,OAAO,CAAC,WAAW;IA8CnB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,iBAAiB;IAYzB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS;IA+BtE,OAAO,CAAC,iBAAiB;IAOzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAkB1C,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkClD;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
4
4
  import { CONFIG } from "../../config.js";
5
5
  import { connectionManager } from "./connection-manager.js";
6
6
  import { log } from "../logger.js";
7
+ import { vectorSearch } from "./vector-search.js";
7
8
  const Database = getDatabase();
8
9
  const METADATA_DB_NAME = "metadata.db";
9
10
  export class ShardManager {
@@ -251,11 +252,21 @@ export class ShardManager {
251
252
  createdAt: row.created_at,
252
253
  };
253
254
  }
254
- deleteShard(shardId) {
255
+ async deleteShard(shardId) {
255
256
  const stmt = this.metadataDb.prepare(`SELECT * FROM shards WHERE id = ?`);
256
257
  const row = stmt.get(shardId);
257
258
  if (row) {
258
259
  const fullPath = this.resolveStoredPath(row.db_path, row.scope);
260
+ await vectorSearch.deleteShardIndexes({
261
+ id: row.id,
262
+ scope: row.scope,
263
+ scopeHash: row.scope_hash,
264
+ shardIndex: row.shard_index,
265
+ dbPath: fullPath,
266
+ vectorCount: row.vector_count,
267
+ isActive: row.is_active === 1,
268
+ createdAt: row.created_at,
269
+ });
259
270
  connectionManager.closeConnection(fullPath);
260
271
  try {
261
272
  const fs = require("node:fs");
@@ -1,9 +1,13 @@
1
- import { HNSWIndexManager } from "./hnsw-index.js";
2
1
  import type { MemoryRecord, SearchResult, ShardInfo } from "./types.js";
2
+ import type { VectorBackend } from "../vector-backends/types.js";
3
3
  declare const Database: typeof import("bun:sqlite").Database;
4
4
  type DatabaseType = typeof Database.prototype;
5
5
  export declare class VectorSearch {
6
- insertVector(db: DatabaseType, record: MemoryRecord, shard?: ShardInfo): void;
6
+ private readonly backendPromise;
7
+ private readonly fallbackBackend;
8
+ constructor(backend?: VectorBackend, fallbackBackend?: VectorBackend);
9
+ private getBackend;
10
+ insertVector(db: DatabaseType, record: MemoryRecord, shard?: ShardInfo): Promise<void>;
7
11
  searchInShard(shard: ShardInfo, queryVector: Float32Array, containerTag: string, limit: number, queryText?: string): Promise<SearchResult[]>;
8
12
  searchAcrossShards(shards: ShardInfo[], queryVector: Float32Array, containerTag: string, limit: number, similarityThreshold: number, queryText?: string): Promise<SearchResult[]>;
9
13
  deleteVector(db: DatabaseType, memoryId: string, shard?: ShardInfo): Promise<void>;
@@ -17,8 +21,8 @@ export declare class VectorSearch {
17
21
  getDistinctTags(db: DatabaseType): any[];
18
22
  pinMemory(db: DatabaseType, memoryId: string): void;
19
23
  unpinMemory(db: DatabaseType, memoryId: string): void;
20
- rebuildHNSWIndex(db: DatabaseType, scope: string, scopeHash: string, shardIndex: number): Promise<void>;
21
- getIndexManager(): HNSWIndexManager;
24
+ rebuildIndexForShard(db: DatabaseType, scope: string, scopeHash: string, shardIndex: number): Promise<void>;
25
+ deleteShardIndexes(shard: ShardInfo): Promise<void>;
22
26
  }
23
27
  export declare const vectorSearch: VectorSearch;
24
28
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAI9C,qBAAa,YAAY;IACvB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,IAAI;IAqDvE,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAmFpB,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,EAC3B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlF,YAAY,CAChB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,IAAI,CAAC;IA4BhB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAW1E,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAKvC,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAK7D,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAgBlE,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAM5D,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM;IAMzC,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAexC,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,gBAAgB,CACpB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAIhB,eAAe,IAAI,gBAAgB;CAGpC;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
1
+ {"version":3,"file":"vector-search.d.ts","sourceRoot":"","sources":["../../../src/services/sqlite/vector-search.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,QAAA,MAAM,QAAQ,sCAAgB,CAAC;AAC/B,KAAK,YAAY,GAAG,OAAO,QAAQ,CAAC,SAAS,CAAC;AAM9C,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgB;gBAEpC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,GAAE,aAAsC;YAO9E,UAAU;IAIlB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCtF,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAyHpB,kBAAkB,CACtB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,YAAY,EACzB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,mBAAmB,EAAE,MAAM,EAC3B,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBpB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAUlF,YAAY,CAChB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,EACpB,KAAK,CAAC,EAAE,SAAS,EACjB,UAAU,CAAC,EAAE,YAAY,GACxB,OAAO,CAAC,IAAI,CAAC;IAkBhB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE;IAW1E,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAKvC,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI;IAK7D,sBAAsB,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,GAAG,GAAG,EAAE;IAgBlE,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM;IAM5D,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,MAAM;IAMzC,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,GAAG,EAAE;IAexC,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKnD,WAAW,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK/C,oBAAoB,CACxB,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC;IAgBV,kBAAkB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;CAI1D;AAED,eAAO,MAAM,YAAY,cAAqB,CAAC"}
@@ -1,45 +1,93 @@
1
1
  import { getDatabase } from "./sqlite-bootstrap.js";
2
2
  import { connectionManager } from "./connection-manager.js";
3
- import { HNSWIndexManager } from "./hnsw-index.js";
4
3
  import { log } from "../logger.js";
5
4
  import { CONFIG } from "../../config.js";
5
+ import { createVectorBackend } from "../vector-backends/backend-factory.js";
6
+ import { ExactScanBackend } from "../vector-backends/exact-scan-backend.js";
6
7
  const Database = getDatabase();
7
- const hnswIndexManager = new HNSWIndexManager(CONFIG.storagePath);
8
+ function toBlob(vector) {
9
+ return vector ? new Uint8Array(vector.buffer) : null;
10
+ }
8
11
  export class VectorSearch {
9
- insertVector(db, record, shard) {
12
+ backendPromise;
13
+ fallbackBackend;
14
+ constructor(backend, fallbackBackend = new ExactScanBackend()) {
15
+ this.backendPromise = backend
16
+ ? Promise.resolve(backend)
17
+ : createVectorBackend({ vectorBackend: CONFIG.vectorBackend });
18
+ this.fallbackBackend = fallbackBackend;
19
+ }
20
+ async getBackend() {
21
+ return this.backendPromise;
22
+ }
23
+ async insertVector(db, record, shard) {
10
24
  const insertMemory = db.prepare(`
11
25
  INSERT INTO memories (
12
26
  id, content, vector, tags_vector, container_tag, tags, type, created_at, updated_at,
13
27
  metadata, display_name, user_name, user_email, project_path, project_name, git_repo_url
14
28
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
15
29
  `);
16
- const vectorBuffer = new Uint8Array(record.vector.buffer);
17
- const tagsVectorBuffer = record.tagsVector ? new Uint8Array(record.tagsVector.buffer) : null;
18
- insertMemory.run(record.id, record.content, vectorBuffer, tagsVectorBuffer, record.containerTag, record.tags || null, record.type || null, record.createdAt, record.updatedAt, record.metadata || null, record.displayName || null, record.userName || null, record.userEmail || null, record.projectPath || null, record.projectName || null, record.gitRepoUrl || null);
19
- if (shard && record.vector) {
20
- const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
21
- contentIndex.insert(record.id, record.vector).catch((err) => {
22
- log("HNSW content insert error", { memoryId: record.id, error: String(err) });
23
- });
24
- if (record.tagsVector) {
25
- const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
26
- tagsIndex.insert(record.id, record.tagsVector).catch((err) => {
27
- log("HNSW tags insert error", { memoryId: record.id, error: String(err) });
28
- });
30
+ insertMemory.run(record.id, record.content, toBlob(record.vector), toBlob(record.tagsVector), record.containerTag, record.tags || null, record.type || null, record.createdAt, record.updatedAt, record.metadata || null, record.displayName || null, record.userName || null, record.userEmail || null, record.projectPath || null, record.projectName || null, record.gitRepoUrl || null);
31
+ try {
32
+ if (shard) {
33
+ const backend = await this.getBackend();
34
+ await backend.insert({ id: record.id, vector: record.vector, shard, kind: "content" });
35
+ if (record.tagsVector) {
36
+ await backend.insert({ id: record.id, vector: record.tagsVector, shard, kind: "tags" });
37
+ }
29
38
  }
30
39
  }
40
+ catch (error) {
41
+ db.prepare(`DELETE FROM memories WHERE id = ?`).run(record.id);
42
+ throw error;
43
+ }
31
44
  }
32
45
  async searchInShard(shard, queryVector, containerTag, limit, queryText) {
33
46
  const db = connectionManager.getConnection(shard.dbPath);
34
- const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
35
- const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
36
- // HNSW indexes are in-memory only (hnswlib-wasm has no real FS bridging).
37
- // Auto-rebuild from SQLite vectors if indexes are empty after process restart.
38
- if (!contentIndex.isPopulated()) {
39
- await hnswIndexManager.rebuildFromShard(db, shard.scope, shard.scopeHash, shard.shardIndex);
47
+ const backend = await this.getBackend();
48
+ let contentResults;
49
+ let tagsResults;
50
+ try {
51
+ await backend.rebuildFromShard({ db, shard, kind: "content" });
52
+ await backend.rebuildFromShard({ db, shard, kind: "tags" });
53
+ contentResults = await backend.search({
54
+ db,
55
+ shard,
56
+ kind: "content",
57
+ queryVector,
58
+ limit: limit * 4,
59
+ });
60
+ tagsResults = await backend.search({
61
+ db,
62
+ shard,
63
+ kind: "tags",
64
+ queryVector,
65
+ limit: limit * 4,
66
+ });
67
+ }
68
+ catch (error) {
69
+ log("Vector search degraded to exact scan in shard", {
70
+ shardId: shard.id,
71
+ backend: backend.getBackendName(),
72
+ error: String(error),
73
+ });
74
+ await this.fallbackBackend.rebuildFromShard({ db, shard, kind: "content" });
75
+ await this.fallbackBackend.rebuildFromShard({ db, shard, kind: "tags" });
76
+ contentResults = await this.fallbackBackend.search({
77
+ db,
78
+ shard,
79
+ kind: "content",
80
+ queryVector,
81
+ limit: limit * 4,
82
+ });
83
+ tagsResults = await this.fallbackBackend.search({
84
+ db,
85
+ shard,
86
+ kind: "tags",
87
+ queryVector,
88
+ limit: limit * 4,
89
+ });
40
90
  }
41
- const contentResults = await contentIndex.search(queryVector, limit * 4);
42
- const tagsResults = await tagsIndex.search(queryVector, limit * 4);
43
91
  const scoreMap = new Map();
44
92
  for (const r of contentResults) {
45
93
  scoreMap.set(r.id, { contentSim: 1 - r.distance, tagsSim: 0 });
@@ -59,7 +107,7 @@ export class VectorSearch {
59
107
  const placeholders = ids.map(() => "?").join(",");
60
108
  const rows = db
61
109
  .prepare(`
62
- SELECT * FROM memories
110
+ SELECT * FROM memories
63
111
  WHERE id IN (${placeholders}) AND container_tag = ?
64
112
  `)
65
113
  .all(...ids, containerTag);
@@ -69,7 +117,7 @@ export class VectorSearch {
69
117
  .split(/[\s,]+/)
70
118
  .filter((w) => w.length > 1)
71
119
  : [];
72
- return rows.map((row) => {
120
+ const hydratedResults = rows.map((row) => {
73
121
  const scores = scoreMap.get(row.id);
74
122
  const memoryTagsStr = row.tags || "";
75
123
  const memoryTags = memoryTagsStr.split(",").map((t) => t.trim().toLowerCase());
@@ -96,6 +144,8 @@ export class VectorSearch {
96
144
  isPinned: row.is_pinned,
97
145
  };
98
146
  });
147
+ hydratedResults.sort((a, b) => b.similarity - a.similarity);
148
+ return hydratedResults;
99
149
  }
100
150
  async searchAcrossShards(shards, queryVector, containerTag, limit, similarityThreshold, queryText) {
101
151
  const shardPromises = shards.map(async (shard) => {
@@ -115,27 +165,27 @@ export class VectorSearch {
115
165
  async deleteVector(db, memoryId, shard) {
116
166
  db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
117
167
  if (shard) {
118
- const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
119
- const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
120
- await Promise.all([contentIndex.delete(memoryId), tagsIndex.delete(memoryId)]);
168
+ const backend = await this.getBackend();
169
+ await backend.delete({ id: memoryId, shard, kind: "content" });
170
+ await backend.delete({ id: memoryId, shard, kind: "tags" });
121
171
  }
122
172
  }
123
173
  async updateVector(db, memoryId, vector, shard, tagsVector) {
124
- const vectorBuffer = new Uint8Array(vector.buffer);
125
- const tagsVectorBuffer = tagsVector ? new Uint8Array(tagsVector.buffer) : null;
126
- db.prepare(`UPDATE memories SET vector = ?, tags_vector = ? WHERE id = ?`).run(vectorBuffer, tagsVectorBuffer, memoryId);
127
- if (shard && vector) {
128
- const contentIndex = hnswIndexManager.getIndex(shard.scope, shard.scopeHash, shard.shardIndex);
129
- await contentIndex.insert(memoryId, vector);
174
+ db.prepare(`UPDATE memories SET vector = ?, tags_vector = ? WHERE id = ?`).run(toBlob(vector), toBlob(tagsVector), memoryId);
175
+ if (shard) {
176
+ const backend = await this.getBackend();
177
+ await backend.insert({ id: memoryId, vector, shard, kind: "content" });
130
178
  if (tagsVector) {
131
- const tagsIndex = hnswIndexManager.getTagsIndex(shard.scope, shard.scopeHash, shard.shardIndex);
132
- await tagsIndex.insert(memoryId, tagsVector);
179
+ await backend.insert({ id: memoryId, vector: tagsVector, shard, kind: "tags" });
180
+ }
181
+ else {
182
+ await backend.delete({ id: memoryId, shard, kind: "tags" });
133
183
  }
134
184
  }
135
185
  }
136
186
  listMemories(db, containerTag, limit) {
137
187
  const stmt = db.prepare(`
138
- SELECT * FROM memories
188
+ SELECT * FROM memories
139
189
  WHERE container_tag = ?
140
190
  ORDER BY created_at DESC
141
191
  LIMIT ?
@@ -152,7 +202,7 @@ export class VectorSearch {
152
202
  }
153
203
  getMemoriesBySessionID(db, sessionID) {
154
204
  const stmt = db.prepare(`
155
- SELECT * FROM memories
205
+ SELECT * FROM memories
156
206
  WHERE metadata LIKE ?
157
207
  ORDER BY created_at DESC
158
208
  `);
@@ -175,7 +225,7 @@ export class VectorSearch {
175
225
  }
176
226
  getDistinctTags(db) {
177
227
  const stmt = db.prepare(`
178
- SELECT DISTINCT
228
+ SELECT DISTINCT
179
229
  container_tag,
180
230
  display_name,
181
231
  user_name,
@@ -195,11 +245,24 @@ export class VectorSearch {
195
245
  const stmt = db.prepare(`UPDATE memories SET is_pinned = 0 WHERE id = ?`);
196
246
  stmt.run(memoryId);
197
247
  }
198
- async rebuildHNSWIndex(db, scope, scopeHash, shardIndex) {
199
- await hnswIndexManager.rebuildFromShard(db, scope, scopeHash, shardIndex);
248
+ async rebuildIndexForShard(db, scope, scopeHash, shardIndex) {
249
+ const backend = await this.getBackend();
250
+ const shard = {
251
+ id: 0,
252
+ scope: scope,
253
+ scopeHash,
254
+ shardIndex,
255
+ dbPath: "",
256
+ vectorCount: 0,
257
+ isActive: true,
258
+ createdAt: Date.now(),
259
+ };
260
+ await backend.rebuildFromShard({ db, shard, kind: "content" });
261
+ await backend.rebuildFromShard({ db, shard, kind: "tags" });
200
262
  }
201
- getIndexManager() {
202
- return hnswIndexManager;
263
+ async deleteShardIndexes(shard) {
264
+ const backend = await this.getBackend();
265
+ await backend.deleteShardIndexes({ shard });
203
266
  }
204
267
  }
205
268
  export const vectorSearch = new VectorSearch();
@@ -0,0 +1,3 @@
1
+ import type { VectorBackend, VectorBackendFactoryOptions } from "./types.js";
2
+ export declare function createVectorBackend(options: VectorBackendFactoryOptions): Promise<VectorBackend>;
3
+ //# sourceMappingURL=backend-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-factory.d.ts","sourceRoot":"","sources":["../../../src/services/vector-backends/backend-factory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AA4E7E,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,aAAa,CAAC,CAsCxB"}
@@ -0,0 +1,104 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { log } from "../logger.js";
3
+ import { ExactScanBackend } from "./exact-scan-backend.js";
4
+ import { USearchBackend } from "./usearch-backend.js";
5
+ class FallbackAwareBackend {
6
+ strategy;
7
+ primary;
8
+ fallback;
9
+ activeBackend;
10
+ constructor(strategy, primary, fallback) {
11
+ this.strategy = strategy;
12
+ this.primary = primary;
13
+ this.fallback = fallback;
14
+ this.activeBackend = primary;
15
+ }
16
+ getBackendName() {
17
+ return this.activeBackend.getBackendName();
18
+ }
19
+ async insert(args) {
20
+ await this.activeBackend.insert(args);
21
+ }
22
+ async insertBatch(args) {
23
+ await this.activeBackend.insertBatch(args);
24
+ }
25
+ async delete(args) {
26
+ await this.activeBackend.delete(args);
27
+ }
28
+ async search(args) {
29
+ try {
30
+ return await this.activeBackend.search(args);
31
+ }
32
+ catch (error) {
33
+ this.logDegrade("search", error);
34
+ this.activeBackend = this.fallback;
35
+ return this.fallback.search(args);
36
+ }
37
+ }
38
+ async rebuildFromShard(args) {
39
+ try {
40
+ await this.activeBackend.rebuildFromShard(args);
41
+ }
42
+ catch (error) {
43
+ this.logDegrade("rebuild", error);
44
+ this.activeBackend = this.fallback;
45
+ await this.fallback.rebuildFromShard(args);
46
+ }
47
+ }
48
+ async deleteShardIndexes(args) {
49
+ await this.primary.deleteShardIndexes(args);
50
+ await this.fallback.deleteShardIndexes(args);
51
+ }
52
+ logDegrade(operation, error) {
53
+ log("Vector backend degraded to exact-scan", {
54
+ strategy: this.strategy,
55
+ severity: this.strategy === "usearch" ? "warning" : "info",
56
+ operation,
57
+ error: String(error),
58
+ });
59
+ }
60
+ }
61
+ async function defaultUSearchProbe() {
62
+ try {
63
+ await import("usearch");
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ export async function createVectorBackend(options) {
71
+ const exactScanBackend = new ExactScanBackend();
72
+ if (options.vectorBackend === "exact-scan") {
73
+ return exactScanBackend;
74
+ }
75
+ const probeUSearch = options.probeUSearch ?? defaultUSearchProbe;
76
+ if (!(await probeUSearch())) {
77
+ if (options.vectorBackend === "usearch") {
78
+ log("Vector backend degraded to exact-scan", {
79
+ strategy: "usearch",
80
+ severity: "warning",
81
+ operation: "probe",
82
+ error: "USearch unavailable",
83
+ });
84
+ }
85
+ return exactScanBackend;
86
+ }
87
+ try {
88
+ const usearchBackend = options.createUSearchBackend?.() ??
89
+ new USearchBackend({
90
+ baseDir: CONFIG.storagePath,
91
+ dimensions: CONFIG.embeddingDimensions,
92
+ });
93
+ return new FallbackAwareBackend(options.vectorBackend, usearchBackend, exactScanBackend);
94
+ }
95
+ catch (error) {
96
+ log("Vector backend degraded to exact-scan", {
97
+ strategy: options.vectorBackend,
98
+ severity: options.vectorBackend === "usearch" ? "warning" : "info",
99
+ operation: "create",
100
+ error: String(error),
101
+ });
102
+ return exactScanBackend;
103
+ }
104
+ }