2d819b476c
* fix(db): forward DatabaseOpts to tauri-plugin-turso NativeDatabaseService.open ignored its opts parameter, dropping any experimental feature flags (e.g. 'index_method' needed for FTS / vector indexes) and any encryption config before they could reach the Tauri plugin. Translate DatabaseOpts to the plugin's LoadOptions shape and forward as the single argument Database.load accepts. Skip translation when no relevant opts are set so the existing path-string call shape is preserved for callers without experimental/encryption needs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(db): cover Turso vector primitives + add benchmark harness Verifies the Turso functions Reedy retrieval depends on, with a brute-force per-book kNN test that runs against every DatabaseService backend (node, native, WASM): SELECT vector_distance_cos(embedding, vector32(?)) AS d FROM book_chunks WHERE book_hash = ? ORDER BY d ASC LIMIT k This is the path Turso's own founder recommended in tursodatabase/turso#3778 ("First, focus on efficient SIMD-accelerated brute-force search") and what shipped at commit 1aba105df4f. Native vector index modules don't exist in this engine: `libsql_vector_idx`, `vector_top_k`, and `USING vector/hnsw/diskann/ivfflat` all parse-error against @tursodatabase/database@0.6.0-pre.28 (libsql_vector_idx is a libSQL/sqld fork feature; DiskANN was closed not-planned upstream in #832). The test asserts cross-book isolation and nearest-first ordering using only `WHERE book_hash = ?` and `ORDER BY` — no DDL, no identifier interpolation, no index plumbing. Also adds bench/ harness for manual perf checks: pnpm bench [name] run benchmarks (refuses in CI) pnpm bench --list list available benchmarks pnpm bench --no-record skip results.jsonl append pnpm bench --force override the CI guard Uses Node 24's --experimental-strip-types so no tsx devDep is needed. Appends one JSON line per run to bench/results.jsonl (gitignored, local history; share by pasting tabular stdout into PRs/issues). Explicitly NOT in CI — shared-tenant variance makes synthetic-benchmark regression detection unreliable; production telemetry (reedy_metrics, plan §M1.9) is the right tool for that. First benchmark: vector-retrieval. Measured on M1 Pro: 400 chunks × 384 dim → 0.35 ms / query 400 chunks × 768 dim → 0.45 ms / query 2000 chunks × 768 dim → 2.23 ms / query 10000 chunks × 768 dim → 14.00 ms / query 400 chunks × 1536 dim → 0.70 ms / query Per-chunk cost ~1.1 µs at 768 dim = ~1.4 ns/dim. NEON-class on Apple Silicon, ~50× faster than scalar — confirms SIMD acceleration is active in 0.6.0-pre.28. Per-query latency stays sub-ms at Reedy MVP corpus sizes; the ceiling is ~10K chunks per book before phone-class hardware notices. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.3 KiB
TypeScript
88 lines
3.3 KiB
TypeScript
import { connect } from '@tursodatabase/database';
|
||
import { avg, randomUnitVectorJson, type Bench, type BenchResult } from './lib.ts';
|
||
|
||
/**
|
||
* Vector-retrieval brute-force kNN benchmark.
|
||
*
|
||
* Reedy MVP retrieval (see plan §M1.5) issues:
|
||
*
|
||
* SELECT id, vector_distance_cos(embedding, vector32(?)) AS d
|
||
* FROM reedy_book_chunk_embeddings
|
||
* WHERE book_hash = ?
|
||
* ORDER BY d ASC LIMIT k
|
||
*
|
||
* Why this matters: Turso has no native vector index module
|
||
* (`libsql_vector_idx` / `vector_top_k` don't exist — confirmed against
|
||
* @tursodatabase/database@0.6.0-pre.28 and acknowledged upstream:
|
||
* tursodatabase/turso#832 closed not-planned, #3778 proposed brute-force-first
|
||
* which shipped at commit 1aba105df4f). The brute-force path with
|
||
* SIMD-accelerated `vector_distance_cos` is what we ship; this bench tracks
|
||
* its per-query latency at realistic MVP corpus sizes.
|
||
*
|
||
* Run it after upgrading @tursodatabase/database, after touching
|
||
* BookRetriever's SQL shape, or when evaluating an architecture change
|
||
* (ANN extension, quantization, engine swap).
|
||
*/
|
||
export default {
|
||
name: 'vector-retrieval',
|
||
description: 'Brute-force per-book kNN over vector32 embeddings filtered by book_hash.',
|
||
|
||
async run(): Promise<BenchResult[]> {
|
||
const db = await connect(':memory:', {});
|
||
await db.exec(
|
||
'CREATE TABLE c (id INTEGER PRIMARY KEY, book_hash TEXT NOT NULL, embedding BLOB)',
|
||
);
|
||
await db.exec('CREATE INDEX idx_c_book ON c(book_hash)');
|
||
|
||
// (dim, chunks-per-book) matrix. Two books per scenario so the WHERE filter
|
||
// does real work; we measure only the active-book query.
|
||
const scenarios = [
|
||
{ dim: 384, chunks: 400 }, // small book, light embedding (e5-small-v2)
|
||
{ dim: 768, chunks: 400 }, // typical novel @ nomic-embed-text
|
||
{ dim: 768, chunks: 2000 }, // long novel
|
||
{ dim: 768, chunks: 10000 }, // multi-volume / textbook
|
||
{ dim: 1536, chunks: 400 }, // text-embedding-3-small
|
||
];
|
||
|
||
const results: BenchResult[] = [];
|
||
|
||
for (const { dim, chunks } of scenarios) {
|
||
await db.exec('DELETE FROM c');
|
||
|
||
const insertA = await db.prepare(
|
||
"INSERT INTO c (book_hash, embedding) VALUES ('book_a', vector32(?))",
|
||
);
|
||
for (let i = 0; i < chunks; i++) await insertA.run(randomUnitVectorJson(dim));
|
||
|
||
const insertB = await db.prepare(
|
||
"INSERT INTO c (book_hash, embedding) VALUES ('book_b', vector32(?))",
|
||
);
|
||
for (let i = 0; i < chunks; i++) await insertB.run(randomUnitVectorJson(dim));
|
||
|
||
const query = randomUnitVectorJson(dim);
|
||
// Embed the query vector literally so SIMD has the same memory layout
|
||
// every call (mirrors the BookRetriever code path which serializes the
|
||
// query embedding inline at the value-binding position).
|
||
const sql = `
|
||
SELECT id, vector_distance_cos(embedding, vector32('${query}')) AS d
|
||
FROM c
|
||
WHERE book_hash = ?
|
||
ORDER BY d ASC
|
||
LIMIT 5
|
||
`;
|
||
const stmt = await db.prepare(sql);
|
||
|
||
const ms = await avg(() => stmt.all('book_a'), 20);
|
||
results.push({
|
||
scenario: `${chunks} chunks × ${dim} dim`,
|
||
unit: 'ms',
|
||
value: ms,
|
||
meta: { chunks, dim, usPerChunk: ((ms * 1000) / chunks).toFixed(2) },
|
||
});
|
||
}
|
||
|
||
await db.close();
|
||
return results;
|
||
},
|
||
} satisfies Bench;
|