Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 44 additions & 13 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
button { font: inherit; padding: 0.5rem 1rem; cursor: pointer; }
pre { background: #111; color: #b9f; padding: 1rem; border-radius: 6px; overflow: auto; }
.ok { color: #5c5; } .bad { color: #f55; } .dim { color: #888; }
.loader { display: none; width: 16px; height: 16px; border: 2px solid #ddd; border-top-color: #5c5; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
Expand All @@ -35,6 +37,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
<button id="persist">Test persistence (M2)</button>
<button id="encrypt">Encrypted persist (M6)</button>
<button id="delete">Delete + compaction</button>
<span class="loader" id="loader"></span>
</p>
<pre id="out">ready.</pre>

Expand All @@ -44,6 +47,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
const out = document.getElementById('out');
const log = (msg, cls = '') =>
(out.innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`);
const showLoader = () => document.getElementById('loader').style.display = 'inline-block';
const hideLoader = () => document.getElementById('loader').style.display = 'none';

function randVec(dim) {
const v = new Float32Array(dim);
Expand Down Expand Up @@ -75,9 +80,10 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>

document.getElementById('run').onclick = async () => {
out.textContent = '';
showLoader();
const support = BrowserVec.isSupported();
log(`support: ${JSON.stringify(support)}`);
if (!support.webgpu) return log('WebGPU not available in this browser.', 'bad');
if (!support.webgpu) { hideLoader(); return log('WebGPU not available in this browser.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -112,12 +118,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(`top hit: ${gpuIds[0]} (gpu score ${hits[0].score.toFixed(4)}, cpu ${ref[0].score.toFixed(4)})`);

db.destroy();
hideLoader();
};

// M3: compare int8 TurboQuant (with GPU kernel) against fp32 flat ground truth.
document.getElementById('quant').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -166,6 +174,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
qd.destroy();
}
log(`(raw recall drops as bits shrink — int4, then 1-bit; exact re-rank recovers it — that's the design)`, 'dim');
hideLoader();
};
function nextPow2(n) { let p = 4; while (p < n) p <<= 1; return p; }

Expand All @@ -191,6 +200,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// adapter limits, OPFS/WASM-SIMD/Worker support, and the CPU-fallback path.
document.getElementById('m6report').onclick = async () => {
out.textContent = '';
showLoader();
log('Running M6 device report — ~10–40s depending on device. Keep this tab focused.\n', 'dim');

// Deterministic PRNG (mulberry32) → identical corpus on every device.
Expand Down Expand Up @@ -309,12 +319,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(JSON.stringify(report));
log('=== END BROWSERVEC M6 REPORT ===', 'ok');
log('\nSelect everything between the BEGIN/END lines and paste it back (one per device).', 'dim');
hideLoader();
};

// M4: IVF approximate index vs fp32 flat ground truth, sweeping nprobe.
document.getElementById('ivf').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -365,13 +377,15 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
);
}
db.destroy();
hideLoader();
};

// M4 combo: IVF × int8 — clusters + quantizes so 1M fits AND only nprobe lists
// are scanned. Compares against fp32 flat truth and reports memory + recall.
document.getElementById('combo').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -441,12 +455,14 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(` nprobe=${nlist} (full scan, diagnostic): recall@${k}=${(dh / (queries * k)).toFixed(3)}`, 'dim');
db.destroy();
}
hideLoader();
};

// M5: end-to-end text retrieval, fully offline (zero-dep hashing embedder).
document.getElementById('text').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const embedder = hashingEmbedder({ dimension: 384 });
const db = await BrowserVec.create({ dimension: 384, metric: 'cosine', embedder });
Expand All @@ -470,6 +486,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(`\nnote: hashingEmbedder is lexical (keyword), not semantic.`, 'dim');
log(`swap transformersEmbedder() for meaning — same store/query code.`, 'dim');
db.destroy();
hideLoader();
};

