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, then mode="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=True and query with vector_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 undefined until the first answer.
  • A failing query throws to the nearest error boundary.
ProviderWhere a query runsA render after a writeLoads
connect(url), @fenecdb/web/clienton the server, each timewaits for the server: the subscription's change, then the query's round tripno module, 9 KB brotli of client
sync({ url, shapes })in the page, over the replicawaits for nothing: this tab's write is local, another client's arrives on the streamfenec.wasm, 236 KB brotli
Fenec.open()in the page, over its own databasewaits for nothingthe 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 in Rows, First, Count or 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[] or ReadOnlyMemory<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], FloatArray or Float32List goes to the library as its f32s, 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.

IntegrationHeld toRun with
LangChainLangChain's standard vector store suite (langchain-tests), sync and async, and filters, relevance, from_texts, the text and hybrid modes and int8 codes besidesmake python-test: fenec-server built and started here, the tests from a python:3.13 container
LlamaIndexthe 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.jswhat 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 suitemake langchain-test
Vercel AI SDKthe 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 returnedmake ai-sdk-test
Reacta stand-in database for the contract, and a real fenec-server, a replica and connect for a row another client writesmake react-test
Goa 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 textmake go-test
.NETthe same, with xunitmake dotnet-test
Python clientthe query builder against every case of integrations/builder-golden.json, sync and async, and its queries' answers against the same statements sent as textmake python-test
Swiftthe 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 simulatormake swift-test
Kotlinthe same with JUnit on the JVM, the library built for Linux with its JNI functions; the AAR built with the NDK in CImake kotlin-test (Docker)
Dart and Flutterthe same with dart test, and the Flutter plugin's test and its example built for Android and the iOS simulator in CImake 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.