flutter_edge_ai_sqlite

Renamed from flutter_gemma_rag_sqlite. Version 2.0.0 also moves RAG orchestration and contracts into flutter_edge_ai_rag: replace the old dependency/imports, add flutter_edge_ai_rag, and register SqliteVectorStoreProvider() in FlutterEdgeAiRag. Existing profile-less stores require verified, explicit adoption or re-indexing. See the migration guide.

First-class SQLite vector-store provider for flutter_edge_ai_rag. KNN runs inside SQLite via sqlite-vec (vec0 virtual table) — no Dart brute-force, no in-memory index.

Register SqliteVectorStoreProvider once; it selects the implementation:

  • Native (Android/iOS/macOS/Linux/Windows): SqliteVectorStore — package:sqlite3 (dart:ffi) + the per-platform vec0 loadable extension.
  • Web: WebSqliteVectorStore — package:sqlite3/wasm.dart driving a custom sqlite3.wasm with sqlite-vec/vec0 statically linked.

Both arms speak the same vec0 SQL dialect, so KNN and Filter behave identically across all six platforms. A vec0 table declares an id TEXT PRIMARY KEY, so KNN returns the document id directly — no JOIN, no rowid bridge.

Teach your AI assistant this package

dart run skills@ get --all

Installs the agent skills from the Flutter Edge AI package graph. The flutter-edge-ai-rag skill covers embedding models, both vector stores, and metadata filters.

Usage

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter_edge_ai_rag/flutter_edge_ai_rag.dart';
import 'package:flutter_edge_ai_sqlite/flutter_edge_ai_sqlite.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

final rag = FlutterEdgeAiRag(
  providers: [const SqliteVectorStoreProvider()],
);
// An absolute file path on native, a plain name on Web (path_provider has no
// Web implementation, so it is only called on native).
final databasePath = kIsWeb
    ? 'knowledge.db'
    : p.join((await getApplicationDocumentsDirectory()).path, 'knowledge.db');
final index = await rag.open(
  spec: VectorStoreSpec(providerId: 'sqlite', location: databasePath),
  embeddingProfile: EmbeddingProfile(
    id: 'my-embedder-v1',
    dimension: 768,
  ),
);

await index.addVector(
  id: 'doc-1',
  content: 'Flutter Edge AI runs on-device.',
  embedding: documentVector,
);
final hits = await index.searchVector(embedding: queryVector);
await index.dispose();

For text RAG with the active core embedder, replace embeddingProfile with a stable activeEmbedderProfileId and use addText / searchText. For a custom embedder, pass embedder: to open(); neither path makes the RAG registry a singleton. Dispose the index before FlutterEdgeAi.dispose() or before disposing the borrowed custom embedder.

The provider removes the application-level kIsWeb branch between the native and web stores; only the location differs. Use a stable profile ID that versions the weights, tokenizer, pooling, normalization, and document/query prefix contract. The profile is stored beside the vectors and prevents a location from being reopened with an incompatible embedder.

Databases created before 2.0.0 contain vectors but no stored profile. Open a known legacy database with allowLegacyProfileAdoption: true only after verifying the exact embedder that created it; the RAG layer checks its dimension before persisting the first profile. Leave the flag false for unverified data.

RagIndex.searchText / searchVector return cosine similarity in RetrievalResult.similarity (1 = identical, higher = better), sorted descending, filtered by threshold — the same contract as the qdrant store (vec0 returns distance; the store converts 1 - distance at the boundary).

