vecfinder

Document searching tool based on embedding vectors
git clone git://git.finwo.net/app/vecfinder
Log | Files | Refs

commit 11ac9246196ae3b3c68a367fdc91afc2b8ef522a
parent 41b9f26d7b3233df867723a217ff673c8b7f2f86
Author: finwo <finwo@pm.me>
Date:   Sat, 21 Mar 2026 03:59:37 +0100

Add caching

Diffstat:
Mvecfinder.js | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 80 insertions(+), 6 deletions(-)

diff --git a/vecfinder.js b/vecfinder.js @@ -8,12 +8,16 @@ const { stdin, stdout, stderr } = process; const EventEmitter = require('node:events'); // Configuration -const CONFIG_PATH = path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'ragfinder.json'); +const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vecfinder'); +const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json'); +const CACHE_DIR = path.join(CONFIG_DIR, 'cache'); +const ONE_GIB = 1073741824; let config = { endpoint: 'http://localhost:1234/v1/embeddings', model: 'text-embedding-qwen3-0.6b-text-embedding', apiKey: '', parallelism: 2, + cacheMaxSize: ONE_GIB, }; async function loadConfig() { @@ -23,6 +27,64 @@ async function loadConfig() { } catch {} } +// Cache +function cachePath(filePath) { + return path.join(CACHE_DIR, filePath); +} + +async function getCachedVector(filePath) { + try { + const cp = cachePath(filePath); + const [cacheStat, fileStat] = await Promise.all([fs.stat(cp), fs.stat(filePath)]); + if (fileStat.mtimeMs >= cacheStat.mtimeMs) return null; + const data = await fs.readFile(cp, 'utf8'); + return JSON.parse(data); + } catch { + return null; + } +} + +async function setCachedVector(filePath, vector) { + try { + const cp = cachePath(filePath); + await fs.mkdir(path.dirname(cp), { recursive: true }); + await fs.writeFile(cp, JSON.stringify(vector)); + enforceCacheLimit(); + } catch {} +} + +async function enforceCacheLimit() { + try { + const files = []; + let totalSize = 0; + + const pending = [CACHE_DIR]; + while (pending.length) { + const current = pending.pop(); + try { + const entries = await fs.readdir(current, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + pending.push(fullPath); + } else { + const stat = await fs.stat(fullPath); + files.push({ path: fullPath, size: stat.size }); + totalSize += stat.size; + } + } + } catch {} + } + + while (totalSize > config.cacheMaxSize && files.length > 0) { + const i = Math.floor(Math.random() * files.length); + const [removed] = files.splice(i, 1); + await fs.unlink(removed.path); + totalSize -= removed.size; + } + } catch {} +} + // Embedding let currentQueryId = 0; @@ -77,9 +139,11 @@ class DocumentStore extends EventEmitter { this.queryVec = null; } - async addDocument(id, content, metadata = {}) { - const raw = await fetchEmbedding(content); - const vector = normalize(raw); + async addDocument(id, content, metadata = {}, vector) { + if (!vector) { + const raw = await fetchEmbedding(content); + vector = normalize(raw); + } this.documents.set(id, { content, vector, metadata }); this.emit('documentAdded'); } @@ -172,7 +236,16 @@ async function indexDirectory(store, dirPath, ui) { const content = await fs.readFile(filePath, 'utf8'); const id = path.relative(dirPath, filePath); - await store.addDocument(id, content, { path: filePath }); + + let vector = await getCachedVector(filePath); + if (vector) { + await store.addDocument(id, content, { path: filePath }, vector); + } else { + const raw = await fetchEmbedding(content); + vector = normalize(raw); + await store.addDocument(id, content, { path: filePath }, vector); + await setCachedVector(filePath, vector); + } } catch {} } } @@ -283,7 +356,7 @@ class UI { const results = this.store.getResults(); // Header - stderr.write('RagFinder\n'); + stderr.write('VecFinder\n'); stderr.write('─'.repeat(leftWidth) + '┬' + '─'.repeat(rightWidth) + '\n'); // Rows @@ -335,6 +408,7 @@ class UI { // Main async function main() { await loadConfig(); + await fs.mkdir(CACHE_DIR, { recursive: true }); const store = new DocumentStore(); const ui = new UI(store);