Qavren.Edge — embeddings + vector store
Qavren.Edge.Onnx, Qavren.Edge.Embeddings.Onnx and Qavren.Edge.VectorData —
ONNX Runtime hosting, a Microsoft.Extensions.AI embedding generator, and a
clean-room Microsoft.Extensions.VectorData provider over vec0 + FTS5 with
reciprocal rank fusion, composed on top of the Qavren.Edge foundation
(foundation/).
The four calls
builder.UseQavrenEdge(edge => edge
.UseSqliteNative()
.AddSqlite(o => o.DatabaseName = "notes.db")
.AddOnnxEmbeddings(o => o.Preset = EmbeddingPresets.MiniLmL6V2Int8)
.AddVectorStore()
.AddVectorCollectionMigration<string, Note>(version: 1, "notes"));
public sealed class Note
{
[VectorStoreKey] public string Key { get; set; } = "";
[VectorStoreData(IsIndexed = true)] public string? Tag { get; set; }
[VectorStoreData(IsFullTextIndexed = true)] public string Title { get; set; } = "";
[VectorStoreData(IsFullTextIndexed = true)] public string Body { get; set; } = "";
[VectorStoreVector(384, DistanceFunction = DistanceFunction.CosineDistance)]
public string? Embedding => Body; // string source -> the generator fills it
}
var notes = store.GetCollection<string, Note>("notes");
await notes.UpsertAsync(new Note { Key = "n1", Title = "Roof leak", Body = "…" });
await foreach (var hit in notes.HybridSearchAsync("water damage", ["roof", "leak"], top: 10))
Console.WriteLine($"{hit.Record.Title} {hit.Score:F4}");
AddOnnxEmbeddings calls AddOnnx() for you; it is idempotent. Nothing above
hands the store a generator explicitly — AddVectorStore's factory resolves
the registered IEmbeddingGenerator from the container at collection-resolve
time and uses it wherever a Note property's vector source is a string
rather than a pre-computed embedding.
Supported platforms
Minimum OS versions: Android 24, iOS 15.1, Mac Catalyst 15.1. These are
ONNX Runtime's own native floors (default_full_aar_build_settings.json and
default_full_apple_framework_build_settings.json), not a number Qavren
chose — see ADR 0008. They are higher than the foundation package's own
minimums (Android 21, iOS/Mac Catalyst 15.0); an app that adds embeddings
raises its effective floor to match.
Mac Catalyst is proven in CI as x64 only. ci.yml runs
device-tests-maccatalyst and device-tests-ios on macos-15-intel with
-r maccatalyst-x64 / -r iossimulator-x64. A green lane there proves the
maccatalyst-x64 RID-graph resolution and exercises the x86_64 slices — it
says nothing about maccatalyst-arm64 or a real device. A manual arm64 run
on the Mac Mini is the only additional evidence, and it is recorded in the
project vault, not re-derived here.
osx-x64 (Intel macOS) is unsupported, not merely untested. ONNX Runtime
1.30.0 ships no osx-x64 native at all (verified: its runtimes/ directory
holds osx-arm64 only). Qavren.Edge.Onnx detects this at startup and
raises OnnxUnsupportedRuntime (error 5006) naming the RID, rather than
letting the failure surface later as a DllNotFoundException on first embed
call.
Packaging
Bundle MiniLmL6V2Int8 (23 MB) if you bundle a model at all. Download
everything larger. The size ceilings that drive this, none of them
estimated:
| Platform | Limit | What it means here |
|---|---|---|
| Google Play, AAB base module | 500 MB compressed download | BgeSmallEnV15 at 133 MB fits with room; two bundled presets does not leave much |
| Google Play, legacy APK | 100 MB | BgeSmallEnV15 (133 MB) does not fit — ship an AAB, or download the model |
| Google Play, any delivery | large-download notice above ~200 MB | a bundled 133 MB model plus the app pushes into the band where users see a warning |
| iOS, cellular install | user is prompted above ~200 MB | a bundled fp32 preset can push an otherwise-small app across this |
iOS / Mac Catalyst, executable __TEXT |
80 MB, no workaround | ORT's xcframework is a static library force-loaded (ForceLoad=True) into the app binary, so its selected slice counts against this cap directly; the model file itself does not, since it is a resource, not code |
On the APK path, trim $(AndroidSupportedAbis) to the ABIs you actually
ship — leave the AAB path's ABI set alone; Play already splits it per device.
No size number of our own is stated here until §19 item 3 is measured —
the actual .apk/.aab delta and iOS __TEXT delta that adding
Qavren.Edge.Onnx costs a real package. That measurement lands in the
project vault first and then here, once it exists. Until then this section
states the platform ceilings and the bundle/download rule, not an estimate.
Search scores: two different directions
SearchAsync (pure vector search) returns a vec0 distance — lower is
better. HybridSearchAsync (vector + full-text, combined by reciprocal rank
fusion) returns an RRF score — higher is better. Microsoft.Extensions.VectorData
has no attribute or convention for declaring which direction a given Score
runs, so this is the one thing a caller has to just know rather than
discover from the type system.
Semantic Kernel caveat
Semantic Kernel's AsTextEmbeddingGenerationService() adapter, when wrapping
a plain Microsoft.Extensions.AI generator, populates only EndpointKey and
ModelIdKey from EmbeddingGeneratorMetadata — it never sets DimensionsKey.
That means GetDimensions() on the SK-wrapped service returns null for a
Qavren generator, regardless of what DefaultModelDimensions reports on the
underlying IEmbeddingGenerator. This is upstream SK behaviour, not a gap in
Qavren's generator — recorded here so it is not mistakenly filed as a
Qavren.Edge bug.
No CompactAsync in v1
vec0 reclaims storage only when an entire chunk empties (roughly 256 rows
at the default chunk size), so a collection with heavy churn (frequent
updates and deletes) grows over time even though most rows are live. There is
no CompactAsync API in this version. The remedy is to drop the collection
and rebuild it from source data when reclaiming that space matters.