libsql-search 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,7 @@ References:
27
27
 
28
28
  In practice, this means:
29
29
 
30
- - `384 local` and `1024 Mistral` can never share a table
30
+ - a legacy `384` embedding space and `1024 Mistral` can never share a table
31
31
  - `1024 Cloudflare` and `1024 Mistral` still need separate rebuilds because equal width does not make the vectors compatible
32
32
  - a custom endpoint change at the same width still needs a new table because the model or serving stack may have changed
33
33
 
@@ -84,14 +84,14 @@ Re-running `createTable()` with the table's existing width is the fix. It is ide
84
84
 
85
85
  ```ts
86
86
  // Same name and same width as the existing table
87
- await createTable(client, "articles_local_384", 384);
87
+ await createTable(client, "articles_legacy_384", 384);
88
88
  ```
89
89
 
90
90
  Equivalently, in SQL:
91
91
 
92
92
  ```sql
93
- CREATE INDEX IF NOT EXISTS "articles_local_384_embedding_idx"
94
- ON "articles_local_384"(libsql_vector_idx(embedding));
93
+ CREATE INDEX IF NOT EXISTS "articles_legacy_384_embedding_idx"
94
+ ON "articles_legacy_384"(libsql_vector_idx(embedding));
95
95
  ```
96
96
 
97
97
  Reindexing does not create the index; `indexContent()` only replaces rows. Any new table created by `createTable()` as part of a migration already has it, so this applies only to pre-existing tables you are carrying forward. Until the index exists, `search({ ..., exact: true })` keeps queries working on the exact full-scan path.
@@ -100,7 +100,7 @@ Reindexing does not create the index; `indexContent()` only replaces rows. Any n
100
100
 
101
101
  | From | To | Why a rebuild is required | Recommended table move |
102
102
  | --- | --- | --- | --- |
103
- | Legacy padded local `768` | Native local `384` | Old tables stored `384` model values plus zero padding; current local provider is a native `384`-dimension space | Build into `articles_local_384`, validate, then retire the legacy table |
103
+ | Legacy in-process Transformers.js index | Any external provider | The runtime and provider were removed; every replacement service has its own embedding space | Build a parallel table named for the external provider/model, validate, then retire the legacy table |
104
104
  | Any `768` space | Any `1024` space | Width changes from `F32_BLOB(768)` to `F32_BLOB(1024)` | Create a new `1024` table and reindex |
105
105
  | Cloudflare `1024` | Mistral `1024` | Width stays the same, but provider/model space changes | Use a parallel `1024` table such as `articles_mistral_1024` |
106
106
  | Mistral `1024` | Cloudflare `1024` | Same reason in reverse | Use a parallel `1024` table such as `articles_cf_bgem3_1024` |
@@ -111,21 +111,21 @@ Reindexing does not create the index; `indexContent()` only replaces rows. Any n
111
111
 
112
112
  ## Scenario Notes
113
113
 
114
- ### Legacy Local `768` To Native Local `384`
114
+ ### Legacy In-Process Embeddings To An External Service
115
115
 
116
- Earlier local migrations sometimes relied on zero padding to fit a `768`-wide table. The current local adapter emits the model's native `384` dimensions and rejects any other local dimension count.
116
+ Versions that exposed the in-process Transformers.js provider produced a separate embedding space that this release can no longer query. Choose an external provider or separately deployed OpenAI-compatible service and rebuild every vector into a new table.
117
117
 
118
118
  Safe path:
119
119
 
120
120
  ```ts
121
- await createTable(client, "articles_local_384", 384);
121
+ await createTable(client, "articles_bge_1024", 1024);
122
122
  ```
123
123
 
124
- Reindex into `articles_local_384`; do not keep writing new local vectors into the legacy padded table.
124
+ Reindex into `articles_bge_1024` with the external service configuration; do not mix its vectors with the legacy table.
125
125
 
126
126
  ### `768` To `1024`
127
127
 
128
- Any move from `768` dimensions to `1024` dimensions changes the schema width. Examples include a legacy local table moving to Cloudflare or Mistral.
128
+ Any move from `768` dimensions to `1024` dimensions changes the schema width. Examples include a legacy table moving to Cloudflare, Mistral, or a 1024-dimensional OpenAI-compatible service.
129
129
 
