Search
-
I Spent a Weekend on My Search Library and Deleted Its Best Feature
A couple weekends ago I shipped nine releases of libsql-search, from 0.3.0 up to 0.11.0. The headline change is that I took out the local embedding feature.
It used to generate embeddings locally. No API key, no account, no provider. You pointed it at a folder of Markdown and it just worked. As of 0.11.0 it requires an external embedding provider, and the README now says the quiet part directly: this package never loads or hosts an embedding model in-process.
I’ll get to why. First, the part I actually want people to know about, because the library has been quietly useful for two years and almost nobody knows it exists.
What it’s for
libsql-searchadds semantic search to Markdown-backed sites. It reads frontmatter and content off disk, embeds it, stores the vectors in libSQL or Turso, and lets you query by meaning instead of exact keywords. It’s a TypeScript library, MIT, on npm and JSR.I built it because the alternatives were a hosted search product I’d have to pay for and pipe my content through, or a vector database that wanted to be the center of my architecture. I have a docs site. I want search on it. I already have a Turso database sitting there.
So it’s deliberately small: one indexing and search API across whichever embedding provider you want, direct control over dimensions and table names, and no service to run. Your vectors live in a table in your own database, next to everything else.
What changed over the weekend
Five things worth knowing:
Four providers, plus an escape hatch. You can now pick from
cloudflare(@cf/baai/bge-m3, 1024 dimensions),mistral(mistral-embed, 1024),gemini(gemini-embedding-2, anywhere from 128 to 3072, defaulting to 3072), andopenai(text-embedding-3-smallor-large, defaulting to 768). There’s alsoopenai-compatible, which points at any trusted OpenAI-shaped endpoint, so a self-hosted TEI instance works without me writing an adapter for it.Search stopped reading your entire table. This one is the real performance change.
search()used to computevector_distance_cosover every row, so cost grew linearly with your corpus. It now queries the<tableName>_embedding_idxvector index throughvector_top_k, joins the candidate rowids back, and recomputes exact distances for just those candidates. If you want the old behavior,exact: trueforces the guaranteed-exact full scan, andcandidatestunes how many rows get over-fetched before re-ranking (default ismax(limit * 4, 32)).Reindexing is atomic now, and the old behavior was worse than I realized. The previous
indexContent()found your files, ranDELETE FROM <table>, then looped inserting one file at a time while swallowing individual failures behind aconsole.errorand a counter. A provider outage halfway through left you with a half-built index and a result object that looked completely normal. Worse: if it discovered zero files, it returned early without deleting, so stale rows kept serving live search traffic indefinitely. Every document is now read, parsed, and embedded into memory before the database is touched at all, and the delete plus every insert go out as a singleclient.batch(..., 'write'), which libSQL wraps inBEGIN IMMEDIATE/COMMITwith automatic rollback. Files process in sorted path order, so rebuilds are deterministic.An optional Turso Database adapter. Behind a separate
libsql-search/tursoentry point, opt-in only, nothing resolved unless you ask for it. It’s experimental and exact-search-only, because Turso Database has no ANN vector index yet.@libsql/client0.17.x support, alongside 0.15.x. Both lines work and the packaged build is smoke-tested against both on every release.Why the local embeddings had to go
Bundling a model meant bundling an inference runtime, and that runtime kept moving under me. It was
@xenova/transformers, then ONNX Runtime, and every swap changed the install footprint, the native build requirements, and the list of packages allowed to compile C++ on your machine at install time.The dimension problem was worse. A local model emitting 384-dimension vectors and a provider emitting 1024 aren’t interchangeable, and if the bundled model changes, everything you indexed before is silently garbage. Vectors don’t fail loudly when they’re the wrong shape. They just return bad results.
Making the provider explicit means the dimension is something you chose and can see, rather than something the library picked for you and might change in a patch release. That’s the trade: a worse first-run experience for a system that can’t quietly corrupt your index.
I don’t love it. “Point it at a folder, no API key” was a nice thing to be able to say. But I’d rather lose the demo than keep a feature whose failure mode is search that’s subtly wrong for months.
The 1.0.0 that lasted eight hours
One thing I’ll own, since it’s in the git history anyway. Partway through the weekend the release automation cut a 1.0.0 off a breaking change. I reverted it about eight hours later and added a rule holding breaking changes to a minor bump while the package is still on the 0.x line.
I’m not ready to call this 1.0. Signing that number means promising not to do exactly what I did to the embedding providers this weekend, and I want at least one more pass at the provider contract before I make that promise. The revert is
0db3167if you want to watch me change my mind in public.It’s at 0.11.1 on JSR now. If you’ve got a Markdown site and a Turso database and you’ve been meaning to add search to it, that’s most of the work already done.
Sources
- libsql-search on GitHub — source, MIT
- @logan/libsql-search on JSR — current version and API docs
- libsql-search on npm — for Node and pnpm installs
- Turso: AI and embeddings —
F32_BLOB,vector_top_k, andlibsql_vector_idx - Cloudflare Workers AI: bge-m3 — one of the four built-in providers
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].