mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-31 18:01:55 +00:00
Merge pull request #356 from PR0M3TH3AN/perf-lexical-search-index-1433868811411746305
⚡ Performance Optimization: Inverted Index for Lexical Search
This commit is contained in:
4 files changed
+127
-9
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# Lexical Search Baseline Performance
|
||||
|
||||
Date: Fri Feb 20 03:31:01 UTC 2026
|
||||
Environment: Sandbox
|
||||
|
||||
## Configuration
|
||||
- Number of memories: 10,000
|
||||
- Number of queries: 100
|
||||
- Search type: Full-scan regex matching
|
||||
|
||||
## Results
|
||||
Total time for 100 queries over 10000 memories: 1579.75ms
|
||||
Average time per query: 15.80ms
|
||||
@@ -0,0 +1,16 @@
|
||||
# Lexical Search Optimized Performance
|
||||
|
||||
Date: Fri Feb 20 03:36:33 UTC 2026
|
||||
|
||||
## Summary
|
||||
Implemented an inverted index with a WeakMap cache. The performance boost is significant, especially as the number of memories grows.
|
||||
|
||||
## Comparison (10,000 memories, 100 queries)
|
||||
- Baseline: 15.80ms / query
|
||||
- Optimized: 7.50ms / query
|
||||
- Improvement: 2.1x speedup
|
||||
|
||||
## Comparison (100,000 memories, 10 queries)
|
||||
- Baseline: 118.91ms / query
|
||||
- Optimized: 13.41ms / query
|
||||
- Improvement: 8.8x speedup
|
||||
@@ -80,9 +80,22 @@ async function saveMemoryStore(store) {
|
||||
const memoryStore = loadMemoryStore();
|
||||
const cache = createMemoryCache();
|
||||
|
||||
let storeVersion = 0;
|
||||
let cachedMemoriesArray = null;
|
||||
let cachedMemoriesVersion = -1;
|
||||
|
||||
function getMemoriesArray() {
|
||||
if (cachedMemoriesVersion !== storeVersion || !cachedMemoriesArray) {
|
||||
cachedMemoriesArray = [...memoryStore.values()];
|
||||
cachedMemoriesVersion = storeVersion;
|
||||
}
|
||||
return cachedMemoriesArray;
|
||||
}
|
||||
|
||||
const memoryRepository = {
|
||||
async insertMemory(memory) {
|
||||
memoryStore.set(memory.id, memory);
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
return memory;
|
||||
},
|
||||
@@ -91,16 +104,18 @@ const memoryRepository = {
|
||||
if (!existing) return null;
|
||||
const updated = { ...existing, last_seen: lastSeen };
|
||||
memoryStore.set(id, updated);
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
return updated;
|
||||
},
|
||||
async listPruneCandidates({ cutoff }) {
|
||||
return [...memoryStore.values()].filter((memory) => !memory.pinned && memory.last_seen < cutoff);
|
||||
return getMemoriesArray().filter((memory) => !memory.pinned && memory.last_seen < cutoff);
|
||||
},
|
||||
async markMerged(id, mergedInto) {
|
||||
const existing = memoryStore.get(id);
|
||||
if (!existing) return false;
|
||||
memoryStore.set(id, { ...existing, merged_into: mergedInto, last_seen: Date.now() });
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
return true;
|
||||
},
|
||||
@@ -109,6 +124,7 @@ const memoryRepository = {
|
||||
if (!existing) return null;
|
||||
const updated = { ...existing, pinned, last_seen: Date.now() };
|
||||
memoryStore.set(id, updated);
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
return updated;
|
||||
},
|
||||
@@ -116,7 +132,7 @@ const memoryRepository = {
|
||||
return memoryStore.get(id) ?? null;
|
||||
},
|
||||
async listMemories() {
|
||||
return [...memoryStore.values()];
|
||||
return getMemoriesArray();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -267,7 +283,7 @@ export async function getRelevantMemories(params) {
|
||||
|
||||
const source = typeof repository.listMemories === 'function'
|
||||
? await repository.listMemories(queryParams)
|
||||
: [...memoryStore.values()];
|
||||
: getMemoriesArray();
|
||||
|
||||
const ranked = await ranker(source, queryParams);
|
||||
await updateMemoryUsage(repository, ranked.map((memory) => memory.id));
|
||||
@@ -308,7 +324,7 @@ export async function runPruneCycle(options = {}) {
|
||||
|
||||
const prunable = dbCandidates.length > 0
|
||||
? dbCandidates
|
||||
: selectPrunableMemories([...memoryStore.values()], {
|
||||
: selectPrunableMemories(getMemoriesArray(), {
|
||||
retentionMs,
|
||||
now: options.now,
|
||||
});
|
||||
@@ -333,6 +349,7 @@ export async function runPruneCycle(options = {}) {
|
||||
}
|
||||
|
||||
if (prunable.length > 0) {
|
||||
storeVersion++;
|
||||
await saveMemoryStore(memoryStore);
|
||||
}
|
||||
|
||||
@@ -422,7 +439,7 @@ export async function listMemories(filters = {}, options = {}) {
|
||||
const repository = options.repository ?? memoryRepository;
|
||||
const source = typeof repository.listMemories === 'function'
|
||||
? await repository.listMemories(filters)
|
||||
: [...memoryStore.values()];
|
||||
: getMemoriesArray();
|
||||
|
||||
const filtered = applyMemoryFilters(source, filters)
|
||||
.sort((left, right) => right.last_seen - left.last_seen);
|
||||
|
||||
@@ -48,6 +48,58 @@ function textMatchSimilarity(queryTerms, queryRegexes, memory) {
|
||||
return matchedTerms / queryTerms.length;
|
||||
}
|
||||
|
||||
class LexicalIndex {
|
||||
constructor(memories) {
|
||||
this.index = new Map();
|
||||
this.memoriesById = new Map();
|
||||
|
||||
for (const memory of memories) {
|
||||
this.memoriesById.set(memory.id, memory);
|
||||
const text = `${memory.summary} ${memory.content}`.toLowerCase();
|
||||
const terms = text.split(/\W+/).filter(Boolean);
|
||||
const uniqueTerms = new Set(terms);
|
||||
|
||||
for (const term of uniqueTerms) {
|
||||
if (!this.index.has(term)) {
|
||||
this.index.set(term, []);
|
||||
}
|
||||
this.index.get(term).push(memory.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
search(queryTerms) {
|
||||
const candidateIds = new Set();
|
||||
for (const term of queryTerms) {
|
||||
const subTerms = term.toLowerCase().split(/\W+/).filter(Boolean);
|
||||
for (const subTerm of subTerms) {
|
||||
const matches = this.index.get(subTerm);
|
||||
if (matches) {
|
||||
for (const id of matches) {
|
||||
candidateIds.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidateIds;
|
||||
}
|
||||
|
||||
getMemory(id) {
|
||||
return this.memoriesById.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
const indexCache = new WeakMap();
|
||||
|
||||
function getOrCreateIndex(memories) {
|
||||
let index = indexCache.get(memories);
|
||||
if (!index) {
|
||||
index = new LexicalIndex(memories);
|
||||
indexCache.set(memories, index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./schema.js').MemoryRecord} memory
|
||||
* @param {{ tags: string[], timeframe?: { from?: number, to?: number }, pinnedPreference?: 'prefer' | 'only' | 'exclude' }} params
|
||||
@@ -108,10 +160,29 @@ async function buildSimilarityIndex(memories, params) {
|
||||
const queryTerms = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const queryRegexes = queryTerms.map((t) => new RegExp(escapeRegExp(t), 'i'));
|
||||
|
||||
for (const memory of memories) {
|
||||
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
|
||||
if (lexicalScore > 0) {
|
||||
similarityById.set(memory.id, lexicalScore);
|
||||
const isFiltered = params.sourceMemories && params.sourceMemories !== memories;
|
||||
const index = getOrCreateIndex(params.sourceMemories || memories);
|
||||
const candidateIds = index.search(queryTerms);
|
||||
|
||||
let targetIds;
|
||||
if (isFiltered) {
|
||||
if (candidateIds.size > memories.length) {
|
||||
targetIds = memories.filter((m) => candidateIds.has(m.id)).map((m) => m.id);
|
||||
} else {
|
||||
const memoriesInScope = new Set(memories.map((m) => m.id));
|
||||
targetIds = [...candidateIds].filter((id) => memoriesInScope.has(id));
|
||||
}
|
||||
} else {
|
||||
targetIds = candidateIds;
|
||||
}
|
||||
|
||||
for (const id of targetIds) {
|
||||
const memory = index.getMemory(id);
|
||||
if (memory) {
|
||||
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
|
||||
if (lexicalScore > 0) {
|
||||
similarityById.set(memory.id, lexicalScore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +225,7 @@ export async function filterAndRankMemories(memories, params) {
|
||||
k,
|
||||
vectorAdapter,
|
||||
embedText,
|
||||
sourceMemories: memories,
|
||||
});
|
||||
|
||||
return filtered
|
||||
|
||||
Reference in new issue
Block a user