// NFR-8: the CPU-heavy rotate+quantize of a quantized ingest runs in a Web
Expand All @@ -479,7 +496,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// the UI would stay interactive during ingest.
document.getElementById('worker').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -573,6 +591,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(`(centroid mean-update ran ${tmode === 'worker' ? 'in the Worker' : 'in-thread'}; ` +
`results are byte-identical either way)`, 'dim');
idb.destroy();
hideLoader();
};

// NFR-10: a single GPU storage buffer is capped (128 MiB on many devices),
Expand All @@ -582,7 +601,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// store (flat is exact, so results must be identical, not just close).
document.getElementById('chunk').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const n = +document.getElementById('n').value;
const dim = +document.getElementById('dim').value;
Expand Down Expand Up @@ -641,6 +661,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
db.destroy();
}
log(`\n→ chunked scan is exact — a corpus can now exceed one GPU buffer.`, 'ok');
hideLoader();
};

// GPU top-k: past a few thousand rows the flat index reduces on the GPU
Expand All @@ -650,7 +671,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// switch-over threshold to show the readback that was eliminated.
document.getElementById('gputopk').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const dim = +document.getElementById('dim').value;
const k = 10;
Expand Down Expand Up @@ -774,6 +796,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>

log(`\n→ GPU top-k returns the same neighbors while reading back only` +
` ceil(N/256)·k pairs instead of all N scores — now on the quantized and IVF paths too.`, 'ok');
hideLoader();
};

// M6 / NFR-7: on browsers without WebGPU the library falls back to an exact
Expand All @@ -782,7 +805,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// returns the same top-k as the real GPU path on the same data.
document.getElementById('cpufallback').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const dim = +document.getElementById('dim').value;
const n = Math.min(20000, +document.getElementById('n').value);
Expand Down Expand Up @@ -826,14 +850,16 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
} finally {
delete globalThis.__BROWSERVEC_FORCE_CPU__;
}
hideLoader();
};

