Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver guides expressions.md
Raw

guides/expressions.md

# Expressions
Aerospike expressions evaluate predicates and computations **on the server**, before results are sent to your client. They serve two distinct purposes:
1. **Filter expressions** — attached to an operation's `filter:` option, they cause the server to silently discard non-matching records before the response is sent. Used on [`get/3`](Aerospike.html#get/3), [`exists/3`](Aerospike.html#exists/3), [`delete/3`](Aerospike.html#delete/3), [`put/4`](Aerospike.html#put/4), [`batch_get/3`](Aerospike.html#batch_get/3), scans, and queries.
2. **Expression operations** — executed inside [`operate/4`](Aerospike.html#operate/4) as computation steps. They read a computed value into a synthetic bin or write a computed result to a real bin, all in a single round-trip.
Both share the same builder API: [`Aerospike.Exp`](Aerospike.Exp.html).
## Building Expressions
Expressions are composed from typed constructor functions. Each function returns an [`%Aerospike.Exp{}`](Aerospike.Exp.html) struct containing the pre-encoded wire representation. Expressions build up a tree by passing `%Exp{}` values as arguments to other builders.
```elixir
alias Aerospike.Exp
# Integer comparison — both sides typed
expr = Exp.gt(Exp.int_bin("age"), Exp.int(21))
# Boolean AND — at least two elements
expr = Exp.and_([
Exp.gte(Exp.int_bin("age"), Exp.val(18)),
Exp.lt(Exp.int_bin("age"), Exp.val(65)),
Exp.eq(Exp.str_bin("status"), Exp.val("active"))
])
# Negation
expr = Exp.not_(Exp.eq(Exp.str_bin("status"), Exp.val("banned")))
```
### Typed literal constructors
All values on the right-hand side of a comparison must be wrapped in a typed expression constructor:
| Constructor | Elixir type | Purpose |
|-------------|-------------|---------|
| `Exp.int(n)` | `integer()` | Integer literal |
| `Exp.float(f)` | `float()` | Float literal |
| `Exp.str(s)` | `binary()` | String literal |
| `Exp.bool(b)` | `boolean()` | Boolean literal |
| `Exp.blob(b)` | `binary()` | Raw binary literal (not UTF-8 string) |
| `Exp.nil_()` || Nil literal |
### [`Exp.val/1`](Aerospike.Exp.html#val/1) — type-inferring convenience
[`Exp.val/1`](Aerospike.Exp.html#val/1) infers the constructor from the Elixir term, following the same mapping as official Aerospike clients:
| Elixir term | Expression type |
|-------------|----------------|
| `integer()` | [`Exp.int/1`](Aerospike.Exp.html#int/1) |
| `float()` | [`Exp.float/1`](Aerospike.Exp.html#float/1) |
| `binary()` | [`Exp.str/1`](Aerospike.Exp.html#str/1) |
| `boolean()` | [`Exp.bool/1`](Aerospike.Exp.html#bool/1) |
| `nil` | [`Exp.nil_/0`](Aerospike.Exp.html#nil_/0) |
**All binaries are treated as strings.** If you need blob (raw binary) semantics, use [`Exp.blob/1`](Aerospike.Exp.html#blob/1) explicitly; [`Exp.val/1`](Aerospike.Exp.html#val/1) maps every binary to [`Exp.str/1`](Aerospike.Exp.html#str/1).
```elixir
# These are equivalent
Exp.gt(Exp.int_bin("age"), Exp.int(21))
Exp.gt(Exp.int_bin("age"), Exp.val(21))
```
### Bin reads
Read the value of a specific bin from the current record:
```elixir
Exp.int_bin("age") # integer bin
Exp.float_bin("score") # float bin
Exp.str_bin("city") # string bin
Exp.bool_bin("active") # boolean bin
Exp.blob_bin("payload") # blob (raw binary) bin
Exp.geo_bin("location") # geo bin
```
### Record metadata
Access server-side record metadata without bin reads:
```elixir
Exp.ttl() # remaining TTL in seconds
Exp.void_time() # absolute expiration epoch timestamp
Exp.last_update() # epoch timestamp of the last write
Exp.key_exists() # true if a user key was stored with the record
Exp.set_name() # the set name as a string
Exp.tombstone?() # true when the record is a tombstone
Exp.record_size() # storage size in bytes on the device
```
```elixir
# Filter records that expire within the next hour
Exp.lt(Exp.ttl(), Exp.int(3_600))
# Records updated since a specific epoch time
Exp.gte(Exp.last_update(), Exp.int(cutoff_ts))
```
### Comparisons
All comparison functions take two [`%Exp{}`](Aerospike.Exp.html) arguments:
```elixir
Exp.eq(left, right) # left == right
Exp.ne(left, right) # left != right
Exp.gt(left, right) # left > right
Exp.gte(left, right) # left >= right
Exp.lt(left, right) # left < right
Exp.lte(left, right) # left <= right
```
### Boolean composition
```elixir
# AND — requires at least two expressions
Exp.and_([
Exp.gt(Exp.int_bin("age"), Exp.val(18)),
Exp.eq(Exp.str_bin("country"), Exp.val("US"))
])
# OR — requires at least two expressions
Exp.or_([
Exp.eq(Exp.str_bin("plan"), Exp.val("premium")),
Exp.eq(Exp.str_bin("plan"), Exp.val("enterprise"))
])
# NOT — single expression
Exp.not_(Exp.eq(Exp.str_bin("status"), Exp.val("banned")))
```
> **Naming convention.** [`Exp.and_/1`](Aerospike.Exp.html#and_/1), [`Exp.or_/1`](Aerospike.Exp.html#or_/1), and [`Exp.not_/1`](Aerospike.Exp.html#not_/1) use a trailing underscore because `and`, `or`, and `not` are Elixir reserved words and cannot be used as bare function names. The trailing underscore is the standard Elixir convention for avoiding keyword collisions.
## Filter Expressions
Attach an expression to any operation using the `filter:` option. The server evaluates the expression before returning a result — if the expression is false, the operation returns `{:error, %Aerospike.Error{code: :filtered_out}}`.
### Single-record CRUD
```elixir
alias Aerospike.{Exp, Key}
key = Aerospike.key("test", "users", "user:42")
only_adults = Exp.gte(Exp.int_bin("age"), Exp.int(18))
# Filtered get — returns {:error, %Aerospike.Error{code: :filtered_out}} if age < 18
{:ok, record} = Aerospike.get(:aero, key, filter: only_adults)
# Filtered exists check
{:ok, true} = Aerospike.exists(:aero, key, filter: only_adults)
# Filtered delete — only delete if the record matches the expression
{:ok, _} = Aerospike.delete(:aero, key, filter: only_adults)
# Filtered put — only write if the expression matches
{:ok, _} = Aerospike.put(:aero, key, %{"score" => 99}, filter: only_adults)
```
### Scans
[`Scan.filter/2`](Aerospike.Scan.html#filter/2) attaches an expression to the scan. Multiple [`filter/2`](Aerospike.Scan.html#filter/2) calls **AND** the expressions together — the server returns only records that satisfy all attached filters.
```elixir
alias Aerospike.{Exp, Scan}
active_adults =
Exp.and_([
Exp.gte(Exp.int_bin("age"), Exp.val(18)),
Exp.eq(Exp.str_bin("status"), Exp.val("active"))
])
{:ok, records} =
Scan.new("test", "users")
|> Scan.filter(active_adults)
|> Scan.max_records(10_000)
|> then(&Aerospike.all(:aero, &1))
```
Multiple separate filter calls compose with AND:
```elixir
Scan.new("test", "users")
|> Scan.filter(Exp.gte(Exp.int_bin("age"), Exp.val(18)))
|> Scan.filter(Exp.eq(Exp.str_bin("country"), Exp.val("US")))
# Both must match — equivalent to Exp.and_([...])
```
### Queries (secondary index)
[`Query.filter/2`](Aerospike.Query.html#filter/2) works the same way as [`Scan.filter/2`](Aerospike.Scan.html#filter/2). It applies an additional expression filter on top of the secondary-index predicate from [`Query.where/2`](Aerospike.Query.html#where/2):
```elixir
alias Aerospike.{Exp, Filter, Query}
result =
Query.new("test", "users")
|> Query.where(Filter.range("age", 18, 65))
|> Query.filter(Exp.eq(Exp.str_bin("country"), Exp.val("US")))
|> then(&Aerospike.stream!(:aero, &1))
|> Enum.to_list()
```
### `filter:` vs [`Query.where/2`](Aerospike.Query.html#where/2)
These serve different purposes and are both valuable:
| | [`Query.where/2`](Aerospike.Query.html#where/2) | `filter:` / [`Query.filter/2`](Aerospike.Query.html#filter/2) |
|---|---|---|
| Mechanism | Secondary index lookup | Expression evaluated per record |
| Requires index | Yes | No |
| Can replace | Never (index narrows candidates) | Can add on top of SI predicate |
| Result | Candidate set | Accepted records |
Use [`Query.where/2`](Aerospike.Query.html#where/2) to narrow candidates with an index, then `filter:` to refine the result with logic the index cannot express (multi-condition, metadata comparisons, or cross-bin logic).
## Expression Operations
Expression operations execute inside [`Aerospike.operate/4`](Aerospike.html#operate/4). They compute a value **server-side** and either return the result in the response record's bins or write the result to a real bin — without an extra round-trip.
Use [`Aerospike.Op.Exp`](Aerospike.Op.Exp.html):
```elixir
alias Aerospike.{Exp, Op}
key = Aerospike.key("test", "stats", "user:42")
{:ok, record} =
Aerospike.operate(:aero, key, [
# Read: compute a value and return it as a synthetic bin
Op.Exp.read("is_adult", Exp.gte(Exp.int_bin("age"), Exp.int(18))),
# Write: compute a value and store it in a real bin
Op.Exp.write("age_bucket",
Exp.and_([
Exp.gte(Exp.int_bin("age"), Exp.val(18)),
Exp.lt(Exp.int_bin("age"), Exp.val(65))
]))
])
record.bins["is_adult"] #=> true
```
### [`Op.Exp.read/3`](Aerospike.Op.Exp.html#read/3)
Returns the expression result as a synthetic bin in the response record. The bin appears in `record.bins` under the name you provide but is never persisted.
```elixir
Op.Exp.read("ttl_seconds", Exp.ttl())
Op.Exp.read("over_limit", Exp.gt(Exp.int_bin("count"), Exp.int(1_000)))
```
### [`Op.Exp.write/3`](Aerospike.Op.Exp.html#write/3)
Evaluates the expression and **stores the result in a real bin**. Subsequent reads of that bin return the stored value.
```elixir
Op.Exp.write("flagged", Exp.gt(Exp.int_bin("violations"), Exp.int(3)))
```
Both [`read/3`](Aerospike.Op.Exp.html#read/3) and [`write/3`](Aerospike.Op.Exp.html#write/3) accept an optional `flags:` integer keyword (default `0`).
## Runtime Composition
Because expressions are plain structs, you can build them dynamically at runtime — this is the Aerospike equivalent of `Ecto.Query.dynamic/2`:
```elixir
alias Aerospike.{Exp, Scan}
def build_user_scan(params) do
filters =
for {key, builder} <- [
min_age: &Exp.gte(Exp.int_bin("age"), Exp.val(&1)),
max_age: &Exp.lte(Exp.int_bin("age"), Exp.val(&1)),
city: &Exp.eq(Exp.str_bin("city"), Exp.val(&1)),
active: fn _ -> Exp.eq(Exp.str_bin("status"), Exp.val("active")) end
],
value = params[key],
not is_nil(value),
do: builder.(value)
expr =
case filters do
[] -> nil
[single] -> single
many -> Exp.and_(many)
end
scan = Scan.new("test", "users")
if expr do
Scan.filter(scan, expr)
else
scan
end
end
# At the call site
scan = build_user_scan(%{min_age: 21, city: "Portland"})
{:ok, records} = Aerospike.all(:aero, Scan.max_records(scan, 5_000))
```
Since each expression builder returns a plain struct, you can also store partial expressions in module attributes, pass them as function arguments, or accumulate them in a `for` comprehension — no macro restrictions apply.
## Best Practices
- **Prefer typed constructors over [`Exp.val/1`](Aerospike.Exp.html#val/1)** when the type matters (especially for blobs — [`Exp.val/1`](Aerospike.Exp.html#val/1) maps all binaries to strings).
- **Combine server-side and client-side filters intentionally.** Push as much as possible into expressions to reduce data transfer; use `Stream.filter/2` client-side only for logic the expression system cannot express.
- **Use [`Exp.and_/1`](Aerospike.Exp.html#and_/1) for clarity** when combining multiple conditions, rather than nesting repeated [`filter/2`](Aerospike.Scan.html#filter/2) calls.
- **On queries, combine [`where/2`](Aerospike.Query.html#where/2) with `filter:`** — the SI predicate narrows candidates cheaply; the expression applies fine-grained logic that the index cannot express.
- **Expression operations are not CDT operations.** [`Op.Exp.write/3`](Aerospike.Op.Exp.html#write/3) writes the expression result as a scalar value. For list/map mutations, use [`Aerospike.Op.List`](Aerospike.Op.List.html) and [`Aerospike.Op.Map`](Aerospike.Op.Map.html).
## Next Steps
- [Queries and Scanning](queries-and-scanning.md)[`Scan.filter/2`](Aerospike.Scan.html#filter/2), [`Query.filter/2`](Aerospike.Query.html#filter/2), execution patterns
- [Batch Operations](batch-operations.md)`filter:` option on [`Batch.read/2`](Aerospike.Batch.html#read/2) and [`Batch.operate/3`](Aerospike.Batch.html#operate/3)
- [`Aerospike.Exp`](Aerospike.Exp.html) — full expression builder API reference
- [`Aerospike.Op.Exp`](Aerospike.Op.Exp.html) — expression operation constructors
- [`Aerospike.Filter`](Aerospike.Filter.html) — secondary-index predicates for [`Query.where/2`](Aerospike.Query.html#where/2)