vecfinder

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

commit 062d92b04571a4f93163cafab7756ced100ad282
parent 56f7e19439a079a67c5d5c873a7353945f0771a2
Author: finwo <finwo@pm.me>
Date:   Sat, 21 Mar 2026 04:39:51 +0100

Parallelism & spinner

Diffstat:
Mvecfinder.js | 71+++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 51 insertions(+), 20 deletions(-)

diff --git a/vecfinder.js b/vecfinder.js @@ -137,6 +137,7 @@ class DocumentStore extends EventEmitter { super(); this.documents = new Map(); this.queryVec = null; + this.consumersActive = 0; } async addDocument(id, content, metadata = {}, vector) { @@ -225,28 +226,46 @@ async function walkDir(dirPath) { return files; } -async function indexDirectory(store, dirPath, ui) { +async function indexDirectory(store, dirPath) { const files = await walkDir(dirPath); + const queue = []; - for (const filePath of files) { - try { - const stats = await fs.stat(filePath); - if (stats.size > 1024 * 1024) continue; - if (!(await isTextFile(filePath))) continue; + function consumer() { + if (queue.length === 0) return; + if (store.consumersActive >= config.parallelism) return; - const content = await fs.readFile(filePath, 'utf8'); - const id = path.relative(dirPath, filePath); + store.consumersActive++; + const filePath = queue.shift(); - 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 {} + (async () => { + try { + const content = await fs.readFile(filePath, 'utf8'); + const id = path.relative(dirPath, 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 {} + + store.consumersActive--; + store.emit('documentAdded'); + setImmediate(consumer); + })(); + } + + for (const filePath of files) { + const stats = await fs.stat(filePath).catch(() => null); + if (!stats || stats.size > 1024 * 1024) continue; + const text = await isTextFile(filePath); + if (!text) continue; + queue.push(filePath); + setImmediate(consumer); } } @@ -261,6 +280,8 @@ class UI { this._previewWrapped = null; this._previewFileId = null; this._previewWidth = null; + this.spinnerFrame = 0; + this.spinnerInterval = null; this.store.on('documentAdded', () => this.render()); this.store.on('queryUpdated', () => { @@ -275,10 +296,15 @@ class UI { stdin.on('keypress', (str, key) => this.handleKeyPress(str, key)); this.rl.on('line', () => this.handleEnter()); this.rl.on('close', () => this.cleanup()); + this.spinnerInterval = setInterval(() => { + this.spinnerFrame++; + this.render(); + }, 80); this.render(); } cleanup() { + clearInterval(this.spinnerInterval); stdin.removeAllListeners('keypress'); if (this.rl) this.rl.close(); } @@ -359,7 +385,12 @@ class UI { const results = this.store.getResults(); // Header - stderr.write('VecFinder\n'); + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + if (this.store.consumersActive > 0) { + stderr.write(`${frames[this.spinnerFrame % frames.length]} VecFinder\n`); + } else { + stderr.write('\u2713 VecFinder\n'); + } stderr.write('─'.repeat(leftWidth) + '┬' + '─'.repeat(rightWidth) + '\n'); // Rows @@ -432,7 +463,7 @@ async function main() { ui.init(); - indexDirectory(store, process.cwd(), ui); + indexDirectory(store, process.cwd()); } main().catch(err => {