Skip to content
Current documentation — tracks the engine's development branch (v0.4.0 at the last sync).v0.2.1v0.3.0v0.3.1v0.3.2v0.4.0v0.4.1About these docs

corvid-swift

corvid-swift is the Swift binding for Apple platforms — iOS 13+ (device and simulator, arm64 + x86_64) and macOS 10.15+ (arm64 + x86_64). One Swift Package, installed by URL, nothing else: the wrapper calls the engine’s frozen C ABI directly through the clang module formed from the prebuilt CorvidEngine.xcframework (iOS device + fat iOS-simulator + fat macOS staticlib slices) that every engine release publishes. The engine is statically linked — no dynamic loading, no symbol search paths — and the pin is double: the binary target’s URL tag plus its sha256 checksum, both verified by the binding’s release gate against the engine release’s own checksums.

When to choose this binding: your project is Swift on iOS or macOS and you want corvid embedded with the platform’s own shape — throws/CorvidError carrying the engine’s frozen error codes, deinit-based handle lifetimes, Data keys, [String: Any?] documents, and closures for scan/update (a throwing closure aborts the engine call and rethrows at the call site).

Swift Package Manager — the package URL is the repository:

dependencies: [
.package(url: "https://github.com/corvid-db/corvid-swift.git", from: "0.4.1")
]
.target(name: "App", dependencies: [
.product(name: "Corvid", package: "corvid-swift"),
])

Requires Xcode 16+ / a Swift 6 toolchain (the package itself builds in the Swift 5 language mode). No other dependencies. Platform scope: iOS + macOS slices ship today; watchOS/visionOS/tvOS are a recorded, additive follow-up in the binding’s PLAN.

import Corvid
let db = try Corvid.openMemory()
let docs = try db.collection("docs")
try docs.insert(
Data("s1".utf8),
["kind": "doc",
"body": "rust embedded database",
"v": [1.0, 0.0] as [Float]] as [String: Any?])
let rows = Array(try docs.query()
.filter(try field("kind").eq("doc"))
.vector("v", [1.0, 0.0], k: 2, metric: .cosine)
.run())
let phrase = Array(try docs.phraseSearch(
field: "body", phrase: "embedded database", k: 2))
db.close() // deinit also closes; Db/Collection are thread-safe

Documents are Swift values: [String: Any?] maps and [Any?] arrays whose leaves are Bool, Int, UInt (by bit pattern), Double, Float, String, Data, and [Float] vectors — note that a bare [1.0, 0.0] inside a document literal infers [Double] and encodes as an array; vectors are [Float], so write [1.0, 0.0] as [Float]. Keys are Data. NaNs cross bit-exact both ways.

Six runnable programs as executable targets in the package (swift run <Name>), executed on every CI leg with deterministic output: quickstart, hybrid (the flagship), vector-index (in-memory / on-disk / binary-quantized HNSW vs the exact scan), text-search (BM25 incl. CJK bigram segmentation, plus the direct phrase search), graph (neighbors/traverse + delete cascade), and geo (radius / bbox / nearest). All six sources are embedded below — imported from the repo so they cannot drift from what CI executes (scripts/sync-binding-examples.sh; the drift gate reddens docs CI if they diverge).

