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
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,3 @@ jobs:
- name: Run dependency review
uses: actions/dependency-review-action@v4
if: github.event_name == 'pull_request'
with:
# Upstream baseline from @huggingface/transformers 4.2.0 native runtime deps.
# Keep this narrow and remove entries when HF/onnxruntime/sharp ship patched versions.
allow-ghsas: >-
GHSA-xcpc-8h2w-3j85,
GHSA-f88m-g3jw-g9cj
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

Use it when you want:

- one indexing/search API across local and hosted embedding providers
- one indexing/search API across external embedding providers
- direct control over vector dimensions, table names, and deployment shape
- a lightweight library instead of a hosted search product

Expand Down Expand Up @@ -45,7 +45,7 @@ Note for `0.17.x`: the client no longer exports `./package.json`, so `require("@

## Quick Start

This example uses the default local provider. Local embeddings run in-process after the initial model download and cache warmup; they are not automatically air-gapped.
This example uses a separately deployed OpenAI-compatible embedding service. `libsql-search` never loads or hosts an embedding model in-process.

```ts
import { createClient } from "@libsql/client";
Expand All @@ -56,25 +56,29 @@ const client = createClient({
authToken: "your-auth-token",
});

await createTable(client, "articles_local_384", 384);
const embeddingOptions = {
provider: "openai-compatible" as const,
baseUrl: process.env.EMBEDDING_BASE_URL!,
apiKey: process.env.EMBEDDING_API_KEY,
model: "bge-large-en-v1.5",
dimensions: 1024,
};

await createTable(client, "articles_bge_1024", 1024);

await indexContent({
client,
contentPath: "./content",
tableName: "articles_local_384",
embeddingOptions: {
provider: "local",
},
tableName: "articles_bge_1024",
embeddingOptions,
});

const results = await search({
client,
query: "how do I deploy my docs site",
tableName: "articles_local_384",
tableName: "articles_bge_1024",
limit: 5,
embeddingOptions: {
provider: "local",
},
embeddingOptions,
});

