Mobile API guide (Swift / Kotlin)

August 17, 2026 · View on GitHub

The complete surface of the velesdb-mobile UniFFI binding. Rust signatures are generated on docs.rs/velesdb-mobile; this guide gives the foreign-language names, the argument shapes that are not obvious from the type system (JSON filters, VelesQL), and the behavioural notes.

Naming rule: every Swift/Kotlin name is the camelCase form of the Rust name (create_collectioncreateCollection). Records keep their field names in camelCase too (properties_jsonpropertiesJson).

To generate the bindings themselves, see the Mobile build guide.

Threading

No method in this binding is async — the generated Swift contains zero async functions, and the Kotlin methods are plain blocking calls. Every object is Send + Sync on the Rust side and is exported as @unchecked Sendable (Swift), so a handle can be shared across threads, but a search executed on the main thread blocks the UI. Dispatch to a background queue (DispatchQueue.global()) or a coroutine dispatcher (Dispatchers.IO).

Quick start

Swift (iOS)

import VelesDB  // the module name is the framework you packaged

// `open` is a named constructor: UniFFI emits a static method, not an init.
let db = try VelesDatabase.open(path: documentsPath + "/velesdb")

// 384 = all-MiniLM-L6-v2; 768 = MiniLM base.
try db.createCollection(name: "documents", dimension: 384, metric: .cosine)

guard let collection = try db.getCollection(name: "documents") else {
    fatalError("Collection not found")
}

let point = VelesPoint(
    id: 1,
    vector: embedding,                          // [Float] from your embedding model
    payload: "{\"title\": \"Hello World\"}"     // JSON string, or nil
)
try collection.upsert(point: point)

let results = try collection.search(vector: queryEmbedding, limit: 10)
for result in results {
    print("ID: \(result.id), Score: \(result.score)")
}

Kotlin (Android)

import uniffi.velesdb_mobile.*   // package emitted by uniffi-bindgen

// `open` is a companion-object factory, not a constructor.
val db = VelesDatabase.open("${context.filesDir}/velesdb")

db.createCollection("documents", 384u, DistanceMetric.COSINE)

val collection = db.getCollection("documents")
    ?: throw IllegalStateException("Collection not found")

val point = VelesPoint(
    id = 1uL,
    vector = embedding,                         // List<Float> from your embedding model
    payload = """{"title": "Hello World"}"""    // JSON string, or null
)
collection.upsert(point)

val results = withContext(Dispatchers.IO) {
    collection.search(queryEmbedding, 10u)
}
results.forEach { result ->
    println("ID: ${result.id}, Score: ${result.score}")
}

VelesDatabase

MethodDescription
VelesDatabase.open(path)Opens or creates a database at path (named constructor)
VelesDatabase.openWithConfig(path, configPath)Opens with a TOML config file, engine sections only; fails fast on a missing/invalid file
VelesDatabase.openWithConfigToml(path, configToml)Same, from an in-memory TOML string
VelesDatabase.openWithObserver(path, observer)Opens with a read-path MobileObserver attached (audit and deny)
VelesDatabase.openWithObserverAndConfig(path, observer, configPath)Observer + config file
VelesDatabase.openWithObserverAndConfigToml(path, observer, configToml)Observer + config string
updateGuardrails(limits)Live-updates query guardrail limits (MobileQueryLimits)
createCollection(name, dimension, metric)Creates a vector collection
createCollectionWithStorage(name, dimension, metric, storageMode)Creates a collection with quantized storage
createMetadataCollection(name)Creates a metadata-only collection (no vectors)
createGraphCollection(name)Creates a schemaless graph collection
createGraphCollectionWithEmbeddings(name, dimension, metric)Graph collection whose nodes carry embeddings
getCollection(name)Returns the collection, or nil/null when absent
listCollections()All collection names
deleteCollection(name)Deletes a collection
trainPq(collectionName, config)Trains Product Quantization (PqTrainConfig)
executeQuery(query, params)Full VelesQL pass-through; returns a QueryResult

Engine configuration semantics (which TOML sections are honoured, how VELESDB_* env vars layer on top): configuration guide.

VelesCollection

