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..7f4bf8f 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[] = []; @@ -52,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; @@ -165,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"); @@ -187,6 +193,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 +260,7 @@ async function performSearch(query: string): Promise { releaseSearchPriority(); } - const allEmbeddings = await getAllEmbeddings(); + const allEmbeddings = await getCachedEmbeddings(); const results = searchEmbeddings( queryEmbedding, allEmbeddings, @@ -410,6 +422,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") {