dean151/swift-embeddings
Text embeddings and reranking for Swift: two small protocols, multiple
Installation
.package(url: "https://github.com/tdurand/swift-embeddings.git", from: "0.1.0"),.target(name: "App", dependencies: [
.product(name: "Embeddings", package: "swift-embeddings"),
])Embedding
import Embeddings
let model = VoyageEmbeddingModel(model: .voyage3, apiKey: key)
let embeddings = try await model.embed("how does Swift manage memory?")
print(embeddings.vectors) // [[Double]]Batch inputs and provider-specific options are supported:
let model = JinaEmbeddingModel(model: .embeddingsV3, apiKey: key)
let embeddings = try await model.embed(
["query one", "query two"],
options: .jina(task: .retrievalQuery, dimensions: 256)
)Reranking
Reranking is a second-stage refinement for retrieval: fetch a broad candidate set cheaply (for example by embedding similarity), then score each candidate against the query for a sharper final ordering.
let reranker = CohereRerankModel(model: .rerankV35, apiKey: key)
let ranking = try await reranker.rerank(
"best fruit for a smoothie",
documents: ["apple pie", "ripe mango", "car engine"],
options: .init(topN: 2)
)
for ranked in ranking.results {
print(ranked.index, ranked.relevanceScore)
}On-device embeddings (NaturalLanguage)
On Apple platforms you can embed text entirely on-device — no API key and no network request — using NaturalLanguageEmbeddingModel, backed by Apple's NaturalLanguage framework.
let model = NaturalLanguageEmbeddingModel() // .automatic
let embeddings = try await model.embed("how does Swift manage memory?")
print(embeddings.vectors) // [[Double]]By default (.automatic) it uses the transformer-based NLContextualEmbedding on iOS 17 / macOS 14 and later — mean-pooling its per-token vectors into one vector per input — and falls back to NLEmbedding's sentence embeddings on earlier systems or when no contextual asset is available for the input's language. Use .contextual or .sentence to pin a backend. The language is auto-detected per input; pass it explicitly to skip detection:
try await model.embed("bonjour le monde", options: .naturalLanguage(language: .french))The result's model reports the backend used (e.g. "contextual:en", "sentence:fr"), and usage is always nil since there is no token billing on-device.
Providers
| Provider | Embedding | Reranking | |----------|:---------:|:---------:| | Voyage | ✅ VoyageEmbeddingModel | ✅ VoyageRerankModel | | Jina | ✅ JinaEmbeddingModel | ✅ JinaRerankModel | | Cohere | ✅ CohereEmbeddingModel | ✅ CohereRerankModel | | OpenAI | ✅ OpenAIEmbeddingModel | — | | Apple NaturalLanguage (on-device) | ✅ NaturalLanguageEmbeddingModel | — |
Each model takes a typed model identifier (e.g. .voyage3, with string-literal fallback for unlisted models), an apiKey, and optionally a custom transport and baseURL — handy for OpenAI-compatible gateways or tests.
Custom transport
Every model issues requests through an HTTPClientTransport. The default is URLSessionTransport; conform your own type to plug in a different HTTP client or a mock for offline tests:
struct MyTransport: HTTPClientTransport {
func execute(_ request: HTTPRequest, body: HTTPRequestBody?) async throws -> HTTPResponseData {
// ...
}
}
let model = OpenAIEmbeddingModel(
model: .textEmbedding3Small,
apiKey: key,
transport: MyTransport()
)License
MIT © 2026 Thomas Durand
Package Metadata
Repository: dean151/swift-embeddings
Default branch: main
README: README.md