commit 11ac9246196ae3b3c68a367fdc91afc2b8ef522a
parent 41b9f26d7b3233df867723a217ff673c8b7f2f86
Author: finwo <finwo@pm.me>
Date: Sat, 21 Mar 2026 03:59:37 +0100
Add caching
Diffstat:
| M | vecfinder.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);