@danielsimonjr/memory-mcp 12.2.3 → 12.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -102
- package/dist/index.js +6 -0
- package/dist/server/toolDefinitions.d.ts.map +1 -1
- package/dist/server/toolDefinitions.js +745 -3
- package/dist/server/toolHandlers.d.ts +1 -9
- package/dist/server/toolHandlers.d.ts.map +1 -1
- package/dist/server/toolHandlers.js +859 -11
- package/package.json +9 -6
- package/dist/core/EntityManager.d.ts +0 -263
- package/dist/core/EntityManager.d.ts.map +0 -1
- package/dist/core/EntityManager.js +0 -478
- package/dist/core/GraphStorage.d.ts +0 -344
- package/dist/core/GraphStorage.d.ts.map +0 -1
- package/dist/core/GraphStorage.js +0 -708
- package/dist/core/GraphTraversal.d.ts +0 -136
- package/dist/core/GraphTraversal.d.ts.map +0 -1
- package/dist/core/GraphTraversal.js +0 -556
- package/dist/core/HierarchyManager.d.ts +0 -111
- package/dist/core/HierarchyManager.d.ts.map +0 -1
- package/dist/core/HierarchyManager.js +0 -224
- package/dist/core/ManagerContext.d.ts +0 -72
- package/dist/core/ManagerContext.d.ts.map +0 -1
- package/dist/core/ManagerContext.js +0 -118
- package/dist/core/ObservationManager.d.ts +0 -85
- package/dist/core/ObservationManager.d.ts.map +0 -1
- package/dist/core/ObservationManager.js +0 -123
- package/dist/core/RelationManager.d.ts +0 -131
- package/dist/core/RelationManager.d.ts.map +0 -1
- package/dist/core/RelationManager.js +0 -210
- package/dist/core/SQLiteStorage.d.ts +0 -354
- package/dist/core/SQLiteStorage.d.ts.map +0 -1
- package/dist/core/SQLiteStorage.js +0 -917
- package/dist/core/StorageFactory.d.ts +0 -45
- package/dist/core/StorageFactory.d.ts.map +0 -1
- package/dist/core/StorageFactory.js +0 -64
- package/dist/core/TransactionManager.d.ts +0 -259
- package/dist/core/TransactionManager.d.ts.map +0 -1
- package/dist/core/TransactionManager.js +0 -392
- package/dist/core/index.d.ts +0 -16
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -17
- package/dist/features/AnalyticsManager.d.ts +0 -44
- package/dist/features/AnalyticsManager.d.ts.map +0 -1
- package/dist/features/AnalyticsManager.js +0 -223
- package/dist/features/ArchiveManager.d.ts +0 -121
- package/dist/features/ArchiveManager.d.ts.map +0 -1
- package/dist/features/ArchiveManager.js +0 -240
- package/dist/features/CompressionManager.d.ts +0 -109
- package/dist/features/CompressionManager.d.ts.map +0 -1
- package/dist/features/CompressionManager.js +0 -404
- package/dist/features/IOManager.d.ts +0 -221
- package/dist/features/IOManager.d.ts.map +0 -1
- package/dist/features/IOManager.js +0 -997
- package/dist/features/StreamingExporter.d.ts +0 -105
- package/dist/features/StreamingExporter.d.ts.map +0 -1
- package/dist/features/StreamingExporter.js +0 -150
- package/dist/features/TagManager.d.ts +0 -147
- package/dist/features/TagManager.d.ts.map +0 -1
- package/dist/features/TagManager.js +0 -210
- package/dist/features/index.d.ts +0 -12
- package/dist/features/index.d.ts.map +0 -1
- package/dist/features/index.js +0 -11
- package/dist/search/BasicSearch.d.ts +0 -51
- package/dist/search/BasicSearch.d.ts.map +0 -1
- package/dist/search/BasicSearch.js +0 -137
- package/dist/search/BooleanSearch.d.ts +0 -98
- package/dist/search/BooleanSearch.d.ts.map +0 -1
- package/dist/search/BooleanSearch.js +0 -430
- package/dist/search/EmbeddingService.d.ts +0 -178
- package/dist/search/EmbeddingService.d.ts.map +0 -1
- package/dist/search/EmbeddingService.js +0 -358
- package/dist/search/FuzzySearch.d.ts +0 -118
- package/dist/search/FuzzySearch.d.ts.map +0 -1
- package/dist/search/FuzzySearch.js +0 -312
- package/dist/search/RankedSearch.d.ts +0 -71
- package/dist/search/RankedSearch.d.ts.map +0 -1
- package/dist/search/RankedSearch.js +0 -238
- package/dist/search/SavedSearchManager.d.ts +0 -79
- package/dist/search/SavedSearchManager.d.ts.map +0 -1
- package/dist/search/SavedSearchManager.js +0 -145
- package/dist/search/SearchFilterChain.d.ts +0 -120
- package/dist/search/SearchFilterChain.d.ts.map +0 -1
- package/dist/search/SearchFilterChain.js +0 -185
- package/dist/search/SearchManager.d.ts +0 -285
- package/dist/search/SearchManager.d.ts.map +0 -1
- package/dist/search/SearchManager.js +0 -338
- package/dist/search/SearchSuggestions.d.ts +0 -27
- package/dist/search/SearchSuggestions.d.ts.map +0 -1
- package/dist/search/SearchSuggestions.js +0 -57
- package/dist/search/SemanticSearch.d.ts +0 -146
- package/dist/search/SemanticSearch.d.ts.map +0 -1
- package/dist/search/SemanticSearch.js +0 -313
- package/dist/search/TFIDFIndexManager.d.ts +0 -63
- package/dist/search/TFIDFIndexManager.d.ts.map +0 -1
- package/dist/search/TFIDFIndexManager.js +0 -215
- package/dist/search/VectorStore.d.ts +0 -235
- package/dist/search/VectorStore.d.ts.map +0 -1
- package/dist/search/VectorStore.js +0 -311
- package/dist/search/index.d.ts +0 -18
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js +0 -21
- package/dist/types/index.d.ts +0 -13
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -12
- package/dist/types/types.d.ts +0 -1234
- package/dist/types/types.d.ts.map +0 -1
- package/dist/types/types.js +0 -9
- package/dist/utils/compressedCache.d.ts +0 -192
- package/dist/utils/compressedCache.d.ts.map +0 -1
- package/dist/utils/compressedCache.js +0 -309
- package/dist/utils/compressionUtil.d.ts +0 -214
- package/dist/utils/compressionUtil.d.ts.map +0 -1
- package/dist/utils/compressionUtil.js +0 -247
- package/dist/utils/constants.d.ts +0 -245
- package/dist/utils/constants.d.ts.map +0 -1
- package/dist/utils/constants.js +0 -252
- package/dist/utils/entityUtils.d.ts +0 -321
- package/dist/utils/entityUtils.d.ts.map +0 -1
- package/dist/utils/entityUtils.js +0 -538
- package/dist/utils/errors.d.ts +0 -77
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -121
- package/dist/utils/formatters.d.ts +0 -145
- package/dist/utils/formatters.d.ts.map +0 -1
- package/dist/utils/formatters.js +0 -132
- package/dist/utils/index.d.ts +0 -22
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -75
- package/dist/utils/indexes.d.ts +0 -270
- package/dist/utils/indexes.d.ts.map +0 -1
- package/dist/utils/indexes.js +0 -526
- package/dist/utils/logger.d.ts +0 -24
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -33
- package/dist/utils/parallelUtils.d.ts +0 -72
- package/dist/utils/parallelUtils.d.ts.map +0 -1
- package/dist/utils/parallelUtils.js +0 -169
- package/dist/utils/schemas.d.ts +0 -374
- package/dist/utils/schemas.d.ts.map +0 -1
- package/dist/utils/schemas.js +0 -484
- package/dist/utils/searchAlgorithms.d.ts +0 -99
- package/dist/utils/searchAlgorithms.d.ts.map +0 -1
- package/dist/utils/searchAlgorithms.js +0 -167
- package/dist/utils/searchCache.d.ts +0 -108
- package/dist/utils/searchCache.d.ts.map +0 -1
- package/dist/utils/searchCache.js +0 -209
- package/dist/utils/taskScheduler.d.ts +0 -290
- package/dist/utils/taskScheduler.d.ts.map +0 -1
- package/dist/utils/taskScheduler.js +0 -466
- package/dist/workers/WorkerPool.d.ts +0 -81
- package/dist/workers/WorkerPool.d.ts.map +0 -1
- package/dist/workers/WorkerPool.js +0 -121
- package/dist/workers/index.d.ts +0 -12
- package/dist/workers/index.d.ts.map +0 -1
- package/dist/workers/index.js +0 -9
- package/dist/workers/levenshteinWorker.d.ts +0 -60
- package/dist/workers/levenshteinWorker.d.ts.map +0 -1
- package/dist/workers/levenshteinWorker.js +0 -98
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fuzzy Search
|
|
3
|
-
*
|
|
4
|
-
* Search with typo tolerance using Levenshtein distance similarity.
|
|
5
|
-
* Uses workerpool for parallel processing on large datasets.
|
|
6
|
-
*
|
|
7
|
-
* @module search/FuzzySearch
|
|
8
|
-
*/
|
|
9
|
-
import { levenshteinDistance } from '../utils/index.js';
|
|
10
|
-
import { SEARCH_LIMITS } from '../utils/constants.js';
|
|
11
|
-
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
12
|
-
import workerpool from '@danielsimonjr/workerpool/modern';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { dirname, join, sep } from 'path';
|
|
15
|
-
/**
|
|
16
|
-
* Default fuzzy search similarity threshold (70% match required).
|
|
17
|
-
* Lower values are more permissive (more typos tolerated).
|
|
18
|
-
* Higher values are stricter (fewer typos tolerated).
|
|
19
|
-
*/
|
|
20
|
-
export const DEFAULT_FUZZY_THRESHOLD = 0.7;
|
|
21
|
-
/**
|
|
22
|
-
* Phase 4 Sprint 3: Maximum cache size to prevent memory bloat.
|
|
23
|
-
*/
|
|
24
|
-
const FUZZY_CACHE_MAX_SIZE = 100;
|
|
25
|
-
/**
|
|
26
|
-
* Phase 4 Sprint 3: Cache TTL in milliseconds (5 minutes).
|
|
27
|
-
*/
|
|
28
|
-
const FUZZY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
29
|
-
/**
|
|
30
|
-
* Phase 7 Sprint 3: Minimum number of entities to activate worker pool.
|
|
31
|
-
*/
|
|
32
|
-
const WORKER_MIN_ENTITIES = 500;
|
|
33
|
-
/**
|
|
34
|
-
* Phase 7 Sprint 3: Maximum threshold for worker pool activation.
|
|
35
|
-
* Higher thresholds have fewer matches, so single-threaded is faster.
|
|
36
|
-
*/
|
|
37
|
-
const WORKER_MAX_THRESHOLD = 0.8;
|
|
38
|
-
/**
|
|
39
|
-
* Performs fuzzy search with configurable similarity threshold.
|
|
40
|
-
*/
|
|
41
|
-
export class FuzzySearch {
|
|
42
|
-
storage;
|
|
43
|
-
/**
|
|
44
|
-
* Phase 4 Sprint 3: Result cache for fuzzy search.
|
|
45
|
-
* Maps cache key -> cached entity names.
|
|
46
|
-
*/
|
|
47
|
-
fuzzyResultCache = new Map();
|
|
48
|
-
/**
|
|
49
|
-
* Phase 8: Worker pool using workerpool library.
|
|
50
|
-
* Initialized lazily when needed.
|
|
51
|
-
*/
|
|
52
|
-
workerPool = null;
|
|
53
|
-
/**
|
|
54
|
-
* Phase 7 Sprint 3: Path to the worker script.
|
|
55
|
-
*/
|
|
56
|
-
workerPath;
|
|
57
|
-
/**
|
|
58
|
-
* Phase 8: Whether to use worker pool for parallel processing.
|
|
59
|
-
* Can be disabled for testing or when workers are not available.
|
|
60
|
-
*/
|
|
61
|
-
useWorkerPool;
|
|
62
|
-
constructor(storage, options = {}) {
|
|
63
|
-
this.storage = storage;
|
|
64
|
-
this.useWorkerPool = options.useWorkerPool ?? true;
|
|
65
|
-
// Calculate worker path using ESM module resolution
|
|
66
|
-
const currentFileUrl = import.meta.url;
|
|
67
|
-
const currentDir = dirname(fileURLToPath(currentFileUrl));
|
|
68
|
-
// Check if we're running from src/ (during tests) or dist/ (production)
|
|
69
|
-
const isRunningFromSrc = currentDir.includes(`${sep}src${sep}`);
|
|
70
|
-
if (isRunningFromSrc) {
|
|
71
|
-
// During tests, worker is in dist/workers/ relative to project root
|
|
72
|
-
const projectRoot = join(currentDir, '..', '..');
|
|
73
|
-
this.workerPath = join(projectRoot, 'dist', 'workers', 'levenshteinWorker.js');
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
// In production, worker is in dist/workers/ relative to current dist/search/
|
|
77
|
-
this.workerPath = join(currentDir, '..', 'workers', 'levenshteinWorker.js');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Phase 4 Sprint 3: Generate cache key for fuzzy search parameters.
|
|
82
|
-
*/
|
|
83
|
-
generateCacheKey(query, threshold, tags, minImportance, maxImportance, offset, limit) {
|
|
84
|
-
return JSON.stringify({
|
|
85
|
-
q: query.toLowerCase(),
|
|
86
|
-
t: threshold,
|
|
87
|
-
tags: tags?.sort().join(',') ?? '',
|
|
88
|
-
min: minImportance,
|
|
89
|
-
max: maxImportance,
|
|
90
|
-
off: offset,
|
|
91
|
-
lim: limit,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Phase 4 Sprint 3: Clear the fuzzy search cache.
|
|
96
|
-
*/
|
|
97
|
-
clearCache() {
|
|
98
|
-
this.fuzzyResultCache.clear();
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Phase 4 Sprint 3: Invalidate stale cache entries.
|
|
102
|
-
*/
|
|
103
|
-
cleanupCache() {
|
|
104
|
-
const now = Date.now();
|
|
105
|
-
const entries = Array.from(this.fuzzyResultCache.entries());
|
|
106
|
-
// Remove expired entries
|
|
107
|
-
for (const [key, entry] of entries) {
|
|
108
|
-
if (now - entry.timestamp > FUZZY_CACHE_TTL_MS) {
|
|
109
|
-
this.fuzzyResultCache.delete(key);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// If still over limit, remove oldest entries
|
|
113
|
-
if (this.fuzzyResultCache.size > FUZZY_CACHE_MAX_SIZE) {
|
|
114
|
-
const sortedEntries = entries
|
|
115
|
-
.filter(([k]) => this.fuzzyResultCache.has(k))
|
|
116
|
-
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
117
|
-
const toRemove = sortedEntries.slice(0, this.fuzzyResultCache.size - FUZZY_CACHE_MAX_SIZE);
|
|
118
|
-
for (const [key] of toRemove) {
|
|
119
|
-
this.fuzzyResultCache.delete(key);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Fuzzy search for entities with typo tolerance and pagination.
|
|
125
|
-
*
|
|
126
|
-
* Uses Levenshtein distance to calculate similarity between strings.
|
|
127
|
-
* Matches if similarity >= threshold (0.0 to 1.0).
|
|
128
|
-
*
|
|
129
|
-
* Phase 4 Sprint 3: Implements result caching for repeated queries.
|
|
130
|
-
*
|
|
131
|
-
* @param query - Search query
|
|
132
|
-
* @param threshold - Similarity threshold (0.0 to 1.0), default DEFAULT_FUZZY_THRESHOLD
|
|
133
|
-
* @param tags - Optional tags filter
|
|
134
|
-
* @param minImportance - Optional minimum importance
|
|
135
|
-
* @param maxImportance - Optional maximum importance
|
|
136
|
-
* @param offset - Number of results to skip (default: 0)
|
|
137
|
-
* @param limit - Maximum number of results (default: 50, max: 200)
|
|
138
|
-
* @returns Filtered knowledge graph with fuzzy matches and pagination applied
|
|
139
|
-
*/
|
|
140
|
-
async fuzzySearch(query, threshold = DEFAULT_FUZZY_THRESHOLD, tags, minImportance, maxImportance, offset = 0, limit = SEARCH_LIMITS.DEFAULT) {
|
|
141
|
-
const graph = await this.storage.loadGraph();
|
|
142
|
-
const queryLower = query.toLowerCase();
|
|
143
|
-
// Phase 4 Sprint 3: Generate cache key and check cache
|
|
144
|
-
const cacheKey = this.generateCacheKey(query, threshold, tags, minImportance, maxImportance, offset, limit);
|
|
145
|
-
const cached = this.fuzzyResultCache.get(cacheKey);
|
|
146
|
-
// Check if cache is valid (entity count hasn't changed)
|
|
147
|
-
if (cached && cached.entityCount === graph.entities.length) {
|
|
148
|
-
const now = Date.now();
|
|
149
|
-
if (now - cached.timestamp < FUZZY_CACHE_TTL_MS) {
|
|
150
|
-
// Return cached results
|
|
151
|
-
const cachedNameSet = new Set(cached.entityNames);
|
|
152
|
-
const cachedEntities = graph.entities.filter(e => cachedNameSet.has(e.name));
|
|
153
|
-
const cachedEntityNames = new Set(cached.entityNames);
|
|
154
|
-
const cachedRelations = graph.relations.filter(r => cachedEntityNames.has(r.from) && cachedEntityNames.has(r.to));
|
|
155
|
-
return { entities: cachedEntities, relations: cachedRelations };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
// Phase 7 Sprint 3: Use worker pool for large graphs with low thresholds
|
|
159
|
-
// Phase 8: Respect useWorkerPool flag for testing
|
|
160
|
-
const shouldUseWorkers = this.useWorkerPool &&
|
|
161
|
-
graph.entities.length >= WORKER_MIN_ENTITIES &&
|
|
162
|
-
threshold < WORKER_MAX_THRESHOLD;
|
|
163
|
-
let fuzzyMatched;
|
|
164
|
-
if (shouldUseWorkers) {
|
|
165
|
-
fuzzyMatched = await this.searchWithWorkers(query, threshold, graph.entities);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
// Perform single-threaded fuzzy search
|
|
169
|
-
fuzzyMatched = this.performFuzzyMatch(graph.entities, queryLower, threshold);
|
|
170
|
-
}
|
|
171
|
-
// Apply tag and importance filters using SearchFilterChain
|
|
172
|
-
const filters = { tags, minImportance, maxImportance };
|
|
173
|
-
const filteredEntities = SearchFilterChain.applyFilters(fuzzyMatched, filters);
|
|
174
|
-
// Apply pagination using SearchFilterChain
|
|
175
|
-
const pagination = SearchFilterChain.validatePagination(offset, limit);
|
|
176
|
-
const paginatedEntities = SearchFilterChain.paginate(filteredEntities, pagination);
|
|
177
|
-
// Phase 4 Sprint 3: Cache the results
|
|
178
|
-
this.fuzzyResultCache.set(cacheKey, {
|
|
179
|
-
entityNames: paginatedEntities.map(e => e.name),
|
|
180
|
-
entityCount: graph.entities.length,
|
|
181
|
-
timestamp: Date.now(),
|
|
182
|
-
});
|
|
183
|
-
// Cleanup old cache entries periodically
|
|
184
|
-
if (this.fuzzyResultCache.size > FUZZY_CACHE_MAX_SIZE / 2) {
|
|
185
|
-
this.cleanupCache();
|
|
186
|
-
}
|
|
187
|
-
const filteredEntityNames = new Set(paginatedEntities.map(e => e.name));
|
|
188
|
-
const filteredRelations = graph.relations.filter(r => filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to));
|
|
189
|
-
return {
|
|
190
|
-
entities: paginatedEntities,
|
|
191
|
-
relations: filteredRelations,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Phase 4 Sprint 3: Perform the actual fuzzy matching logic.
|
|
196
|
-
* Extracted from fuzzySearch for cleaner code structure.
|
|
197
|
-
*/
|
|
198
|
-
performFuzzyMatch(entities, queryLower, threshold) {
|
|
199
|
-
return entities.filter(e => {
|
|
200
|
-
const lowercased = this.storage.getLowercased(e.name);
|
|
201
|
-
// Check name match (use pre-computed lowercase)
|
|
202
|
-
const nameLower = lowercased?.name ?? e.name.toLowerCase();
|
|
203
|
-
if (this.isFuzzyMatchLower(nameLower, queryLower, threshold))
|
|
204
|
-
return true;
|
|
205
|
-
// Check type match (use pre-computed lowercase)
|
|
206
|
-
const typeLower = lowercased?.entityType ?? e.entityType.toLowerCase();
|
|
207
|
-
if (this.isFuzzyMatchLower(typeLower, queryLower, threshold))
|
|
208
|
-
return true;
|
|
209
|
-
// Check observations (use pre-computed lowercase array)
|
|
210
|
-
const obsLower = lowercased?.observations ?? e.observations.map(o => o.toLowerCase());
|
|
211
|
-
return obsLower.some(o =>
|
|
212
|
-
// For observations, split into words and check each word
|
|
213
|
-
o
|
|
214
|
-
.split(/\s+/)
|
|
215
|
-
.some(word => this.isFuzzyMatchLower(word, queryLower, threshold)) ||
|
|
216
|
-
// Also check if the observation contains the query
|
|
217
|
-
this.isFuzzyMatchLower(o, queryLower, threshold));
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Check if two already-lowercase strings match with fuzzy logic.
|
|
222
|
-
*
|
|
223
|
-
* OPTIMIZED: Skips toLowerCase() calls when strings are already lowercase.
|
|
224
|
-
*
|
|
225
|
-
* @param s1 - First string (already lowercase)
|
|
226
|
-
* @param s2 - Second string (already lowercase)
|
|
227
|
-
* @param threshold - Similarity threshold (0.0 to 1.0)
|
|
228
|
-
* @returns True if strings match fuzzily
|
|
229
|
-
*/
|
|
230
|
-
isFuzzyMatchLower(s1, s2, threshold = 0.7) {
|
|
231
|
-
// Exact match
|
|
232
|
-
if (s1 === s2)
|
|
233
|
-
return true;
|
|
234
|
-
// One contains the other
|
|
235
|
-
if (s1.includes(s2) || s2.includes(s1))
|
|
236
|
-
return true;
|
|
237
|
-
// Calculate similarity using Levenshtein distance
|
|
238
|
-
const distance = levenshteinDistance(s1, s2);
|
|
239
|
-
const maxLength = Math.max(s1.length, s2.length);
|
|
240
|
-
const similarity = 1 - distance / maxLength;
|
|
241
|
-
return similarity >= threshold;
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Phase 8: Perform fuzzy search using workerpool for parallel processing.
|
|
245
|
-
*
|
|
246
|
-
* Splits entities into chunks and processes them in parallel using worker threads.
|
|
247
|
-
* Falls back to single-threaded search if worker execution fails.
|
|
248
|
-
*
|
|
249
|
-
* @param query - Search query
|
|
250
|
-
* @param threshold - Similarity threshold
|
|
251
|
-
* @param entities - Entities to search
|
|
252
|
-
* @returns Array of matched entities
|
|
253
|
-
*/
|
|
254
|
-
async searchWithWorkers(query, threshold, entities) {
|
|
255
|
-
try {
|
|
256
|
-
// Initialize worker pool lazily using workerpool
|
|
257
|
-
if (!this.workerPool) {
|
|
258
|
-
// Enable ESM module support for Node.js 20+
|
|
259
|
-
// The 'type: module' option is needed for ESM workers but may not be in @types/node
|
|
260
|
-
const workerThreadOpts = { type: 'module' };
|
|
261
|
-
this.workerPool = workerpool.pool(this.workerPath, {
|
|
262
|
-
maxWorkers: Math.max(1, workerpool.cpus - 1),
|
|
263
|
-
workerType: 'thread',
|
|
264
|
-
workerThreadOpts,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
// Split entities into chunks based on CPU count
|
|
268
|
-
const numWorkers = Math.max(1, workerpool.cpus - 1);
|
|
269
|
-
const chunkSize = Math.ceil(entities.length / numWorkers);
|
|
270
|
-
const chunks = [];
|
|
271
|
-
for (let i = 0; i < entities.length; i += chunkSize) {
|
|
272
|
-
chunks.push(entities.slice(i, i + chunkSize));
|
|
273
|
-
}
|
|
274
|
-
// Prepare worker inputs with lowercased data
|
|
275
|
-
const workerInputs = chunks.map(chunk => ({
|
|
276
|
-
query,
|
|
277
|
-
threshold,
|
|
278
|
-
entities: chunk.map(e => ({
|
|
279
|
-
name: e.name,
|
|
280
|
-
nameLower: e.name.toLowerCase(),
|
|
281
|
-
observations: e.observations.map(o => o.toLowerCase()),
|
|
282
|
-
})),
|
|
283
|
-
}));
|
|
284
|
-
// Execute all chunks in parallel using workerpool with timeout
|
|
285
|
-
const WORKER_TIMEOUT_MS = 30000; // 30 seconds
|
|
286
|
-
const results = await Promise.all(workerInputs.map(input => this.workerPool.exec('searchEntities', [input])
|
|
287
|
-
.timeout(WORKER_TIMEOUT_MS)));
|
|
288
|
-
// Flatten results and extract matched entity names
|
|
289
|
-
const matchedNames = new Set(results.flat().map(r => r.name));
|
|
290
|
-
// Return entities that matched
|
|
291
|
-
return entities.filter(e => matchedNames.has(e.name));
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
// Worker execution failed - fall back to single-threaded mode
|
|
295
|
-
console.warn(`Worker pool execution failed, falling back to single-threaded fuzzy search: ${error instanceof Error ? error.message : String(error)}`);
|
|
296
|
-
// Use the existing single-threaded implementation
|
|
297
|
-
const queryLower = query.toLowerCase();
|
|
298
|
-
return this.performFuzzyMatch(entities, queryLower, threshold);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* Phase 8: Shutdown the worker pool and clean up resources.
|
|
303
|
-
*
|
|
304
|
-
* Should be called when FuzzySearch is no longer needed.
|
|
305
|
-
*/
|
|
306
|
-
async shutdown() {
|
|
307
|
-
if (this.workerPool) {
|
|
308
|
-
await this.workerPool.terminate();
|
|
309
|
-
this.workerPool = null;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ranked Search
|
|
3
|
-
*
|
|
4
|
-
* TF-IDF relevance-based search with scoring and pre-calculated indexes.
|
|
5
|
-
*
|
|
6
|
-
* @module search/RankedSearch
|
|
7
|
-
*/
|
|
8
|
-
import type { SearchResult } from '../types/index.js';
|
|
9
|
-
import type { GraphStorage } from '../core/GraphStorage.js';
|
|
10
|
-
/**
|
|
11
|
-
* Performs TF-IDF ranked search with optional pre-calculated indexes.
|
|
12
|
-
*/
|
|
13
|
-
export declare class RankedSearch {
|
|
14
|
-
private storage;
|
|
15
|
-
private indexManager;
|
|
16
|
-
/**
|
|
17
|
-
* Phase 4 Sprint 2: Fallback token cache for entities.
|
|
18
|
-
* Maps entity name -> pre-tokenized entity data.
|
|
19
|
-
* Invalidated when graph changes (detected by entity count mismatch).
|
|
20
|
-
*/
|
|
21
|
-
private fallbackTokenCache;
|
|
22
|
-
private cachedEntityCount;
|
|
23
|
-
constructor(storage: GraphStorage, storageDir?: string);
|
|
24
|
-
/**
|
|
25
|
-
* Phase 4 Sprint 2: Clear the fallback token cache.
|
|
26
|
-
* Called when graph changes are detected or explicitly by external code.
|
|
27
|
-
*/
|
|
28
|
-
clearTokenCache(): void;
|
|
29
|
-
/**
|
|
30
|
-
* Initialize and build the TF-IDF index for fast searches.
|
|
31
|
-
*
|
|
32
|
-
* Should be called after graph changes to keep index up-to-date.
|
|
33
|
-
*/
|
|
34
|
-
buildIndex(): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
* Update the index incrementally after entity changes.
|
|
37
|
-
*
|
|
38
|
-
* @param changedEntityNames - Names of entities that were created, updated, or deleted
|
|
39
|
-
*/
|
|
40
|
-
updateIndex(changedEntityNames: Set<string>): Promise<void>;
|
|
41
|
-
/**
|
|
42
|
-
* Load the TF-IDF index from disk if available.
|
|
43
|
-
*/
|
|
44
|
-
private ensureIndexLoaded;
|
|
45
|
-
/**
|
|
46
|
-
* Search with TF-IDF relevance ranking.
|
|
47
|
-
*
|
|
48
|
-
* Uses pre-calculated index if available, falls back to on-the-fly calculation.
|
|
49
|
-
*
|
|
50
|
-
* @param query - Search query
|
|
51
|
-
* @param tags - Optional tags filter
|
|
52
|
-
* @param minImportance - Optional minimum importance
|
|
53
|
-
* @param maxImportance - Optional maximum importance
|
|
54
|
-
* @param limit - Maximum results to return (default 50, max 200)
|
|
55
|
-
* @returns Array of search results sorted by relevance
|
|
56
|
-
*/
|
|
57
|
-
searchNodesRanked(query: string, tags?: string[], minImportance?: number, maxImportance?: number, limit?: number): Promise<SearchResult[]>;
|
|
58
|
-
/**
|
|
59
|
-
* Search using pre-calculated TF-IDF index (fast path).
|
|
60
|
-
*/
|
|
61
|
-
private searchWithIndex;
|
|
62
|
-
/**
|
|
63
|
-
* Search without index (on-the-fly calculation, slow path).
|
|
64
|
-
*
|
|
65
|
-
* OPTIMIZED: Phase 4 Sprint 2 - Uses fallback token cache to avoid
|
|
66
|
-
* repeated tokenization of entities. Pre-tokenizes all documents once
|
|
67
|
-
* and caches for subsequent searches.
|
|
68
|
-
*/
|
|
69
|
-
private searchWithoutIndex;
|
|
70
|
-
}
|
|
71
|
-
//# sourceMappingURL=RankedSearch.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RankedSearch.d.ts","sourceRoot":"","sources":["../../src/search/RankedSearch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAU,YAAY,EAA+B,MAAM,mBAAmB,CAAC;AAC3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAM5D;;GAEG;AACH,qBAAa,YAAY;IAYrB,OAAO,CAAC,OAAO;IAXjB,OAAO,CAAC,YAAY,CAAkC;IAEtD;;;;OAIG;IACH,OAAO,CAAC,kBAAkB,CAA2C;IACrE,OAAO,CAAC,iBAAiB,CAAa;gBAG5B,OAAO,EAAE,YAAY,EAC7B,UAAU,CAAC,EAAE,MAAM;IAQrB;;;OAGG;IACH,eAAe,IAAI,IAAI;IAKvB;;;;OAIG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAUjC;;;;OAIG;IACG,WAAW,CAAC,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjE;;OAEG;YACW,iBAAiB;IAe/B;;;;;;;;;;;OAWG;IACG,iBAAiB,CACrB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EAAE,EACf,aAAa,CAAC,EAAE,MAAM,EACtB,aAAa,CAAC,EAAE,MAAM,EACtB,KAAK,GAAE,MAA8B,GACpC,OAAO,CAAC,YAAY,EAAE,CAAC;IAsB1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAgEvB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;CAqF3B"}
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ranked Search
|
|
3
|
-
*
|
|
4
|
-
* TF-IDF relevance-based search with scoring and pre-calculated indexes.
|
|
5
|
-
*
|
|
6
|
-
* @module search/RankedSearch
|
|
7
|
-
*/
|
|
8
|
-
import { calculateTF, calculateIDFFromTokenSets, tokenize } from '../utils/index.js';
|
|
9
|
-
import { SEARCH_LIMITS } from '../utils/constants.js';
|
|
10
|
-
import { TFIDFIndexManager } from './TFIDFIndexManager.js';
|
|
11
|
-
import { SearchFilterChain } from './SearchFilterChain.js';
|
|
12
|
-
/**
|
|
13
|
-
* Performs TF-IDF ranked search with optional pre-calculated indexes.
|
|
14
|
-
*/
|
|
15
|
-
export class RankedSearch {
|
|
16
|
-
storage;
|
|
17
|
-
indexManager = null;
|
|
18
|
-
/**
|
|
19
|
-
* Phase 4 Sprint 2: Fallback token cache for entities.
|
|
20
|
-
* Maps entity name -> pre-tokenized entity data.
|
|
21
|
-
* Invalidated when graph changes (detected by entity count mismatch).
|
|
22
|
-
*/
|
|
23
|
-
fallbackTokenCache = new Map();
|
|
24
|
-
cachedEntityCount = 0;
|
|
25
|
-
constructor(storage, storageDir) {
|
|
26
|
-
this.storage = storage;
|
|
27
|
-
// Initialize index manager if storage directory is provided
|
|
28
|
-
if (storageDir) {
|
|
29
|
-
this.indexManager = new TFIDFIndexManager(storageDir);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Phase 4 Sprint 2: Clear the fallback token cache.
|
|
34
|
-
* Called when graph changes are detected or explicitly by external code.
|
|
35
|
-
*/
|
|
36
|
-
clearTokenCache() {
|
|
37
|
-
this.fallbackTokenCache.clear();
|
|
38
|
-
this.cachedEntityCount = 0;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Initialize and build the TF-IDF index for fast searches.
|
|
42
|
-
*
|
|
43
|
-
* Should be called after graph changes to keep index up-to-date.
|
|
44
|
-
*/
|
|
45
|
-
async buildIndex() {
|
|
46
|
-
if (!this.indexManager) {
|
|
47
|
-
throw new Error('Index manager not initialized. Provide storageDir to constructor.');
|
|
48
|
-
}
|
|
49
|
-
const graph = await this.storage.loadGraph();
|
|
50
|
-
await this.indexManager.buildIndex(graph);
|
|
51
|
-
await this.indexManager.saveIndex();
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Update the index incrementally after entity changes.
|
|
55
|
-
*
|
|
56
|
-
* @param changedEntityNames - Names of entities that were created, updated, or deleted
|
|
57
|
-
*/
|
|
58
|
-
async updateIndex(changedEntityNames) {
|
|
59
|
-
if (!this.indexManager) {
|
|
60
|
-
return; // No index manager, skip
|
|
61
|
-
}
|
|
62
|
-
const graph = await this.storage.loadGraph();
|
|
63
|
-
await this.indexManager.updateIndex(graph, changedEntityNames);
|
|
64
|
-
await this.indexManager.saveIndex();
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Load the TF-IDF index from disk if available.
|
|
68
|
-
*/
|
|
69
|
-
async ensureIndexLoaded() {
|
|
70
|
-
if (!this.indexManager) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
// Return cached index if already loaded
|
|
74
|
-
const cached = this.indexManager.getIndex();
|
|
75
|
-
if (cached) {
|
|
76
|
-
return cached;
|
|
77
|
-
}
|
|
78
|
-
// Try to load from disk
|
|
79
|
-
return await this.indexManager.loadIndex();
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Search with TF-IDF relevance ranking.
|
|
83
|
-
*
|
|
84
|
-
* Uses pre-calculated index if available, falls back to on-the-fly calculation.
|
|
85
|
-
*
|
|
86
|
-
* @param query - Search query
|
|
87
|
-
* @param tags - Optional tags filter
|
|
88
|
-
* @param minImportance - Optional minimum importance
|
|
89
|
-
* @param maxImportance - Optional maximum importance
|
|
90
|
-
* @param limit - Maximum results to return (default 50, max 200)
|
|
91
|
-
* @returns Array of search results sorted by relevance
|
|
92
|
-
*/
|
|
93
|
-
async searchNodesRanked(query, tags, minImportance, maxImportance, limit = SEARCH_LIMITS.DEFAULT) {
|
|
94
|
-
// Enforce maximum search limit
|
|
95
|
-
const effectiveLimit = Math.min(limit, SEARCH_LIMITS.MAX);
|
|
96
|
-
const graph = await this.storage.loadGraph();
|
|
97
|
-
// Apply tag and importance filters using SearchFilterChain
|
|
98
|
-
const filters = { tags, minImportance, maxImportance };
|
|
99
|
-
const filteredEntities = SearchFilterChain.applyFilters(graph.entities, filters);
|
|
100
|
-
// Try to use pre-calculated index
|
|
101
|
-
const index = await this.ensureIndexLoaded();
|
|
102
|
-
const queryTerms = tokenize(query);
|
|
103
|
-
if (index) {
|
|
104
|
-
// Use pre-calculated index for fast search
|
|
105
|
-
return this.searchWithIndex(filteredEntities, queryTerms, index, effectiveLimit);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
// Fall back to on-the-fly calculation
|
|
109
|
-
return this.searchWithoutIndex(filteredEntities, queryTerms, effectiveLimit);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Search using pre-calculated TF-IDF index (fast path).
|
|
114
|
-
*/
|
|
115
|
-
searchWithIndex(entities, queryTerms, index, limit) {
|
|
116
|
-
const results = [];
|
|
117
|
-
for (const entity of entities) {
|
|
118
|
-
const docVector = index.documents.get(entity.name);
|
|
119
|
-
if (!docVector) {
|
|
120
|
-
continue; // Entity not in index
|
|
121
|
-
}
|
|
122
|
-
// Calculate total terms in document (sum of all term frequencies)
|
|
123
|
-
const totalTerms = Object.values(docVector.terms).reduce((sum, count) => sum + count, 0);
|
|
124
|
-
if (totalTerms === 0)
|
|
125
|
-
continue;
|
|
126
|
-
// Calculate score using pre-calculated term frequencies and IDF
|
|
127
|
-
let totalScore = 0;
|
|
128
|
-
const matchedFields = {};
|
|
129
|
-
for (const term of queryTerms) {
|
|
130
|
-
const termCount = docVector.terms[term] || 0;
|
|
131
|
-
const idf = index.idf.get(term) || 0;
|
|
132
|
-
// Calculate TF-IDF: (termCount / totalTerms) * IDF
|
|
133
|
-
const tf = termCount / totalTerms;
|
|
134
|
-
const tfidf = tf * idf;
|
|
135
|
-
totalScore += tfidf;
|
|
136
|
-
// Track which fields matched
|
|
137
|
-
if (termCount > 0) {
|
|
138
|
-
if (entity.name.toLowerCase().includes(term)) {
|
|
139
|
-
matchedFields.name = true;
|
|
140
|
-
}
|
|
141
|
-
if (entity.entityType.toLowerCase().includes(term)) {
|
|
142
|
-
matchedFields.entityType = true;
|
|
143
|
-
}
|
|
144
|
-
const matchedObs = entity.observations.filter(o => o.toLowerCase().includes(term));
|
|
145
|
-
if (matchedObs.length > 0) {
|
|
146
|
-
matchedFields.observations = matchedObs;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
// Only include entities with non-zero scores
|
|
151
|
-
if (totalScore > 0) {
|
|
152
|
-
results.push({
|
|
153
|
-
entity,
|
|
154
|
-
score: totalScore,
|
|
155
|
-
matchedFields,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// Sort by score descending and apply limit
|
|
160
|
-
return results
|
|
161
|
-
.sort((a, b) => b.score - a.score)
|
|
162
|
-
.slice(0, limit);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Search without index (on-the-fly calculation, slow path).
|
|
166
|
-
*
|
|
167
|
-
* OPTIMIZED: Phase 4 Sprint 2 - Uses fallback token cache to avoid
|
|
168
|
-
* repeated tokenization of entities. Pre-tokenizes all documents once
|
|
169
|
-
* and caches for subsequent searches.
|
|
170
|
-
*/
|
|
171
|
-
searchWithoutIndex(entities, queryTerms, limit) {
|
|
172
|
-
const results = [];
|
|
173
|
-
// Phase 4 Sprint 2: Check if cache needs invalidation
|
|
174
|
-
if (entities.length !== this.cachedEntityCount) {
|
|
175
|
-
this.clearTokenCache();
|
|
176
|
-
this.cachedEntityCount = entities.length;
|
|
177
|
-
}
|
|
178
|
-
// Phase 4 Sprint 2: Get or compute tokenized data for each entity
|
|
179
|
-
const documentData = entities.map(e => {
|
|
180
|
-
// Check cache first
|
|
181
|
-
const cached = this.fallbackTokenCache.get(e.name);
|
|
182
|
-
if (cached) {
|
|
183
|
-
return cached;
|
|
184
|
-
}
|
|
185
|
-
// Compute and cache tokenized data
|
|
186
|
-
const text = [e.name, e.entityType, ...e.observations].join(' ');
|
|
187
|
-
const tokens = tokenize(text);
|
|
188
|
-
const tokenized = {
|
|
189
|
-
entity: e,
|
|
190
|
-
text,
|
|
191
|
-
tokens,
|
|
192
|
-
tokenSet: new Set(tokens),
|
|
193
|
-
};
|
|
194
|
-
this.fallbackTokenCache.set(e.name, tokenized);
|
|
195
|
-
return tokenized;
|
|
196
|
-
});
|
|
197
|
-
// Pre-compute token sets for IDF calculation (O(1) lookup per document)
|
|
198
|
-
const tokenSets = documentData.map(d => d.tokenSet);
|
|
199
|
-
for (const docData of documentData) {
|
|
200
|
-
const { entity, text } = docData;
|
|
201
|
-
// Calculate score for each query term
|
|
202
|
-
let totalScore = 0;
|
|
203
|
-
const matchedFields = {};
|
|
204
|
-
for (const term of queryTerms) {
|
|
205
|
-
// Calculate TF using pre-tokenized tokens
|
|
206
|
-
const tf = calculateTF(term, text);
|
|
207
|
-
// Calculate IDF using pre-computed token sets (O(1) per document)
|
|
208
|
-
const idf = calculateIDFFromTokenSets(term, tokenSets);
|
|
209
|
-
// TF-IDF score
|
|
210
|
-
const score = tf * idf;
|
|
211
|
-
totalScore += score;
|
|
212
|
-
// Track which fields matched
|
|
213
|
-
if (entity.name.toLowerCase().includes(term)) {
|
|
214
|
-
matchedFields.name = true;
|
|
215
|
-
}
|
|
216
|
-
if (entity.entityType.toLowerCase().includes(term)) {
|
|
217
|
-
matchedFields.entityType = true;
|
|
218
|
-
}
|
|
219
|
-
const matchedObs = entity.observations.filter(o => o.toLowerCase().includes(term));
|
|
220
|
-
if (matchedObs.length > 0) {
|
|
221
|
-
matchedFields.observations = matchedObs;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
// Only include entities with non-zero scores
|
|
225
|
-
if (totalScore > 0) {
|
|
226
|
-
results.push({
|
|
227
|
-
entity,
|
|
228
|
-
score: totalScore,
|
|
229
|
-
matchedFields,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Sort by score descending and apply limit
|
|
234
|
-
return results
|
|
235
|
-
.sort((a, b) => b.score - a.score)
|
|
236
|
-
.slice(0, limit);
|
|
237
|
-
}
|
|
238
|
-
}
|