let db = try Corvid.openMemory()
defer { db.close() }
let docs = try db.collection("docs")
try docs.insert(
Data("p1".utf8),
["title": "rust embedded database", "kind": "doc",
"v": [1.0, 0.0] as [Float]] as [String: Any?])
try docs.insert(
Data("p2".utf8),
["title": "python web frameworks", "kind": "doc",
"v": [0.0, 1.0] as [Float]] as [String: Any?])
try docs.insert(
Data("p3".utf8),
["title": "rust again database", "kind": "doc",
"v": [0.9, 0.1] as [Float]] as [String: Any?])
// kNN: the 3 nearest documents to (1, 0) under cosine. Project the
// field the printout needs (docs decode in full either way; select
// trims the payload). render flattens the Any?-boxed values Swift's
// dictionary printing would show as Optional(...).
func render(_ doc: Any?) -> String {
guard let d = doc as? [String: Any?] else { return String(describing: doc ?? "nil") }
return "[" + d.sorted { $0.key < <!-- corvid-examples:quickstart BEGIN -->.key }
.map { "\($0.key): \($0.value.map { String(describing: $0) } ?? "nil")" }
.joined(separator: ", ") + "]"
}
let rows = Array(try docs.query()
.vector("v", [1.0, 0.0], k: 3, metric: .cosine)
.select(["title"])
.run())
for (rank, r) in rows.enumerated() {
print(String(format: "%d. %@ score=%.6f %@",
rank + 1, String(decoding: r.key, as: UTF8.self), r.score, render(r.doc)))
}
let db = try Corvid.openMemory()
defer { db.close() }
let docs = try db.collection("docs")
try docs.insert(
Data("s1".utf8),
["kind": "doc", "body": "rust embedded database",
"v": [1.0, 0.0] as [Float]] as [String: Any?])
try docs.insert(
Data("s2".utf8),
["kind": "doc", "body": "python web frameworks",
"v": [0.0, 1.0] as [Float]] as [String: Any?])
try docs.insert(
Data("s3".utf8),
["kind": "doc", "body": "rust again database",
"v": [0.9, 0.1] as [Float]] as [String: Any?])
try docs.insert(Data("m1".utf8), ["kind": "meta"]) // filtered out below
// The flagship query: filter + vector + text, RRF + MMR + limit.
// render flattens the Any?-boxed values Swift's dictionary printing
// would show as Optional(...).
func render(_ doc: Any?) -> String {
guard let d = doc as? [String: Any?] else { return String(describing: doc ?? "nil") }
return "[" + d.sorted { $0.key < <!-- corvid-examples:hybrid BEGIN -->.key }
.map { "\($0.key): \($0.value.map { String(describing: $0) } ?? "nil")" }
.joined(separator: ", ") + "]"
}
let rows = Array(try docs.query()
.filter(try field("kind").eq("doc"))
.vector("v", [1.0, 0.0], k: 2, metric: .cosine)
.text("body", "rust database", k: 2)
.fuseRRF(k: 60)
.rerankMMR(lambda: 1)
.limit(n: 2)
.select(["body"])
.run())
for (rank, r) in rows.enumerated() {
print(String(format: "%d. %@ score=%.6f %@",
rank + 1, String(decoding: r.key, as: UTF8.self), r.score, render(r.doc)))
}
let path = NSTemporaryDirectory() + "corvid-swift-example-vector-index.redb"
try? FileManager.default.removeItem(atPath: path) // reruns start clean (single-file db)
do {
let db = try Corvid.open(path)
defer { db.close() }
let items = try db.collection("items")
for (key, v) in corpus {
try items.insert(Data("\(key)".utf8),
["v_mem": v, "v_disk": v, "v_q": v] as [String: Any?])
}
try items.createVectorIndex("v_mem", metric: .cosine)
try items.createVectorIndexOnDisk("v_disk", metric: .cosine)
try items.createVectorIndexQuantized("v_q", metric: .cosine, quant: .binary)
print("top-4 nearest to (1,0,0,0) under cosine:")
try runQuery(items, "v_mem", approx: false, "exact (scan):")
try runQuery(items, "v_mem", approx: true, "ann in-memory HNSW:")
try runQuery(items, "v_disk", approx: true, "ann on-disk HNSW:")
try runQuery(items, "v_q", approx: true, "ann binary-quantized:")
print("(the quantized lane trades recall for a ~32x smaller index)")
}
// Reopen: the on-disk graph reloads (no rebuild) and answers again.
do {
let db = try Corvid.open(path)
defer { db.close() }
let items = try db.collection("items")
try runQuery(items, "v_disk", approx: true, "ann on-disk after reopen:")
}
try? FileManager.default.removeItem(atPath: path)
let db = try Corvid.openMemory()
defer { db.close() }
let notes = try db.collection("notes")
for (key, body) in corpus {
try notes.insert(Data(key.utf8), ["body": body] as [String: Any?])
}
try notes.createTextIndex("body")
try search(notes, "quick fox", "bm25 \"quick fox\":")
try search(notes, "quick dog", "bm25 \"quick dog\":")
try search(notes, "城市", "bm25 CJK 城市 (city):")
try search(notes, "数据库", "bm25 CJK 数据库 (database):")
try phrase(notes, "fox jumps over", "phrase \"fox jumps over\":")
try phrase(notes, "over jumps fox", "phrase \"over jumps fox\" (reversed — no match):")
try phrase(notes, "leaps over a sleeping", "phrase with stop words collapsed:")