MethodDescription
search(vector, limit)k nearest neighbours
searchWithQuality(vector, limit, quality)Search with a SearchQuality preset
searchWithFilter(vector, limit, filterJson)Search with a metadata filter
batchSearch(searches)Batch of IndividualSearchRequest, each with its own filter; returns one result list per request
textSearch(query, limit)BM25 full-text search
textSearchWithFilter(query, limit, filterJson)Text search with filter
hybridSearch(vector, textQuery, limit, vectorWeight)Vector + text fused search
hybridSearchWithFilter(vector, textQuery, limit, vectorWeight, filterJson)Hybrid search with filter
multiQuerySearch(vectors, limit, strategy)Multi-query fusion (MQG)
multiQuerySearchIds(vectors, limit, strategy)Same, ID-only result path
multiQuerySearchWithFilter(vectors, limit, strategy, filterJson)Multi-query fusion with filter
sparseSearch(sparseVector, limit, indexName)Sparse-only search over an inverted index (indexName optional)
hybridSparseSearch(vector, sparseVector, limit, indexName)Dense + sparse fused with RRF (k=60)
query(queryStr, paramsJson)VelesQL query scoped to this collection; returns SearchResults

An empty vectors list on any multi-query method is an error (multi_query_search requires at least one vector), never an empty result.

Write

MethodDescription
upsert(point)Inserts or updates one point
upsertBatch(points)Batch insert/update — the path to use for bulk loading
upsertWithSparse(point, sparseVector)Inserts a point together with its sparse vector
delete(id)Deletes a point
enableStreaming(config)Enables streaming ingestion; defaults bufferSize=10000, batchSize=128, flushIntervalMs=50. Calling it again replaces the runtime
streamInsert(points)Queues a batch on the streaming channel; returns the count queued. Fails when streaming was never enabled or the buffer is full
flush()Flushes to durable storage
compactStorage()Compacts the payload store; returns the bytes reclaimed

Read / introspection

MethodDescription
get(ids)Points by ID (missing IDs are skipped silently)
getById(id)One point, or nil/null
allIds()Every point ID in the collection
count()Number of points
dimension()Configured vector dimension
isMetadataOnly()True for a metadata-only collection
analyze()Runs ANALYZE, returns fresh MobileCollectionStats
getStats()Latest statistics snapshot (no recomputation)
diagnostics()MobileCollectionDiagnostics: readiness + index health
guardRails()The guardrail limits currently in force for this collection
applyAdvancedConfig(config)Post-creation overrides (MobileAdvancedConfig); None fields are left unchanged

Indexes

MethodDescription
createIndex(fieldName)Secondary metadata index
hasSecondaryIndex(fieldName)Existence check
createPropertyIndex(label, property)Graph property index
createRangeIndex(label, property)Graph range index
hasPropertyIndex(label, property) / hasRangeIndex(label, property)Existence checks
listIndexes()All index definitions (MobileIndexInfo)
dropIndex(label, property)Drops an index
indexesMemoryUsage()Memory used by indexes, in bytes

The filterJson shape

Every *WithFilter method and IndividualSearchRequest.filter take a JSON string using the canonical filter shape shared with the core engine and the REST API:

{"condition": {"type": "<op>", "field": "...", "value": "..."}}

Depending on the operator the payload carries value, values, pattern, or a nested conditions array. Operators: eq, neq, gt, gte, lt, lte, in, contains, like, ilike, is_null, is_not_null, array_contains, array_contains_any, array_contains_all, geo_distance, geo_bbox, plus and / or / not for composition.

Swift:

let results = try collection.searchWithFilter(
    vector: queryVector,
    limit: 5,
    filterJson: #"{"condition": {"type": "eq", "field": "category", "value": "tech"}}"#
)

Kotlin:

val results = collection.searchWithFilter(
    queryVector,
    5u,
    """{"condition": {"type": "eq", "field": "category", "value": "tech"}}"""
)

VelesQL from mobile

db.executeQuery(query, params) is the full VelesQL pass-through (SELECT / NEAR / MATCH, including cross-collection @collection annotations and aggregates). It returns a QueryResult:

FieldTypeMeaning
kindQueryResultKindRows, Mutation, Deletion, Ddl, Train, Admin
rows[QueryResultRow]Empty for DDL/TRAIN/FLUSH
rowCountUInt32Convenience count
messageStringHuman-readable status, e.g. "3 rows inserted"

