Object mapping, and more, for Redis and Swift
RedisOM Swift is a high-level Redis client and object mapper for Swift inspired by redis-om-python and the other official RedisOM libraries published under the Redis org. It provides a typed, declarative way to model, persist, and query JSON documents in Redis using Swift's key paths and macros.
RedisOM Swift combines RedisJSON, RediSearch, and connection pooling into a unified API that feels native to Swift. It integrates with both Vapor's app lifecycle and the Swift Service Lifecycle framwork, making it ideal for server applications, background workers, or distributed systems.
Key Features
- Declarative Models - Define models using the @Model macro and store them as queryable RedisJSON documnts.
- Full-text and numeric search - Query your data using a fluent, type-safe biulder powered by RediSearch.
- Lifecycle Integration - Works out of the box with Vapor and Service Lifecycle.
- Automatic Index Migration -
RedisOM Swiftautomatically creates and updates RediSearch indexes
You can add RedisOM Swift to your project using Swift Package Manager.
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/eric-musliner/redis-om-swift.git", from: "0.1.0")
]
...
targets : [
.product(name:"RedisOM", package: "redis-om-swift")
]This ensures that the Redis connections and index migrations are automatically started and stopped as part of your service lifecycle
Define your data models by implementing the JsonModel protocol
Use the the @Id Property wrapper to define the id field and auto assign an UUID on save to the model records.
struct User: JsonModel {
@Id var id: String?
var name: String
var email: String
var aliases: [String]?
var age: Int?
var createdAt: Date?
static let keyPrefix: String = "user"
}You can then persist, retrieve, update, or delete models like:
var user = User(name: "Alice", email: "alice@example.com")
try await user.save()
var user2 = User(name: "Alice", email: "alice@example.com", age: 45)
try await user2.save()
// Retrieve model from Redis by id
let user = try await User.get(id: user.id!)
try await user.delete()
// Make updates to model
user.name = "Alicia"
user.save()
// Delete by id
try await User.delete(id: user2.id!)
// Get all keys in Redis for a given model
try await User.allKeys()Model fields are automatically "Indexed" using the @Index Property wrapper on your model fields. @Index supports passing a type parameter to specify what type the index is in Redis for your field: eg. (.text, .tag, .numeric). The default type is .tag
@Index
var address: [Address]?
@Index(type: .text)
var description: String?
@Index(type: .numeric)
var weight: IntWhen a model is registered, RedisOM Swift automatically builds and synchronizes its schema with Redis.
redis.register(User.self)If you change your model fields or index configuration, the migrator updates the RediSearch index definitions automatically on next startup.
You can also manually run migrations:
let migrator = try Migrator(client: redis.poolService)
try await migrator.migrate(models: [User.self])RedisOM supports rich, type-safe queries via RediSearch, allowing you to filter, sort, and combine complex predicates with a Fluent-style API.
To make your models searchable, simply annotate them with the @Model macro.
This macro generates the necessary schema metadata for RediSearch, enabling RedisOM Swift to:
- Register the model automatically with the
RedisOM Swiftinstance. - Create and migrate RediSearch indexes.
- Support type-safe and chainable query builders.
Here's a basic example of a searchable model:
@Model
struct User: JsonModel {
@Id var id: String?
@Index(type: .text) var name: String
@Index var email: String
@Index var aliases: [String]?
@Index(type: .numeric) var age: Int?
static let keyPrefix: String = "user"
}Perform queries using a fluent API:
let users: [User] = try await User.find().where(\.$name == "Alice").all()Chain multiple predicates together:
let users: [User] = try await User
.find()
.where(\.$name == "Alice")
.or(\.$name == "Sandra")
.and(\.$age == 33)
.all()You can also use range operators, in, and between
@Model
struct Item: JsonModel {
@Id var id: String?
@Index(type: .numeric) var price: Double
@Index var name: String
static let keyPrefix: String = "item"
}
let items: [Item] = try await Item
.find()
.where(\.$price <= 65.99)
.and(\.$price > 10)
.all()
// Between
let users: [User] = try await User
.find()
.where(\.$age...(34, 60))
.and(\.$name == "Bill")
.all()
// In
let items: [Item] = try await Item.find()
.where(\.$price ~= [24.99, 50.99])
.all()You can invert any query predicate using the .not() modifier at the end of a query chain.
This tells RedisOM Swift to negate the preceding condition or group of conditions.
For example:
let users: [User] = try await User
.find()
.where(\.$name == "Alice")
.not()
.all()This generates a RediSearch query equivalent to:
-(@name:Alice)
You can also chain .not() with other predicates to express complex filters:
let users: [User] = try await User
.find()
.where(\.$age >= 18)
.and(\.$email == "alice@example.com")
.not()
.all()This translates to
-((@age:[18 inf] @email:alice@example.com))
RedisOM Swift allows you to embed nested models within your root model, while still making their fields searchable using the same key-path syntax
For example
@Model
struct Address: JsonModel {
@Id var id: String?
@Index var city: String
@Index var state: String
@Index var zip: String
}
@Model
struct Person: JsonModel {
@Id var id: String?
@Index var name: String
@Index var address: Address
}Under the hood, RedisOM Swift automatically flattens the nested schema so that RediSearch can index it with fully qualified field names (e.g. address__city, address__state).
You can then query nested fields directly using key paths:
let people = try await Person
.find()
.where(\.$address.city == "Boston")
.and(\.$address.state == "MA")
.all()Even nested collections are supported:
@Model
struct Bike: JsonModel {
@Id var id: String?
@Index var model: String
@Index var brand: String
@Index(type: .numeric) var price: Int
@Index var type: String
@Index var specs: [Spec]
@Index(type: .text) var description: String?
var addons: [String]?
@Index var helmetIncluded: Bool
var createdAt: Date?
static let keyPrefix: String = "bike"
}
@Model
struct Spec: JsonModel {
var id: String?
@Index var manufacturer: String
@Index var material: String
@Index(type: .numeric) var weight: Int
static let keyPrefix: String = "spec"
}You can query deeply into arrays of embedded models:
let bikes = try await Bike
.find()
.where(\.$specs[\.$manufacturer] == "Giant")
.all()
let bikes = try await Bike
.find()
.where(\.$specs[\.$weight]...(40, 60))
.and(\.$specs[\.$material] == "carbon fiber")
.all()You can freely mix predicates across nested and root fields:
let results = try await Person
.find()
.where(\.$name == "Alice")
.and(\.$address.city == "Cambridge")
.or(\.$address.state == "MA")
.all()Internally, RedisOM Swift automatically generates a valid RediSearch query such as:
(@name:Alice @address__city:Cambridge) | (@address__state:MA)
You can also control the number of results, get only the first, or simply check if a record exists
let items = try await Item
.find()
.where(\.$price...(24.00, 70.0))
.limit(0..<2)
.all()
let result: Item? = try await Item
.find()
.where(\.$price...(24.00, 70.0))
.first()
let exists = try await Item.find()
.where(\.$price <= 65.99)
.exists()Traditional Redis clients treat Redis as a key-value store.
RedisOM Swift turns it into a typed, searchable document store, allowing you to:
- Model data like you would in an ORM.
- Query using Swift key paths instead of strings.
- Combine nested model fields in a type-safe way.
- Let Redis handle full-text search and indexing behind the scenes.
By default RedisOM Swift will connect to a Redis instance using the environment variable REDIS_URL.
export REDIS_URL=redis://localhost:6379
You can also pass the URL directly:
let redis = try RedisOM(url: "redis://localhost:6379")or for secure connections:
let redis = try RedisOM(
url: "rediss://:mySecretPassword@redis.example.com:6379"
)You can also customize the logger and retry policy:
let redis = try RedisOM(
url: "redis://localhost:6379",
retryPolicy: .limited(3),
logger: Logger(label: "redis.om")
)For more granular control — such as setting custom authentication, selecting a database, or enabling TLS manually — you can construct a full RedisConfiguration object and pass it directly.
Pragmatic Configuration:
var config = try RedisConfiguration(hostname: "redis.prod.internal", port: 6380)
config.password = "prodSecret"
config.tlsConfiguration = .forClient()
let logger = Logger(label: "redis.om.prod")
let redis = try RedisOM(
config: config,
logger: logger,
retryPolicy: .infinite
)This is ideal when your app runs in environments that require explicit control over:
- Authentication credentials
- Database selection
- TLS certificates / client auth
- Socket options or timeouts
When used in a Vapor app, RedisOM Swift can participate in the lifecycle and automatically migrate your search indexes during startup
public func configure(_ app: Application) throws {
let redis = try RedisOM(url: "redis://localhost:6379")
# Register models for automatic indexing
redis.register(User.self)
app.lifecycle.use(redis)
}When the application boots, RedisOM Swift will automatically
- Establish a connection pool to Redis
- Create or re-create RediSearch indexes for all registered models
- Cleanly shut down connections on app termination
If you're building a service using Swift Service Lifecycle, RedisOM Swift can run as a managed service
import RedisOM
import ServiceLifecycle
@main
struct App {
static func main() async throws {
let redis = try RedisOM()
redis.register(User.self)
// Run as part of the Swift Service Lifecycle
let group = ServiceGroup(services: [redis])
try await group.run()
}
}This ensures that the Redis connections and index migrations are automatically started and stopped as part of your service lifecycle
RedisOM Swift integrates with Swift's swift-log system. You can inject a custom Logger to control verbosity:
var logger = Logger(label: "redis.om.debug")
logger.logLevel = .debug
let redis = try RedisOM(logger: logger)redis-om-swift is available under the MIT License. See LICENSE for details.