130
130
  ```ts
131
131
  await createTable(client, "articles_mistral_1024", 1024);
package/docs/PROVIDERS.md CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  Use this page to choose an embedding provider, confirm the table width it needs, and understand what crosses a network boundary.
4
4
 
5
- `libsql-search` supports these provider values:
5
+ `libsql-search` only talks to external embedding services; it never loads or hosts an embedding model in-process. It supports these provider values:
6
6
 
7
- - `local`
8
7
  - `cloudflare`
9
8
  - `mistral`
10
9
  - `gemini`
@@ -17,8 +16,7 @@ All providers share the same `EmbeddingOptions` surface:
17
16
 
18
17
  ```ts
19
18
  interface EmbeddingOptions {
20
- provider?:
21
- | "local"
19
+ provider:
22
20
  | "cloudflare"
23
21
  | "mistral"
24
22
  | "gemini"
@@ -40,7 +38,7 @@ interface EmbeddingOptions {
40
38
 
41
39
  Shared defaults and rules:
42
40
 
43
- - `provider` defaults to `local`
41
+ - `provider` is required; there is no implicit embedding runtime or service
44
42
  - `maxLength` defaults to `8000` code units
45
43
  - `timeoutMs` defaults to `30000`
46
44
  - `indexContent()` defaults to `intent: "document"`
@@ -59,7 +57,6 @@ The `model` option is only used by `openai-compatible`.
59
57
 
60
58
  | Provider | Literal | Upstream model used by this adapter | Dimensions | Credentials | Batching | Network and privacy boundary | Cost and table planning |
61
59
  | --- | --- | --- | --- | --- | --- | --- | --- |
62
- | Local | `local` | `Xenova/all-MiniLM-L6-v2` | Fixed `384` | None | Sequential in-process | No hosted API call. First use may download model artifacts and cache them locally. | No hosted API bill. Table must be `F32_BLOB(384)`. |
63
60
  | Cloudflare Workers AI | `cloudflare` | `@cf/baai/bge-m3` | Fixed `1024` | `accountId` and `apiToken`, or `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` | Native batch in one request | Indexed and queried text is sent to Cloudflare. | Check Cloudflare pricing before large rebuilds. Table must be `F32_BLOB(1024)`. |
64
61
  | Mistral | `mistral` | `mistral-embed` | Fixed `1024` | `apiKey`, or `MISTRAL_API_KEY` | Native batch in one request | Indexed and queried text is sent to Mistral. | Check Mistral pricing before rebuilds. Table must be `F32_BLOB(1024)`. |
65
62
  | Gemini | `gemini` | `gemini-embedding-2` | Default `3072`; allowed integers `128-3072` | `apiKey`, or `GEMINI_API_KEY` | Sequential SDK request per input | Indexed and queried text is sent to Google. The adapter currently rewrites payload text by intent. | Check Gemini pricing before rebuilds. Table width must match the chosen dimension count exactly. |
@@ -68,23 +65,6 @@ The `model` option is only used by `openai-compatible`.
68
65
 
69
66
  ## Provider Notes
70
67
 
71
- ### Local
72
-
73
- ```ts
74
- embeddingOptions: {
75
- provider: "local",
76
- }
77
- ```
78
-
79
- - fixed at `384` dimensions
80
- - rejects any other `dimensions` value before loading the runtime
81
- - uses `@huggingface/transformers` lazily and caches the local pipeline by model name
82
-
83
- References:
84
-
85
- - [Transformers.js in Node.js](https://huggingface.co/docs/transformers.js/en/tutorials/node)
86
- - [Transformers.js environment and cache controls](https://huggingface.co/docs/transformers.js/en/api/env)
87
-
88
68
  ### Cloudflare Workers AI
89
69
 
90
70
  ```ts
package/docs/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory holds the longer-form reference material for `libsql-search`.
4
4
 
5
- - [Provider selection and configuration](./PROVIDERS.md): compare local, hosted, and custom embedding providers before you build an index
5
+ - [Provider selection and configuration](./PROVIDERS.md): compare external and custom embedding providers before you build an index
6
6
  - [Integration examples](./INTEGRATIONS.md): reusable provider flow plus Astro and Next.js examples
7
7
  - [Migration and reindexing guide](./MIGRATIONS.md): table-width changes, provider/model swaps, and safe cutovers
8
8
  - [API reference](./API.md): exported functions, option shapes, and result data
package/docs/TESTING.md CHANGED
@@ -9,30 +9,31 @@ Routine unit tests and CI should not make live embedding-provider calls and shou
9
9
  - validate option handling and response parsing with mocks first
10
10
  - assert failures happen before network calls when configuration is invalid
11
11
 
12
- The current test suite follows that pattern in `tests/embeddings.test.ts` and [`tests/huggingface-transformers.mock.ts`](../tests/huggingface-transformers.mock.ts).
12
+ The current test suite follows that pattern in `tests/embeddings.test.ts` and [`tests/embedding-service.mock.ts`](../tests/embedding-service.mock.ts).
13
13
 
14
- ## Local Provider Mocks
14
+ ## Shared Embedding Service Mock
15
15
 
16
- The local provider should use a lightweight Transformers.js mock instead of downloading the real model during routine tests.
16
+ Indexer, search, and database tests use a deterministic OpenAI-compatible service mock. This keeps the tests on the same external-service boundary as production without making network calls.
17
17
 
18
18
  ```ts
19
19
  import {
20
- huggingFaceTransformersMock,
21
- resetHuggingFaceTransformersMock,
22
- } from "./huggingface-transformers.mock.js";
20
+ embeddingServiceMock,
21
+ resetEmbeddingServiceMock,
22
+ TEST_EMBEDDING_OPTIONS,
23
+ } from "./embedding-service.mock.js";
23
24
 
24
25
  beforeEach(() => {
25
- resetHuggingFaceTransformersMock();
26
+ resetEmbeddingServiceMock();
26
27
  });
27
28
  ```
28
29
 
29
- The repository source file is `huggingface-transformers.mock.ts`. The example keeps the `.js` import suffix because this repo's ESM TypeScript source uses explicit `.js` relative imports that resolve after compilation.
30
+ The example keeps the `.js` import suffix because this repo's ESM TypeScript source uses explicit `.js` relative imports that resolve after compilation.
30
31
 
31
32
  Test the contract you care about:
32
33
 
33
- - the library requests `Xenova/all-MiniLM-L6-v2`
34
- - the call uses `pooling: "mean"` and `normalize: true`
35
- - non-`384` local dimensions fail before runtime loading
34
+ - indexing and querying use the same endpoint, model, and dimensions
35
+ - provider failures remain classified as build-stage failures
36
+ - queued deterministic vectors exercise exact ranking and tie behavior
36
37
 
37
38
  ## HTTP Provider Mocks
38
39
 
@@ -116,9 +117,9 @@ Apply the same pattern to `GEMINI_API_KEY`, `MISTRAL_API_KEY`, `CLOUDFLARE_ACCOU
116
117
  Prefer tests that prove bad inputs fail locally:
117
118
 
118
119
  - unknown provider
120
+ - missing provider
119
121
  - missing provider credentials
120
122
  - blank credentials where trimming is expected
121
- - invalid local dimensions
122
123
  - invalid Gemini dimensions
123
124
  - invalid `openai-compatible` `baseUrl`
124
125
  - invalid `openai-compatible` `batchSize`
@@ -1,9 +1,5 @@
1
1
  # Troubleshooting
2
2
 
3
- Use the page that matches the failure mode:
4
-
5
- - [Sharp native module issues](./TROUBLESHOOTING-SHARP.md)
6
-
7
3
  Common operational checks:
8
4
 
9
5
  - verify you called `createTable()` before indexing or searching
@@ -15,8 +11,6 @@ Common operational checks:
15
11
  - after upgrading an existing Gemini index, fully re-embed with
16
12
  `gemini-embedding-2`; for 3072-dimensional Gemini indexes, recreate the table
17
13
  or use a new table name before rebuilding
18
- - after upgrading an existing local 768-dimensional padded index, create or
19
- recreate a 384-dimensional table and fully re-index before querying it
20
14
  - if `search()` reports that the `<tableName>_embedding_idx` vector index could
21
15
  not be used, the table has no embedding vector index: re-run `createTable()`
22
16
  with the table's existing name and width to add it without touching rows, or
package/docs/TURSO.md CHANGED
@@ -44,20 +44,32 @@ import { createTable, indexContent, search } from "libsql-search";
44
44
  const database = await connect("./local.db");
45
45
  const client = tursoAdapter(database);
46
46
 
47
- await createTable(client, "articles", 384);
47
+ const embeddingOptions = {
48
+ provider: "openai-compatible" as const,
49
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
50
+ model: "bge-large-en-v1.5",
51
+ dimensions: 1024,
52
+ };
53
+
54
+ await createTable(client, "articles", 1024);
48
55
 
49
56
  await indexContent({
50
57
  client,
51
58
  contentPath: "./content",
52
- embeddingOptions: { provider: "local" },
59
+ embeddingOptions,
53
60
  });
54
61
 
55
62
  const results = await search({
56
63
  client,
57
64
  query: "how do I deploy my docs site",
58
65
  limit: 5,
59
- embeddingOptions: { provider: "local" },
66
+ embeddingOptions,
60
67
  });
68
+
69
+ // When this adapter's lifetime ends, drain and close its cached statements
70
+ // before closing the caller-owned database handle.
71
+ await client.dispose();
72
+ await database.close();
61
73
  ```
62
74
 
63
75
  Use `":memory:"` instead of a file path for an ephemeral database.
@@ -67,6 +79,29 @@ Use `":memory:"` instead of a file path for an ephemeral database.
67
79
  result shapes — `SearchOptions`, `SearchResult`, `IndexerOptions`,
68
80
  `IndexResult` — are identical on both backends.
69
81
 
82
+ ### Adapter lifetime and disposal
83
+
84
+ One adapter caches up to **32 query statements**, keyed by SQL and evicted in
85
+ least-recently-used order. The bound matters because public `tableName` options
86
+ are embedded in SQL: an unbounded cache would let a long-lived process retain a
87
+ new native statement for every valid table name it sees. Statements that are
88
+ currently running or queued are closed after they finish rather than being
89
+ evicted out from under a call.
90
+
91
+ Call `await client.dispose()` after all work using that adapter has settled.
92
+ Disposal drains queued query calls and closes every cached statement, but does
93
+ **not** close the database handle you supplied. It is terminal: later calls on
94
+ that adapter reject. Close the database separately, after disposal. If you omit
95
+ disposal, the reusable cache is still bounded at 32 entries and lives until the
96
+ underlying handle or process exits; statements evicted while in flight live
97
+ only until their queued calls finish.
98
+
99
+ The adapter serializes concurrent calls that share one cached statement. This
100
+ is required for correctness, not just memory use: the native statement mutates
101
+ its current bindings, so overlapping `all()` calls with different arguments can
102
+ otherwise return another caller's rows. Different cached SQL statements remain
103
+ independent.
104
+
70
105
  ## What is different on Turso
71
106
 
72
107
  ### There is no ANN vector index, so search is a full scan
@@ -185,13 +220,14 @@ point is still type-checked by `deno task check` so the claim above stays true.
185
220
  unchanged, and the main entry point exports no new symbol and references no
186
221
  Turso type.
187
222
 
188
- `tursoAdapter()` returns a `DatabaseAdapter`, and that type is exported from
189
- `libsql-search/turso` so you can name it:
223
+ `tursoAdapter()` returns a `TursoAdapter`, which extends `DatabaseAdapter` with
224
+ the disposal hook. Both types are exported from `libsql-search/turso` so you can
225
+ name them:
190
226
 
191
227
  ```ts
192
- import { tursoAdapter, type DatabaseAdapter } from "libsql-search/turso";
228
+ import { tursoAdapter, type TursoAdapter } from "libsql-search/turso";
193
229
 
194
- let client: DatabaseAdapter;
230
+ let client: TursoAdapter;
195
231
  ```
196
232
 
197
233
  It is exported from the subpath only. The main entry point does not export it,
@@ -210,15 +246,18 @@ to prove the two stay interchangeable, and runs as part of
210
246
  Three things in `src/turso.ts` look like noise and are not. Each has a
211
247
  regression test; none of them fails loudly at runtime if removed.
212
248
 
213
- **Prepared statements must be closed.** A statement holds native memory that the
214
- garbage collector cannot reclaim, because it is not JavaScript heap. Every
215
- `prepare()` is released with `close()` in a `finally`. Measured through the
216
- built bundle, 60 000 `executeQuery()` calls on one handle grow RSS by ~570 MB
217
- without the close and ~150 MB with it. `search()` issues exactly one query, so
218
- an SSR site calling it per request is the case that turns this from untidy into
219
- an OOM. Inside `executeAtomicWrite()` the cached statements are released only
220
- *after* `COMMIT` or `ROLLBACK` a statement stays bound to the transaction
221
- while it is open.
249
+ **Prepared statements are bounded, serialized, and explicitly disposed.** A
250
+ statement holds native memory that the garbage collector cannot reclaim,
251
+ because it is not JavaScript heap. The query path therefore reuses a 32-entry
252
+ LRU instead of preparing on every request, serializes rebinds per entry, and
253
+ closes evictions only after their queued calls finish. `TursoAdapter.dispose()`
254
+ drains and closes the remaining cache. Measured through the built bundle,
255
+ 60 000 `executeQuery()` calls grew RSS by ~570 MB when statements were never
256
+ closed, ~150 MB when each call prepared and closed, and ~7 MB when one statement
257
+ was reused. `search()` issues exactly one query, so the SSR-per-request path is
258
+ where reuse matters most. Inside `executeAtomicWrite()` the transaction-local
259
+ statements are still released only *after* `COMMIT` or `ROLLBACK` — a statement
260
+ stays bound to the transaction while it is open.
222
261
 
223
262
  **`BEGIN IMMEDIATE` sits outside the `try`.** If it were inside, a `BEGIN` that
224
263
  fails because another rebuild already holds the write lock would fall into the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.34.5",
@@ -75,7 +75,6 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@huggingface/transformers": "4.2.0",
79
78
  "gray-matter": "^4.0.3"
80
79
  },
81
80
  "devDependencies": {
@@ -1,65 +0,0 @@
1
- # Troubleshooting: Transitive `sharp` Install Errors
2
-
3
- `libsql-search` does not directly import `sharp`, but local embeddings use
4
- `@huggingface/transformers`, which currently brings in `sharp` as a transitive
5
- runtime dependency. If you see an install error mentioning `sharp`, it is
6
- usually a native-package install or approval issue.
7
-
8
- This page exists because the error can show up before your application reaches
9
- any `libsql-search` code.
10
-
11
- ## Typical Error
12
-
13
- ```text
14
- Cannot find module '../build/Release/sharp-*.node'
15
- ```
16
-
17
- Or:
18
-
19
- ```text
20
- Error: Something went wrong installing the "sharp" module
21
- ```
22
-
23
- ## Why It Happens
24
-
25
- With pnpm, native packages may need explicit build-script approval. If the
26
- relevant install script is blocked, the native binary is never downloaded or
27
- built.
28
-
29
- ## What To Do
30
-
31
- First inspect which build scripts pnpm blocked:
32
-
33
- ```bash
34
- pnpm ignored-builds
35
- ```
36
-
37
- Then approve the package that is actually failing and reinstall:
38
-
39
- ```bash
40
- pnpm approve-builds
41
- pnpm install
42
- ```
43
-
44
- In the interactive `pnpm approve-builds` prompt, select `sharp` if that is the
45
- package reporting the native-module failure.
46
-
47
- For a committed repository-level fix, you can also allow the package explicitly
48
- in `pnpm-workspace.yaml` with `onlyBuiltDependencies`.
49
-
50
- ## Relation To `libsql-search`
51
-
52
- - local embeddings use `@huggingface/transformers`
53
- - the first local embedding run may download a model at runtime
54
- - that runtime model download is separate from a pnpm native-module install
55
- failure
56
-
57
- ## Verification
58
-
59
- After reinstalling, rerun the command that originally failed. If your app uses
60
- `sharp` directly, verify that import in your own project context.
61
-
62
- ## Additional Resources
63
-
64
- - [pnpm approve-builds](https://pnpm.io/10.x/cli/approve-builds)
65
- - [Sharp installation docs](https://sharp.pixelplumbing.com/install)