Each QueryResultRow carries id, score, and dataJson — a JSON object string merging id, score, and every payload field at the top level.

Query language reference: multi-model queries and graph patterns.

VelesSemanticMemory

Agent memory for on-device AI: knowledge facts stored as vectors in a dedicated _semantic_memory collection, created on first use.

MethodDescription
VelesSemanticMemory(db, dimension)Constructor; binds to the database and the embedding dimension
store(id, content, embedding)Stores a fact; content is kept in the point payload
query(embedding, topK)Similarity query, returns SemanticResult (id, score, content)
delete(id)Deletes a fact
remove(id)Deprecated alias for delete
clear()Removes every fact
len() / isEmpty()Size helpers
dimension()Embedding dimension

Scope note: mobile exposes semantic memory only. Episodic and procedural memory, TTL setters, and snapshots — available in Python/WASM/MCP — are not bridged here (see ecosystem parity).

MobileGraphStore

A RAM-only knowledge graph, deliberately independent from core's persistent graph engine (rationale: known limitations §14).

MethodDescription
MobileGraphStore()New empty store (constructor)
save(path) / MobileGraphStore.load(path)Explicit persistence to/from a file
addNode(node)Adds a MobileGraphNode
addEdge(edge)Adds a MobileGraphEdge; errors on a duplicate edge ID
getNode(id) / getEdge(id)Lookup, nil/null when absent
hasNode(id) / hasEdge(id)Existence checks
nodeCount() / edgeCount()Sizes
getOutgoing(nodeId) / getIncoming(nodeId)Incident edges
getOutgoingByLabel(nodeId, label)Outgoing edges filtered by label
getNeighbors(nodeId)1-hop neighbour IDs
getNodesByLabel(label) / getEdgesByLabel(label)Label scans
outDegree(nodeId) / inDegree(nodeId)Degrees
bfsTraverse(sourceId, maxDepth, limit)Breadth-first traversal
bfsTraverseParallel(sourceIds, maxDepth, limit)Multi-source BFS with deduplication
dfsTraverse(sourceId, maxDepth, limit)Depth-first traversal
removeNode(nodeId)Removes the node and every connected edge
removeEdge(edgeId)Removes one edge
clear()Empties the store

Traversal semantics (pinned by tests/coverage_native.rs): the source node is never emitted, each TraversalResult.path lists the edge IDs taken from the source, and a node reachable by several paths is emitted exactly once.

Read gate: MobileObserver

openWithObserver attaches a Swift/Kotlin callback consulted before every governed read (dense / text / hybrid / sparse / multi-query search, VelesQL SELECT and MATCH).

  • onQueryRequest(context) -> MobileAccessDecision — return Allow, or Deny { reason } to abort the read with zero results.
  • MobileQueryContext: collection, operation (VectorSearch / TextSearch / HybridSearch / GraphTraversal / Select), plus the opaque principal and tenantHint hints forwarded untouched — the gate never interprets them.
  • Implementations must not throw or panic; express refusal with Deny.
  • Core's scope-narrowing decision (AllowWithScope) is intentionally not bridged yet.

Guardrails are the other half of the control plane: updateGuardrails(limits) sets maxDepth, maxCardinality, memoryLimitBytes, timeoutMs (0 disables), rateLimitQps, circuitFailureThreshold, and circuitRecoverySeconds.

Enums

Distance metrics

MetricDescriptionUse case
CosineCosine similarity (1 − cosine distance)Text embeddings, normalized vectors
EuclideanL2 distanceImage features, unnormalized vectors
DotProductDot productPre-normalized vectors, MaxSim
HammingHamming distanceBinary embeddings, LSH
JaccardJaccard similaritySparse vectors, tag sets

Search quality

Presetef_searchNote
Fast96~95% recall, lowest latency
Balanced160~99.5% recall, default
Accurate512~100% recall
Perfect4096Guaranteed 100% recall
Custom { ef }caller-setFine-grained control
Adaptive { minEf, maxEf }two-phaseStarts low, doubles until the cap

Storage modes

ModeCompressionMemory/dimRecall lossUse case
Full1x4 bytes0%Best quality
Sq84x1 byte~1%Recommended for mobile
Binary32x1 bit~5–10%Extreme constraints (IoT)
ProductQuantization8x–16x typicalcodebookaggressiveTrain first with trainPq
Rabitq32x1 bit~1–2%1-bit plus rotation and scalar correction