Graph (neighbors, traverse, delete cascade)

Section titled “Graph (neighbors, traverse, delete cascade)”
let db = try Corvid.openMemory()
defer { db.close() }
let nodes = try db.collection("nodes")
for key in ["ga", "gb", "gc"] {
try nodes.insert(Data(key.utf8), ["n": key] as [String: Any?])
}
try nodes.link(Data("ga".utf8), "parent_of", Data("gb".utf8))
try nodes.link(Data("ga".utf8), "parent_of", Data("gc".utf8))
try nodes.link(Data("gb".utf8), "parent_of", Data("gd".utf8)) // gd never a document
try nodes.linkWeighted(Data("ga".utf8), "route", Data("gb".utf8), weight: 2.5)
try nodes.linkWeighted(Data("ga".utf8), "route", Data("gd".utf8), weight: 0.75)
let ga = Data("ga".utf8)
let gb = Data("gb".utf8)
show("neighbors(ga)", Array(try nodes.neighbors(ga, "parent_of")))
show("in_neighbors(gb)", Array(try nodes.inNeighbors(gb, "parent_of")))
let routes = try nodes.neighborsWeighted(ga, "route")
.map { String(format: "%@=%0.2f", String(decoding: $0.key, as: UTF8.self), $0.weight) }
.joined(separator: " ")
print("\(pad("routes from ga (weighted):", 36)) [\(routes)]")
show("traverse(ga, 1 hop)", Array(try nodes.traverse(ga, "parent_of", hops: 1)))
show("traverse(ga, 2 hops)", Array(try nodes.traverse(ga, "parent_of", hops: 2)))
// Delete cascade: remove gc (a document) and gd (never a document).
print("delete gc: existed = \(try nodes.delete(Data("gc".utf8)))")
let existedGd = try nodes.delete(Data("gd".utf8))
print("delete gd: existed = \(existedGd) (never a document; its edges still cascade)")
show("neighbors(ga) after deletes", Array(try nodes.neighbors(ga, "parent_of")))
show("neighbors(gb) after deletes", Array(try nodes.neighbors(gb, "parent_of")))
show("traverse(ga, 2 hops) after", Array(try nodes.traverse(ga, "parent_of", hops: 2)))
let db = try Corvid.openMemory()
defer { db.close() }
let places = try db.collection("places")
for (name, lat, lon) in cities {
try places.insert(Data(name.utf8), [
"name": name,
"loc": [lat, lon] as [Any?], // the [lat, lon] array encoding
] as [String: Any?])
}
try places.createGeoIndex("loc")
show("within 600km of Berlin:", Array(try places.geoWithinRadius("loc", lat: 52.52, lon: 13.40, radiusKm: 600.0)))
show("bbox 47..55N, 5..15E:", Array(try places.geoWithinBBox("loc", minLat: 47.0, minLon: 5.0, maxLat: 55.0, maxLon: 15.0)))
show("nearest 2 to Berlin:", Array(try places.geoNearest("loc", lat: 52.52, lon: 13.40, k: 2)))

Generated from the binding’s docs/SURFACE.tsv (every engine construct at the pinned tag mapped or N/A with a reason) — regenerated by the docs sync, so it cannot drift.

