A Primordial collection stores your app’s data locally and makes its text searchable by meaning, not just exact words.
Create a Primordial Client
Import Primordial and create a client. The identifier scopes collections and their stored data, so use a stable value for the same app account or workspace.
import Primordial
let primordial = PrimordialClient(
identifier: "account-42" // Optional
)
Two PrimordialClient() with different identifiers do not share collections. If your app does not need separate
accounts or workspaces, PrimordialClient() uses the stable default identifier.
Check Semantic Search Availability
A collection is a named group of records that Primordial stores and searches together.
Collection synchronization, search, related-record lookup, and reindexing all use the
.semanticSearch capability. Check its status before presenting the feature or asking the user
to download anything.
let requirement = await primordial.ai.status(
for: .semanticSearch
)
print(requirement.isInstalled)
print(requirement.isAvailable)
print(requirement.downloadBytes)
print(requirement.requiredAvailableBytes)
isInstalled tells you whether the required AI model is already on the device.
isAvailable also checks whether the current device can use it. Primordial loads an downloaded model when a collection operation
needs it.
When a download is required, use downloadBytes and requiredAvailableBytes to explain
the download and storage requirement in your own UI before continuing.
Activate Primordial
Activate the SDK before downloading the semantic-search model or working with a collection.
try await primordial.activate(
.evaluationKey("pk_eval_your_key_here")
)
You can check primordial.isActivated when your UI needs to reflect activation state. Keep the
evaluation key out of logs and user-visible error messages.
Prepare Semantic Search
makeAvailable checks storage,
downloads embedding model or anything missing, verifies it, and reports progress as an asynchronous stream.
for try await progress in primordial.ai.makeAvailable(
.semanticSearch
) {
print(progress.fractionCompleted)
print(progress.downloadedBytes)
print(progress.totalBytes)
print(progress.phase)
}
Calling makeAvailable again reuses an existing valid download.
Primordial supports background model downloads on iOS. Downloads can continue while your app is suspended, as long as it forwards background URL-session events. See Background downloads for the required app delegate code. If the user force-quits your app, the download is canceled.
Use Different Embedding Model
Primordial by default uses Paraphrase Multilingual MiniLM L12 v2. If you want to use different model, see Advanced API: Use a custom embedding model.
Create a Collection
A collection groups records that belong to the same searchable dataset. Opening the same collection name with the same client identifier returns access to the same durable local data.
let notes = try primordial.collection("research notes")
print(notes.name) // "research-notes"
print(notes.clientIdentifier) // "account-42"
Primordial normalizes collection names, including whitespace and capitalization. Keep the intended name stable and use separate collections when records have different purposes or retention rules.
Describe Your Data with Typed Fields
Every record has an ID and a set of fields. Your app chooses the ID, and Primordial uses it to find and update the same record later. Keep the ID non-empty, unique within the collection, and stable.
let record = PrimordialRecord(
id: "9087978swiftactors", // generated by your app
fields: [
"title": .text("Swift actors"),
"body": .text("Actors protect isolated mutable state."),
"kind": .keyword("guide"),
"tags": .stringList(["swift", "concurrency"]),
"priority": .integer(2),
"rating": .double(4.8),
"published": .bool(true),
"createdAt": .date(.now)
]
)
| Field type | Use it for | Semantically indexed |
|---|---|---|
.text |
Natural-language content that people should be able to find by meaning. | Yes |
.keyword |
A category, state, or other exact string value. | No |
.stringList |
Tags and other lists of exact string values. | No |
.integer, .double |
Counts, ranks, measurements, and numeric filters. | No |
.bool |
Flags such as published, favorite, or archived. | No |
.date |
Timestamps and date-range filters. | No |
Only .text fields are split into searchable passages and embedded. Every other type remains
available for filtering, display, and source information. Use the same field type for a field in every record for example, always store priority as an integer.
Choose How Your App Supplies Records
Your app remains the source of truth. Primordial keeps a searchable projection of that data and does not replace your app database.
| App data | Use |
|---|---|
| A complete in-memory dataset | replaceAll(with:) |
| Explicit inserts, updates, and removals | apply(_:) |
| A durable database or another paged source | synchronize(from:options:) |
| SwiftData models | PrimordialSwiftDataSource |
Replace a Complete Dataset
Use replaceAll(with:) when your app already has the complete set of records. Records omitted
from the new snapshot are removed from the collection.
let currentRecords = [record]
for try await event in notes.replaceAll(with: currentRecords) {
switch event {
case .progress(let progress):
print(progress.phase, progress.changedRecords)
case .completed(let report):
print(report.generatedEmbeddings)
print(report.reusedEmbeddings)
}
}
Search continues to use the previous complete snapshot until the replacement commits. A failed or cancelled replacement does not expose a partial dataset, and unchanged text can reuse its embedding.
Apply Individual Changes
Use apply(_:) when your app already knows which complete records changed. Update your source
of truth first, then apply an upsert or removal to its searchable projection.
let changes: [PrimordialRecordChange] = [
.upsert(record),
.remove(id: "old-record")
]
for try await event in notes.apply(changes) {
if case .completed(let report) = event {
print(report.changedRecords, report.removedRecords)
}
}
Partial field updates are intentionally not part of the collection API. Change the application record,
then pass its complete current fields with .upsert.
Synchronize SwiftData
Use PrimordialSwiftDataSource to map a SwiftData model into searchable records. Mark the stable
record ID with .preserveValueOnDeletion so removals can be read from SwiftData history.
import Primordial
import SwiftData
@Model
final class Note {
@Attribute(.unique, .preserveValueOnDeletion)
var id: UUID
var title: String
var body: String
var modifiedAt: Date
}
let source = try PrimordialSwiftDataSource<Note>(
identifier: "app.notes",
schemaVersion: "1",
container: modelContainer,
recordID: \Note.id,
encodeID: { $0.uuidString }
) { note in
[
"title": .text(note.title),
"body": .text(note.body),
"modifiedAt": .date(note.modifiedAt)
]
}
for try await event in notes.synchronize(from: source) {
switch event {
case .progress(let progress):
print(progress.phase, progress.changedRecords)
case .completed(let report):
print(report.changedRecords, report.removedRecords)
}
}
Continue using your normal ModelContext and @Query code. Primordial reads the
supplied ModelContainer, creates the first snapshot, and uses saved history checkpoints on
later synchronizations.
- Keep
identifierstable for the same logical data source. - Increase
schemaVersionwhen the record or field mapping changes meaning. - The encoded record ID must be nonempty and unique.
- Filtered SwiftData fetch descriptors are not supported.
Choose a Synchronization Mode
let options = PrimordialSynchronizationOptions(
mode: .automatic,
batchSize: 100
)
for try await _ in notes.synchronize(
from: source,
options: options
) { }
| Mode | Behavior |
|---|---|
.automatic |
Uses incremental changes when possible and falls back to a complete snapshot when required. |
.incremental |
Requires a valid saved checkpoint and fails instead of rebuilding automatically. |
.snapshot |
Forces a complete snapshot from the source. |
The default batch size is 100. Valid values are 1...1000; Primordial may reduce
work per batch when the device is under resource pressure.
Inspect Synchronization Status
let status = try await notes.synchronizationStatus()
print(status.sourceIdentifier)
print(status.sourceSchemaVersion)
print(status.hasCheckpoint)
print(status.requiresSnapshot)
print(status.lastCompletedAt)
print(status.lastReport)
Use the status for diagnostics or application UI. Your app decides when to synchronize—for example, after model availability, after committing meaningful source changes, when the app becomes active, or from an app-owned background task when the system grants execution time.
Reset or Change the Source
A synchronized collection stays bound to its source identifier. Reset it before binding that collection name to a different logical source.
try await notes.reset()
Reset removes the collection's searchable records, synchronization checkpoint, and source binding.
Powerful Search by Meaning
Pass a natural-language query to search. You can choose which text fields participate, give
important fields more weight, filter the eligible records, and limit the number of matches.
let matches = try await notes.search(
"safe shared state",
fields: ["title", "body"],
weights: ["title": 1.5, "body": 1.0],
filter: .field(
"kind",
.equals(.keyword("guide"))
),
limit: 8
)
Each PrimordialMatch tells you which collection, record, field, and source range matched. It
also includes the matching excerpt, the record’s stored fields, and a relevance score.
for match in matches {
print(match.collection)
print(match.recordID)
print(match.field)
print(match.excerpt)
print(match.score)
print(match.fields)
print(match.sourceRange)
}
Filter Search Results
Filters are typed, so their values should match the field type stored in the collection. Combine filters
with .all, .any, and .not when one condition is not enough.
let filter = PrimordialFilter.all([
.field("kind", .equals(.keyword("guide"))),
.field("priority", .lessThanOrEqual(.integer(2))),
.field("tags", .contains("swift")),
.not(.field("published", .equals(.bool(false))))
])
let matches = try await notes.search(
"structured concurrency",
filter: filter
)
| Comparison | Meaning |
|---|---|
.equals, .notEquals |
Match or exclude one typed value. |
.oneOf |
Match any value in a supplied list. |
.lessThan, .lessThanOrEqual |
Keep values below a boundary. |
.greaterThan, .greaterThanOrEqual |
Keep values above a boundary. |
.contains |
Check text, keyword, or string-list content for a string. |
Unknown fields, incompatible comparisons, invalid weights, and non-positive result limits fail explicitly instead of silently changing the query.
Find Related Records
Use related(to:) when you already have a record and want to find similar content. Primordial
builds the query from that record’s selected text fields and leaves the source record out of the results.
let related = try await notes.related(
to: "9087978swiftactors",
fields: ["title", "body"],
filter: .field("published", .equals(.bool(true))),
limit: 4
)
Inspect a Collection
Collection statistics let your app report what is stored without reading private storage details.
let statistics = try await notes.statistics()
print(statistics.recordCount)
print(statistics.fieldCount)
print(statistics.chunkCount)
print(statistics.embeddingModelID)
print(statistics.embeddingDimensions)
print(statistics.chunkingVersion)
Rebuild a Collection
A collection records the embedding configuration used to build its index. If that configuration or the
index format changes or you want to use different embedding model, Primordial reports an incompatibility instead of mixing unlike embeddings. Use reindex() to rebuild
the index deliberately.
for try await progress in notes.reindex() {
print(progress.phase)
print(progress.fractionCompleted)
}
Handle Collection Errors
Invalid names, records, fields, queries, missing record IDs, incompatible indexes, and storage failures
are reported as PrimordialCollectionError. Cancellation remains Swift’s
CancellationError, so normal structured-concurrency cancellation handling still applies.
do {
let matches = try await notes.search("local AI")
print(matches)
} catch is CancellationError {
print("Search cancelled")
} catch let error as PrimordialCollectionError {
print(error.localizedDescription)
}
Advanced vector workflows
Managed collections are the recommended path for application records. If you deliberately need precomputed embeddings, multiple named representations, or direct semantic vector search, see Use a custom embedding model.