(Compression and recall figures above are the ones documented on the enum itself in crates/velesdb-mobile/src/types.rs.)

// iOS — SQ8: 4x less memory, ~1% recall loss
try db.createCollectionWithStorage(
    name: "embeddings",
    dimension: 384,
    metric: .cosine,
    storageMode: .sq8
)
// Android — binary quantization for IoT devices (32x compression)
db.createCollectionWithStorage(
    "embeddings", 384u, DistanceMetric.COSINE, StorageMode.BINARY
)

Compression trade-offs in depth: quantization guide.

Fusion strategies

Used by the multiQuerySearch* methods.

StrategyDescription
AverageAverage score across queries
MaximumBest score per document
Rrf { k }Reciprocal Rank Fusion (core default k = 60)
Weighted { avgWeight, maxWeight, hitWeight }Weighted mix of average, max, hit ratio
RelativeScore { denseWeight, sparseWeight }Relative Score Fusion for dense + sparse

Records

TypeFields
VelesPointid: UInt64, vector: [Float], payload: String? (JSON string)
SearchResultid: UInt64, score: Float
SemanticResultid: UInt64, score: Float, content: String
VelesSparseVectorindices: [UInt32], values: [Float] (parallel arrays)
IndividualSearchRequestvector: [Float], topK: UInt32, filter: String?
PqTrainConfigm: UInt32, k: UInt32, opq: Bool
MobileStreamingConfigbufferSize, batchSize, flushIntervalMs
MobileQueryLimitsmaxDepth, maxCardinality, memoryLimitBytes, timeoutMs, rateLimitQps, circuitFailureThreshold, circuitRecoverySeconds
MobileAdvancedConfigpqRescoreOversampling?, deferredIndexing?, asyncIndexBuilder?
MobileGraphNodeid: UInt64, label: String, propertiesJson: String?, vector: [Float]?
MobileGraphEdgeid: UInt64, source: UInt64, target: UInt64, label: String, propertiesJson: String?
TraversalResultnodeId: UInt64, path: [UInt64] (edge IDs), depth: UInt32
MobileCollectionStatstotalPoints, payloadSizeBytes, rowCount, deletedCount, avgRowSizeBytes, totalSizeBytes, fieldStatsCount, columnStatsCount, indexStatsCount
MobileCollectionDiagnosticshasVectors, searchReady, dimensionConfigured, pointCount, indexHealth, indexHealthDetail?
MobileIndexInfolabel, property, indexType, cardinality, memoryBytes

The Swift memberwise initializers are explicit and take every field: pass payload: nil rather than omitting it.

Errors

VelesError (Kotlin: VelesException) has three variants:

VariantFieldsRaised when
Databasemessage, code, recoverableAny engine failure, plus binding-level failures (JSON parsing, runtime setup). code carries the core taxonomy code ("VELES-006", …) or is empty for binding-level errors
CollectionmessageCollection-level failure
DimensionMismatchexpected, actualVector length differs from the collection dimension

recoverable mirrors core's Error::is_recoverable, so a retry policy can be driven from the FFI boundary without string matching.

Performance notes

  1. Prefer Sq8 on phones, Binary on constrained IoT hardware.
  2. Load in bulk with upsertBatch, or enableStreaming + streamInsert for a continuous feed — not upsert in a loop.
  3. Run every call off the main thread (see Threading).
  4. Reuse the embedding buffers you hand to the binding; each call copies the vector across the FFI boundary.
  5. ARM64 builds use core's NEON paths (velesdb_core::simd_neon, simd_neon_prefetch) for distance computation and prefetching.

Memory footprint

Raw vector payload only — arithmetic from dimension × bytes-per-dimension × count. It excludes the HNSW graph, payloads, and index structures, so treat it as a floor, not a measurement.

VectorsDimensionStorage modeVector memory
10,000384Full (f32)~15 MB
10,000384SQ8~4 MB
10,000384Binary~0.5 MB
100,000768Full (f32)~300 MB
100,000768Binary~10 MB

See also


Last updated: 2026-07-25 · Applies to: velesdb-core 5.1.0