API groupengine constructsproven by
the Swift value mapping (nil/Bool/Int/Double/String/Data/[Float]/[Any?]/[String: Any?])10golden:values.txt:VTYPE
FieldExpr.eq/ne/lt/le/gt/ge7golden:queries.txt:QF_*
Predicate via field()/Predicate.not()27golden:queries.txt:QF_* + golden:mutations.txt:DELETE_IN
Metric enum (Metric.cosine/dot/l2)4golden:queries.txt:QVEC
Quant enum (Quant.none/binary/scalar)4golden:schema.txt:IDX_VEC_Q
throws CorvidError1golden:mutations.txt:INSERT_ERR
CorvidError.code (CorvidErrorCode table)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.database (code 1)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.transaction (code 2)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.table (code 3)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.storage (code 4)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.commit (code 5)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.setDurability (code 6)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.compaction (code 7)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.decode (code 8)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.corruptIndex (code 9)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.reservedCollection (code 10)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:mutations.txt:INSERT_ERR(err:10)
CorvidErrorCode.invalidName (code 11)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:mutations.txt:INSERT_ERR(err:11)
CorvidErrorCode.argument (code 12)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:mutations.txt:UPDATE_ABORT(err:12)
CorvidErrorCode.incompatibleFormat (code 13)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.emptyIndexTraining (code 14)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:schema.txt:IDX_PQ_ERR(err:14)
CorvidErrorCode.schemaViolation (code 15)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:schema.txt:SCHEMA_ERR(err:15)
CorvidErrorCode.invalidDump (code 16)1ErrCodesTest.swift errorCodeTableIsFrozen
CorvidErrorCode.backupTargetExists (code 17)1ErrCodesTest.swift errorCodeTableIsFrozen; golden:admin.txt:BACKUP_DUP(err:17)
CorvidErrorCode.io (code 18)1ErrCodesTest.swift errorCodeTableIsFrozen
Row { key, doc, score }1golden:queries.txt
Query (Collection.query())2golden:queries.txt
Query.filter1golden:queries.txt:QF_COUNT
Query.vector1golden:queries.txt:QVEC
Query.text1golden:queries.txt:QTEXT
Query.fuseRRF1golden:queries.txt:HYBRID_F
Query.rerankMMR1golden:queries.txt:HYBRID
Query.limit1golden:queries.txt:ORDER_BY
Query.offset1golden:queries.txt:ORDER_BY
Query.orderBy1golden:queries.txt:ORDER_BY
Query.approx1golden:queries.txt:APPROX
Query.select1golden:queries.txt:SELECT
Query.count1golden:queries.txt:AGG_COUNT
Query.groupCount1golden:queries.txt:AGG_GCOUNT
Query.sum1golden:queries.txt:AGG_SUM
Query.avg1golden:queries.txt:AGG_AVG
Query.min1golden:queries.txt:AGG_MIN
Query.max1golden:queries.txt:AGG_MAX
Query.countDistinct1golden:queries.txt:AGG_DISTINCT
Query.groupSum1golden:queries.txt:AGG_GSUM
Query.groupAvg1golden:queries.txt:AGG_GAVG
Query.run1golden:queries.txt:QVEC
Db1golden:admin.txt:FILEDB
Corvid.open/openMemory + Db.collection/collections/backup/compact6golden:admin.txt (COLLECTIONS/BACKUP/COMPACT)
Collection1golden:mutations.txt:COLL
Collection.insert/update/patch/compareAndSet4golden:mutations.txt (INSERT/UPDATE/PATCH/CAS)
Collection.scan(callback, early stop)1golden:mutations.txt:SCAN/SCAN_STOP
Collection.len (len()==0 for empty)2golden:mutations.txt:LEN
Collection.putMany1golden:mutations.txt:PUTMANY + golden:schema.txt:PUTMANY_ROLLBACK
Collection.insertAuto1golden:mutations.txt:INSERT_AUTO
Collection.get1golden:mutations.txt:GET
Collection.delete/deleteWhere/deleteBatch3golden:mutations.txt (DELETE/DELETE_WHERE/DELETE_BATCH)
Collection.scan1golden:mutations.txt:SCAN
Collection.page -> Page(rows, nextAfter)2golden:mutations.txt:PAGE
Row.score (Query.vector().run())1golden:queries.txt:QVEC
Row.score (Query.text().run())1golden:queries.txt:QTEXT
Collection.phraseSearch(field:phrase:k:) — the direct positional search (corvid_phrase_search, v0.3.0) over the rows cursor1golden:queries.txt:PHRASE
Query.fuseRRF default k=601golden:queries.txt:HYBRID
GeoHit { key, doc, distanceKm }1golden:geo.txt:RADIUS/NEAREST/BBOX
Collection.geoWithinRadius/geoNearest/geoWithinBBox/createGeoIndex4golden:geo.txt (RADIUS/NEAREST/BBOX/IDX_GEO)
Collection.link/linkWeighted/unlink/neighbors/inNeighbors/neighborsWeighted/traverse7golden:graph.txt
Collection.createScalarIndex/createCompoundIndex/createTextIndex[/OnDisk]/createGeoIndex/createVectorIndex* (variants)10golden:schema.txt:IDX_*
FieldType enum (FieldType.any/bool/int/float/text/bytes/vector/array/map)10golden:schema.txt:SET_SCHEMA/SCHEMA
Collection.setSchema/schema + FieldDef(name:type:required:unique:)10golden:schema.txt:SET_SCHEMA/SCHEMA/SCHEMA_ERR
Collection.insertWithTTL/setTTL/getTTL/purgeExpired4golden:mutations.txt (INSERT_TTL/SET_TTL/GET_TTL/PURGE)
Db.dumpToPath/loadFromPath/loadFromPathWithRenames3golden:admin.txt (DUMP/LOAD/LOAD_RENAMES)

