sehawq.db 4.0.3 → 4.0.5
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/.github/workflows/npm-publish.yml +30 -30
- package/LICENSE +21 -21
- package/index.js +1 -1
- package/package.json +36 -36
- package/readme.md +413 -413
- package/src/core/Database.js +294 -294
- package/src/core/Events.js +285 -285
- package/src/core/IndexManager.js +813 -813
- package/src/core/Persistence.js +375 -375
- package/src/core/QueryEngine.js +447 -447
- package/src/core/Storage.js +321 -321
- package/src/core/Validator.js +324 -324
- package/src/index.js +115 -115
- package/src/performance/Cache.js +338 -338
- package/src/performance/LazyLoader.js +354 -354
- package/src/performance/MemoryManager.js +495 -495
- package/src/server/api.js +687 -687
- package/src/server/websocket.js +527 -527
- package/src/utils/benchmark.js +51 -51
- package/src/utils/dot-notation.js +247 -247
- package/src/utils/helpers.js +275 -275
- package/src/utils/profiler.js +70 -70
- package/src/version.js +37 -37
package/src/core/IndexManager.js
CHANGED
|
@@ -1,814 +1,814 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IndexManager - Makes queries lightning fast ⚡
|
|
3
|
-
*
|
|
4
|
-
* From O(n) to O(1) with the magic of indexing
|
|
5
|
-
* Because scanning millions of records should be illegal 😅
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class IndexManager {
|
|
9
|
-
constructor(database, options = {}) {
|
|
10
|
-
this.db = database;
|
|
11
|
-
this.options = {
|
|
12
|
-
autoIndex: true,
|
|
13
|
-
backgroundIndexing: true,
|
|
14
|
-
maxIndexes: 10,
|
|
15
|
-
indexUpdateBatchSize: 1000,
|
|
16
|
-
...options
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Index storage
|
|
20
|
-
this.indexes = new Map(); // indexName -> Index instance
|
|
21
|
-
this.fieldIndexes = new Map(); // fieldName -> Set of indexNames
|
|
22
|
-
|
|
23
|
-
// Performance tracking
|
|
24
|
-
this.stats = {
|
|
25
|
-
indexesCreated: 0,
|
|
26
|
-
indexesDropped: 0,
|
|
27
|
-
queriesWithIndex: 0,
|
|
28
|
-
queriesWithoutIndex: 0,
|
|
29
|
-
indexUpdates: 0,
|
|
30
|
-
backgroundJobs: 0
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Background indexing queue
|
|
34
|
-
this.indexQueue = [];
|
|
35
|
-
this.isProcessingQueue = false;
|
|
36
|
-
|
|
37
|
-
console.log('📊 IndexManager initialized - Ready to speed things up!');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create a new index on a field
|
|
42
|
-
*/
|
|
43
|
-
async createIndex(fieldName, indexType = 'hash', options = {}) {
|
|
44
|
-
if (this.indexes.size >= this.options.maxIndexes) {
|
|
45
|
-
throw new Error(`Maximum index limit (${this.options.maxIndexes}) reached`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const indexName = this._getIndexName(fieldName, indexType);
|
|
49
|
-
|
|
50
|
-
if (this.indexes.has(indexName)) {
|
|
51
|
-
throw new Error(`Index '${indexName}' already exists`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
console.log(`🔄 Creating ${indexType} index on field: ${fieldName}`);
|
|
55
|
-
|
|
56
|
-
let index;
|
|
57
|
-
switch (indexType) {
|
|
58
|
-
case 'hash':
|
|
59
|
-
index = new HashIndex(fieldName, options);
|
|
60
|
-
break;
|
|
61
|
-
case 'range':
|
|
62
|
-
index = new RangeIndex(fieldName, options);
|
|
63
|
-
break;
|
|
64
|
-
case 'text':
|
|
65
|
-
index = new TextIndex(fieldName, options);
|
|
66
|
-
break;
|
|
67
|
-
default:
|
|
68
|
-
throw new Error(`Unsupported index type: ${indexType}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Build the index
|
|
72
|
-
await this._buildIndex(index, fieldName);
|
|
73
|
-
|
|
74
|
-
// Store the index
|
|
75
|
-
this.indexes.set(indexName, index);
|
|
76
|
-
|
|
77
|
-
// Track field indexes
|
|
78
|
-
if (!this.fieldIndexes.has(fieldName)) {
|
|
79
|
-
this.fieldIndexes.set(fieldName, new Set());
|
|
80
|
-
}
|
|
81
|
-
this.fieldIndexes.get(fieldName).add(indexName);
|
|
82
|
-
|
|
83
|
-
this.stats.indexesCreated++;
|
|
84
|
-
|
|
85
|
-
console.log(`✅ Index created: ${indexName} (${index.getStats().entries} entries)`);
|
|
86
|
-
|
|
87
|
-
return indexName;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Build index from existing data
|
|
92
|
-
*/
|
|
93
|
-
async _buildIndex(index, fieldName) {
|
|
94
|
-
const startTime = Date.now();
|
|
95
|
-
let entries = 0;
|
|
96
|
-
|
|
97
|
-
// Process in batches for large datasets
|
|
98
|
-
const batchSize = this.options.indexUpdateBatchSize;
|
|
99
|
-
const keys = Array.from(this.db.data.keys());
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < keys.length; i += batchSize) {
|
|
102
|
-
const batchKeys = keys.slice(i, i + batchSize);
|
|
103
|
-
|
|
104
|
-
for (const key of batchKeys) {
|
|
105
|
-
const value = this.db.data.get(key);
|
|
106
|
-
const fieldValue = this._getFieldValue(value, fieldName);
|
|
107
|
-
|
|
108
|
-
if (fieldValue !== undefined) {
|
|
109
|
-
index.add(fieldValue, key);
|
|
110
|
-
entries++;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Yield to event loop for large datasets
|
|
115
|
-
if (this.options.backgroundIndexing) {
|
|
116
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const buildTime = Date.now() - startTime;
|
|
121
|
-
console.log(`🔨 Built index in ${buildTime}ms: ${entries} entries`);
|
|
122
|
-
|
|
123
|
-
return entries;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Drop an index
|
|
128
|
-
*/
|
|
129
|
-
dropIndex(indexName) {
|
|
130
|
-
if (!this.indexes.has(indexName)) {
|
|
131
|
-
throw new Error(`Index '${indexName}' does not exist`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const index = this.indexes.get(indexName);
|
|
135
|
-
const fieldName = index.fieldName;
|
|
136
|
-
|
|
137
|
-
// Remove from indexes
|
|
138
|
-
this.indexes.delete(indexName);
|
|
139
|
-
|
|
140
|
-
// Remove from field indexes tracking
|
|
141
|
-
if (this.fieldIndexes.has(fieldName)) {
|
|
142
|
-
this.fieldIndexes.get(fieldName).delete(indexName);
|
|
143
|
-
if (this.fieldIndexes.get(fieldName).size === 0) {
|
|
144
|
-
this.fieldIndexes.delete(fieldName);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.stats.indexesDropped++;
|
|
149
|
-
|
|
150
|
-
console.log(`🗑️ Dropped index: ${indexName}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get all indexes
|
|
155
|
-
*/
|
|
156
|
-
getIndexes() {
|
|
157
|
-
const result = {};
|
|
158
|
-
|
|
159
|
-
for (const [name, index] of this.indexes) {
|
|
160
|
-
result[name] = index.getStats();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Check if a field has indexes
|
|
168
|
-
*/
|
|
169
|
-
hasIndex(fieldName) {
|
|
170
|
-
return this.fieldIndexes.has(fieldName) && this.fieldIndexes.get(fieldName).size > 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Find records using indexes
|
|
175
|
-
*/
|
|
176
|
-
find(fieldName, operator, value) {
|
|
177
|
-
const indexes = this.fieldIndexes.get(fieldName);
|
|
178
|
-
|
|
179
|
-
if (!indexes || indexes.size === 0) {
|
|
180
|
-
this.stats.queriesWithoutIndex++;
|
|
181
|
-
return null; // No index available
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Try to use the most appropriate index
|
|
185
|
-
for (const indexName of indexes) {
|
|
186
|
-
const index = this.indexes.get(indexName);
|
|
187
|
-
|
|
188
|
-
if (index.supportsOperator(operator)) {
|
|
189
|
-
const results = index.find(operator, value);
|
|
190
|
-
|
|
191
|
-
if (results !== null) {
|
|
192
|
-
this.stats.queriesWithIndex++;
|
|
193
|
-
|
|
194
|
-
if (this.options.debug) {
|
|
195
|
-
console.log(`⚡ Used index ${indexName} for ${fieldName} ${operator} ${value}`);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return results;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
this.stats.queriesWithoutIndex++;
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Update index when data changes
|
|
209
|
-
*/
|
|
210
|
-
updateIndex(key, newValue, oldValue = null) {
|
|
211
|
-
this.stats.indexUpdates++;
|
|
212
|
-
|
|
213
|
-
// Queue the update for background processing
|
|
214
|
-
this.indexQueue.push({ key, newValue, oldValue });
|
|
215
|
-
|
|
216
|
-
if (!this.isProcessingQueue && this.options.backgroundIndexing) {
|
|
217
|
-
this._processIndexQueue();
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Process index update queue in background
|
|
223
|
-
*/
|
|
224
|
-
async _processIndexQueue() {
|
|
225
|
-
if (this.isProcessingQueue || this.indexQueue.length === 0) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
this.isProcessingQueue = true;
|
|
230
|
-
this.stats.backgroundJobs++;
|
|
231
|
-
|
|
232
|
-
while (this.indexQueue.length > 0) {
|
|
233
|
-
const update = this.indexQueue.shift();
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
await this._processSingleUpdate(update);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
console.error('🚨 Index update failed:', error);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Yield to event loop every 100 updates
|
|
242
|
-
if (this.indexQueue.length % 100 === 0) {
|
|
243
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.isProcessingQueue = false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Process single index update
|
|
252
|
-
*/
|
|
253
|
-
async _processSingleUpdate(update) {
|
|
254
|
-
const { key, newValue, oldValue } = update;
|
|
255
|
-
|
|
256
|
-
for (const [indexName, index] of this.indexes) {
|
|
257
|
-
const fieldName = index.fieldName;
|
|
258
|
-
|
|
259
|
-
const oldFieldValue = oldValue ? this._getFieldValue(oldValue, fieldName) : undefined;
|
|
260
|
-
const newFieldValue = newValue ? this._getFieldValue(newValue, fieldName) : undefined;
|
|
261
|
-
|
|
262
|
-
// Remove old value from index
|
|
263
|
-
if (oldFieldValue !== undefined) {
|
|
264
|
-
index.remove(oldFieldValue, key);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Add new value to index
|
|
268
|
-
if (newFieldValue !== undefined) {
|
|
269
|
-
index.add(newFieldValue, key);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Get nested field value using dot notation
|
|
276
|
-
*/
|
|
277
|
-
_getFieldValue(obj, fieldPath) {
|
|
278
|
-
if (!fieldPath.includes('.')) {
|
|
279
|
-
return obj[fieldPath];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const parts = fieldPath.split('.');
|
|
283
|
-
let value = obj;
|
|
284
|
-
|
|
285
|
-
for (const part of parts) {
|
|
286
|
-
value = value?.[part];
|
|
287
|
-
if (value === undefined) break;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return value;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Generate index name from field and type
|
|
295
|
-
*/
|
|
296
|
-
_getIndexName(fieldName, indexType) {
|
|
297
|
-
return `${fieldName}_${indexType}_index`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Get performance statistics
|
|
302
|
-
*/
|
|
303
|
-
getStats() {
|
|
304
|
-
const totalQueries = this.stats.queriesWithIndex + this.stats.queriesWithoutIndex;
|
|
305
|
-
const indexUsage = totalQueries > 0
|
|
306
|
-
? (this.stats.queriesWithIndex / totalQueries * 100).toFixed(2)
|
|
307
|
-
: 0;
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
...this.stats,
|
|
311
|
-
totalIndexes: this.indexes.size,
|
|
312
|
-
indexUsage: `${indexUsage}%`,
|
|
313
|
-
queuedUpdates: this.indexQueue.length,
|
|
314
|
-
isProcessing: this.isProcessingQueue,
|
|
315
|
-
fieldsWithIndexes: Array.from(this.fieldIndexes.keys())
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Optimize indexes (reclaim memory, rebuild fragmented indexes)
|
|
321
|
-
*/
|
|
322
|
-
async optimize() {
|
|
323
|
-
console.log('🔧 Optimizing indexes...');
|
|
324
|
-
|
|
325
|
-
for (const [name, index] of this.indexes) {
|
|
326
|
-
await index.optimize();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
console.log('✅ Index optimization complete');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Clear all indexes
|
|
334
|
-
*/
|
|
335
|
-
clear() {
|
|
336
|
-
this.indexes.clear();
|
|
337
|
-
this.fieldIndexes.clear();
|
|
338
|
-
this.indexQueue = [];
|
|
339
|
-
|
|
340
|
-
console.log('🧹 Cleared all indexes');
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Base Index class
|
|
346
|
-
*/
|
|
347
|
-
class Index {
|
|
348
|
-
constructor(fieldName, options = {}) {
|
|
349
|
-
this.fieldName = fieldName;
|
|
350
|
-
this.options = options;
|
|
351
|
-
this.entries = 0;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
add(value, key) {
|
|
355
|
-
throw new Error('Method not implemented');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
remove(value, key) {
|
|
359
|
-
throw new Error('Method not implemented');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
find(operator, value) {
|
|
363
|
-
throw new Error('Method not implemented');
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
supportsOperator(operator) {
|
|
367
|
-
throw new Error('Method not implemented');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
optimize() {
|
|
371
|
-
// Default implementation - override if needed
|
|
372
|
-
return Promise.resolve();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
getStats() {
|
|
376
|
-
return {
|
|
377
|
-
fieldName: this.fieldName,
|
|
378
|
-
entries: this.entries,
|
|
379
|
-
type: this.constructor.name
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Hash Index - for equality queries (=, !=)
|
|
386
|
-
*/
|
|
387
|
-
class HashIndex extends Index {
|
|
388
|
-
constructor(fieldName, options = {}) {
|
|
389
|
-
super(fieldName, options);
|
|
390
|
-
this.index = new Map(); // value -> Set of keys
|
|
391
|
-
this.nullKeys = new Set();
|
|
392
|
-
this.undefinedKeys = new Set();
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
add(value, key) {
|
|
396
|
-
if (value === null) {
|
|
397
|
-
this.nullKeys.add(key);
|
|
398
|
-
} else if (value === undefined) {
|
|
399
|
-
this.undefinedKeys.add(key);
|
|
400
|
-
} else {
|
|
401
|
-
if (!this.index.has(value)) {
|
|
402
|
-
this.index.set(value, new Set());
|
|
403
|
-
}
|
|
404
|
-
this.index.get(value).add(key);
|
|
405
|
-
}
|
|
406
|
-
this.entries++;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
remove(value, key) {
|
|
410
|
-
if (value === null) {
|
|
411
|
-
this.nullKeys.delete(key);
|
|
412
|
-
} else if (value === undefined) {
|
|
413
|
-
this.undefinedKeys.delete(key);
|
|
414
|
-
} else {
|
|
415
|
-
const keys = this.index.get(value);
|
|
416
|
-
if (keys) {
|
|
417
|
-
keys.delete(key);
|
|
418
|
-
if (keys.size === 0) {
|
|
419
|
-
this.index.delete(value);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
this.entries--;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
find(operator, value) {
|
|
427
|
-
switch (operator) {
|
|
428
|
-
case '=':
|
|
429
|
-
return this._findEquals(value);
|
|
430
|
-
case '!=':
|
|
431
|
-
return this._findNotEquals(value);
|
|
432
|
-
case 'in':
|
|
433
|
-
return this._findIn(value);
|
|
434
|
-
default:
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
supportsOperator(operator) {
|
|
440
|
-
return ['=', '!=', 'in'].includes(operator);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
_findEquals(value) {
|
|
444
|
-
if (value === null) {
|
|
445
|
-
return Array.from(this.nullKeys);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const keys = this.index.get(value);
|
|
449
|
-
return keys ? Array.from(keys) : [];
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
_findNotEquals(value) {
|
|
453
|
-
const allKeys = new Set();
|
|
454
|
-
|
|
455
|
-
// Add all keys from other values
|
|
456
|
-
for (const [val, keys] of this.index) {
|
|
457
|
-
if (val !== value) {
|
|
458
|
-
for (const key of keys) {
|
|
459
|
-
allKeys.add(key);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Add null/undefined keys if not searching for them
|
|
465
|
-
if (value !== null) {
|
|
466
|
-
for (const key of this.nullKeys) {
|
|
467
|
-
allKeys.add(key);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (value !== undefined) {
|
|
471
|
-
for (const key of this.undefinedKeys) {
|
|
472
|
-
allKeys.add(key);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return Array.from(allKeys);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
_findIn(values) {
|
|
480
|
-
if (!Array.isArray(values)) {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const result = new Set();
|
|
485
|
-
|
|
486
|
-
for (const value of values) {
|
|
487
|
-
const keys = this._findEquals(value);
|
|
488
|
-
for (const key of keys) {
|
|
489
|
-
result.add(key);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
return Array.from(result);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
getStats() {
|
|
497
|
-
return {
|
|
498
|
-
...super.getStats(),
|
|
499
|
-
uniqueValues: this.index.size,
|
|
500
|
-
nullEntries: this.nullKeys.size,
|
|
501
|
-
undefinedEntries: this.undefinedKeys.size
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* Range Index - for comparison queries (>, <, >=, <=)
|
|
508
|
-
*/
|
|
509
|
-
class RangeIndex extends Index {
|
|
510
|
-
constructor(fieldName, options = {}) {
|
|
511
|
-
super(fieldName, options);
|
|
512
|
-
this.sortedValues = []; // Array of {value, key} sorted by value
|
|
513
|
-
this.valueMap = new Map(); // value -> Array of keys
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
add(value, key) {
|
|
517
|
-
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
518
|
-
return; // Only support numbers and strings for range queries
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (!this.valueMap.has(value)) {
|
|
522
|
-
this.valueMap.set(value, []);
|
|
523
|
-
|
|
524
|
-
// Insert into sorted array (maintain sorted order)
|
|
525
|
-
const index = this._findInsertionIndex(value);
|
|
526
|
-
this.sortedValues.splice(index, 0, { value, key });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
this.valueMap.get(value).push(key);
|
|
530
|
-
this.entries++;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
remove(value, key) {
|
|
534
|
-
const keys = this.valueMap.get(value);
|
|
535
|
-
if (keys) {
|
|
536
|
-
const keyIndex = keys.indexOf(key);
|
|
537
|
-
if (keyIndex > -1) {
|
|
538
|
-
keys.splice(keyIndex, 1);
|
|
539
|
-
|
|
540
|
-
if (keys.length === 0) {
|
|
541
|
-
this.valueMap.delete(value);
|
|
542
|
-
|
|
543
|
-
// Remove from sorted array
|
|
544
|
-
const index = this._findValueIndex(value);
|
|
545
|
-
if (index > -1) {
|
|
546
|
-
this.sortedValues.splice(index, 1);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
this.entries--;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
find(operator, value) {
|
|
555
|
-
switch (operator) {
|
|
556
|
-
case '>':
|
|
557
|
-
return this._findGreaterThan(value, false);
|
|
558
|
-
case '>=':
|
|
559
|
-
return this._findGreaterThan(value, true);
|
|
560
|
-
case '<':
|
|
561
|
-
return this._findLessThan(value, false);
|
|
562
|
-
case '<=':
|
|
563
|
-
return this._findLessThan(value, true);
|
|
564
|
-
default:
|
|
565
|
-
return null;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
supportsOperator(operator) {
|
|
570
|
-
return ['>', '>=', '<', '<='].includes(operator);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
_findGreaterThan(value, inclusive) {
|
|
574
|
-
const startIndex = this._findFirstIndexGreaterThan(value, inclusive);
|
|
575
|
-
if (startIndex === -1) return [];
|
|
576
|
-
|
|
577
|
-
const result = [];
|
|
578
|
-
for (let i = startIndex; i < this.sortedValues.length; i++) {
|
|
579
|
-
const { value: val, key } = this.sortedValues[i];
|
|
580
|
-
const keys = this.valueMap.get(val);
|
|
581
|
-
result.push(...keys);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return result;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
_findLessThan(value, inclusive) {
|
|
588
|
-
const endIndex = this._findLastIndexLessThan(value, inclusive);
|
|
589
|
-
if (endIndex === -1) return [];
|
|
590
|
-
|
|
591
|
-
const result = [];
|
|
592
|
-
for (let i = 0; i <= endIndex; i++) {
|
|
593
|
-
const { value: val, key } = this.sortedValues[i];
|
|
594
|
-
const keys = this.valueMap.get(val);
|
|
595
|
-
result.push(...keys);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
return result;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
_findFirstIndexGreaterThan(value, inclusive) {
|
|
602
|
-
let low = 0;
|
|
603
|
-
let high = this.sortedValues.length - 1;
|
|
604
|
-
let result = -1;
|
|
605
|
-
|
|
606
|
-
while (low <= high) {
|
|
607
|
-
const mid = Math.floor((low + high) / 2);
|
|
608
|
-
const midValue = this.sortedValues[mid].value;
|
|
609
|
-
|
|
610
|
-
if (inclusive ? midValue >= value : midValue > value) {
|
|
611
|
-
result = mid;
|
|
612
|
-
high = mid - 1;
|
|
613
|
-
} else {
|
|
614
|
-
low = mid + 1;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return result;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
_findLastIndexLessThan(value, inclusive) {
|
|
622
|
-
let low = 0;
|
|
623
|
-
let high = this.sortedValues.length - 1;
|
|
624
|
-
let result = -1;
|
|
625
|
-
|
|
626
|
-
while (low <= high) {
|
|
627
|
-
const mid = Math.floor((low + high) / 2);
|
|
628
|
-
const midValue = this.sortedValues[mid].value;
|
|
629
|
-
|
|
630
|
-
if (inclusive ? midValue <= value : midValue < value) {
|
|
631
|
-
result = mid;
|
|
632
|
-
low = mid + 1;
|
|
633
|
-
} else {
|
|
634
|
-
high = mid - 1;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
_findInsertionIndex(value) {
|
|
642
|
-
let low = 0;
|
|
643
|
-
let high = this.sortedValues.length;
|
|
644
|
-
|
|
645
|
-
while (low < high) {
|
|
646
|
-
const mid = Math.floor((low + high) / 2);
|
|
647
|
-
if (this.sortedValues[mid].value < value) {
|
|
648
|
-
low = mid + 1;
|
|
649
|
-
} else {
|
|
650
|
-
high = mid;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return low;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
_findValueIndex(value) {
|
|
658
|
-
let low = 0;
|
|
659
|
-
let high = this.sortedValues.length - 1;
|
|
660
|
-
|
|
661
|
-
while (low <= high) {
|
|
662
|
-
const mid = Math.floor((low + high) / 2);
|
|
663
|
-
const midValue = this.sortedValues[mid].value;
|
|
664
|
-
|
|
665
|
-
if (midValue === value) {
|
|
666
|
-
return mid;
|
|
667
|
-
} else if (midValue < value) {
|
|
668
|
-
low = mid + 1;
|
|
669
|
-
} else {
|
|
670
|
-
high = mid - 1;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
return -1;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
optimize() {
|
|
678
|
-
// Re-sort the array (should already be sorted, but just in case)
|
|
679
|
-
this.sortedValues.sort((a, b) => {
|
|
680
|
-
if (a.value < b.value) return -1;
|
|
681
|
-
if (a.value > b.value) return 1;
|
|
682
|
-
return 0;
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
getStats() {
|
|
687
|
-
return {
|
|
688
|
-
...super.getStats(),
|
|
689
|
-
valueRange: this.sortedValues.length > 0
|
|
690
|
-
? [this.sortedValues[0].value, this.sortedValues[this.sortedValues.length - 1].value]
|
|
691
|
-
: null
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/**
|
|
697
|
-
* Text Index - for text search queries (contains, startsWith, endsWith)
|
|
698
|
-
*/
|
|
699
|
-
class TextIndex extends Index {
|
|
700
|
-
constructor(fieldName, options = {}) {
|
|
701
|
-
super(fieldName, options);
|
|
702
|
-
this.trie = new Map(); // Simple prefix tree implementation
|
|
703
|
-
this.keys = new Set();
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
add(value, key) {
|
|
707
|
-
if (typeof value !== 'string') return;
|
|
708
|
-
|
|
709
|
-
const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
710
|
-
|
|
711
|
-
for (const word of words) {
|
|
712
|
-
if (!this.trie.has(word)) {
|
|
713
|
-
this.trie.set(word, new Set());
|
|
714
|
-
}
|
|
715
|
-
this.trie.get(word).add(key);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
this.keys.add(key);
|
|
719
|
-
this.entries++;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
remove(value, key) {
|
|
723
|
-
if (typeof value !== 'string') return;
|
|
724
|
-
|
|
725
|
-
const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
726
|
-
|
|
727
|
-
for (const word of words) {
|
|
728
|
-
const keys = this.trie.get(word);
|
|
729
|
-
if (keys) {
|
|
730
|
-
keys.delete(key);
|
|
731
|
-
if (keys.size === 0) {
|
|
732
|
-
this.trie.delete(word);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
this.keys.delete(key);
|
|
738
|
-
this.entries--;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
find(operator, value) {
|
|
742
|
-
if (typeof value !== 'string') return null;
|
|
743
|
-
|
|
744
|
-
switch (operator) {
|
|
745
|
-
case 'contains':
|
|
746
|
-
return this._findContains(value);
|
|
747
|
-
case 'startsWith':
|
|
748
|
-
return this._findStartsWith(value);
|
|
749
|
-
case 'endsWith':
|
|
750
|
-
return this._findEndsWith(value);
|
|
751
|
-
default:
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
supportsOperator(operator) {
|
|
757
|
-
return ['contains', 'startsWith', 'endsWith'].includes(operator);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
_findContains(searchTerm) {
|
|
761
|
-
const term = searchTerm.toLowerCase();
|
|
762
|
-
const result = new Set();
|
|
763
|
-
|
|
764
|
-
for (const [word, keys] of this.trie) {
|
|
765
|
-
if (word.includes(term)) {
|
|
766
|
-
for (const key of keys) {
|
|
767
|
-
result.add(key);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return Array.from(result);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
_findStartsWith(prefix) {
|
|
776
|
-
const prefixLower = prefix.toLowerCase();
|
|
777
|
-
const result = new Set();
|
|
778
|
-
|
|
779
|
-
for (const [word, keys] of this.trie) {
|
|
780
|
-
if (word.startsWith(prefixLower)) {
|
|
781
|
-
for (const key of keys) {
|
|
782
|
-
result.add(key);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
return Array.from(result);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
_findEndsWith(suffix) {
|
|
791
|
-
const suffixLower = suffix.toLowerCase();
|
|
792
|
-
const result = new Set();
|
|
793
|
-
|
|
794
|
-
for (const [word, keys] of this.trie) {
|
|
795
|
-
if (word.endsWith(suffixLower)) {
|
|
796
|
-
for (const key of keys) {
|
|
797
|
-
result.add(key);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
return Array.from(result);
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
getStats() {
|
|
806
|
-
return {
|
|
807
|
-
...super.getStats(),
|
|
808
|
-
uniqueWords: this.trie.size,
|
|
809
|
-
indexedKeys: this.keys.size
|
|
810
|
-
};
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
1
|
+
/**
|
|
2
|
+
* IndexManager - Makes queries lightning fast ⚡
|
|
3
|
+
*
|
|
4
|
+
* From O(n) to O(1) with the magic of indexing
|
|
5
|
+
* Because scanning millions of records should be illegal 😅
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class IndexManager {
|
|
9
|
+
constructor(database, options = {}) {
|
|
10
|
+
this.db = database;
|
|
11
|
+
this.options = {
|
|
12
|
+
autoIndex: true,
|
|
13
|
+
backgroundIndexing: true,
|
|
14
|
+
maxIndexes: 10,
|
|
15
|
+
indexUpdateBatchSize: 1000,
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Index storage
|
|
20
|
+
this.indexes = new Map(); // indexName -> Index instance
|
|
21
|
+
this.fieldIndexes = new Map(); // fieldName -> Set of indexNames
|
|
22
|
+
|
|
23
|
+
// Performance tracking
|
|
24
|
+
this.stats = {
|
|
25
|
+
indexesCreated: 0,
|
|
26
|
+
indexesDropped: 0,
|
|
27
|
+
queriesWithIndex: 0,
|
|
28
|
+
queriesWithoutIndex: 0,
|
|
29
|
+
indexUpdates: 0,
|
|
30
|
+
backgroundJobs: 0
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Background indexing queue
|
|
34
|
+
this.indexQueue = [];
|
|
35
|
+
this.isProcessingQueue = false;
|
|
36
|
+
|
|
37
|
+
console.log('📊 IndexManager initialized - Ready to speed things up!');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new index on a field
|
|
42
|
+
*/
|
|
43
|
+
async createIndex(fieldName, indexType = 'hash', options = {}) {
|
|
44
|
+
if (this.indexes.size >= this.options.maxIndexes) {
|
|
45
|
+
throw new Error(`Maximum index limit (${this.options.maxIndexes}) reached`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const indexName = this._getIndexName(fieldName, indexType);
|
|
49
|
+
|
|
50
|
+
if (this.indexes.has(indexName)) {
|
|
51
|
+
throw new Error(`Index '${indexName}' already exists`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`🔄 Creating ${indexType} index on field: ${fieldName}`);
|
|
55
|
+
|
|
56
|
+
let index;
|
|
57
|
+
switch (indexType) {
|
|
58
|
+
case 'hash':
|
|
59
|
+
index = new HashIndex(fieldName, options);
|
|
60
|
+
break;
|
|
61
|
+
case 'range':
|
|
62
|
+
index = new RangeIndex(fieldName, options);
|
|
63
|
+
break;
|
|
64
|
+
case 'text':
|
|
65
|
+
index = new TextIndex(fieldName, options);
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
throw new Error(`Unsupported index type: ${indexType}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build the index
|
|
72
|
+
await this._buildIndex(index, fieldName);
|
|
73
|
+
|
|
74
|
+
// Store the index
|
|
75
|
+
this.indexes.set(indexName, index);
|
|
76
|
+
|
|
77
|
+
// Track field indexes
|
|
78
|
+
if (!this.fieldIndexes.has(fieldName)) {
|
|
79
|
+
this.fieldIndexes.set(fieldName, new Set());
|
|
80
|
+
}
|
|
81
|
+
this.fieldIndexes.get(fieldName).add(indexName);
|
|
82
|
+
|
|
83
|
+
this.stats.indexesCreated++;
|
|
84
|
+
|
|
85
|
+
console.log(`✅ Index created: ${indexName} (${index.getStats().entries} entries)`);
|
|
86
|
+
|
|
87
|
+
return indexName;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build index from existing data
|
|
92
|
+
*/
|
|
93
|
+
async _buildIndex(index, fieldName) {
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
let entries = 0;
|
|
96
|
+
|
|
97
|
+
// Process in batches for large datasets
|
|
98
|
+
const batchSize = this.options.indexUpdateBatchSize;
|
|
99
|
+
const keys = Array.from(this.db.data.keys());
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
102
|
+
const batchKeys = keys.slice(i, i + batchSize);
|
|
103
|
+
|
|
104
|
+
for (const key of batchKeys) {
|
|
105
|
+
const value = this.db.data.get(key);
|
|
106
|
+
const fieldValue = this._getFieldValue(value, fieldName);
|
|
107
|
+
|
|
108
|
+
if (fieldValue !== undefined) {
|
|
109
|
+
index.add(fieldValue, key);
|
|
110
|
+
entries++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Yield to event loop for large datasets
|
|
115
|
+
if (this.options.backgroundIndexing) {
|
|
116
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const buildTime = Date.now() - startTime;
|
|
121
|
+
console.log(`🔨 Built index in ${buildTime}ms: ${entries} entries`);
|
|
122
|
+
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Drop an index
|
|
128
|
+
*/
|
|
129
|
+
dropIndex(indexName) {
|
|
130
|
+
if (!this.indexes.has(indexName)) {
|
|
131
|
+
throw new Error(`Index '${indexName}' does not exist`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const index = this.indexes.get(indexName);
|
|
135
|
+
const fieldName = index.fieldName;
|
|
136
|
+
|
|
137
|
+
// Remove from indexes
|
|
138
|
+
this.indexes.delete(indexName);
|
|
139
|
+
|
|
140
|
+
// Remove from field indexes tracking
|
|
141
|
+
if (this.fieldIndexes.has(fieldName)) {
|
|
142
|
+
this.fieldIndexes.get(fieldName).delete(indexName);
|
|
143
|
+
if (this.fieldIndexes.get(fieldName).size === 0) {
|
|
144
|
+
this.fieldIndexes.delete(fieldName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.stats.indexesDropped++;
|
|
149
|
+
|
|
150
|
+
console.log(`🗑️ Dropped index: ${indexName}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get all indexes
|
|
155
|
+
*/
|
|
156
|
+
getIndexes() {
|
|
157
|
+
const result = {};
|
|
158
|
+
|
|
159
|
+
for (const [name, index] of this.indexes) {
|
|
160
|
+
result[name] = index.getStats();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a field has indexes
|
|
168
|
+
*/
|
|
169
|
+
hasIndex(fieldName) {
|
|
170
|
+
return this.fieldIndexes.has(fieldName) && this.fieldIndexes.get(fieldName).size > 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Find records using indexes
|
|
175
|
+
*/
|
|
176
|
+
find(fieldName, operator, value) {
|
|
177
|
+
const indexes = this.fieldIndexes.get(fieldName);
|
|
178
|
+
|
|
179
|
+
if (!indexes || indexes.size === 0) {
|
|
180
|
+
this.stats.queriesWithoutIndex++;
|
|
181
|
+
return null; // No index available
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Try to use the most appropriate index
|
|
185
|
+
for (const indexName of indexes) {
|
|
186
|
+
const index = this.indexes.get(indexName);
|
|
187
|
+
|
|
188
|
+
if (index.supportsOperator(operator)) {
|
|
189
|
+
const results = index.find(operator, value);
|
|
190
|
+
|
|
191
|
+
if (results !== null) {
|
|
192
|
+
this.stats.queriesWithIndex++;
|
|
193
|
+
|
|
194
|
+
if (this.options.debug) {
|
|
195
|
+
console.log(`⚡ Used index ${indexName} for ${fieldName} ${operator} ${value}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return results;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.stats.queriesWithoutIndex++;
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Update index when data changes
|
|
209
|
+
*/
|
|
210
|
+
updateIndex(key, newValue, oldValue = null) {
|
|
211
|
+
this.stats.indexUpdates++;
|
|
212
|
+
|
|
213
|
+
// Queue the update for background processing
|
|
214
|
+
this.indexQueue.push({ key, newValue, oldValue });
|
|
215
|
+
|
|
216
|
+
if (!this.isProcessingQueue && this.options.backgroundIndexing) {
|
|
217
|
+
this._processIndexQueue();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Process index update queue in background
|
|
223
|
+
*/
|
|
224
|
+
async _processIndexQueue() {
|
|
225
|
+
if (this.isProcessingQueue || this.indexQueue.length === 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.isProcessingQueue = true;
|
|
230
|
+
this.stats.backgroundJobs++;
|
|
231
|
+
|
|
232
|
+
while (this.indexQueue.length > 0) {
|
|
233
|
+
const update = this.indexQueue.shift();
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await this._processSingleUpdate(update);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('🚨 Index update failed:', error);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Yield to event loop every 100 updates
|
|
242
|
+
if (this.indexQueue.length % 100 === 0) {
|
|
243
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.isProcessingQueue = false;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Process single index update
|
|
252
|
+
*/
|
|
253
|
+
async _processSingleUpdate(update) {
|
|
254
|
+
const { key, newValue, oldValue } = update;
|
|
255
|
+
|
|
256
|
+
for (const [indexName, index] of this.indexes) {
|
|
257
|
+
const fieldName = index.fieldName;
|
|
258
|
+
|
|
259
|
+
const oldFieldValue = oldValue ? this._getFieldValue(oldValue, fieldName) : undefined;
|
|
260
|
+
const newFieldValue = newValue ? this._getFieldValue(newValue, fieldName) : undefined;
|
|
261
|
+
|
|
262
|
+
// Remove old value from index
|
|
263
|
+
if (oldFieldValue !== undefined) {
|
|
264
|
+
index.remove(oldFieldValue, key);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add new value to index
|
|
268
|
+
if (newFieldValue !== undefined) {
|
|
269
|
+
index.add(newFieldValue, key);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get nested field value using dot notation
|
|
276
|
+
*/
|
|
277
|
+
_getFieldValue(obj, fieldPath) {
|
|
278
|
+
if (!fieldPath.includes('.')) {
|
|
279
|
+
return obj[fieldPath];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const parts = fieldPath.split('.');
|
|
283
|
+
let value = obj;
|
|
284
|
+
|
|
285
|
+
for (const part of parts) {
|
|
286
|
+
value = value?.[part];
|
|
287
|
+
if (value === undefined) break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generate index name from field and type
|
|
295
|
+
*/
|
|
296
|
+
_getIndexName(fieldName, indexType) {
|
|
297
|
+
return `${fieldName}_${indexType}_index`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get performance statistics
|
|
302
|
+
*/
|
|
303
|
+
getStats() {
|
|
304
|
+
const totalQueries = this.stats.queriesWithIndex + this.stats.queriesWithoutIndex;
|
|
305
|
+
const indexUsage = totalQueries > 0
|
|
306
|
+
? (this.stats.queriesWithIndex / totalQueries * 100).toFixed(2)
|
|
307
|
+
: 0;
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...this.stats,
|
|
311
|
+
totalIndexes: this.indexes.size,
|
|
312
|
+
indexUsage: `${indexUsage}%`,
|
|
313
|
+
queuedUpdates: this.indexQueue.length,
|
|
314
|
+
isProcessing: this.isProcessingQueue,
|
|
315
|
+
fieldsWithIndexes: Array.from(this.fieldIndexes.keys())
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Optimize indexes (reclaim memory, rebuild fragmented indexes)
|
|
321
|
+
*/
|
|
322
|
+
async optimize() {
|
|
323
|
+
console.log('🔧 Optimizing indexes...');
|
|
324
|
+
|
|
325
|
+
for (const [name, index] of this.indexes) {
|
|
326
|
+
await index.optimize();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log('✅ Index optimization complete');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Clear all indexes
|
|
334
|
+
*/
|
|
335
|
+
clear() {
|
|
336
|
+
this.indexes.clear();
|
|
337
|
+
this.fieldIndexes.clear();
|
|
338
|
+
this.indexQueue = [];
|
|
339
|
+
|
|
340
|
+
console.log('🧹 Cleared all indexes');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Base Index class
|
|
346
|
+
*/
|
|
347
|
+
class Index {
|
|
348
|
+
constructor(fieldName, options = {}) {
|
|
349
|
+
this.fieldName = fieldName;
|
|
350
|
+
this.options = options;
|
|
351
|
+
this.entries = 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
add(value, key) {
|
|
355
|
+
throw new Error('Method not implemented');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
remove(value, key) {
|
|
359
|
+
throw new Error('Method not implemented');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
find(operator, value) {
|
|
363
|
+
throw new Error('Method not implemented');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
supportsOperator(operator) {
|
|
367
|
+
throw new Error('Method not implemented');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
optimize() {
|
|
371
|
+
// Default implementation - override if needed
|
|
372
|
+
return Promise.resolve();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
getStats() {
|
|
376
|
+
return {
|
|
377
|
+
fieldName: this.fieldName,
|
|
378
|
+
entries: this.entries,
|
|
379
|
+
type: this.constructor.name
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Hash Index - for equality queries (=, !=)
|
|
386
|
+
*/
|
|
387
|
+
class HashIndex extends Index {
|
|
388
|
+
constructor(fieldName, options = {}) {
|
|
389
|
+
super(fieldName, options);
|
|
390
|
+
this.index = new Map(); // value -> Set of keys
|
|
391
|
+
this.nullKeys = new Set();
|
|
392
|
+
this.undefinedKeys = new Set();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
add(value, key) {
|
|
396
|
+
if (value === null) {
|
|
397
|
+
this.nullKeys.add(key);
|
|
398
|
+
} else if (value === undefined) {
|
|
399
|
+
this.undefinedKeys.add(key);
|
|
400
|
+
} else {
|
|
401
|
+
if (!this.index.has(value)) {
|
|
402
|
+
this.index.set(value, new Set());
|
|
403
|
+
}
|
|
404
|
+
this.index.get(value).add(key);
|
|
405
|
+
}
|
|
406
|
+
this.entries++;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
remove(value, key) {
|
|
410
|
+
if (value === null) {
|
|
411
|
+
this.nullKeys.delete(key);
|
|
412
|
+
} else if (value === undefined) {
|
|
413
|
+
this.undefinedKeys.delete(key);
|
|
414
|
+
} else {
|
|
415
|
+
const keys = this.index.get(value);
|
|
416
|
+
if (keys) {
|
|
417
|
+
keys.delete(key);
|
|
418
|
+
if (keys.size === 0) {
|
|
419
|
+
this.index.delete(value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
this.entries--;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
find(operator, value) {
|
|
427
|
+
switch (operator) {
|
|
428
|
+
case '=':
|
|
429
|
+
return this._findEquals(value);
|
|
430
|
+
case '!=':
|
|
431
|
+
return this._findNotEquals(value);
|
|
432
|
+
case 'in':
|
|
433
|
+
return this._findIn(value);
|
|
434
|
+
default:
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
supportsOperator(operator) {
|
|
440
|
+
return ['=', '!=', 'in'].includes(operator);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
_findEquals(value) {
|
|
444
|
+
if (value === null) {
|
|
445
|
+
return Array.from(this.nullKeys);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const keys = this.index.get(value);
|
|
449
|
+
return keys ? Array.from(keys) : [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
_findNotEquals(value) {
|
|
453
|
+
const allKeys = new Set();
|
|
454
|
+
|
|
455
|
+
// Add all keys from other values
|
|
456
|
+
for (const [val, keys] of this.index) {
|
|
457
|
+
if (val !== value) {
|
|
458
|
+
for (const key of keys) {
|
|
459
|
+
allKeys.add(key);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Add null/undefined keys if not searching for them
|
|
465
|
+
if (value !== null) {
|
|
466
|
+
for (const key of this.nullKeys) {
|
|
467
|
+
allKeys.add(key);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (value !== undefined) {
|
|
471
|
+
for (const key of this.undefinedKeys) {
|
|
472
|
+
allKeys.add(key);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return Array.from(allKeys);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_findIn(values) {
|
|
480
|
+
if (!Array.isArray(values)) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const result = new Set();
|
|
485
|
+
|
|
486
|
+
for (const value of values) {
|
|
487
|
+
const keys = this._findEquals(value);
|
|
488
|
+
for (const key of keys) {
|
|
489
|
+
result.add(key);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return Array.from(result);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
getStats() {
|
|
497
|
+
return {
|
|
498
|
+
...super.getStats(),
|
|
499
|
+
uniqueValues: this.index.size,
|
|
500
|
+
nullEntries: this.nullKeys.size,
|
|
501
|
+
undefinedEntries: this.undefinedKeys.size
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Range Index - for comparison queries (>, <, >=, <=)
|
|
508
|
+
*/
|
|
509
|
+
class RangeIndex extends Index {
|
|
510
|
+
constructor(fieldName, options = {}) {
|
|
511
|
+
super(fieldName, options);
|
|
512
|
+
this.sortedValues = []; // Array of {value, key} sorted by value
|
|
513
|
+
this.valueMap = new Map(); // value -> Array of keys
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
add(value, key) {
|
|
517
|
+
if (typeof value !== 'number' && typeof value !== 'string') {
|
|
518
|
+
return; // Only support numbers and strings for range queries
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!this.valueMap.has(value)) {
|
|
522
|
+
this.valueMap.set(value, []);
|
|
523
|
+
|
|
524
|
+
// Insert into sorted array (maintain sorted order)
|
|
525
|
+
const index = this._findInsertionIndex(value);
|
|
526
|
+
this.sortedValues.splice(index, 0, { value, key });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
this.valueMap.get(value).push(key);
|
|
530
|
+
this.entries++;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
remove(value, key) {
|
|
534
|
+
const keys = this.valueMap.get(value);
|
|
535
|
+
if (keys) {
|
|
536
|
+
const keyIndex = keys.indexOf(key);
|
|
537
|
+
if (keyIndex > -1) {
|
|
538
|
+
keys.splice(keyIndex, 1);
|
|
539
|
+
|
|
540
|
+
if (keys.length === 0) {
|
|
541
|
+
this.valueMap.delete(value);
|
|
542
|
+
|
|
543
|
+
// Remove from sorted array
|
|
544
|
+
const index = this._findValueIndex(value);
|
|
545
|
+
if (index > -1) {
|
|
546
|
+
this.sortedValues.splice(index, 1);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
this.entries--;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
find(operator, value) {
|
|
555
|
+
switch (operator) {
|
|
556
|
+
case '>':
|
|
557
|
+
return this._findGreaterThan(value, false);
|
|
558
|
+
case '>=':
|
|
559
|
+
return this._findGreaterThan(value, true);
|
|
560
|
+
case '<':
|
|
561
|
+
return this._findLessThan(value, false);
|
|
562
|
+
case '<=':
|
|
563
|
+
return this._findLessThan(value, true);
|
|
564
|
+
default:
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
supportsOperator(operator) {
|
|
570
|
+
return ['>', '>=', '<', '<='].includes(operator);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
_findGreaterThan(value, inclusive) {
|
|
574
|
+
const startIndex = this._findFirstIndexGreaterThan(value, inclusive);
|
|
575
|
+
if (startIndex === -1) return [];
|
|
576
|
+
|
|
577
|
+
const result = [];
|
|
578
|
+
for (let i = startIndex; i < this.sortedValues.length; i++) {
|
|
579
|
+
const { value: val, key } = this.sortedValues[i];
|
|
580
|
+
const keys = this.valueMap.get(val);
|
|
581
|
+
result.push(...keys);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_findLessThan(value, inclusive) {
|
|
588
|
+
const endIndex = this._findLastIndexLessThan(value, inclusive);
|
|
589
|
+
if (endIndex === -1) return [];
|
|
590
|
+
|
|
591
|
+
const result = [];
|
|
592
|
+
for (let i = 0; i <= endIndex; i++) {
|
|
593
|
+
const { value: val, key } = this.sortedValues[i];
|
|
594
|
+
const keys = this.valueMap.get(val);
|
|
595
|
+
result.push(...keys);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return result;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_findFirstIndexGreaterThan(value, inclusive) {
|
|
602
|
+
let low = 0;
|
|
603
|
+
let high = this.sortedValues.length - 1;
|
|
604
|
+
let result = -1;
|
|
605
|
+
|
|
606
|
+
while (low <= high) {
|
|
607
|
+
const mid = Math.floor((low + high) / 2);
|
|
608
|
+
const midValue = this.sortedValues[mid].value;
|
|
609
|
+
|
|
610
|
+
if (inclusive ? midValue >= value : midValue > value) {
|
|
611
|
+
result = mid;
|
|
612
|
+
high = mid - 1;
|
|
613
|
+
} else {
|
|
614
|
+
low = mid + 1;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_findLastIndexLessThan(value, inclusive) {
|
|
622
|
+
let low = 0;
|
|
623
|
+
let high = this.sortedValues.length - 1;
|
|
624
|
+
let result = -1;
|
|
625
|
+
|
|
626
|
+
while (low <= high) {
|
|
627
|
+
const mid = Math.floor((low + high) / 2);
|
|
628
|
+
const midValue = this.sortedValues[mid].value;
|
|
629
|
+
|
|
630
|
+
if (inclusive ? midValue <= value : midValue < value) {
|
|
631
|
+
result = mid;
|
|
632
|
+
low = mid + 1;
|
|
633
|
+
} else {
|
|
634
|
+
high = mid - 1;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return result;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
_findInsertionIndex(value) {
|
|
642
|
+
let low = 0;
|
|
643
|
+
let high = this.sortedValues.length;
|
|
644
|
+
|
|
645
|
+
while (low < high) {
|
|
646
|
+
const mid = Math.floor((low + high) / 2);
|
|
647
|
+
if (this.sortedValues[mid].value < value) {
|
|
648
|
+
low = mid + 1;
|
|
649
|
+
} else {
|
|
650
|
+
high = mid;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return low;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
_findValueIndex(value) {
|
|
658
|
+
let low = 0;
|
|
659
|
+
let high = this.sortedValues.length - 1;
|
|
660
|
+
|
|
661
|
+
while (low <= high) {
|
|
662
|
+
const mid = Math.floor((low + high) / 2);
|
|
663
|
+
const midValue = this.sortedValues[mid].value;
|
|
664
|
+
|
|
665
|
+
if (midValue === value) {
|
|
666
|
+
return mid;
|
|
667
|
+
} else if (midValue < value) {
|
|
668
|
+
low = mid + 1;
|
|
669
|
+
} else {
|
|
670
|
+
high = mid - 1;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return -1;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
optimize() {
|
|
678
|
+
// Re-sort the array (should already be sorted, but just in case)
|
|
679
|
+
this.sortedValues.sort((a, b) => {
|
|
680
|
+
if (a.value < b.value) return -1;
|
|
681
|
+
if (a.value > b.value) return 1;
|
|
682
|
+
return 0;
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
getStats() {
|
|
687
|
+
return {
|
|
688
|
+
...super.getStats(),
|
|
689
|
+
valueRange: this.sortedValues.length > 0
|
|
690
|
+
? [this.sortedValues[0].value, this.sortedValues[this.sortedValues.length - 1].value]
|
|
691
|
+
: null
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Text Index - for text search queries (contains, startsWith, endsWith)
|
|
698
|
+
*/
|
|
699
|
+
class TextIndex extends Index {
|
|
700
|
+
constructor(fieldName, options = {}) {
|
|
701
|
+
super(fieldName, options);
|
|
702
|
+
this.trie = new Map(); // Simple prefix tree implementation
|
|
703
|
+
this.keys = new Set();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
add(value, key) {
|
|
707
|
+
if (typeof value !== 'string') return;
|
|
708
|
+
|
|
709
|
+
const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
710
|
+
|
|
711
|
+
for (const word of words) {
|
|
712
|
+
if (!this.trie.has(word)) {
|
|
713
|
+
this.trie.set(word, new Set());
|
|
714
|
+
}
|
|
715
|
+
this.trie.get(word).add(key);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
this.keys.add(key);
|
|
719
|
+
this.entries++;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
remove(value, key) {
|
|
723
|
+
if (typeof value !== 'string') return;
|
|
724
|
+
|
|
725
|
+
const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
726
|
+
|
|
727
|
+
for (const word of words) {
|
|
728
|
+
const keys = this.trie.get(word);
|
|
729
|
+
if (keys) {
|
|
730
|
+
keys.delete(key);
|
|
731
|
+
if (keys.size === 0) {
|
|
732
|
+
this.trie.delete(word);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
this.keys.delete(key);
|
|
738
|
+
this.entries--;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
find(operator, value) {
|
|
742
|
+
if (typeof value !== 'string') return null;
|
|
743
|
+
|
|
744
|
+
switch (operator) {
|
|
745
|
+
case 'contains':
|
|
746
|
+
return this._findContains(value);
|
|
747
|
+
case 'startsWith':
|
|
748
|
+
return this._findStartsWith(value);
|
|
749
|
+
case 'endsWith':
|
|
750
|
+
return this._findEndsWith(value);
|
|
751
|
+
default:
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
supportsOperator(operator) {
|
|
757
|
+
return ['contains', 'startsWith', 'endsWith'].includes(operator);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
_findContains(searchTerm) {
|
|
761
|
+
const term = searchTerm.toLowerCase();
|
|
762
|
+
const result = new Set();
|
|
763
|
+
|
|
764
|
+
for (const [word, keys] of this.trie) {
|
|
765
|
+
if (word.includes(term)) {
|
|
766
|
+
for (const key of keys) {
|
|
767
|
+
result.add(key);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return Array.from(result);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_findStartsWith(prefix) {
|
|
776
|
+
const prefixLower = prefix.toLowerCase();
|
|
777
|
+
const result = new Set();
|
|
778
|
+
|
|
779
|
+
for (const [word, keys] of this.trie) {
|
|
780
|
+
if (word.startsWith(prefixLower)) {
|
|
781
|
+
for (const key of keys) {
|
|
782
|
+
result.add(key);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return Array.from(result);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_findEndsWith(suffix) {
|
|
791
|
+
const suffixLower = suffix.toLowerCase();
|
|
792
|
+
const result = new Set();
|
|
793
|
+
|
|
794
|
+
for (const [word, keys] of this.trie) {
|
|
795
|
+
if (word.endsWith(suffixLower)) {
|
|
796
|
+
for (const key of keys) {
|
|
797
|
+
result.add(key);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return Array.from(result);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
getStats() {
|
|
806
|
+
return {
|
|
807
|
+
...super.getStats(),
|
|
808
|
+
uniqueWords: this.trie.size,
|
|
809
|
+
indexedKeys: this.keys.size
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
814
|
module.exports = IndexManager;
|