No-holds-barred take (based on the portion you pasted — it cuts off mid-function, so I’m reviewing what’s visible + the implied next steps).
You’re explicitly validating the exact libSQL/Turso “native vectors” surface area Val Town would expose:
F32_BLOB(D)vector column typesvector(...)constructorlibsql_vector_idx(...)index markervector_top_k(...)query function
That’s exactly the feature set Turso documents for vector search. (docs.turso.tech)
Printing embedding.length is the first sanity check that prevents 90% of “why
won’t inserts / index queries work?” issues.
In libSQL, vector is an alias conversion function (effectively vector32) and
it accepts a JSON array string. (docs.turso.tech)
Right now you print length, but you don’t fail fast if the returned length differs.
Why this matters:
- libSQL requires the query vector to have the same type and dimensionality
as the column/index. If your table is
F32_BLOB(4096)and the API returns 3072 or 1024, you’re dead. (docs.turso.tech)
Fix: assert the dimension before any DB work.
function assertDim(vec: number[], expected: number) {
if (vec.length !== expected) {
throw new Error(
`Embedding dim mismatch: got ${vec.length}, expected ${expected}`,
);
}
}
Then in testEmbeddingAPI():
Also: Qwen3-Embedding supports up to 4096 dims (and sometimes providers always return 4096). (Hugging Face)
You’re rounding every component to 8 decimals. That is:
- wasted CPU
- makes the SQL string bigger than it needs to be (because you force a fixed representation)
- introduces quantization noise you didn’t ask for
libSQL will store as float32 anyway; you don’t need to pre-round.
Fix: remove rounding completely (or keep minimal rounding only for debugging prints).
A 4096-dim embedding as JSON text is huge. You’re generating a SQL statement
that contains a massive vector('[...]') blob.
This can bite you in multiple ways:
- request payload size to the DB endpoint
- SQL parsing overhead
- log output bloat if anything errors and prints the query
- harder to parameterize safely
Turso’s docs explicitly support converting a text JSON array to the target
vector type via conversion functions like vector32(...).
(docs.turso.tech)
Much better fix: parameterize the JSON and wrap it in vector32(?):
const vecJson = JSON.stringify(embedding);
await sqlite.execute({
sql: `INSERT INTO t(embedding) VALUES (vector32(?))`,
args: [vecJson],
});
This keeps SQL smaller, avoids injection-y patterns, and is closer to the documented usage model. (docs.turso.tech)
You used:
Val Town’s own std/sqlite README shows:
So unless you need that exact tag/branch, you’re taking on avoidable breakage risk. (Val Town)
Fix (simple):
If you want pinning, pin a commit/version you control—but don’t invent tag shapes unless you know they exist.
The real “gotchas” are:
-
Conversion function/type match Your table will likely be
F32_BLOB(D). Your inserted values must be float32 vectors.vector(...)is alias for float32 conversion, which is good. (docs.turso.tech) -
Index query requires same type + same dimensionality
vector_top_kwill fail if the query vector differs in either. (docs.turso.tech)
Your script should include a test that:
- creates the table with
F32_BLOB(4096) - creates the index with
libsql_vector_idx(embedding, 'metric=cosine') - inserts at least 2 rows
- runs
vector_top_k(...)and joins back to the table
Turso’s docs + blog show this exact flow. (turso.tech)
Turso docs: libSQL can operate up to 65536 dimensions. So 4096 is fine. (docs.turso.tech)
But storage math: float32 = 4 bytes.
- 4096 × 4 = 16384 bytes (~16 KB) per row, just for the embedding, before index overhead.
If you chunk docs and store many rows, that balloons fast.
Hard truth: Unless you truly need 4096, you’ll often be better with a smaller vector (common sweet spots: 768–1536) because your recall gain from bigger dims is frequently marginal vs your cost/index size. (Not always, but often.)
Your embedding call has:
- no timeout
- no retry/backoff
- no provider error normalization
- no batching guardrails
Nebius will occasionally spike latency or return transient errors—this will make indexing brittle.
Fix: add an AbortController timeout + small retry.
Don’t log the entire embedding ever (you currently only print first 5 values — good). Keep it that way.
function vectorArgs(embedding: number[]) {
// no rounding; keep raw floats
return JSON.stringify(embedding);
}
// usage:
await sqlite.execute({
sql: `INSERT INTO test_vectors(id, embedding)
VALUES (?, vector32(?))`,
args: ["row1", vectorArgs(embedding)],
});
This matches Turso’s documented “text JSON array → vector32 conversion” path. (docs.turso.tech)
function assertDim(vec: number[], expected: number) {
if (vec.length !== expected) {
throw new Error(
`Embedding dim mismatch: got ${vec.length}, expected ${expected}`,
);
}
}
As documented by Val Town’s std/sqlite README + docs. (Val Town)
Turso docs explain: vector_top_k returns ROWID (or PRIMARY KEY for WITHOUT
ROWID tables), and you usually join back to the base table.
(docs.turso.tech)
So your test should look like:
await sqlite.execute(`DROP TABLE IF EXISTS test_vectors`);
await sqlite.execute(`
CREATE TABLE test_vectors (
id TEXT PRIMARY KEY,
embedding F32_BLOB(4096)
)
`);
await sqlite.execute(`
CREATE INDEX test_vectors_idx
ON test_vectors (libsql_vector_idx(embedding, 'metric=cosine'))
`);
const [e1] = await embedText("dogs bark");
const [e2] = await embedText("github api client");
assertDim(e1, 4096);
assertDim(e2, 4096);
await sqlite.execute({
sql: `INSERT INTO test_vectors(id, embedding) VALUES (?, vector32(?))`,
args: ["a", JSON.stringify(e1)],
});
await sqlite.execute({
sql: `INSERT INTO test_vectors(id, embedding) VALUES (?, vector32(?))`,
args: ["b", JSON.stringify(e2)],
});
const [q] = await embedText("discord bot integration");
assertDim(q, 4096);
const res = await sqlite.execute({
sql: `
SELECT tv.id
FROM vector_top_k('test_vectors_idx', vector32(?), 2) v
JOIN test_vectors tv ON tv.rowid = v.id
`,
args: [JSON.stringify(q)],
});
console.log(res.rows);
That test is aligned with how Turso describes indexing/querying. (docs.turso.tech)
Your direction is correct, but your current implementation style (giant SQL string literals + unused dimension constant + nonstandard sqlite import) is exactly the kind of thing that “works once” and then becomes flaky and painful to productionize.
If you implement just these 3 changes:
- parameterize vectors with
vector32(?) - assert dimensions
- use canonical
std/sqlite/main.tsimport
…your script goes from “prototype that might lie to you” to “reliable foundation test”.
If you paste the rest (especially the SQLite tests you wrote after the cutoff), I’ll do an equally ruthless line-by-line review of the actual table/index/query parts—because that’s where most people accidentally test the wrong thing and think vectors “don’t work” when it’s really a join/type mismatch.