⚠️ Chroma Swift is currently in BetaThis means that the core APIs work well - but we are still gaining full confidence over all possible edge cases.
Chroma is a Swift package that provides a high-performance, cross-platform interface for working with vector stores and embeddings collections, backed by the Chroma database engine. It is designed for use in macOS and iOS applications, supporting both Apple Silicon and Intel architectures.
- Initialize in-memory or persistent storage
- Create, list, count, update, and delete collections
- Add, get, update, upsert, count, query, and delete documents
- Query by embeddings, including batched queries
- Optional field selection via
includeingetDocumentsandqueryCollection - Database-level APIs (
createDatabase,getDatabase,listDatabases,deleteDatabase) - Typed metadata decode helpers via
ChromaMetadataValueanddecodedMetadatas() - Local embeddings with
ChromaEmbedder+ MLXEmbedders models
- Swift 6.2+
- macOS 14+ or iOS 17+
- Add this package dependency:
.package(url: "https://github.com/chroma-core/chroma-swift.git", from: "1.0.2")- Add the product to your app target:
.target(
name: "YourApp",
dependencies: [
.product(name: "Chroma", package: "ChromaSwift")
]
)- Import the module where you use it:
import ChromaThis repository ships a local skill bundle at:
Skill/chroma-swift/
Copy the chroma-swift/ directory (not just SKILL.md) into one of the skill locations below.
Use one of these locations:
- Project scope:
.agents/skills/chroma-swift/ - User scope:
~/.agents/skills/chroma-swift/
Use one of these locations:
- Project scope:
.claude/skills/chroma-swift/ - User scope:
~/.claude/skills/chroma-swift/
After installation, refer to the skill by name only:
Use the chroma skill to review my collection schema and retrieval flow, then propose fixes.
- Initialize Chroma in memory
- Create one collection
- Add two documents with sample embeddings
- Use realistic document text content (not file names or titles)
- Define realistic metadata shape for each document
- Run one nearest-neighbour query
- Call
Chroma.initialize(...)before any collection or document operation. - This example uses tiny hand-written vectors so the end-to-end flow is easy to read.
- In real apps, generate embeddings with an embedding model, and use the same model for writes and queries in the same collection.
- Add this code:
import Chroma
try Chroma.initialize(allowReset: true)
let collectionName = "my_collection"
_ = try Chroma.createCollection(name: collectionName)
let ids = ["cats_doc", "dogs_doc"]
let embeddings: [[Float]] = [
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0]
]
let documents = [
"Cats are small carnivores often kept as companion animals.",
"Dogs are domesticated canids known for companionship and work."
]
let metadatas: [ChromaMetadata?] = [
[
"source_url": "file:///knowledge/animals/cats.txt",
"content_type": "text/plain"
],
[
"source_url": "file:///knowledge/animals/dogs.md",
"content_type": "text/markdown",
"reviewed": true
]
]
_ = try Chroma.addDocuments(
collectionName: collectionName,
ids: ids,
embeddings: embeddings,
documents: documents,
metadatas: metadatas
)
let result = try Chroma.queryCollection(
collectionName: collectionName,
queryEmbeddings: [[1.0, 0.0, 0.0]],
nResults: 1,
whereFilter: nil,
ids: nil,
include: ["documents"]
)
let topId = result.ids[0][0]
let topDocument = result.documents[0][0] ?? "<missing document>"
print("Top match: \(topId) -> \(topDocument)") // expected: Top match: cats_doc -> Cats are small carnivores often kept as companion animals.try Chroma.countCollections()returns1.try Chroma.countDocuments(collectionName: "my_collection")returns2.- Printed output includes
Top match: cats_doc -> Cats are small carnivores often kept as companion animals..
By default, Chroma operates in an ephemeral mode where data is stored in memory and lost when your application terminates. For persistent storage, initialize Chroma with a specific file path:
- Initialize with
initializeWithPath. - Reuse the same path next launch.
let chromaDirectory = URL.documentsDirectory
.appendingPathComponent("chroma_db")
.path
try Chroma.initializeWithPath(path: chromaDirectory, allowReset: false)
// Data will be preserved between app sessions- Set
allowResettofalsein production to prevent accidental data loss. - Use one consistent path across launches.
- Back up and migrate your on-disk database as part of app updates.
- On iOS, prefer the app Documents directory if you want backup inclusion.
- On macOS, choose a user-visible location policy for supportability.
ChromaEmbedder lets you embed text on-device using MLXEmbedders from mlx-swift-lm.
- Create an embedder.
- Load the model once.
- Add text documents with automatic embedding.
- Query with text directly.
import Chroma
// Initialize Chroma and create a collection
try Chroma.initialize(allowReset: true)
let collectionName = "my_collection"
let collectionId = try Chroma.createCollection(name: collectionName)
// Create an embedder with your chosen model
let embedder = ChromaEmbedder(model: .miniLML6)
// Load the model (only needs to be done once)
await embedder.loadModel()
// Add documents with automatic embedding
let ids = ["doc1", "doc2"]
let texts = ["Document 1 text", "Document 2 text"]
let count = try await embedder.addDocuments(
to: collectionName,
ids: ["doc1", "doc2"],
texts: ["Document 1 text", "Document 2 text"]
)
// Query using text instead of pre-computed embeddings
let results = try await embedder.queryCollection(
collectionName,
queryText: "similar document",
nResults: 5
)embedder.embeddingDimensionsmatches the selected model.try await embedder.embed(text: "...")returns non-zero values.- Result vector L2 norm is approximately
1.0for supported models.
Approximate sizes come from current model cards and may change as upstream revisions ship.
| Case | Hugging Face model ID | Dimensions | Approximate size | Typical use |
|---|---|---|---|---|
bgeMicro |
TaylorAI/bge-micro-v2 | 384 | ~17MB | Mobile, constrained memory |
gteTiny |
TaylorAI/gte-tiny | 384 | ~25MB | Mobile, lightweight |
miniLML6 |
sentence-transformers/all-MiniLM-L6-v2 | 384 | ~90MB | Balanced quality/speed |
miniLML12 |
sentence-transformers/all-MiniLM-L12-v2 | 384 | ~130MB | Higher quality than L6 |
bgeSmall |
BAAI/bge-small-en-v1.5 | 384 | ~130MB | Strong small model |
bgeBase |
BAAI/bge-base-en-v1.5 | 768 | ~440MB | Desktop quality |
bgeLarge |
BAAI/bge-large-en-v1.5 | 1024 | ~1.3GB | Maximum quality |
mixedbreadLarge |
mixedbread-ai/mxbai-embed-large-v1 | 1024 | ~1.3GB | Maximum quality |
ChromaMetadata is a typealias:
public typealias ChromaMetadata = [String: ChromaMetadataValue]Supported value types:
bool(Bool)int(Int64)float(Double)string(String)
You can decode metadata returned by getDocuments:
let result = try Chroma.getDocuments(
collectionName: "my_collection",
ids: nil,
whereClause: nil,
limit: nil,
offset: nil,
whereDocument: nil,
include: ["metadatas"]
)
let decoded = result.decodedMetadatas()CAUTION Metadata writes are currently blocked by the shipped Chroma core binary.
Chroma.addDocuments(..., metadatas: ...)throwsChromaMetadataError.metadataWriteUnsupportedif any metadata entry is non-nil.
| Type | Fields |
|---|---|
CollectionInfo |
name: String, collectionId: String, numDocuments: UInt32 |
DatabaseInfo |
id: String, name: String, tenant: String |
GetResult |
ids: [String], documents: [String?] |
AdvancedGetResult |
ids: [String], embeddings: [[Float]]?, documents: [String?]?, metadatas: [String?]?, uris: [String?]? |
QueryResult |
ids: [[String]], documents: [[String?]], distances: [[Float?]]? |
ChromaError |
.Generic(message: String) |
ChromaMetadataError |
.countMismatch(expected:actual:), .metadataWriteUnsupported |
ChromaEmbedderError |
.modelNotLoaded, .modelLoadingFailed(_,_), .embeddingFailed(_,_) |
// Initialization and system
func initialize(allowReset: Bool) throws
func initializeWithPath(path: String?, allowReset: Bool) throws
func reset() throws
func getVersion() throws -> String
func getMaxBatchSize() throws -> UInt32
func heartbeat() throws -> Int64
// Collections
func createCollection(name: String) throws -> String
func getCollection(collectionName: String) throws -> CollectionInfo
func listCollections() throws -> [String]
func updateCollection(collectionName: String, newName: String?) throws
func deleteCollection(collectionName: String) throws
func countCollections() throws -> UInt32
// Documents
func addDocuments(collectionName: String, ids: [String], embeddings: [[Float]], documents: [String]) throws -> UInt32
func addDocuments(collectionName: String, ids: [String], embeddings: [[Float]], documents: [String], metadatas: [ChromaMetadata?]) throws -> UInt32
func getAllDocuments(collectionName: String) throws -> GetResult
func getDocuments(collectionName: String, ids: [String]?, whereClause: String?, limit: UInt32?, offset: UInt32?, whereDocument: String?, include: [String]?) throws -> AdvancedGetResult
func updateDocuments(collectionName: String, ids: [String], embeddings: [[Float]]?, documents: [String]?) throws
func upsertDocuments(collectionName: String, ids: [String], embeddings: [[Float]]?, documents: [String]?) throws
func deleteDocuments(collectionName: String, ids: [String]?) throws
func countDocuments(collectionName: String) throws -> UInt32
// Queries
func queryCollection(collectionName: String, queryEmbeddings: [[Float]], nResults: UInt32, whereFilter: String?, ids: [String]?, include: [String]?) throws -> QueryResult
// Databases
func createDatabase(name: String) throws -> String
func getDatabase(name: String) throws -> DatabaseInfo
func listDatabases() throws -> [String]
func deleteDatabase(name: String) throwspublic init(model: ChromaEmbedder.EmbeddingModel = .miniLML6)
public func loadModel() async throws
public func embed(text: String) async throws -> [Float]
public func embed(texts: [String]) async throws -> [[Float]]
public func addDocuments(to collectionName: String, ids: [String], texts: [String], metadatas: [ChromaMetadata?]? = nil) async throws -> UInt32
public func queryCollection(_ collectionName: String, queryTexts: [String], nResults: UInt32 = 10, whereFilter: String? = nil, ids: [String]? = nil, include: [String]? = nil) async throws -> QueryResult
public func queryCollection(_ collectionName: String, queryText: String, nResults: UInt32 = 10, whereFilter: String? = nil, ids: [String]? = nil, include: [String]? = nil) async throws -> QueryResult
public func createCollection(name: String) throws -> String
public var modelInfo: [String: Any] { get }case bgeMicro
case gteTiny
case miniLML6
case miniLML12
case bgeSmall
case bgeBase
case bgeLarge
case mixedbreadLargeEach case exposes:
rawValue: Hugging Face model IDdisplayName: StringembeddingDimensions: Int
createCollection(name:)is idempotent by name in current tests.upsertDocuments(...)inserts new IDs and updates existing IDs.deleteDocuments(collectionName:ids: nil)deletes all documents in the collection.queryCollection(..., nResults: large)returns only available matches.includecontrols optional return fields. Example: include["embeddings"]to receive embeddings fromgetDocuments.decodedMetadatas()converts metadata JSON strings to[ChromaMetadata?].- Use
countDocuments(collectionName:)when you need an authoritative post-write document count. FfiConverter*anduniffiEnsureChromaSwiftInitialized()symbols are generated UniFFI scaffolding, not application-level API.
- Symptom:
Embedding model not loaded. Call loadModel() first.Cause:embedor embedder query called before model load. Fix: calltry await embedder.loadModel()once at startup. - Symptom:
Metadata count (...) does not match ids count (...). Cause: metadata array length differs from IDs. Fix: pass one metadata entry per ID. - Symptom:
Writing document metadata is not supported .... Cause: current binary does not support metadata writes. Fix: omit metadata on writes or pass onlynilmetadata placeholders. - Symptom: query returns IDs but missing text. Cause:
documentsnot requested ininclude. Fix: passinclude: ["documents"]. - Symptom:
reset()fails. Cause: initialization may not allow reset. Fix: initialize withallowReset: truefor environments where reset is required.
ChromaSwift normally downloads the published XCFramework from GitHub Releases.
When iterating on Rust bindings, point the package to a local framework instead of editing the manifest by hand.
- Rebuild Swift bindings:
cd ../chroma/rust/swift_bindings
./build_swift_package.shThis produces Chroma/chroma_swift_framework.xcframework.
- Switch manifest to local framework:
./scripts/use_local_framework.sh- Build and run your app.
- Switch back to release framework when ready:
./scripts/use_release_framework.sh <download-url> <checksum>Pass the URL and checksum from the GitHub release asset.
- Build the XCFramework as above. The artifact path is
chroma/rust/swift_bindings/Chroma/chroma_swift_framework.xcframework. - Zip it for release:
cd chroma/rust/swift_bindings/Chroma
ditto -c -k --sequesterRsrc --keepParent chroma_swift_framework.xcframework chroma_swift_framework.xcframework.zip- Compute checksum from
chroma-swift/:
swift package compute-checksum ../chroma/rust/swift_bindings/Chroma/chroma_swift_framework.xcframework.zip- Upload zip to GitHub Releases and update
Package.swiftwith the new URL and checksum. - Or run:
./scripts/use_release_framework.sh <url> <checksum>These steps keep local debugging and published builds separate without extra automation.
See ChromaDemos/README.md for ephemeral, persistent, local-embeddings, and cloud-sync examples.
ChromaSwift is available under the Apache License 2.0, same as the underlying Chroma library. See the LICENSE file for more info.
This package uses cargo-swift to generate Swift bindings and FFI code. The underlying FFI bindings are created with UniFFI, enabling seamless interop between Swift and the Chroma core implemented in Rust.
This package includes FFI bindings generated by UniFFI and links to a binary framework for the Chroma core. For advanced usage and troubleshooting, see the Chroma source code and documentation comments.