console.log(results.map((result) => ({
Expand All @@ -91,7 +95,7 @@ Important behavior:
- `indexContent()` embeds every document before it touches the database, then replaces the table in one transaction, so a failed rebuild leaves the previous index intact.
- `indexContent()` throws `IndexingError` when a file fails; pass `failurePolicy: "skip"` to rebuild from the remaining files.
- `indexContent()` throws `IndexingError` when no source files are found; pass `allowEmptyIndex: true` to intentionally empty the index.
- Hosted providers send indexed and queried text to external services and may incur provider charges.
- Every provider sends indexed and queried text to an external service; review that service's privacy, retention, and pricing terms.

## Search Accuracy And Performance

Expand All @@ -103,10 +107,10 @@ Two options control the trade-off:

```ts
// Widen the index probe to raise recall (default: max(limit * 4, 32))
await search({ client, query, limit: 10, candidates: 200 });
await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });

// Bypass the index entirely: exact, but linear in table size
await search({ client, query, exact: true });
await search({ client, query, embeddingOptions, exact: true });
```

`exact: true` is the only way to guarantee exactness. Use it for small corpora, for correctness checks against the index path, and for tables that have no vector index.
Expand All @@ -117,7 +121,6 @@ Requirements: `vector_top_k()` and `libsql_vector_idx()` need a libSQL build wit

Built-in providers:

- `local` with `Xenova/all-MiniLM-L6-v2` at 384 dimensions
- `cloudflare` with `@cf/baai/bge-m3` at 1024 dimensions
- `mistral` with `mistral-embed` at 1024 dimensions
- `gemini` with `gemini-embedding-2` at 128-3072 dimensions, default 3072
Expand Down
39 changes: 22 additions & 17 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Indexes Markdown files from a directory on disk.
interface IndexerOptions {
client: Client | DatabaseAdapter;
contentPath: string;
embeddingOptions?: EmbeddingOptions;
embeddingOptions: EmbeddingOptions;
fileExtensions?: string[];
exclude?: string[];
tableName?: string;
Expand Down Expand Up @@ -187,7 +187,16 @@ class IndexingError extends Error {
import { indexContent, IndexingError } from "libsql-search";

try {
await indexContent({ client, contentPath: "./content" });
await indexContent({
client,
contentPath: "./content",
embeddingOptions: {
provider: "openai-compatible",
baseUrl: process.env.EMBEDDING_BASE_URL!,
model: "bge-large-en-v1.5",
dimensions: 1024,
},
});
} catch (error) {
if (error instanceof IndexingError) {
console.error(error.phase, error.failures);
Expand All @@ -212,7 +221,7 @@ interface SearchOptions {
query: string;
limit?: number;
tableName?: string;
embeddingOptions?: EmbeddingOptions;
embeddingOptions: EmbeddingOptions;
candidates?: number;
exact?: boolean;
}
Expand Down Expand Up @@ -246,7 +255,7 @@ Controls how many rows the index returns for the exact re-rank. It must be an in

```ts
// Trade query cost for recall on a large corpus
const results = await search({ client, query, limit: 10, candidates: 200 });
const results = await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });
```

`candidates` has no effect when `exact` is `true` — that path scans every row — but it is **still validated**. `search({ exact: true, limit: 10, candidates: 5 })` throws, exactly as it would on the index path. Validity does not depend on which path a call happens to take.
Expand All @@ -262,7 +271,7 @@ Related exported constants:
Set `exact: true` to bypass the index and score every row in the table.

```ts
const results = await search({ client, query, exact: true });
const results = await search({ client, query, embeddingOptions, exact: true });
```

This is the guaranteed-exact path: it computes `vector_distance_cos` for every row with a non-`NULL` embedding, sorts by `(distance, id)`, and trims to `limit`. Cost grows linearly with table size, so it is intended for small corpora, correctness checks against the index path, and tables that have no vector index.
Expand Down Expand Up @@ -330,8 +339,7 @@ All retrieval helpers validate `tableName` before executing SQL, and all of them

```ts
interface EmbeddingOptions {
provider?:
| "local"
provider:
| "cloudflare"
| "mistral"
| "gemini"
Expand All @@ -353,7 +361,7 @@ interface EmbeddingOptions {

Important option rules:

- `provider` defaults to `local`
- `provider` is required; every provider is an external service
- `maxLength` defaults to `8000`
- `timeoutMs` defaults to `30000`
- `model` is only used by `openai-compatible`
Expand All @@ -364,7 +372,6 @@ Important option rules:

Dimension rules:

- local: fixed `384`
- Cloudflare: fixed `1024`
- Mistral: fixed `1024`
- Gemini: default `3072`, allowed integer range `128-3072`
Expand All @@ -373,7 +380,7 @@ Dimension rules:

See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.

### `generateEmbedding(text, options?)`
### `generateEmbedding(text, options)`

Generates one embedding vector.

Expand All @@ -385,15 +392,15 @@ const embedding = await generateEmbedding("deploy docs", {
});
```

### `generateEmbeddings(texts, options?)`
### `generateEmbeddings(texts, options)`

Generates an ordered batch of embeddings.

- empty batches return `[]` without loading the local model or making a hosted call
- empty batches return `[]` without configuring credentials or making a provider call
- OpenAI batches above `2048` inputs are rejected before network work
- `openai-compatible` batches are chunked sequentially according to `batchSize`

### `createEmbeddingProvider(options?)`
### `createEmbeddingProvider(options)`

Creates a provider client with immutable metadata and an `embed(texts, options?)` method.

Expand All @@ -413,7 +420,7 @@ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers

Hosted provider clients are scoped to their current options. The library does not reuse a Cloudflare, Mistral, Gemini, or OpenAI client across different credential sets or configurations.

### `getEmbeddingProviderMetadata(options?)`
### `getEmbeddingProviderMetadata(options)`

Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.

Expand All @@ -422,7 +429,6 @@ Metadata shape:
```ts
interface EmbeddingProviderMetadata {
name:
| "local"
| "cloudflare"
| "mistral"
| "gemini"
Expand Down Expand Up @@ -451,7 +457,6 @@ Batch interpretation:
interface EmbeddingBatchResult {
embeddings: number[][];
provider:
| "local"
| "cloudflare"
| "mistral"
| "gemini"
Expand All @@ -474,7 +479,7 @@ Validates provider responses before they reach the database:

### `padEmbedding(embedding, targetDimensions)`

Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows, but the current local provider uses its native `384` dimensions rather than padding by default.
Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows; provider adapters otherwise validate and preserve the vectors returned by their external service.

### `prepareTextForEmbedding(fields)`

Expand Down
20 changes: 13 additions & 7 deletions docs/INDEXING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ The slug is derived from the file path relative to `contentPath`.
`indexContent()` replaces the whole target table:

```ts
const embeddingOptions = {
provider: "openai-compatible" as const,
baseUrl: process.env.EMBEDDING_BASE_URL!,
model: "bge-large-en-v1.5",
dimensions: 1024,
};

await indexContent({
client,
contentPath: "./content",
tableName: "articles_local_384",
embeddingOptions: {
provider: "local",
},
tableName: "articles_bge_1024",
embeddingOptions,
});
```

Expand Down Expand Up @@ -69,7 +74,7 @@ Both are governed by `failurePolicy` like any other build failure, so they abort
import { indexContent, IndexingError } from "libsql-search";

try {
await indexContent({ client, contentPath: "./content" });
await indexContent({ client, contentPath: "./content", embeddingOptions });
} catch (error) {
if (error instanceof IndexingError) {
for (const failure of error.failures) {
Expand All @@ -87,6 +92,7 @@ By default one bad file aborts the whole rebuild. To index everything that can b
const result = await indexContent({
client,
contentPath: "./content",
embeddingOptions,
failurePolicy: "skip",
});

Expand All @@ -105,6 +111,7 @@ An empty source directory throws by default, because silently leaving stale rows
await indexContent({
client,
contentPath: "./content",
embeddingOptions,
allowEmptyIndex: true,
});
```
Expand Down Expand Up @@ -173,7 +180,6 @@ Many projects wire indexing into a dedicated script and call it before their sit

## Runtime Notes

- local embeddings may download and cache a model on the first run
- Node users need `@libsql/client` installed alongside the package, at `^0.15.0 || ^0.17.0`; the packaged build is smoke-tested against both arms (`0.15.15` and `0.17.4`), which covers table and vector-index creation. `batch()` rollback behaves identically on both at the contract level, though its error text differs — see [Version differences](./TROUBLESHOOTING.md#libsqlclient-version-differences). Upgrading the client is not a prerequisite for upgrading this package. Deno/JSR users are not covered by that range and should pin the client themselves — see [Install](../README.md#install)
- hosted providers send indexed or queried text to external services
- all embedding providers send indexed or queried text to external services; this library never loads a model in-process
- the repository validates package build and `deno check`, but indexing still depends on filesystem access
Loading
Loading