Integrations
Use fenecdb from LangChain, LlamaIndex, LangChain.js, the Vercel AI SDK and React, each tested the way its framework tests its own, from Go and .NET through their SDKs, and embedded in Swift, Kotlin and Dart apps.
LangChain
A LangChain vector store over fenec-server's HTTP endpoint. Install it, point it at the server, and use it like any other store.
pip install "fenecdb[langchain]"from fenecdb.langchain import FenecVectorStore
store = FenecVectorStore(
embeddings, "handbook",
url="http://127.0.0.1:8080", token="...",
metadata_fields={"source": "text", "year": "int"},
)
store.add_documents(documents)
store.similarity_search("how do I compact", k=4, filter={"source": "ops"})
retriever = store.as_retriever(search_kwargs={"k": 8})- Filter only on declared fields: list them in
metadata_fields. - Keyword and hybrid search: pass
full_text=True, thenmode="text"or"hybrid".
Details
A document is a row: its id, its text, its metadata as JSON and its vector
under @hnsw, in a collection created on the first write, once the
embedding's size is known. Adding an id that is already there replaces it —
the old row out and the new one in, sent as one /batch and run
under one write lock, so no reader sees the id twice or not at all.
A metadata field named in metadata_fields gets a column of its
own under @hash, and filter takes equality and
$in over those: {"source": "ops"} becomes
where source = $2, answered by the index before
near looks for neighbours among the rows that passed. A filter on
a field not declared is refused rather than applied to the JSON after the
search, which would return fewer than k rows and not say
so.
The score is fenecdb's own, the one the HTTP endpoint and the browser
module return: the cosine similarity under cosine (the default),
the distance under l2, the product under dot.
similarity_search_with_relevance_scores maps them the way
LangChain's own functions do.
store = FenecVectorStore(embeddings, "handbook", url=..., token=...,
full_text=True, quant="int8")
store.similarity_search("compact dead records", k=4, mode="text") # BM25 alone
store.similarity_search("how do I compact", k=4, mode="hybrid") # both, fused
retriever = store.as_retriever(search_kwargs={"k": 8, "mode": "hybrid"})With full_text=True the text is indexed for BM25 as well
(@text, added to a collection made before without it), and a
search takes mode: "text" ranks by the words alone
with match and embeds nothing, and "hybrid" runs
match and near each to their own depth and fuses
the two rankings (fuse). Their
scores are BM25's and the fusion's, not similarities. quant,
"int8" or "bit" (cosine only), keeps the
graph over codes, and near still orders by the vectors
themselves.
LangChain.js
The same store from JavaScript. It runs over a database in the page, with no server, or over fenec-server's HTTP endpoint.
import { Fenec, connect } from '@fenecdb/web';
import { FenecVectorStore } from '@fenecdb/langchain';
// The database in the page -- or behind fenec-server: connect('http://127.0.0.1:8080')
const db = await Fenec.open('./fenec.wasm');
const store = new FenecVectorStore(embeddings, {
client: db,
collection: 'handbook',
metadataFields: { source: 'text' },
fullText: true,
});
await store.addDocuments(documents);
await store.similaritySearch('how do I compact', 4, { source: 'handbook' });
await store.search('compact', 4, { mode: 'hybrid' });
const retriever = store.asRetriever({ k: 4 });Details
The Python store's collection, from JavaScript: a document is a row of
its id, text, metadata as JSON and vector under @hnsw, made on
the first write; metadataFields get columns of their own under
@hash, and filter takes equality and
$in over them; with fullText,
search(query, k, {mode}) ranks by the words
('text') or both ('hybrid'). client
is anything with run(sql, params): a Fenec keeps
the whole RAG index in the page, no server, and a FenecHttp the
server's. Writing an id already there replaces it — two statements, which
over HTTP are two requests.
LlamaIndex
A LlamaIndex vector store over fenec-server's HTTP endpoint, with metadata filters, deletion by document, and text and hybrid search.
pip install "fenecdb[llama-index]"from fenecdb.llama_index import FenecVectorStore
from llama_index.core import StorageContext, VectorStoreIndex
store = FenecVectorStore("handbook", url="http://127.0.0.1:8080", token="...",
metadata_fields={"author": "text", "year": "int"})
index = VectorStoreIndex.from_documents(
documents, storage_context=StorageContext.from_defaults(vector_store=store)
)
index.as_retriever(similarity_top_k=4).retrieve("how do I compact")- Hybrid search: pass
full_text=Trueand query withvector_store_query_mode="hybrid".
Details
A node is a row with its document's id beside it, so
index.delete_ref_doc(...) is one del over a hashed
field. Filters take ==, !=, <,
<=, >, >=, in and
nin over the declared fields, joined by and or
or and nested; node_ids and doc_ids
narrow the same way. get_nodes, delete_nodes and
clear are there; delete_nodes() with neither ids nor
filters deletes nothing — everything is clear, asked for by name.
With full_text=True the text is indexed for BM25 as well, and
a query takes the modes that need it: TEXT_SEARCH ranks by the
words alone (match), and HYBRID runs
match and near each to their own depth —
hybrid_top_k, or sparse_top_k, deep — and fuses the
two rankings (fuse). The fusion
adds ranks rather than scores, so alpha has nothing to weigh and
is not read. quant is taken as the LangChain store takes it.
store = FenecVectorStore("handbook", url=..., token=..., full_text=True)
retriever = index.as_retriever(similarity_top_k=4, vector_store_query_mode="hybrid")Vercel AI SDK
Three functions to copy from integrations/ai-sdk/rag.js:
index your documents, retrieve for a question, and
searchTool for a model to call.
import { generateText, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { Fenec } from '@fenecdb/web';
import { index, retrieve, searchTool } from './rag.js'; // integrations/ai-sdk/rag.js
const db = await Fenec.open('./fenec.wasm');
const model = openai.textEmbeddingModel('text-embedding-3-small');
await index(db, { collection: 'docs', model, documents }); // [{text, source?}]
const { text } = await generateText({
model: openai('gpt-4.1'),
tools: { search: searchTool(db, { collection: 'docs', model }) },
stopWhen: stepCountIs(4),
prompt: 'How do I compact the database?',
});Details
The AI SDK has no vector store interface to implement — it has embedding
models and tool — so the integration is a file of three
functions to copy, integrations/ai-sdk/rag.js:
index embeds with embedMany and writes the
documents, their text under @text and their vectors under
@hnsw; retrieve embeds the question with
embed and answers with near, or with
hybrid the words and the vector fused, kept to a
source through its @hash index;
searchTool is a tool a model calls with a query.
It takes a Fenec, so the index lives in the page or a Worker,
or a FenecHttp for the server's.
React
useLiveQuery renders a query over the database in the page
— a Fenec of its own or a synced replica — and runs it again
after every write it may read. No render waits on the network.
import { sync, type FenecSync, type Row } from '@fenecdb/web';
import { FenecProvider, useFenec, useLiveQuery } from '@fenecdb/react';
import type { FenecSchema } from './fenec-schema.js'; // fenec types, over the server's file
type TaskRow = Row<FenecSchema['tasks']>;
const db = await sync<FenecSchema>({ url: 'http://127.0.0.1:8080', shapes: [{ collection: 'tasks' }] });
function Open() {
const db = useFenec<FenecSync<FenecSchema>>();
const rows = useLiveQuery<TaskRow>(db.from('tasks').where('done', false).order('title'));
if (rows === undefined) return <Spinner />;
return rows.map((t) => <Task key={t.id} task={t} />);
}
<FenecProvider db={db}><Open /></FenecProvider>- No server needed:
<FenecProvider db={await Fenec.open()}>makes the page's own database the app's state (State in the page). - No module needed:
<FenecProvider db={connect(url)}>, from@fenecdb/web/client, runs each query on the server and again after every write to a collection it reads. - It is
undefineduntil the first answer. - A failing query throws to the nearest error boundary.
| Provider | Where a query runs | A render after a write | Loads |
|---|---|---|---|
connect(url), @fenecdb/web/client | on the server, each time | waits for the server: the subscription's change, then the query's round trip | no module, 9 KB brotli of client |
sync({ url, shapes }) | in the page, over the replica | waits for nothing: this tab's write is local, another client's arrives on the stream | fenec.wasm, 236 KB brotli |
Fenec.open() | in the page, over its own database | waits for nothing | the same fenec.wasm |
Details
Over a replica or a database in the page the rows come from the page, so
no render waits on the network: live runs the query again after
every write it may read — a write of this tab's, or one the server streamed
into a replica from another client — and the component renders what it
answers. Over connect the query runs on the server, again after
each write the server's subscriptions report to a collection it reads; a
FenecQL text names those with { collections } there, since
nothing in the page can know what a text reads. A FenecQL text
works too, on the provider's database:
useLiveQuery('get todos count', { collections: ['todos'] }). useLiveQuery is
undefined until the first answer, and subscribes by the query's
text and parameters, so the query built again on every render, which is the
natural way to write one, does not subscribe again unless it asks something
else; the rows of the query it asked before are not handed out as this one's.
A query that fails throws while rendering, to the nearest error boundary. A
query that asks for facets has them on
its rows, rows.facets.brand, counted over every match: a
results list and its filter sidebar from one hook, live alike. It
lives in integrations/react, with React as its one peer
dependency.
Go and .NET
An SDK each over fenec-server's HTTP endpoint, each its standard library
alone, beside Python's fenecdb and JavaScript's
@fenecdb/web.
go get github.com/fenecdb/fenec/integrations/go
dotnet add package FenecDb- The same surface in both: a statement and its rows, typed rows, writes with the change they left the database at, atomic batches, an idempotency key, a read that waits for a write on a replica, a subscription, the change stream, a tenant, the health check, and a typed error carrying the status.
- The query builder, as Python's and JavaScript's have it:
db.From("docs").Where("year", ">=", 2024).Near("embed", v).Limit(5), immutable, ending inRows,First,Countor a write. All four build the same text from the same chain, held to one golden file (The builder). - Vectors round-trip to the bit: a
[]float32,float[]orReadOnlyMemory<float>goes out as the decimals that read back as each float.
Each language's example is on Your
language, and the API in integrations/go and
integrations/dotnet.
Details
The server reads a number as a double and rounds it to a float, and one
float, 7.038531e-26, has a shortest text whose double lands
exactly between it and the float above: Go's encoding/json
wrote it so, and the server stored the float above. Both SDKs write that one
as its double's text, as the server itself writes it, and their tests try
one float in 4 099 over every exponent and read a vector holding it back
from a server.
A subscription takes the REST surface's filter
(year=gte.2024, where=, select=),
not a statement with parameters: the stream is
GET /<collection>/changes. A request is bounded by the
client's timeout through its context or token, never the HTTP client's own,
which would cut a subscription short.
Swift, Kotlin and Dart
Not over HTTP: the database in the app, a file on the device, through
the native library (crates/fenec-ffi). A live query drives the
UI -- a SwiftUI observable, a Kotlin Flow for Compose, a Dart
Stream for a StreamBuilder.
.package(url: "https://github.com/fenecdb/fenec", from: "0.1.7") // Swift: product FenecDB
implementation("com.fenecdb:fenecdb-android:0.1.7") // Kotlin, Android
flutter pub add fenecdb_flutter // Flutter- The query builder, the same seven ways, held to the one golden file (The builder).
- Vectors as their bytes: a
[Float],FloatArrayorFloat32Listgoes to the library as itsf32s, never as digits. - Live queries, run again once a burst of writes to a collection they read.
The examples, where the file goes and what a write survives are on Mobile and native apps.
Their tests
Each integration runs its framework's own tests, or the ones its framework holds its integrations to, against a real fenecdb.
| Integration | Held to | Run with |
|---|---|---|
| LangChain | LangChain's standard vector store suite (langchain-tests), sync and async, and filters, relevance, from_texts, the text and hybrid modes and int8 codes besides | make python-test: fenec-server built and started here, the tests from a python:3.13 container |
| LlamaIndex | the tests LlamaIndex's own vector store integrations run — the class, a write and a nearest match, deletion by document, nodes by id and by filter, every filter operator, an index on top — and the text search and hybrid modes | |
| LangChain.js | what LangChain.js's own vector store integrations are held to — documents in and found again with their ids and scores, replaced under an id, deleted, filtered, fromTexts, a retriever — and the text and hybrid modes, each over a database in the page and over fenec-server's HTTP endpoint; LangChain.js publishes no standard suite | make langchain-test |
| Vercel AI SDK | the SDK's own mock models: documents embedded with embedMany and found again with near, kept to a source and fused with the words, and a model that calls the search tool in generateText and answers from what it returned | make ai-sdk-test |
| React | a stand-in database for the contract, and a real fenec-server, a replica and connect for a row another client writes | make react-test |
| Go | a primary with a token, a JWT policy and the change stream, a replica and a node of tenants, started by the tests: near with exact scores, parameters of every type, the errors, a batch put back whole, an idempotency key, After on the replica, a subscription, /_changes, a tenant and a scoped token; the query builder against every case of integrations/builder-golden.json, and its queries' answers against the same statements sent as text | make go-test |
| .NET | the same, with xunit | make dotnet-test |
| Python client | the query builder against every case of integrations/builder-golden.json, sync and async, and its queries' answers against the same statements sent as text | make python-test |
| Swift | the engine through the library -- near with exact scores, rows decoded, a json field's list as written, every kind of error, a file opened again and refused a second open, many tasks at once -- the query builder against every case of the golden file and its answers against the statements as text, live queries as a stream and as observables, a burst of writes running each once; on macOS, and in CI on an iOS simulator | make swift-test |
| Kotlin | the same with JUnit on the JVM, the library built for Linux with its JNI functions; the AAR built with the NDK in CI | make kotlin-test (Docker) |
| Dart and Flutter | the same with dart test, and the Flutter plugin's test and its example built for Android and the iOS simulator in CI | make dart-test |
Details
LangChain's suite writes the same documents twice under the same ids, and
that is how it found a bug in the graph: a node whose every candidate
neighbour had been deleted was linked to nothing, so a collection deleted and
written again answered near with nothing. It is fixed in the
engine, with the test that shows it.