// M2: save to OPFS/IndexedDB, drop the instance, reload, confirm it survives.
document.getElementById('persist').onclick = async () => {
out.textContent = '';
showLoader();
const support = BrowserVec.isSupported();
log(`support: ${JSON.stringify(support)}`);
if (!support.webgpu) return log('WebGPU not available.', 'bad');
if (!support.webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const dim = 64;
const name = 'demo-session';
Expand Down Expand Up @@ -873,15 +899,17 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>

db2.destroy();
db3.destroy();
hideLoader();
};

// M6: persisted snapshots + exported blobs encrypted at rest with a passphrase
// (AES-256-GCM, PBKDF2 key). Auto-load decrypts; a wrong passphrase or tamper
// fails loudly. Data never leaves the device — and now it's safe at rest too.
document.getElementById('encrypt').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
if (!globalThis.crypto?.subtle) return log('WebCrypto (crypto.subtle) unavailable.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }
if (!globalThis.crypto?.subtle) { hideLoader(); return log('WebCrypto (crypto.subtle) unavailable.', 'bad'); }

const dim = 64;
const name = 'enc-demo';
Expand Down Expand Up @@ -936,6 +964,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
log(`\nencrypted export→import round-trip: [${imported}]`, imported === before ? 'ok' : 'bad');
db2.destroy();
db3.destroy();
hideLoader();
};

// Deletion: rows are tombstoned (filtered from results immediately, cheap) and
Expand All @@ -944,7 +973,8 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
// that a reload drops them for good. Runs on fp32 and int8×IVF.
document.getElementById('delete').onclick = async () => {
out.textContent = '';
if (!BrowserVec.isSupported().webgpu) return log('WebGPU not available.', 'bad');
showLoader();
if (!BrowserVec.isSupported().webgpu) { hideLoader(); return log('WebGPU not available.', 'bad'); }

const dim = 128;
const n = 5000;
Expand Down Expand Up @@ -985,6 +1015,7 @@ <h1>BrowserVec — M1 (flat GPU brute-force)</h1>
db.destroy();
reloaded.destroy();
}
hideLoader();
};
</script>
</body>
Expand Down
7 changes: 7 additions & 0 deletions examples/benchmark-dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
.log-area { margin-top: 1rem; }
.log-area summary { cursor: pointer; font-weight: 600; color: #555; font-size: .85rem; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
.loader { display: none; width: 18px; height: 18px; border: 2px solid #ddd; border-top-color: #1a73e8; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
Expand Down Expand Up @@ -112,6 +114,7 @@ <h1>Benchmark Dashboard</h1>
</select>
</label>
<button id="runBtn">Run Benchmark</button>
<span class="loader" id="loader"></span>
</div>
<div class="note" id="configNote"></div>

Expand Down Expand Up @@ -151,6 +154,8 @@ <h1>Benchmark Dashboard</h1>

const $ = (id) => document.getElementById(id);
const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`; };
const showLoader = () => $('loader').style.display = 'inline-block';
const hideLoader = () => $('loader').style.display = 'none';

// ---- Synthetic clustered corpus ----
// Uniform-random vectors are a worst case for approximate search (no structure
Expand Down Expand Up @@ -205,6 +210,7 @@ <h1>Benchmark Dashboard</h1>
const efSearch = Math.max(parseInt($('hnswEfSearch').value), k);

$('runBtn').disabled = true;
showLoader();
$('log').textContent = '';
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#999;">Running…</td></tr>`;

Expand Down Expand Up @@ -242,6 +248,7 @@ <h1>Benchmark Dashboard</h1>
`${n.toLocaleString()} rows × ${dim} dim, ${queryCount} queries, k=${k}. ` +
`Ground truth: naive JS brute force (exact). HNSW: efConstruction=${efConstruction}, M=${m}, efSearch=${efSearch}.`;
$('runBtn').disabled = false;
hideLoader();
}

function updateConfigNote() {
Expand Down
8 changes: 8 additions & 0 deletions examples/benchmark-real-embeddings.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
.qcard .hit .score { color: #888; margin-right: .5rem; }
.qcard .hit .topic { color: #aaa; font-size: .72rem; }
::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
.loader { display: none; width: 18px; height: 18px; border: 2px solid #ddd; border-top-color: #1a73e8; border-radius: 50%; animation: spin .6s linear infinite; margin-left: .5rem; vertical-align: middle; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
Expand Down Expand Up @@ -101,6 +103,7 @@ <h2>1. Embedding model</h2>
</select>
</label>
<button id="runBtn">Embed corpus &amp; run benchmark</button>
<span class="loader" id="loader"></span>
</div>
<div class="note" id="configNote">
Model weights download once (tens of MiB) and are cached by the browser — later runs with the same model
Expand Down Expand Up @@ -153,6 +156,8 @@ <h2>3. Qualitative semantic search</h2>

const $ = (id) => document.getElementById(id);
const log = (msg, cls = '') => { $('log').innerHTML += `\n${cls ? `<span class="${cls}">${msg}</span>` : msg}`; };
const showLoader = () => $('loader').style.display = 'inline-block';
const hideLoader = () => $('loader').style.display = 'none';

// Retrieval-tuned models expect asymmetric prefixes on queries vs. documents —
// this is each model's own documented convention (E5: "query:"/"passage:",
Expand Down Expand Up @@ -241,6 +246,7 @@ <h2>3. Qualitative semantic search</h2>
const k = parseInt($('kVal').value);

$('runBtn').disabled = true;
showLoader();
$('log').textContent = '';
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#999;">Embedding corpus…</td></tr>`;
$('qualitative').innerHTML = '';
Expand All @@ -254,6 +260,7 @@ <h2>3. Qualitative semantic search</h2>
console.error(e);
$('resultsBody').innerHTML = `<tr><td colspan="9" style="text-align:center;color:#d32f2f;">Embedding failed — see log below.</td></tr>`;
$('runBtn').disabled = false;
hideLoader();
return;
}
const { modelCfg, docs, docVectors, queryVectors } = embedded;
Expand Down Expand Up @@ -298,6 +305,7 @@ <h2>3. Qualitative semantic search</h2>
await runQualitative(modelCfg, docs, docVectors, queryVectors);

$('runBtn').disabled = false;
hideLoader();
}

// Builds a fresh BrowserVec (WebGPU flat) instance over the already-embedded
Expand Down
Loading
Loading