rag-lite-ts 2.0.0 → 2.0.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/README.md +0 -1
- package/dist/core/binary-index-format.d.ts +52 -0
- package/dist/core/binary-index-format.js +122 -0
- package/dist/core/vector-index.d.ts +1 -1
- package/dist/core/vector-index.js +31 -32
- package/dist/factories/index.d.ts +2 -0
- package/dist/factories/index.js +2 -0
- package/dist/factories/polymorphic-factory.d.ts +50 -0
- package/dist/factories/polymorphic-factory.js +159 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +18 -0
- package/dist/multimodal/clip-embedder.d.ts +18 -5
- package/dist/multimodal/clip-embedder.js +62 -15
- package/dist/search.d.ts +34 -9
- package/dist/search.js +28 -10
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -433,7 +433,6 @@ Now Claude can search your docs directly! Works with any MCP-compatible AI tool.
|
|
|
433
433
|
- **Content management** - Deduplication, cleanup
|
|
434
434
|
- **Model compatibility** - Auto-detection, rebuilds
|
|
435
435
|
- **Error recovery** - Clear messages, helpful hints
|
|
436
|
-
- **Battle-tested** - Used in real applications
|
|
437
436
|
|
|
438
437
|
</td>
|
|
439
438
|
</tr>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Index Format Module
|
|
3
|
+
*
|
|
4
|
+
* Provides efficient binary serialization for HNSW vector indices.
|
|
5
|
+
*
|
|
6
|
+
* Format Specification:
|
|
7
|
+
* - Header: 24 bytes (6 × uint32)
|
|
8
|
+
* - Vectors: N × (4 + D × 4) bytes
|
|
9
|
+
* - Little-endian encoding for cross-platform compatibility
|
|
10
|
+
* - 4-byte alignment for Float32Array zero-copy views
|
|
11
|
+
*
|
|
12
|
+
* Performance:
|
|
13
|
+
* - 3.66x smaller than JSON format
|
|
14
|
+
* - 3.5x faster loading
|
|
15
|
+
* - Zero-copy Float32Array views
|
|
16
|
+
*/
|
|
17
|
+
export interface BinaryIndexData {
|
|
18
|
+
dimensions: number;
|
|
19
|
+
maxElements: number;
|
|
20
|
+
M: number;
|
|
21
|
+
efConstruction: number;
|
|
22
|
+
seed: number;
|
|
23
|
+
currentSize: number;
|
|
24
|
+
vectors: Array<{
|
|
25
|
+
id: number;
|
|
26
|
+
vector: Float32Array;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export declare class BinaryIndexFormat {
|
|
30
|
+
/**
|
|
31
|
+
* Save index data to binary format
|
|
32
|
+
*
|
|
33
|
+
* File structure:
|
|
34
|
+
* - Header (24 bytes): dimensions, maxElements, M, efConstruction, seed, currentSize
|
|
35
|
+
* - Vectors: For each vector: id (4 bytes) + vector data (dimensions × 4 bytes)
|
|
36
|
+
*
|
|
37
|
+
* @param indexPath Path to save the binary index file
|
|
38
|
+
* @param data Index data to serialize
|
|
39
|
+
*/
|
|
40
|
+
static save(indexPath: string, data: BinaryIndexData): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Load index data from binary format
|
|
43
|
+
*
|
|
44
|
+
* Uses zero-copy Float32Array views for efficient loading.
|
|
45
|
+
* Copies the views to ensure data persistence after buffer lifecycle.
|
|
46
|
+
*
|
|
47
|
+
* @param indexPath Path to the binary index file
|
|
48
|
+
* @returns Deserialized index data
|
|
49
|
+
*/
|
|
50
|
+
static load(indexPath: string): Promise<BinaryIndexData>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=binary-index-format.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Index Format Module
|
|
3
|
+
*
|
|
4
|
+
* Provides efficient binary serialization for HNSW vector indices.
|
|
5
|
+
*
|
|
6
|
+
* Format Specification:
|
|
7
|
+
* - Header: 24 bytes (6 × uint32)
|
|
8
|
+
* - Vectors: N × (4 + D × 4) bytes
|
|
9
|
+
* - Little-endian encoding for cross-platform compatibility
|
|
10
|
+
* - 4-byte alignment for Float32Array zero-copy views
|
|
11
|
+
*
|
|
12
|
+
* Performance:
|
|
13
|
+
* - 3.66x smaller than JSON format
|
|
14
|
+
* - 3.5x faster loading
|
|
15
|
+
* - Zero-copy Float32Array views
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
export class BinaryIndexFormat {
|
|
19
|
+
/**
|
|
20
|
+
* Save index data to binary format
|
|
21
|
+
*
|
|
22
|
+
* File structure:
|
|
23
|
+
* - Header (24 bytes): dimensions, maxElements, M, efConstruction, seed, currentSize
|
|
24
|
+
* - Vectors: For each vector: id (4 bytes) + vector data (dimensions × 4 bytes)
|
|
25
|
+
*
|
|
26
|
+
* @param indexPath Path to save the binary index file
|
|
27
|
+
* @param data Index data to serialize
|
|
28
|
+
*/
|
|
29
|
+
static async save(indexPath, data) {
|
|
30
|
+
// Calculate total size
|
|
31
|
+
const headerSize = 24; // 6 uint32 fields
|
|
32
|
+
const vectorSize = 4 + (data.dimensions * 4); // id + vector
|
|
33
|
+
const totalSize = headerSize + (data.currentSize * vectorSize);
|
|
34
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
35
|
+
const view = new DataView(buffer);
|
|
36
|
+
let offset = 0;
|
|
37
|
+
// Write header (24 bytes, all little-endian)
|
|
38
|
+
view.setUint32(offset, data.dimensions, true);
|
|
39
|
+
offset += 4;
|
|
40
|
+
view.setUint32(offset, data.maxElements, true);
|
|
41
|
+
offset += 4;
|
|
42
|
+
view.setUint32(offset, data.M, true);
|
|
43
|
+
offset += 4;
|
|
44
|
+
view.setUint32(offset, data.efConstruction, true);
|
|
45
|
+
offset += 4;
|
|
46
|
+
view.setUint32(offset, data.seed, true);
|
|
47
|
+
offset += 4;
|
|
48
|
+
view.setUint32(offset, data.currentSize, true);
|
|
49
|
+
offset += 4;
|
|
50
|
+
// Write vectors
|
|
51
|
+
for (const item of data.vectors) {
|
|
52
|
+
// Ensure 4-byte alignment (should always be true with our format)
|
|
53
|
+
if (offset % 4 !== 0) {
|
|
54
|
+
throw new Error(`Offset ${offset} is not 4-byte aligned`);
|
|
55
|
+
}
|
|
56
|
+
// Write vector ID
|
|
57
|
+
view.setUint32(offset, item.id, true);
|
|
58
|
+
offset += 4;
|
|
59
|
+
// Write vector data
|
|
60
|
+
for (let i = 0; i < item.vector.length; i++) {
|
|
61
|
+
view.setFloat32(offset, item.vector[i], true);
|
|
62
|
+
offset += 4;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Write to file
|
|
66
|
+
writeFileSync(indexPath, Buffer.from(buffer));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load index data from binary format
|
|
70
|
+
*
|
|
71
|
+
* Uses zero-copy Float32Array views for efficient loading.
|
|
72
|
+
* Copies the views to ensure data persistence after buffer lifecycle.
|
|
73
|
+
*
|
|
74
|
+
* @param indexPath Path to the binary index file
|
|
75
|
+
* @returns Deserialized index data
|
|
76
|
+
*/
|
|
77
|
+
static async load(indexPath) {
|
|
78
|
+
const buffer = readFileSync(indexPath);
|
|
79
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
80
|
+
let offset = 0;
|
|
81
|
+
// Read header (24 bytes, all little-endian)
|
|
82
|
+
const dimensions = view.getUint32(offset, true);
|
|
83
|
+
offset += 4;
|
|
84
|
+
const maxElements = view.getUint32(offset, true);
|
|
85
|
+
offset += 4;
|
|
86
|
+
const M = view.getUint32(offset, true);
|
|
87
|
+
offset += 4;
|
|
88
|
+
const efConstruction = view.getUint32(offset, true);
|
|
89
|
+
offset += 4;
|
|
90
|
+
const seed = view.getUint32(offset, true);
|
|
91
|
+
offset += 4;
|
|
92
|
+
const currentSize = view.getUint32(offset, true);
|
|
93
|
+
offset += 4;
|
|
94
|
+
// Read vectors
|
|
95
|
+
const vectors = [];
|
|
96
|
+
for (let i = 0; i < currentSize; i++) {
|
|
97
|
+
// Ensure 4-byte alignment (should always be true with our format)
|
|
98
|
+
if (offset % 4 !== 0) {
|
|
99
|
+
throw new Error(`Offset ${offset} is not 4-byte aligned`);
|
|
100
|
+
}
|
|
101
|
+
// Read vector ID
|
|
102
|
+
const id = view.getUint32(offset, true);
|
|
103
|
+
offset += 4;
|
|
104
|
+
// Zero-copy Float32Array view (fast!)
|
|
105
|
+
const vectorView = new Float32Array(buffer.buffer, buffer.byteOffset + offset, dimensions);
|
|
106
|
+
// Copy to avoid buffer lifecycle issues
|
|
107
|
+
const vector = new Float32Array(vectorView);
|
|
108
|
+
offset += dimensions * 4;
|
|
109
|
+
vectors.push({ id, vector });
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
dimensions,
|
|
113
|
+
maxElements,
|
|
114
|
+
M,
|
|
115
|
+
efConstruction,
|
|
116
|
+
seed,
|
|
117
|
+
currentSize,
|
|
118
|
+
vectors
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=binary-index-format.js.map
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* CORE MODULE — Shared between text-only (rag-lite-ts) and future multimodal (rag-lite-mm)
|
|
3
3
|
* Model-agnostic. No transformer or modality-specific logic.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
6
|
import { JSDOM } from 'jsdom';
|
|
7
7
|
import { ErrorCategory, ErrorSeverity, safeExecute } from './error-handler.js';
|
|
8
8
|
import { createMissingFileError, createDimensionMismatchError } from './actionable-error-messages.js';
|
|
9
|
+
import { BinaryIndexFormat } from './binary-index-format.js';
|
|
9
10
|
// Set up browser-like environment for hnswlib-wasm
|
|
10
11
|
if (typeof window === 'undefined') {
|
|
11
12
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
@@ -153,66 +154,64 @@ export class VectorIndex {
|
|
|
153
154
|
}
|
|
154
155
|
// Create new HNSW index (third parameter is autoSaveFilename, but we'll handle persistence manually)
|
|
155
156
|
this.index = new this.hnswlib.HierarchicalNSW('cosine', this.options.dimensions, '');
|
|
156
|
-
// Load from
|
|
157
|
-
const data =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (stored.dimensions && stored.dimensions !== this.options.dimensions) {
|
|
157
|
+
// Load from binary format
|
|
158
|
+
const data = await BinaryIndexFormat.load(this.indexPath);
|
|
159
|
+
// Validate dimensions
|
|
160
|
+
if (data.dimensions !== this.options.dimensions) {
|
|
161
161
|
console.log(`⚠️ Dimension mismatch detected:`);
|
|
162
|
-
console.log(` Stored dimensions: ${
|
|
162
|
+
console.log(` Stored dimensions: ${data.dimensions}`);
|
|
163
163
|
console.log(` Expected dimensions: ${this.options.dimensions}`);
|
|
164
|
-
console.log(` Number of vectors: ${
|
|
165
|
-
if (
|
|
166
|
-
console.log(` Actual vector length: ${
|
|
164
|
+
console.log(` Number of vectors: ${data.vectors.length}`);
|
|
165
|
+
if (data.vectors.length > 0) {
|
|
166
|
+
console.log(` Actual vector length: ${data.vectors[0].vector.length}`);
|
|
167
167
|
}
|
|
168
|
-
throw createDimensionMismatchError(this.options.dimensions,
|
|
168
|
+
throw createDimensionMismatchError(this.options.dimensions, data.dimensions, 'vector index loading', { operationContext: 'VectorIndex.loadIndex' });
|
|
169
169
|
}
|
|
170
170
|
// Update options from stored data
|
|
171
|
-
this.options.maxElements =
|
|
172
|
-
this.options.M =
|
|
173
|
-
this.options.efConstruction =
|
|
174
|
-
this.options.seed =
|
|
175
|
-
//
|
|
176
|
-
this.index.initIndex(this.options.maxElements, this.options.M
|
|
171
|
+
this.options.maxElements = data.maxElements;
|
|
172
|
+
this.options.M = data.M;
|
|
173
|
+
this.options.efConstruction = data.efConstruction;
|
|
174
|
+
this.options.seed = data.seed;
|
|
175
|
+
// Initialize HNSW index
|
|
176
|
+
this.index.initIndex(this.options.maxElements, this.options.M, this.options.efConstruction, this.options.seed);
|
|
177
177
|
// Clear and repopulate vector storage
|
|
178
178
|
this.vectorStorage.clear();
|
|
179
|
-
// Add all stored vectors
|
|
180
|
-
for (const item of
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
this.vectorStorage.set(item.id, vector);
|
|
179
|
+
// Add all stored vectors to HNSW index
|
|
180
|
+
for (const item of data.vectors) {
|
|
181
|
+
this.index.addPoint(item.vector, item.id, false);
|
|
182
|
+
this.vectorStorage.set(item.id, item.vector);
|
|
184
183
|
}
|
|
185
|
-
this.currentSize =
|
|
186
|
-
console.log(
|
|
184
|
+
this.currentSize = data.currentSize;
|
|
185
|
+
console.log(`✓ Loaded HNSW index with ${this.currentSize} vectors from ${this.indexPath}`);
|
|
187
186
|
}
|
|
188
187
|
catch (error) {
|
|
189
188
|
throw new Error(`Failed to load index from ${this.indexPath}: ${error}`);
|
|
190
189
|
}
|
|
191
190
|
}
|
|
192
191
|
/**
|
|
193
|
-
* Save index to
|
|
192
|
+
* Save index to binary format
|
|
194
193
|
*/
|
|
195
194
|
async saveIndex() {
|
|
196
195
|
if (!this.index) {
|
|
197
196
|
throw new Error('Index not initialized');
|
|
198
197
|
}
|
|
199
198
|
try {
|
|
200
|
-
//
|
|
199
|
+
// Collect all vectors from storage
|
|
201
200
|
const vectors = Array.from(this.vectorStorage.entries()).map(([id, vector]) => ({
|
|
202
201
|
id,
|
|
203
|
-
vector
|
|
202
|
+
vector
|
|
204
203
|
}));
|
|
205
|
-
|
|
204
|
+
// Save to binary format
|
|
205
|
+
await BinaryIndexFormat.save(this.indexPath, {
|
|
206
206
|
dimensions: this.options.dimensions,
|
|
207
207
|
maxElements: this.options.maxElements,
|
|
208
208
|
M: this.options.M || 16,
|
|
209
209
|
efConstruction: this.options.efConstruction || 200,
|
|
210
210
|
seed: this.options.seed || 100,
|
|
211
211
|
currentSize: this.currentSize,
|
|
212
|
-
vectors
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
console.log(`Saved HNSW index with ${this.currentSize} vectors to ${this.indexPath}`);
|
|
212
|
+
vectors
|
|
213
|
+
});
|
|
214
|
+
console.log(`✓ Saved HNSW index with ${this.currentSize} vectors to ${this.indexPath}`);
|
|
216
215
|
}
|
|
217
216
|
catch (error) {
|
|
218
217
|
throw new Error(`Failed to save index to ${this.indexPath}: ${error}`);
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
export { TextSearchFactory, TextIngestionFactory, TextRAGFactory, TextFactoryHelpers } from './text-factory.js';
|
|
39
|
+
export { PolymorphicSearchFactory } from './polymorphic-factory.js';
|
|
40
|
+
export type { PolymorphicSearchOptions } from './polymorphic-factory.js';
|
|
39
41
|
export type { TextSearchOptions, TextIngestionOptions, ContentSystemConfig } from './text-factory.js';
|
|
40
42
|
export { TextSearchFactory as SearchFactory } from './text-factory.js';
|
|
41
43
|
export { TextIngestionFactory as IngestionFactory } from './text-factory.js';
|
package/dist/factories/index.js
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
*/
|
|
38
38
|
// Main factory classes
|
|
39
39
|
export { TextSearchFactory, TextIngestionFactory, TextRAGFactory, TextFactoryHelpers } from './text-factory.js';
|
|
40
|
+
// Polymorphic factory for mode-aware search
|
|
41
|
+
export { PolymorphicSearchFactory } from './polymorphic-factory.js';
|
|
40
42
|
// Convenience re-exports for common patterns
|
|
41
43
|
export { TextSearchFactory as SearchFactory } from './text-factory.js';
|
|
42
44
|
export { TextIngestionFactory as IngestionFactory } from './text-factory.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polymorphic factory for creating mode-aware search engines
|
|
3
|
+
* Automatically detects mode from database and uses appropriate embedder
|
|
4
|
+
*
|
|
5
|
+
* This factory implements the Chameleon Architecture principle:
|
|
6
|
+
* - Detects mode (text/multimodal) from database configuration
|
|
7
|
+
* - Uses appropriate embedder based on detected mode
|
|
8
|
+
* - Provides seamless polymorphic behavior without user intervention
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Automatically detects mode and creates appropriate search engine
|
|
13
|
+
* const search = await PolymorphicSearchFactory.create('./index.bin', './db.sqlite');
|
|
14
|
+
*
|
|
15
|
+
* // Works for both text and multimodal modes
|
|
16
|
+
* const results = await search.search('query');
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { SearchEngine } from '../core/search.js';
|
|
20
|
+
export interface PolymorphicSearchOptions {
|
|
21
|
+
/** Whether to enable reranking (default: true) */
|
|
22
|
+
enableReranking?: boolean;
|
|
23
|
+
/** Top-k results to return (default: from config) */
|
|
24
|
+
topK?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Factory for creating mode-aware search engines
|
|
28
|
+
* Automatically detects mode from database and uses appropriate embedder
|
|
29
|
+
*/
|
|
30
|
+
export declare class PolymorphicSearchFactory {
|
|
31
|
+
/**
|
|
32
|
+
* Create a SearchEngine that automatically adapts to the mode stored in the database
|
|
33
|
+
*
|
|
34
|
+
* This method:
|
|
35
|
+
* 1. Validates that required files exist
|
|
36
|
+
* 2. Opens database and reads system configuration
|
|
37
|
+
* 3. Detects mode (text/multimodal) from database
|
|
38
|
+
* 4. Creates appropriate embedder based on mode
|
|
39
|
+
* 5. Optionally creates reranker based on configuration
|
|
40
|
+
* 6. Returns fully configured SearchEngine
|
|
41
|
+
*
|
|
42
|
+
* @param indexPath - Path to the vector index file (must exist)
|
|
43
|
+
* @param dbPath - Path to the SQLite database file (must exist)
|
|
44
|
+
* @param options - Optional configuration overrides
|
|
45
|
+
* @returns Promise resolving to configured SearchEngine
|
|
46
|
+
* @throws {Error} If required files don't exist or initialization fails
|
|
47
|
+
*/
|
|
48
|
+
static create(indexPath: string, dbPath: string, options?: PolymorphicSearchOptions): Promise<SearchEngine>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=polymorphic-factory.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polymorphic factory for creating mode-aware search engines
|
|
3
|
+
* Automatically detects mode from database and uses appropriate embedder
|
|
4
|
+
*
|
|
5
|
+
* This factory implements the Chameleon Architecture principle:
|
|
6
|
+
* - Detects mode (text/multimodal) from database configuration
|
|
7
|
+
* - Uses appropriate embedder based on detected mode
|
|
8
|
+
* - Provides seamless polymorphic behavior without user intervention
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Automatically detects mode and creates appropriate search engine
|
|
13
|
+
* const search = await PolymorphicSearchFactory.create('./index.bin', './db.sqlite');
|
|
14
|
+
*
|
|
15
|
+
* // Works for both text and multimodal modes
|
|
16
|
+
* const results = await search.search('query');
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
import { SearchEngine } from '../core/search.js';
|
|
20
|
+
import { IndexManager } from '../index-manager.js';
|
|
21
|
+
import { openDatabase, getSystemInfo } from '../core/db.js';
|
|
22
|
+
import { createTextEmbedFunction } from '../text/embedder.js';
|
|
23
|
+
import { createTextRerankFunction } from '../text/reranker.js';
|
|
24
|
+
import { config, getModelDefaults } from '../core/config.js';
|
|
25
|
+
import { existsSync } from 'fs';
|
|
26
|
+
import { createMissingFileError, createInvalidPathError, createFactoryCreationError } from '../core/actionable-error-messages.js';
|
|
27
|
+
/**
|
|
28
|
+
* Factory for creating mode-aware search engines
|
|
29
|
+
* Automatically detects mode from database and uses appropriate embedder
|
|
30
|
+
*/
|
|
31
|
+
export class PolymorphicSearchFactory {
|
|
32
|
+
/**
|
|
33
|
+
* Create a SearchEngine that automatically adapts to the mode stored in the database
|
|
34
|
+
*
|
|
35
|
+
* This method:
|
|
36
|
+
* 1. Validates that required files exist
|
|
37
|
+
* 2. Opens database and reads system configuration
|
|
38
|
+
* 3. Detects mode (text/multimodal) from database
|
|
39
|
+
* 4. Creates appropriate embedder based on mode
|
|
40
|
+
* 5. Optionally creates reranker based on configuration
|
|
41
|
+
* 6. Returns fully configured SearchEngine
|
|
42
|
+
*
|
|
43
|
+
* @param indexPath - Path to the vector index file (must exist)
|
|
44
|
+
* @param dbPath - Path to the SQLite database file (must exist)
|
|
45
|
+
* @param options - Optional configuration overrides
|
|
46
|
+
* @returns Promise resolving to configured SearchEngine
|
|
47
|
+
* @throws {Error} If required files don't exist or initialization fails
|
|
48
|
+
*/
|
|
49
|
+
static async create(indexPath, dbPath, options = {}) {
|
|
50
|
+
try {
|
|
51
|
+
console.log('🏭 PolymorphicSearchFactory: Initializing mode-aware search engine...');
|
|
52
|
+
// Validate input paths
|
|
53
|
+
if (!indexPath || !dbPath) {
|
|
54
|
+
throw createInvalidPathError([
|
|
55
|
+
{ name: 'indexPath', value: indexPath },
|
|
56
|
+
{ name: 'dbPath', value: dbPath }
|
|
57
|
+
], { operationContext: 'PolymorphicSearchFactory.create' });
|
|
58
|
+
}
|
|
59
|
+
// Check if required files exist
|
|
60
|
+
if (!existsSync(indexPath)) {
|
|
61
|
+
throw createMissingFileError(indexPath, 'index', {
|
|
62
|
+
operationContext: 'PolymorphicSearchFactory.create'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!existsSync(dbPath)) {
|
|
66
|
+
throw createMissingFileError(dbPath, 'database', {
|
|
67
|
+
operationContext: 'PolymorphicSearchFactory.create'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Step 1: Open database and detect mode
|
|
71
|
+
console.log('💾 Opening database and detecting mode...');
|
|
72
|
+
const db = await openDatabase(dbPath);
|
|
73
|
+
let mode = 'text';
|
|
74
|
+
let embeddingModel;
|
|
75
|
+
let modelDimensions;
|
|
76
|
+
try {
|
|
77
|
+
const systemInfo = await getSystemInfo(db);
|
|
78
|
+
if (systemInfo) {
|
|
79
|
+
mode = systemInfo.mode;
|
|
80
|
+
embeddingModel = systemInfo.modelName;
|
|
81
|
+
modelDimensions = systemInfo.modelDimensions;
|
|
82
|
+
console.log(`📊 Detected mode: ${mode}`);
|
|
83
|
+
console.log(`📊 Detected model: ${embeddingModel} (${modelDimensions} dimensions)`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Fallback to default if no system info
|
|
87
|
+
embeddingModel = config.embedding_model;
|
|
88
|
+
const modelDefaults = getModelDefaults(embeddingModel);
|
|
89
|
+
modelDimensions = modelDefaults.dimensions;
|
|
90
|
+
console.log(`📊 No system info found, using default: ${embeddingModel} (${modelDimensions} dimensions)`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// If getSystemInfo fails, use defaults
|
|
95
|
+
embeddingModel = config.embedding_model;
|
|
96
|
+
const modelDefaults = getModelDefaults(embeddingModel);
|
|
97
|
+
modelDimensions = modelDefaults.dimensions;
|
|
98
|
+
console.log(`📊 Using default configuration: ${embeddingModel} (${modelDimensions} dimensions)`);
|
|
99
|
+
}
|
|
100
|
+
// Step 2: Create appropriate embedder based on mode
|
|
101
|
+
let embedFn;
|
|
102
|
+
if (mode === 'multimodal') {
|
|
103
|
+
console.log('📊 Loading CLIP embedder for multimodal mode...');
|
|
104
|
+
const { createEmbedder } = await import('../core/embedder-factory.js');
|
|
105
|
+
const clipEmbedder = await createEmbedder(embeddingModel);
|
|
106
|
+
// Wrap CLIP embedder to match EmbedFunction signature
|
|
107
|
+
embedFn = async (content, contentType) => {
|
|
108
|
+
if (contentType === 'image') {
|
|
109
|
+
return await clipEmbedder.embedImage(content);
|
|
110
|
+
}
|
|
111
|
+
return await clipEmbedder.embedText(content);
|
|
112
|
+
};
|
|
113
|
+
console.log('✓ CLIP embedder loaded for multimodal mode');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log('📊 Loading text embedder for text mode...');
|
|
117
|
+
embedFn = createTextEmbedFunction(embeddingModel);
|
|
118
|
+
console.log('✓ Text embedder loaded');
|
|
119
|
+
}
|
|
120
|
+
// Step 3: Initialize reranking function (optional)
|
|
121
|
+
let rerankFn;
|
|
122
|
+
if (options.enableReranking === true) {
|
|
123
|
+
console.log('🔄 Loading reranking model...');
|
|
124
|
+
rerankFn = createTextRerankFunction();
|
|
125
|
+
await rerankFn('test query', []);
|
|
126
|
+
console.log('✓ Reranking model loaded successfully');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log('🔄 Reranking disabled (local-first, fast mode)');
|
|
130
|
+
}
|
|
131
|
+
// Step 4: Initialize database schema
|
|
132
|
+
const { initializeSchema } = await import('../core/db.js');
|
|
133
|
+
await initializeSchema(db);
|
|
134
|
+
console.log('✓ Database connection established');
|
|
135
|
+
// Step 5: Initialize index manager
|
|
136
|
+
console.log('📇 Loading vector index...');
|
|
137
|
+
const indexManager = new IndexManager(indexPath, dbPath, modelDimensions, embeddingModel);
|
|
138
|
+
await indexManager.initialize();
|
|
139
|
+
console.log('✓ Vector index loaded successfully');
|
|
140
|
+
// Step 6: Create ContentResolver
|
|
141
|
+
console.log('📁 Initializing content resolver...');
|
|
142
|
+
const { ContentResolver } = await import('../core/content-resolver.js');
|
|
143
|
+
const contentResolver = new ContentResolver(db);
|
|
144
|
+
console.log('✓ Content resolver ready');
|
|
145
|
+
// Step 7: Create SearchEngine with dependency injection
|
|
146
|
+
const searchEngine = new SearchEngine(embedFn, indexManager, db, rerankFn, contentResolver);
|
|
147
|
+
// Step 8: Validate the setup
|
|
148
|
+
const stats = await searchEngine.getStats();
|
|
149
|
+
console.log(`✓ Search engine ready: ${stats.totalChunks} chunks indexed, mode: ${mode}, reranking ${stats.rerankingEnabled ? 'enabled' : 'disabled'}`);
|
|
150
|
+
console.log('🎉 PolymorphicSearchFactory: Mode-aware search engine initialized successfully');
|
|
151
|
+
return searchEngine;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
console.error('❌ PolymorphicSearchFactory: Failed to create search engine');
|
|
155
|
+
throw createFactoryCreationError('PolymorphicSearchFactory', error instanceof Error ? error.message : 'Unknown error', { operationContext: 'polymorphic search engine creation' });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=polymorphic-factory.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -41,8 +41,31 @@
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
export { TextSearchFactory, TextIngestionFactory, TextRAGFactory, TextFactoryHelpers } from './factories/index.js';
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated PolymorphicSearchFactory is no longer needed - SearchEngine now automatically
|
|
46
|
+
* detects mode from database and adapts accordingly (Chameleon Architecture).
|
|
47
|
+
*
|
|
48
|
+
* Migration Guide:
|
|
49
|
+
* ```typescript
|
|
50
|
+
* // Old way (deprecated):
|
|
51
|
+
* const search = await PolymorphicSearchFactory.create('./index.bin', './db.sqlite');
|
|
52
|
+
*
|
|
53
|
+
* // New way (recommended):
|
|
54
|
+
* const search = new SearchEngine('./index.bin', './db.sqlite');
|
|
55
|
+
* await search.search('query'); // Mode automatically detected
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* The SearchEngine constructor now uses the polymorphic factory internally,
|
|
59
|
+
* providing the same automatic mode detection without requiring explicit factory usage.
|
|
60
|
+
*/
|
|
61
|
+
export { PolymorphicSearchFactory } from './factories/index.js';
|
|
44
62
|
export { TextSearchFactory as SearchFactory, TextIngestionFactory as IngestionFactory, TextRAGFactory as RAGFactory } from './factories/index.js';
|
|
45
63
|
export type { TextSearchOptions, TextIngestionOptions } from './factories/index.js';
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated PolymorphicSearchOptions is no longer needed - use SearchEngineOptions instead.
|
|
66
|
+
* SearchEngine now automatically detects mode and adapts (Chameleon Architecture).
|
|
67
|
+
*/
|
|
68
|
+
export type { PolymorphicSearchOptions } from './factories/index.js';
|
|
46
69
|
export type { TextSearchOptions as SearchEngineOptions, TextIngestionOptions as IngestionPipelineOptions } from './factories/index.js';
|
|
47
70
|
export { SearchEngine as CoreSearchEngine } from './core/search.js';
|
|
48
71
|
export { IngestionPipeline as CoreIngestionPipeline } from './core/ingestion.js';
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,24 @@
|
|
|
45
45
|
// =============================================================================
|
|
46
46
|
// Main factory classes for simple usage
|
|
47
47
|
export { TextSearchFactory, TextIngestionFactory, TextRAGFactory, TextFactoryHelpers } from './factories/index.js';
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated PolymorphicSearchFactory is no longer needed - SearchEngine now automatically
|
|
50
|
+
* detects mode from database and adapts accordingly (Chameleon Architecture).
|
|
51
|
+
*
|
|
52
|
+
* Migration Guide:
|
|
53
|
+
* ```typescript
|
|
54
|
+
* // Old way (deprecated):
|
|
55
|
+
* const search = await PolymorphicSearchFactory.create('./index.bin', './db.sqlite');
|
|
56
|
+
*
|
|
57
|
+
* // New way (recommended):
|
|
58
|
+
* const search = new SearchEngine('./index.bin', './db.sqlite');
|
|
59
|
+
* await search.search('query'); // Mode automatically detected
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* The SearchEngine constructor now uses the polymorphic factory internally,
|
|
63
|
+
* providing the same automatic mode detection without requiring explicit factory usage.
|
|
64
|
+
*/
|
|
65
|
+
export { PolymorphicSearchFactory } from './factories/index.js';
|
|
48
66
|
// Convenience aliases for common usage
|
|
49
67
|
export { TextSearchFactory as SearchFactory, TextIngestionFactory as IngestionFactory, TextRAGFactory as RAGFactory } from './factories/index.js';
|
|
50
68
|
// =============================================================================
|
|
@@ -84,6 +84,19 @@ export declare class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
84
84
|
* during cleanup - errors are logged but don't prevent cleanup completion.
|
|
85
85
|
*/
|
|
86
86
|
cleanup(): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Apply L2-normalization to an embedding vector
|
|
89
|
+
*
|
|
90
|
+
* L2-normalization ensures that all embeddings have unit length (magnitude = 1),
|
|
91
|
+
* which is essential for CLIP models as they were trained with normalized embeddings.
|
|
92
|
+
* This normalization makes cosine similarity calculations more reliable and ensures
|
|
93
|
+
* that vector magnitudes don't affect similarity scores.
|
|
94
|
+
*
|
|
95
|
+
* @param embedding - The embedding vector to normalize (modified in-place)
|
|
96
|
+
* @returns The normalized embedding vector (same reference as input)
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
private normalizeEmbedding;
|
|
87
100
|
/**
|
|
88
101
|
* Embed text using CLIP text encoder
|
|
89
102
|
*
|
|
@@ -91,11 +104,11 @@ export declare class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
91
104
|
* pixel_values errors. Text is tokenized with CLIP's 77 token limit and
|
|
92
105
|
* automatically truncated if necessary.
|
|
93
106
|
*
|
|
94
|
-
* Returns a 512-dimensional embedding vector in the unified CLIP
|
|
95
|
-
* which is directly comparable to image embeddings for cross-modal search.
|
|
107
|
+
* Returns a 512-dimensional L2-normalized embedding vector in the unified CLIP
|
|
108
|
+
* embedding space, which is directly comparable to image embeddings for cross-modal search.
|
|
96
109
|
*
|
|
97
110
|
* @param text - The text to embed (will be trimmed and validated)
|
|
98
|
-
* @returns EmbeddingResult with 512-dimensional vector and metadata
|
|
111
|
+
* @returns EmbeddingResult with 512-dimensional normalized vector and metadata
|
|
99
112
|
* @throws {Error} If text is empty, model not loaded, or embedding fails
|
|
100
113
|
*
|
|
101
114
|
* @example
|
|
@@ -117,10 +130,10 @@ export declare class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
117
130
|
* - Converted to proper pixel_values format using AutoProcessor
|
|
118
131
|
* - Normalized for CLIP vision model
|
|
119
132
|
*
|
|
120
|
-
* Returns a 512-dimensional embedding vector directly comparable to text embeddings.
|
|
133
|
+
* Returns a 512-dimensional L2-normalized embedding vector directly comparable to text embeddings.
|
|
121
134
|
*
|
|
122
135
|
* @param imagePath - Local file path or URL to the image
|
|
123
|
-
* @returns EmbeddingResult with 512-dimensional vector and metadata
|
|
136
|
+
* @returns EmbeddingResult with 512-dimensional normalized vector and metadata
|
|
124
137
|
* @throws {Error} If image not found, unsupported format, or embedding fails
|
|
125
138
|
*
|
|
126
139
|
* @example
|
|
@@ -268,6 +268,33 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
// =============================================================================
|
|
271
|
+
// NORMALIZATION UTILITIES
|
|
272
|
+
// =============================================================================
|
|
273
|
+
/**
|
|
274
|
+
* Apply L2-normalization to an embedding vector
|
|
275
|
+
*
|
|
276
|
+
* L2-normalization ensures that all embeddings have unit length (magnitude = 1),
|
|
277
|
+
* which is essential for CLIP models as they were trained with normalized embeddings.
|
|
278
|
+
* This normalization makes cosine similarity calculations more reliable and ensures
|
|
279
|
+
* that vector magnitudes don't affect similarity scores.
|
|
280
|
+
*
|
|
281
|
+
* @param embedding - The embedding vector to normalize (modified in-place)
|
|
282
|
+
* @returns The normalized embedding vector (same reference as input)
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
normalizeEmbedding(embedding) {
|
|
286
|
+
// Calculate L2 norm (magnitude)
|
|
287
|
+
const magnitude = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
288
|
+
// Avoid division by zero
|
|
289
|
+
if (magnitude > 0) {
|
|
290
|
+
// Normalize each component by dividing by magnitude
|
|
291
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
292
|
+
embedding[i] /= magnitude;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return embedding;
|
|
296
|
+
}
|
|
297
|
+
// =============================================================================
|
|
271
298
|
// TEXT EMBEDDING METHODS
|
|
272
299
|
// =============================================================================
|
|
273
300
|
/**
|
|
@@ -277,11 +304,11 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
277
304
|
* pixel_values errors. Text is tokenized with CLIP's 77 token limit and
|
|
278
305
|
* automatically truncated if necessary.
|
|
279
306
|
*
|
|
280
|
-
* Returns a 512-dimensional embedding vector in the unified CLIP
|
|
281
|
-
* which is directly comparable to image embeddings for cross-modal search.
|
|
307
|
+
* Returns a 512-dimensional L2-normalized embedding vector in the unified CLIP
|
|
308
|
+
* embedding space, which is directly comparable to image embeddings for cross-modal search.
|
|
282
309
|
*
|
|
283
310
|
* @param text - The text to embed (will be trimmed and validated)
|
|
284
|
-
* @returns EmbeddingResult with 512-dimensional vector and metadata
|
|
311
|
+
* @returns EmbeddingResult with 512-dimensional normalized vector and metadata
|
|
285
312
|
* @throws {Error} If text is empty, model not loaded, or embedding fails
|
|
286
313
|
*
|
|
287
314
|
* @example
|
|
@@ -349,10 +376,17 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
349
376
|
if (nonZeroValues.length === 0) {
|
|
350
377
|
throw new Error('CLIP embedding is all zeros');
|
|
351
378
|
}
|
|
352
|
-
// Calculate embedding magnitude for quality assessment
|
|
353
|
-
const
|
|
354
|
-
if (
|
|
355
|
-
throw new Error(`CLIP embedding has critically low magnitude: ${
|
|
379
|
+
// Calculate embedding magnitude before normalization for quality assessment
|
|
380
|
+
const magnitudeBeforeNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
381
|
+
if (magnitudeBeforeNorm < 1e-6) {
|
|
382
|
+
throw new Error(`CLIP embedding has critically low magnitude: ${magnitudeBeforeNorm.toExponential(3)}`);
|
|
383
|
+
}
|
|
384
|
+
// Apply L2-normalization (CLIP models are trained with normalized embeddings)
|
|
385
|
+
this.normalizeEmbedding(embedding);
|
|
386
|
+
// Verify normalization was successful
|
|
387
|
+
const magnitudeAfterNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
388
|
+
if (Math.abs(magnitudeAfterNorm - 1.0) > 0.01) {
|
|
389
|
+
console.warn(`Warning: Embedding normalization may be imprecise (magnitude: ${magnitudeAfterNorm.toFixed(6)})`);
|
|
356
390
|
}
|
|
357
391
|
// Generate unique embedding ID
|
|
358
392
|
const embeddingId = this.generateEmbeddingId(finalProcessedText, 'text');
|
|
@@ -364,7 +398,9 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
364
398
|
originalText: text,
|
|
365
399
|
processedText: finalProcessedText,
|
|
366
400
|
textLength: finalProcessedText.length,
|
|
367
|
-
|
|
401
|
+
embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
|
|
402
|
+
embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
|
|
403
|
+
normalized: true,
|
|
368
404
|
modelName: this.modelName,
|
|
369
405
|
modelType: this.modelType,
|
|
370
406
|
dimensions: this.dimensions
|
|
@@ -389,10 +425,10 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
389
425
|
* - Converted to proper pixel_values format using AutoProcessor
|
|
390
426
|
* - Normalized for CLIP vision model
|
|
391
427
|
*
|
|
392
|
-
* Returns a 512-dimensional embedding vector directly comparable to text embeddings.
|
|
428
|
+
* Returns a 512-dimensional L2-normalized embedding vector directly comparable to text embeddings.
|
|
393
429
|
*
|
|
394
430
|
* @param imagePath - Local file path or URL to the image
|
|
395
|
-
* @returns EmbeddingResult with 512-dimensional vector and metadata
|
|
431
|
+
* @returns EmbeddingResult with 512-dimensional normalized vector and metadata
|
|
396
432
|
* @throws {Error} If image not found, unsupported format, or embedding fails
|
|
397
433
|
*
|
|
398
434
|
* @example
|
|
@@ -459,10 +495,17 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
459
495
|
if (nonZeroValues.length === 0) {
|
|
460
496
|
throw new Error('CLIP image embedding is all zeros');
|
|
461
497
|
}
|
|
462
|
-
// Calculate embedding magnitude for quality assessment
|
|
463
|
-
const
|
|
464
|
-
if (
|
|
465
|
-
throw new Error(`CLIP image embedding has critically low magnitude: ${
|
|
498
|
+
// Calculate embedding magnitude before normalization for quality assessment
|
|
499
|
+
const magnitudeBeforeNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
500
|
+
if (magnitudeBeforeNorm < 1e-6) {
|
|
501
|
+
throw new Error(`CLIP image embedding has critically low magnitude: ${magnitudeBeforeNorm.toExponential(3)}`);
|
|
502
|
+
}
|
|
503
|
+
// Apply L2-normalization (CLIP models are trained with normalized embeddings)
|
|
504
|
+
this.normalizeEmbedding(embedding);
|
|
505
|
+
// Verify normalization was successful
|
|
506
|
+
const magnitudeAfterNorm = Math.sqrt(Array.from(embedding).reduce((sum, val) => sum + val * val, 0));
|
|
507
|
+
if (Math.abs(magnitudeAfterNorm - 1.0) > 0.01) {
|
|
508
|
+
console.warn(`Warning: Image embedding normalization may be imprecise (magnitude: ${magnitudeAfterNorm.toFixed(6)})`);
|
|
466
509
|
}
|
|
467
510
|
// Generate unique embedding ID
|
|
468
511
|
const embeddingId = this.generateEmbeddingId(processedPath, 'image');
|
|
@@ -472,7 +515,9 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
472
515
|
contentType: 'image',
|
|
473
516
|
metadata: {
|
|
474
517
|
imagePath: processedPath,
|
|
475
|
-
|
|
518
|
+
embeddingMagnitudeBeforeNorm: magnitudeBeforeNorm,
|
|
519
|
+
embeddingMagnitudeAfterNorm: magnitudeAfterNorm,
|
|
520
|
+
normalized: true,
|
|
476
521
|
modelName: this.modelName,
|
|
477
522
|
modelType: this.modelType,
|
|
478
523
|
dimensions: this.dimensions
|
|
@@ -749,6 +794,8 @@ export class CLIPEmbedder extends BaseUniversalEmbedder {
|
|
|
749
794
|
if (embedding.length !== this.dimensions) {
|
|
750
795
|
throw new Error(`CLIP embedding dimension mismatch for item ${i}: expected ${this.dimensions}, got ${embedding.length}`);
|
|
751
796
|
}
|
|
797
|
+
// Apply L2-normalization (CLIP models are trained with normalized embeddings)
|
|
798
|
+
this.normalizeEmbedding(embedding);
|
|
752
799
|
const embeddingId = this.generateEmbeddingId(item.content, 'text');
|
|
753
800
|
results.push({
|
|
754
801
|
embedding_id: embeddingId,
|
package/dist/search.d.ts
CHANGED
|
@@ -1,25 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public API SearchEngine - Simple constructor
|
|
2
|
+
* Public API SearchEngine - Simple constructor with Chameleon Architecture
|
|
3
3
|
*
|
|
4
|
-
* This class provides a clean, simple API
|
|
5
|
-
*
|
|
4
|
+
* This class provides a clean, simple API that automatically adapts to the mode
|
|
5
|
+
* (text or multimodal) stored in the database during ingestion. The system detects
|
|
6
|
+
* the mode and creates the appropriate embedder and reranker without user intervention.
|
|
7
|
+
*
|
|
8
|
+
* Chameleon Architecture Features:
|
|
9
|
+
* - Automatic mode detection from database configuration
|
|
10
|
+
* - Seamless switching between text and multimodal modes
|
|
11
|
+
* - Appropriate embedder selection (sentence-transformer or CLIP)
|
|
12
|
+
* - Mode-specific reranking strategies
|
|
6
13
|
*
|
|
7
14
|
* @example
|
|
8
15
|
* ```typescript
|
|
9
|
-
* // Simple usage
|
|
16
|
+
* // Simple usage - mode automatically detected from database
|
|
10
17
|
* const search = new SearchEngine('./index.bin', './db.sqlite');
|
|
11
18
|
* const results = await search.search('query');
|
|
12
19
|
*
|
|
13
|
-
* //
|
|
20
|
+
* // Works for both text and multimodal databases
|
|
21
|
+
* // Text mode: uses sentence-transformer embeddings
|
|
22
|
+
* // Multimodal mode: uses CLIP embeddings for cross-modal search
|
|
23
|
+
*
|
|
24
|
+
* // With options (advanced)
|
|
14
25
|
* const search = new SearchEngine('./index.bin', './db.sqlite', {
|
|
15
|
-
* embeddingModel: 'all-MiniLM-L6-v2',
|
|
16
26
|
* enableReranking: true
|
|
17
27
|
* });
|
|
18
28
|
* ```
|
|
19
29
|
*/
|
|
20
|
-
import { type TextSearchOptions } from './factories/index.js';
|
|
21
30
|
import type { SearchResult, SearchOptions, EmbedFunction, RerankFunction } from './core/types.js';
|
|
22
|
-
export interface SearchEngineOptions
|
|
31
|
+
export interface SearchEngineOptions {
|
|
32
|
+
/** Embedding model name override */
|
|
33
|
+
embeddingModel?: string;
|
|
34
|
+
/** Embedding batch size override */
|
|
35
|
+
batchSize?: number;
|
|
36
|
+
/** Reranking model name override */
|
|
37
|
+
rerankingModel?: string;
|
|
38
|
+
/** Whether to enable reranking (default: true) */
|
|
39
|
+
enableReranking?: boolean;
|
|
40
|
+
/** Top-k results to return (default: from config) */
|
|
41
|
+
topK?: number;
|
|
23
42
|
/** Custom embedding function (advanced usage) */
|
|
24
43
|
embedFn?: EmbedFunction;
|
|
25
44
|
/** Custom reranking function (advanced usage) */
|
|
@@ -33,7 +52,13 @@ export declare class SearchEngine {
|
|
|
33
52
|
private initPromise;
|
|
34
53
|
constructor(indexPath: string, dbPath: string, options?: SearchEngineOptions);
|
|
35
54
|
/**
|
|
36
|
-
* Initialize the search engine using
|
|
55
|
+
* Initialize the search engine using polymorphic factory or direct injection
|
|
56
|
+
*
|
|
57
|
+
* Chameleon Architecture Implementation:
|
|
58
|
+
* - Automatically detects mode from database (text or multimodal)
|
|
59
|
+
* - Creates appropriate embedder based on detected mode
|
|
60
|
+
* - Applies mode-specific reranking strategies
|
|
61
|
+
* - Provides seamless polymorphic behavior
|
|
37
62
|
*/
|
|
38
63
|
private initialize;
|
|
39
64
|
/**
|
package/dist/search.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public API SearchEngine - Simple constructor
|
|
2
|
+
* Public API SearchEngine - Simple constructor with Chameleon Architecture
|
|
3
3
|
*
|
|
4
|
-
* This class provides a clean, simple API
|
|
5
|
-
*
|
|
4
|
+
* This class provides a clean, simple API that automatically adapts to the mode
|
|
5
|
+
* (text or multimodal) stored in the database during ingestion. The system detects
|
|
6
|
+
* the mode and creates the appropriate embedder and reranker without user intervention.
|
|
7
|
+
*
|
|
8
|
+
* Chameleon Architecture Features:
|
|
9
|
+
* - Automatic mode detection from database configuration
|
|
10
|
+
* - Seamless switching between text and multimodal modes
|
|
11
|
+
* - Appropriate embedder selection (sentence-transformer or CLIP)
|
|
12
|
+
* - Mode-specific reranking strategies
|
|
6
13
|
*
|
|
7
14
|
* @example
|
|
8
15
|
* ```typescript
|
|
9
|
-
* // Simple usage
|
|
16
|
+
* // Simple usage - mode automatically detected from database
|
|
10
17
|
* const search = new SearchEngine('./index.bin', './db.sqlite');
|
|
11
18
|
* const results = await search.search('query');
|
|
12
19
|
*
|
|
13
|
-
* //
|
|
20
|
+
* // Works for both text and multimodal databases
|
|
21
|
+
* // Text mode: uses sentence-transformer embeddings
|
|
22
|
+
* // Multimodal mode: uses CLIP embeddings for cross-modal search
|
|
23
|
+
*
|
|
24
|
+
* // With options (advanced)
|
|
14
25
|
* const search = new SearchEngine('./index.bin', './db.sqlite', {
|
|
15
|
-
* embeddingModel: 'all-MiniLM-L6-v2',
|
|
16
26
|
* enableReranking: true
|
|
17
27
|
* });
|
|
18
28
|
* ```
|
|
19
29
|
*/
|
|
20
30
|
import { SearchEngine as CoreSearchEngine } from './core/search.js';
|
|
21
|
-
import { TextSearchFactory } from './factories/index.js';
|
|
22
31
|
export class SearchEngine {
|
|
23
32
|
indexPath;
|
|
24
33
|
dbPath;
|
|
@@ -42,7 +51,13 @@ export class SearchEngine {
|
|
|
42
51
|
}
|
|
43
52
|
}
|
|
44
53
|
/**
|
|
45
|
-
* Initialize the search engine using
|
|
54
|
+
* Initialize the search engine using polymorphic factory or direct injection
|
|
55
|
+
*
|
|
56
|
+
* Chameleon Architecture Implementation:
|
|
57
|
+
* - Automatically detects mode from database (text or multimodal)
|
|
58
|
+
* - Creates appropriate embedder based on detected mode
|
|
59
|
+
* - Applies mode-specific reranking strategies
|
|
60
|
+
* - Provides seamless polymorphic behavior
|
|
46
61
|
*/
|
|
47
62
|
async initialize() {
|
|
48
63
|
if (this.coreEngine) {
|
|
@@ -81,8 +96,11 @@ export class SearchEngine {
|
|
|
81
96
|
this.coreEngine = new CoreSearchEngine(embedFn, indexManager, db, this.options.rerankFn, contentResolver);
|
|
82
97
|
}
|
|
83
98
|
else {
|
|
84
|
-
// Use factory for
|
|
85
|
-
|
|
99
|
+
// Use core polymorphic factory for automatic mode detection (Chameleon Architecture)
|
|
100
|
+
// This enables SearchEngine to automatically adapt to text or multimodal mode
|
|
101
|
+
// based on the configuration stored in the database during ingestion
|
|
102
|
+
const { PolymorphicSearchFactory } = await import('./core/polymorphic-search-factory.js');
|
|
103
|
+
this.coreEngine = await PolymorphicSearchFactory.create(this.indexPath, this.dbPath);
|
|
86
104
|
}
|
|
87
105
|
})();
|
|
88
106
|
return this.initPromise;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rag-lite-ts",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Local-first TypeScript retrieval engine with Chameleon Multimodal Architecture for semantic search over text and image content",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,9 +31,16 @@
|
|
|
31
31
|
"build:test": "tsc --project tsconfig.test.json",
|
|
32
32
|
"clean": "rimraf dist",
|
|
33
33
|
"dev": "tsc --watch",
|
|
34
|
-
"test": "npm run build:test && node --
|
|
35
|
-
"test:
|
|
36
|
-
"test:
|
|
34
|
+
"test": "npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__/core dist/__tests__/text dist/__tests__/preprocessors",
|
|
35
|
+
"test:verbose": "npm run build:test && node --expose-gc --test --test-concurrency=1 --test-reporter=tap dist/__tests__/core dist/__tests__/text dist/__tests__/preprocessors",
|
|
36
|
+
"test:core": "npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__/core",
|
|
37
|
+
"test:core:verbose": "npm run build:test && node --expose-gc --test --test-concurrency=1 --test-reporter=tap dist/__tests__/core",
|
|
38
|
+
"test:text": "npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__/text",
|
|
39
|
+
"test:preprocessors": "npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__/preprocessors",
|
|
40
|
+
"test:integration": "npm run build && npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__/integration",
|
|
41
|
+
"test:integration:verbose": "npm run build && npm run build:test && node --expose-gc --test --test-concurrency=1 --test-reporter=tap dist/__tests__/integration",
|
|
42
|
+
"test:all": "npm run build:test && node --expose-gc --test --test-concurrency=1 dist/__tests__",
|
|
43
|
+
"test:all:verbose": "npm run build:test && node --expose-gc --test --test-concurrency=1 --test-reporter=tap dist/__tests__",
|
|
37
44
|
"prepublishOnly": "npm run clean && npm run build"
|
|
38
45
|
},
|
|
39
46
|
"keywords": [
|
|
@@ -71,6 +78,7 @@
|
|
|
71
78
|
"dependencies": {
|
|
72
79
|
"@huggingface/transformers": "^3.7.5",
|
|
73
80
|
"@modelcontextprotocol/sdk": "^1.18.2",
|
|
81
|
+
"csv-parse": "^6.1.0",
|
|
74
82
|
"hnswlib-wasm": "^0.8.2",
|
|
75
83
|
"jsdom": "^27.0.0",
|
|
76
84
|
"lru-cache": "^11.2.2",
|
|
@@ -84,6 +92,7 @@
|
|
|
84
92
|
"@types/node": "^20.11.0",
|
|
85
93
|
"js-yaml": "^4.1.0",
|
|
86
94
|
"rimraf": "^5.0.5",
|
|
95
|
+
"tsx": "^4.20.6",
|
|
87
96
|
"typescript": "^5.3.0"
|
|
88
97
|
},
|
|
89
98
|
"optionalDependencies": {
|