index.flush() is a no-op on native: the connection autocommits, so a statement that returned is on disk. On web it drains the IndexedDB storage and waits for it. sqlite3 3.4.0 through 3.5.2 returned early over a write batch already in flight (upstream sqlite3.dart#408), which is why this package requires 3.6.0 and, with it, Flutter 3.47; close() drains on every version. When neither OPFS nor IndexedDB is available the store runs in memory, and flush() throws VectorStoreException.

On web, one location may be open by only one WebSqliteVectorStore at a time. The store holds an exclusive Web Lock for its complete lifetime, so another tab, worker, or store instance fails immediately with an actionable VectorStoreException instead of opening a second IndexedDB/OPFS snapshot. Current Chrome, Edge, Firefox, and Safari releases expose Web Locks. The API is feature-detected: a runtime or insecure context without navigator.locks fails closed with VectorStoreException before opening OPFS or IndexedDB, because it cannot safely coordinate another tab or worker. Always call index.dispose() or store.close() before reopening that location. The in-memory VFS remains a last resort only when Web Locks are available but persistent browser storage is not; it cannot bind a durable embedding profile, so FlutterEdgeAiRag.open() rejects it.

Declared-column filters

vec0 filters KNN only on declared, typed metadata columns (not arbitrary JSON). Declare the filterable fields in VectorStoreSpec.filterSchema; the store promotes those fields out of each document's metadata JSON into real columns and translates Filter (must/should/mustNot) into a vec0 WHERE:

final index = await rag.open(
  spec: VectorStoreSpec(
    providerId: 'sqlite',
    location: databasePath,
    filterSchema: FilterSchema(fields: [
      FilterField(name: 'lang', type: FilterFieldType.string),
      FilterField(name: 'year', type: FilterFieldType.number),
      FilterField(name: 'archived', type: FilterFieldType.bool),
    ]),
  ),
  embeddingProfile: EmbeddingProfile(id: 'my-embedder-v1', dimension: 768),
);

// later, at query time:
final hits = await index.searchVector(
  embedding: queryVec,
  topK: 10,
  filter: Filter(
    must:    [FieldRange(key: 'year', gte: 2000)],
    mustNot: [FieldEquals(key: 'archived', value: true)],
  ),
);

FilterField.name must match ^[A-Za-z][A-Za-z0-9_]*$, and must not be a name vec0 already declares: id, embedding, content, metadata, and the hidden distance and k. Otherwise rag.open() throws an ArgumentError (from the store's configure()) — at open, not at the first write, which is when the table is really built.

The name becomes a real vec0 column, and sqlite-vec's DDL grammar accepts no quoted identifier form ("doc-type", [doc-type] and `doc-type` all fail), so a name outside that set is unrepresentable rather than merely unescaped. qdrant accepts most of these names, so a schema written for it may be refused here — this set is the portable one.

Filtering on an undeclared key is a safe no-op (never throws). With no filterSchema, the store ignores filters entirely — identical to filter: null. Supported operators: =, !=, >, >=, <, <=, BETWEEN, IN (FieldEquals, FieldRange, FieldMatchAny); max 16 declared columns.

Upgrading from 1.0.x

1.1.0 does not read an index written by 1.0.x. The switch to in-SQLite vec0 KNN moved the data from a plain documents table into a vec_documents virtual table. Nothing errors on upgrade: initialize() succeeds, getStats() reports 0 documents, searchSimilar() returns no hits, and the old rows sit untouched in documents. This was not called out when 1.1.0 shipped.

Your data is intact and needs no re-embedding — 1.0.x stored the vector as a Float32 BLOB alongside the id, content and metadata. The one-time copy (verify the embedder that produced the old vectors, bind its profile, re-add the rows, then drop documents) is in the migration guide. There is no built-in migration call — this is a one-time fix for an upgrade that has already happened.

Setup

Native needs no setup — the vec0 loadable extension is fetched per platform by this package's Native Assets hook (hook/build.dart), SHA256-verified, and loaded automatically before any database is opened.

New in 1.3.0: that fetch is real. Until 1.2.0 the loadables were committed into the package, so every install carried all seven platforms' binaries to use one of them. They now come from this repository's native-sqlite-vec-v* GitHub Release, which means the first build of each platform needs github.com reachable; the library is cached under ~/.cache/flutter_gemma/native/ (~/Library/Caches/… on macOS, %LOCALAPPDATA%\… on Windows) and later builds do not go out again. flutter_edge_ai_litertlm has always worked this way. If you build in an air-gapped environment, pre-populate that cache directory.

Web ships the custom sqlite3.wasm (with sqlite-vec linked in) as the package web asset web/rag/sqlite3.wasm. Copy it into your app's web root so it sits next to index.html at rag/sqlite3.wasm — that's the URL WasmSqlite3.loadFromUrl fetches. After flutter pub get, the package directory is the rootUri printed under its name in your app's .dart_tool/package_config.json:

grep -A1 '"name": "flutter_edge_ai_sqlite"' .dart_tool/package_config.json
# <pkg> = that rootUri without file:// (a relative one is relative to .dart_tool/)
mkdir -p web/rag
cp <pkg>/web/rag/sqlite3.wasm web/rag/sqlite3.wasm

OPFS persistence and SharedArrayBuffer require your web server to send the cross-origin isolation headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

There is no CDN <script>, no wa-sqlite worker, and no index.html wiring anymore.

Libraries

flutter_edge_ai_sqlite
SQLite vector search (sqlite-vec / vec0) on-device RAG vector store for Flutter Edge AI RAG.