From bc567812207ef1b36f9b20b43db80fcd54785b24 Mon Sep 17 00:00:00 2001 From: Tim Waugh Date: Tue, 17 Mar 2026 10:05:02 +0000 Subject: [PATCH 1/2] Cache embeddings in memory for faster repeated searches Load embeddings from IndexedDB on first search, then reuse the cached array for subsequent searches within the same modal session. The cache is invalidated on IDB writes, when the modal opens, and when indexing completes. An eviction timer releases memory 60s after the modal closes. Co-Authored-By: Claude Opus 4.6 --- src/indexer.ts | 4 ++++ src/storage.ts | 22 +++++++++++++++++++++- src/ui.ts | 18 ++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/indexer.ts b/src/indexer.ts index 1e44992..d8cbb21 100644 --- a/src/indexer.ts +++ b/src/indexer.ts @@ -9,6 +9,7 @@ import { getMetadata, setMetadata, getEmbeddingCount, + invalidateEmbeddingCache, } from "./storage"; import { getSettings } from "./settings"; @@ -406,6 +407,9 @@ export async function indexBlocks( await setMetadata("blockCount", count); await setMetadata("lastIndexed", Date.now()); + // Ensure search cache is fresh after indexing + invalidateEmbeddingCache(); + if (!abort.signal.aborted && total > 0) { logseq.UI.showMsg(`Indexed ${total} blocks`); } diff --git a/src/storage.ts b/src/storage.ts index 75e1812..9e6765e 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -89,6 +89,7 @@ export async function putEmbeddings(records: EmbeddingRecord[]): Promise { } tx.oncomplete = () => { db.close(); + invalidateEmbeddingCache(); resolve(); }; tx.onerror = () => { db.close(); reject(tx.error); }; @@ -107,6 +108,24 @@ export async function getAllEmbeddings(): Promise { }); } +// In-memory embedding cache for fast repeated searches +let embeddingCache: EmbeddingRecord[] | null = null; + +export async function getCachedEmbeddings(): Promise { + if (!embeddingCache) { + embeddingCache = await getAllEmbeddings(); + } + return embeddingCache; +} + +export function invalidateEmbeddingCache(): void { + embeddingCache = null; +} + +export function evictEmbeddingCache(): void { + embeddingCache = null; +} + export async function deleteEmbeddings(blockIds: string[]): Promise { if (blockIds.length === 0) return; const db = await openDB(); @@ -118,6 +137,7 @@ export async function deleteEmbeddings(blockIds: string[]): Promise { } tx.oncomplete = () => { db.close(); + invalidateEmbeddingCache(); resolve(); }; tx.onerror = () => { db.close(); reject(tx.error); }; @@ -130,7 +150,7 @@ export async function clearAllEmbeddings(): Promise { const tx = db.transaction(EMBEDDINGS_STORE, "readwrite"); const store = tx.objectStore(EMBEDDINGS_STORE); store.clear(); - tx.oncomplete = () => { db.close(); resolve(); }; + tx.oncomplete = () => { db.close(); invalidateEmbeddingCache(); resolve(); }; tx.onerror = () => { db.close(); reject(tx.error); }; }); } diff --git a/src/ui.ts b/src/ui.ts index 053fcfc..049afcf 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,6 +1,6 @@ import { debounce } from "./utils"; import { embedTexts } from "./embeddings"; -import { getAllEmbeddings, getEmbeddingCount } from "./storage"; +import { getCachedEmbeddings, getEmbeddingCount, evictEmbeddingCache } from "./storage"; import { searchEmbeddings, type SearchResult } from "./search"; import { indexBlocks, indexingState, acquireSearchPriority, releaseSearchPriority } from "./indexer"; import { getSettings } from "./settings"; @@ -13,6 +13,8 @@ interface DisplayResult extends SearchResult { } let progressInterval: ReturnType | undefined; +let evictTimer: ReturnType | undefined; +const CACHE_EVICT_MS = 60_000; let lastDisplayResults: DisplayResult[] = []; let lastQuery = ""; const queryHistory: string[] = []; @@ -187,6 +189,12 @@ function hideModal(): void { } addToHistory(lastQuery); logseq.hideMainUI(); + // Evict embedding cache after idle period + if (evictTimer) clearTimeout(evictTimer); + evictTimer = setTimeout(() => { + evictEmbeddingCache(); + evictTimer = undefined; + }, CACHE_EVICT_MS); } async function updateStatus(): Promise { @@ -248,7 +256,7 @@ async function performSearch(query: string): Promise { releaseSearchPriority(); } - const allEmbeddings = await getAllEmbeddings(); + const allEmbeddings = await getCachedEmbeddings(); const results = searchEmbeddings( queryEmbedding, allEmbeddings, @@ -410,6 +418,12 @@ function escapeHtml(text: string): string { } export function showModal(): void { + if (evictTimer) { + clearTimeout(evictTimer); + evictTimer = undefined; + } + // Invalidate cache so first search loads fresh from IDB + evictEmbeddingCache(); clearResults(); logseq.showMainUI(); if (indexingState.status === "idle") { From 5318c63088c8590190ef09f12bf4d9cbe51cdcbd Mon Sep 17 00:00:00 2001 From: Tim Waugh Date: Tue, 17 Mar 2026 10:07:54 +0000 Subject: [PATCH 2/2] Increase search debounce to 500ms and add Enter to search Reduce unnecessary API calls while typing by increasing the debounce delay. Allow pressing Enter to trigger an immediate search when no result is selected. Co-Authored-By: Claude Opus 4.6 --- src/ui.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ui.ts b/src/ui.ts index 049afcf..7f4bf8f 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -54,7 +54,7 @@ export function createSearchModal(): void { const debouncedSearch = debounce(async (...args: unknown[]) => { const query = args[0] as string; await performSearch(query); - }, 300); + }, 500); input.addEventListener("input", () => { historyIndex = -1; @@ -167,6 +167,10 @@ function handleKeydown(e: KeyboardEvent): void { (active as HTMLElement).dispatchEvent( new MouseEvent("click", { shiftKey: e.shiftKey, bubbles: true }), ); + } else if (e.key === "Enter" && !active && input && document.activeElement === input) { + e.preventDefault(); + const query = input.value.trim(); + if (query) performSearch(query); } else if (e.key === "c" && (e.ctrlKey || e.metaKey) && active) { e.preventDefault(); const blockId = active.getAttribute("data-block-id");