159 engine constructs are deliberately not exposed (each with its reason in the repo’s docs/SURFACE.tsv).

The Swift Package Index renders DocC-style reference from the wrapper’s doc comments (also Quick Help in Xcode).

The architecture ruling: SPM binary target, no shim

Section titled “The architecture ruling: SPM binary target, no shim”

Per the repo’s docs/PLAN.md: Swift consumes C natively — unlike corvid-jvm, whose JNI demands a C shim, the wrapper (Sources/Corvid) imports CorvidEngine and calls the 124 frozen symbols directly. The xcframework carries corvid.h plus an umbrella and module map so SwiftPM forms the clang module; each slice is the engine’s staticlib build (bare dylibs outside frameworks are not supported on iOS — static linking is the Rust-on-Apple norm).

The lifetime mapping is the binding’s review center-of-gravity: opaque handles → final classes with deinit frees; consumed-by-call args (predicate trees, query builders) invalidated before the native call whatever its outcome (FFI.md §8’s unconditional consumption — the double-free class cannot happen); borrowed views copied inside the same call that observed them; Db/Collection are @unchecked Sendable exactly where the ABI’s §6 thread contract says so, builders and iterators are not. Cursors (Rows, GeoHits, Strs, GroupIter) are single-pass — iterate once.

The golden-suite port (Tests/CorvidTests/GoldenTest.swift) replays the engine’s 8-fixture, 267-executable-line suite through this binding against the downloaded xcframework at the pin — same grammar, same dispatch, every line, first failure naming file:line. A deep integration pass covers the surfaces the fixture grammar does not reach (admin paths, scan aborts, graph, geo, schema, TTL, the full aggregate set), and a frozen error-code table test pins the 19+1 code mapping. CI runs the suite on macOS plus an iOS-Simulator compile leg (the simulator slice links), and the surface gate resolves every engine construct at the pinned tag (docs/SURFACE.tsv: mapped with a proving test, or N/A with a reason).

Releases ride the engine’s cascade: engine tag vX.Y.Zfrom: "X.Y.Z" here, with the manifest’s URL and checksum rewritten in the same bump PR (bump.sh downloads the new zip and re-hashes it — a stale checksum would break every consumer’s resolve).