Expressions
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
Aerospike expressions are a specialized, strictly typed, functional language. They are designed specifically for manipulating and comparing data fields (bins) and record metadata.
AEL text reference (Developer SDK)
The Aerospike Expression Language (AEL) is a text syntax for the same server expression engine documented on this page. If you use the Developer SDK (Java or Python), author filters and operation expressions as AEL strings and look up the full grammar in the AEL reference.
This expressions section documents the programmatic Exp builder API used by classic Aerospike clients (Java Exp.*, Python aerospike_helpers.expressions, C as_exp_*, and so on). Use builders when your client does not accept AEL text, or when you compose expressions in code rather than as strings.
Because they are intentionally non-Turing complete, meaning they cannot perform all possible computations that a universal Turing machine could, they can’t handle complex features like iteration or recursion, which ensures their execution remains fast and predictable.
Expressions are used for several key purposes:
- Filter records: Select which records to read or process.
- Control commands: Determine if a record command (like a write) should proceed.
- XDR filtering: Filter which data gets replicated to remote datacenters (DCs).
- Extend transactions: Add custom logic and functionality to database transactions.
Each operation’s Example shows the code in nine tabs: Aerospike Expression Language (AEL) text on the Java SDK and Python SDK tabs, and the Exp builder on the other seven. See the AEL reference for AEL grammar.
Most data types also have an operation API, and the expressions for that type mirror it. An operation changes what is stored; an expression evaluates to a value you can filter on, return from a projection, or store. See Operations and expressions for which to reach for, and each type’s own reference for the set itself: String, List, Map, Bit, and HLL expressions.
For working with nested collection data, see:
- Working with nested collection data types (CDTs): access elements within nested maps and lists using composed expressions.
- Path expressions: select, filter, and retrieve multiple elements from nested structures in a single operation.
Types of expressions
Aerospike supports four types of expressions:
- Secondary index expressions introduced in Database 8.1.0
- Operation expressions introduced in Database 5.6.0
- XDR filter expressions introduced in Database 5.3.0
- Record filter expressions introduced in Database 5.2.0
Secondary index expression
A secondary index can index either the value of a specific bin or the computed value of an expression. When indexing very large data sets, you can create more memory-efficient secondary indexes by indexing on the computed value of an expression rather than bin data.
Use the Aerospike admin tool,
asadm, to create
secondary indexes.
Queries can use an expression index
either by matching the expression used by the index, or by referring to the
index name in a predicate. For a hands-on walkthrough, see the
expression index tutorial.
Operation expressions
An operation expression atomically computes a value from data already in the
record or supplied by the expression itself. It travels in an operate
command’s operation list, wrapped to say what becomes of the value it produces.
- Read expression: Evaluates the expression and returns the result under a
name you provide, called a computed bin. The computed bin exists only in
the response; nothing is written to storage. Read expressions work in
single-record
operate, batchedoperate, and query projection. - Write expression: Evaluates the expression and persists the result
into a named bin on the record. The bin name is a real, stored bin. Write
expressions work in single-record
operateand batchedoperate(not in query projection, which is read-only).
Operation expressions allow for atomic, cross-bin operations. This means they complete entirely or not at all across multiple data fields.
Operation expressions can be used in query projections as read expressions. A
query’s projection accepts the same read operations and read expressions used
in single-key and batched operate commands, enabling
path expressions and computed values directly in
the query result.
Example: read projection with an operation expression
Read expressions return a computed value in the response without writing to storage. The following read projection computes a line total from two bins:
// Assume session and key are already configured.Record record = session.query(key) .bin("lineTotal") .selectFrom("$.price * $.qty:INT.toFloat()") .execute() .getFirstRecord();
double total = record.getDouble("lineTotal");# Assume session and key are already configured.result = ( session.query(key) .bin("lineTotal") .select_from("$.price * $.qty:INT.toFloat()") .first_or_raise())total = result.record.bins["lineTotal"]use aerospike::expressions::{float_bin, int_bin, num_mul, to_float};use aerospike::operations::exp::{read_exp, ExpReadFlags};use aerospike::WritePolicy;
// Assume client and key are already configured.let record = client.operate(&WritePolicy::default(), &key, &[ read_exp("lineTotal", num_mul(vec![float_bin("price".into()), to_float(int_bin("qty".into()))]), ExpReadFlags::Default),]).await?;
let total = record.bins.get("lineTotal");using Aerospike.Client;
Record record = client.Operate(null, key, ExpOperation.Read("lineTotal", Exp.Build(Exp.Mul(Exp.FloatBin("price"), Exp.ToFloat(Exp.IntBin("qty")))), ExpReadFlags.DEFAULT));
double total = record.GetDouble("lineTotal");// Requires: import as "github.com/aerospike/aerospike-client-go/v8"record, err := client.Operate(nil, key, as.ExpReadOp("lineTotal", as.ExpNumMul(as.ExpFloatBin("price"), as.ExpToFloat(as.ExpIntBin("qty"))), as.ExpReadFlagDefault))
total := record.Bins["lineTotal"]// The Node.js client's exp.toFloat builds an expression the client cannot send,// so this example has no Node.js form.#include <aerospike/as_exp.h>#include <aerospike/as_exp_operations.h>
as_exp_build(line_total, as_exp_mul(as_exp_bin_float("price"), as_exp_to_float(as_exp_bin_int("qty"))));
as_operations ops;as_operations_inita(&ops, 1);as_operations_exp_read(&ops, "lineTotal", line_total, AS_EXP_READ_DEFAULT);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);double total = as_record_get_double(rec, "lineTotal", 0.0);
as_record_destroy(rec);as_operations_destroy(&ops);as_exp_destroy(line_total);import com.aerospike.client.exp.Exp;import com.aerospike.client.exp.ExpOperation;import com.aerospike.client.exp.ExpReadFlags;
Record record = client.operate(null, key, ExpOperation.read("lineTotal", Exp.build(Exp.mul(Exp.floatBin("price"), Exp.toFloat(Exp.intBin("qty")))), ExpReadFlags.DEFAULT));
double total = record.getDouble("lineTotal");from aerospike_helpers import expressions as expfrom aerospike_helpers.operations import expression_operations as exp_ops
read_exp = exp.Mul(exp.FloatBin("price"), exp.ToFloat(exp.IntBin("qty"))).compile()ops = [exp_ops.expression_read("lineTotal", read_exp)](key, meta, bins) = client.operate(key, ops)print(bins["lineTotal"])Record filtering with expressions
Record filtering selects only the records that satisfy a boolean expression,
meaning the expression must evaluate to true.
Example: filter a single-record command
You attach a compiled boolean expression to the policy, and it acts as a conditional for the command. The server evaluates it if the record exists; see Execution timing for more details.
// Assume session and key are already configured.// The stream is empty when the filter skips the record.RecordStream rs = session.query(key).where("$.age > 18").execute();# Assume session and key are already configured.# first() is None when the filter skips the record.stream = session.query(key).where("$.age > 18").execute()use aerospike::expressions::{gt, int_bin, int_val};use aerospike::{Bins, ReadPolicy};
// Assume client and key are already configured.let mut policy = ReadPolicy::default();policy.base_policy.filter_expression = Some(gt(int_bin("age".into()), int_val(18)));// Err with ResultCode::FilteredOut when the filter skips the record.let record = client.get(&policy, &key, Bins::All).await;using Aerospike.Client;
Policy policy = new Policy();policy.filterExp = Exp.Build( Exp.GT(Exp.IntBin("age"), Exp.Val(18)));
Record record = client.Get(policy, key);// Requires: import aerospike "github.com/aerospike/aerospike-client-go/v8"policy := aerospike.NewPolicy()policy.FilterExpression = aerospike.ExpGreater( aerospike.ExpIntBin("age"), aerospike.ExpIntVal(18))
record, err := client.Get(policy, key)const Aerospike = await import("aerospike");
const exp = Aerospike.exp
const readPolicy = new Aerospike.ReadPolicy({ filterExpression: exp.gt(exp.binInt('age'), exp.int(18)),})
const record = await client.get(key_obj, readPolicy)#include <aerospike/as_exp.h>#include <aerospike/as_policy.h>
as_exp_build(read_filter, as_exp_cmp_gt(as_exp_bin_int("age"), as_exp_int(18)));
as_policy_read policy;as_policy_read_init(&policy);policy.base.filter_exp = read_filter;
// Returns AEROSPIKE_FILTERED_OUT when the filter skips the record.as_record* rec = NULL;aerospike_key_get(&as, &err, &policy, &key, &rec);as_exp_destroy(read_filter);import com.aerospike.client.AerospikeClient;import com.aerospike.client.Key;import com.aerospike.client.Record;import com.aerospike.client.policy.Policy;import com.aerospike.client.exp.Exp;
// Assume client and key are already configured.Policy policy = new Policy();policy.filterExp = Exp.build(Exp.gt(Exp.intBin("age"), Exp.val(18)));Record record = client.get(policy, key);from aerospike_helpers.expressions import GT, IntBin
policy = {'expressions': GT(IntBin("age"), 18).compile()}# Assume client and key_obj are already configured._, meta, bins = client.get(key_obj, policy=policy)In a query, setting QueryPolicy.filterExp
filters records out of the query.
Execution timing
Filter expressions are only executed when the record already exists in the
database. They will not execute if a read operation fails to find the record,
which surfaces a missing-record signal to the client, usually the
RecordNotFound error; the exact behavior of exists() is client-specific,
so consult your client reference.
If a write operation is creating a new record the filter expression
will not execute. To veto record creation, combine the filter expression with a
WritePolicy.recordExistsAction
set to UPDATE_ONLY.
Behavior when the filter does not evaluate to true
A record is accepted only when its filter expression evaluates to true.
An explicit false, or an unknown that survives the
two-phase Execution model is treated identically:
the record is rejected.
How that rejection reaches the caller depends on the command class.
| Command class | Commands | Rejection surface |
|---|---|---|
| Single-record | get, put, operate, remove, touch | C, Go, Node.js, Python and Rust reject with result code 27 (FILTERED_OUT), distinct from a missing record’s code 2. Java and C# return no record, as for a missing one, unless the policy sets failOnFilteredOut; the Developer SDKs return an empty result unless the command calls failOnFilteredOut(). With it set, the rejection raises code 27. |
| Single-record existence | exists | Most clients return false, as if the record was absent. Consult your client’s API reference. |
| Batch | batch_read, batch_write, batch_operate, batch_remove | The top-level call does not raise. Each per-record result carries the rejection as a status code (FILTERED_OUT, 27) in the batch result array. Callers must inspect each entry. |
| Queries | query.results / query.foreach, scan_all, background variants | Rejected records are silently omitted from the result stream. |
| Transactions | Any of the above, inside the transaction (strong-consistency true namespaces) | Per-operation. FilteredOut on any command does not roll back the transaction. |
| XDR ship filters | Configured via namespace config, not client API | Filtered-out records are not shipped. No application-level signal. |
Two practical consequences:
- Single-record callers can catch the rejection (code 27), in Java, C# and the Developer SDKs once
failOnFilteredOutis set. Batch callers cannot — the exception would short-circuit the rest of the batch. Inspect eachBatchRecord’s result code instead. - Query and scan callers get a filtered view, not a rejection report. If you need to distinguish “this key was filtered” from “this key doesn’t match”, do the filter as a single-record command per key rather than as part of the query.
Filter XDR records with expressions
You can filter records before they are sent to remote destinations using XDR (Cross-Datacenter Replication).
These XDR filters are dynamic and must be defined uniquely for each namespace going to a specific destination datacenter (DC).
You have two main ways to set these filter expressions:
Using the info command: Execute the command xdr-set-filter.
Programmatically: Use the appropriate client API in your application code.
XDR filtering reduces the volume of data that you replicate. When you reduce the volume of replicated data, you also:
- Reduce network traffic.
- Reduce storage and processing requirements at destination datacenters, which avoids the costs of overprovisioning, most significantly in hub-and-spoke XDR topologies.
- Reduce the cost of moving data across or from public clouds.
For configuration details, see XDR filters.
Syntax and behavior
For authoritative AEL text grammar (paths, selectors, operators, functions, postfix flags, and compile limits), see AEL reference. The subsections below describe the programmatic builder syntax shared by classic clients.
Aerospike expressions use Polish Notation (PN) syntax and have strict typing, which broadens the criteria you can use to select specific records.
Key rules
-
Immutability: All data within an expression is immutable (cannot be changed).
-
Bin modifications: If an expression performs modifications to a bin, those changes operate on a temporary copy and are not saved to the actual bin once the expression finishes.
-
Conditional logic but no iteration: Expressions do support conditional branching via the
condoperator. However, the expression system does not support loops, iteration, or recursion, and therefore cannot perform general control-flow constructs such as repeated evaluation. If performed as an expression write operation, the final result of the expression is stored to the target bin.
This means expressions are designed for fast, single-pass evaluation without persistent state changes or complex control flow within the evaluation logic itself.
Types
Expressions are strictly typed. Each expression evaluates to a value of one of the supported data types: nil, boolean, integer (64-bit signed), float (64-bit), string, blob, list, map, GeoJSON, or HyperLogLog. The type must match what the expression expects; for example, a comparison requires both operands to be the same type.
Expressions compose
An expression evaluates to a value, and that value is what the next expression operates on. At the leaves are bin values and record metadata. Above the leaves, every operand is a value that some earlier expression produced.
# Pseudocode — not runnable# a = 60, b = 55
gt(add(bin_int("a"), bin_int("b")), 100) # trueadd produces an integer; gt compares that integer. Nothing is written to the
record. See Operations and
expressions for how this differs
from the Operate API, which acts in place on a named bin.
This is also what a reference card means when it names an argument bin and
gives it a type. The name belongs to the parameter, not to a bin: the argument
takes a bin of that type, and equally takes any expression evaluating to that
type.
Literals
Some arguments are typed string literal, integer literal, or boolean literal. A literal is a constant fixed when the expression is built, before the
server receives it. It cannot be read from a bin or produced by another
expression.
The distinction is not cosmetic.
cmp_regex compiles its pattern
once at build time, so the pattern is a literal, while the string it matches
against is an ordinary operand and may be any expression evaluating to a string.
Context
A context is the path from a bin to a
nested element. List and
Map expressions take one as a context argument;
omit it to work at the top level of the collection.
Context belongs to the collection types because only they nest. A list element can be another list, a map value another map, so reaching an element means walking a path — and a context is that path. Strings, blobs, and HLLs do not nest. They are the leaves a path arrives at.
So no other expression takes a context, and none needs one. A List or Map expression carrying a context walks the path and returns the element, whatever type it is; that value is then the operand of the String, Bit, or HLL expression. The walking has already happened.
# Pseudocode — not runnable# doc = {"license": "abc-123"}
string_upper( policy, map_get_by_key(STRING, context, VALUE, "license", bin_map("doc")))# "ABC-123"This is why the two APIs differ in shape here. A String operation acts in place on a named bin and cannot take its input from another operation, so to reach a nested string it has to carry the path itself — which is why it takes a context argument. A String expression has no such constraint, because an expression’s operand can be another expression.
Unknown results
An expression evaluates to unknown instead of a value when it cannot produce
one. This happens when:
- The bin it reads is missing from the record.
- The stored value is a different type than the expression expects.
- The value is not yet available, during the metadata-only phase of filter evaluation.
unknown is not confined to boolean expressions. An expression that returns an
integer yields unknown on a missing bin exactly as a comparison does, and the
result propagates up the expression tree. Logic
operators apply trilean rules to it and can still resolve to a definite answer.
In a record filter, an unknown result excludes the record. In an operate
projection, it fails that sub-operation. Individual references note any
additional cause specific to their data type.
Execution model
Metadata resolution is critical for the performance of Aerospike expressions. Since metadata is stored in the primary index and doesn’t require loading data from disk for namespaces with data on disk, expressions that can be entirely processed using only this metadata can avoid disk access.
This ability to forgo disk operations yields a significant performance improvement—an order of magnitude gain. Aerospike expressions achieve this efficiency using a two-phase execution model. When an expression’s logic for a specific operation can be satisfied only with metadata, the system executes it that way, resulting in major speed enhancements.
Phase 1: Metadata check (fast path)
This phase starts by trying to resolve the expression using only the metadata stored in the primary index, which is very fast and avoids disk access.
Storage data is unknown: During this phase, any expression that tries to read the actual stored data evaluates as unknown.
Trilean Logic: Most expressions output unknown if they cannot be evaluated. The value propagates up the expression tree. Logic operators apply trilean rules and can sometimes return a definite answer; notably, or resolves to true when at least one child is true, even if other children are unknown. See also unknown().
Outcomes:
- If the result is
false, the record is immediately filtered out, and no storage is accessed. - If the result is
true, the operation proceeds, but storage is only accessed if absolutely necessary for the ongoing operation. - If the result is
unknown, the system moves to Phase 2.
Phase 2: Storage-data access
This phase is executed only if the first phase failed to produce a definite
true or false answer.
- Data Load: The system loads the full record, incurring physical I/O if the data resides on disk.
- Re-execution: The expression is executed a second time.
- Definitive result: This phase always resolves the expression to a definite
trueorfalseanswer.
This two-phase approach ensures that expressions only proceed to the slower disk-access phase when the outcome absolutely depends on the actual stored data.
Example: metadata first, trilean short-circuit
The since_update() expression reads
milliseconds since the record was last updated from metadata (see
Record metadata). It is a practical predicate
for the two-phase model: unlike matching on set_name, which often duplicates
what the client already chose when issuing the operation. since_update
always reflects the record in storage.
Use a recent window in milliseconds (here, two hours), aligned with the
since_update reference examples.
Conjunction (and)
Require both a recent update and a positive integer bin priority.
Here is how a conjunction (AND) behaves during evaluation:
Scenario A: The record is stale
- Phase 1 (Metadata): The
since_updatecheck returnsfalse. - Short-circuit: Since
false AND (anything)is alwaysfalse, the entire expression fails immediately. - Result: The server skips Phase 2 and does not touch the disk.
Scenario B: The record is recent
- Phase 1 (Metadata): The
since_updatecheck returnstrue. - The conflict: The system doesn’t know the
prioritybin value yet (it’s on disk), so that part is unknown. - Result:
true AND unknownequalsunknown. The system proceeds to Phase 2, loads the record from storage, and re-evaluates to get a final answer.
String recentAndHighPriority = "$.timeSinceLastUpdate() < 2 * 60 * 60 * 1000 and $.priority > 0";recent_and_high_priority = ( "$.timeSinceLastUpdate() < 2 * 60 * 60 * 1000 and $.priority > 0")use aerospike::expressions::{and, gt, int_bin, int_val, lt, since_update};
let recent_ms = 2 * 60 * 60 * 1000;
let recent_and_high_priority = and(vec![ lt(since_update(), int_val(recent_ms)), gt(int_bin("priority".into()), int_val(0)),]);using Aerospike.Client;
long recentMs = 2L * 60 * 60 * 1000;
Expression recentAndHighPriority = Exp.Build( Exp.And( Exp.LT(Exp.SinceUpdate(), Exp.Val(recentMs)), Exp.GT(Exp.IntBin("priority"), Exp.Val(0))));// Requires: import as "github.com/aerospike/aerospike-client-go/v8"recentMs := int64(2 * 60 * 60 * 1000)recentAndHighPriority := as.ExpAnd( as.ExpLess(as.ExpSinceUpdate(), as.ExpIntVal(recentMs)), as.ExpGreater(as.ExpIntBin("priority"), as.ExpIntVal(0)))const Aerospike = await import("aerospike");
const exp = Aerospike.exp
const recentMs = 2 * 60 * 60 * 1000const recentAndHighPriority = exp.and( exp.lt(exp.sinceUpdate(), exp.int(recentMs)), exp.gt(exp.binInt('priority'), exp.int(0)),)as_exp_build(predexp, as_exp_and( as_exp_cmp_lt( as_exp_since_update(), as_exp_int(2 * 60 * 60 * 1000)), as_exp_cmp_gt( as_exp_bin_int("priority"), as_exp_int(0))));import com.aerospike.client.exp.Expression;import com.aerospike.client.exp.Exp;
long recentMs = 2L * 60 * 60 * 1000;
Expression recentAndHighPriority = Exp.build( Exp.and( Exp.lt(Exp.sinceUpdate(), Exp.val(recentMs)), Exp.gt(Exp.intBin("priority"), Exp.val(0))));from aerospike_helpers.expressions import And, GT, IntBin, LT, SinceUpdateTime
recent_ms = 2 * 60 * 60 * 1000
recent_and_high_priority = And( LT(SinceUpdateTime(), recent_ms), GT(IntBin("priority"), 0),).compile()Disjunction (or)
Keep records that are either recently updated or have priority
greater than zero.
Here is how a disjunction (OR) behaves during evaluation:
Scenario A: The record is recent
- Phase 1 (Metadata): The
since_updatecheck returnstrue. - Short-circuit: Since
true OR (anything)is alwaystrue, the entire expression succeeds immediately. - Result: The server skips Phase 2. It has already found a reason to include the record, so it doesn’t need to read the bin data from disk.
Scenario B: The record is not recent
- Phase 1 (Metadata): The
since_updatecheck returnsfalse. - The conflict: The system doesn’t know the
prioritybin value yet (unknown). Sincefalse OR unknownis stillunknown, the system cannot make a final decision. - Result: The system proceeds to Phase 2, loads the record from storage, and checks the actual bin value to reach a definitive
trueorfalse.
String recentOrHighPriority = "$.timeSinceLastUpdate() < 2 * 60 * 60 * 1000 or $.priority > 0";recent_or_high_priority = ( "$.timeSinceLastUpdate() < 2 * 60 * 60 * 1000 or $.priority > 0")use aerospike::expressions::{gt, int_bin, int_val, lt, or, since_update};
let recent_ms = 2 * 60 * 60 * 1000;
let recent_or_high_priority = or(vec![ lt(since_update(), int_val(recent_ms)), gt(int_bin("priority".into()), int_val(0)),]);using Aerospike.Client;
long recentMs = 2L * 60 * 60 * 1000;
Expression recentOrHighPriority = Exp.Build( Exp.Or( Exp.LT(Exp.SinceUpdate(), Exp.Val(recentMs)), Exp.GT(Exp.IntBin("priority"), Exp.Val(0))));// Requires: import as "github.com/aerospike/aerospike-client-go/v8"recentMs := int64(2 * 60 * 60 * 1000)recentOrHighPriority := as.ExpOr( as.ExpLess(as.ExpSinceUpdate(), as.ExpIntVal(recentMs)), as.ExpGreater(as.ExpIntBin("priority"), as.ExpIntVal(0)))const Aerospike = await import("aerospike");
const exp = Aerospike.exp
const recentMs = 2 * 60 * 60 * 1000const recentOrHighPriority = exp.or( exp.lt(exp.sinceUpdate(), exp.int(recentMs)), exp.gt(exp.binInt('priority'), exp.int(0)),)as_exp_build(predexp, as_exp_or( as_exp_cmp_lt( as_exp_since_update(), as_exp_int(2 * 60 * 60 * 1000)), as_exp_cmp_gt( as_exp_bin_int("priority"), as_exp_int(0))));import com.aerospike.client.exp.Expression;import com.aerospike.client.exp.Exp;
long recentMs = 2L * 60 * 60 * 1000;
Expression recentOrHighPriority = Exp.build( Exp.or( Exp.lt(Exp.sinceUpdate(), Exp.val(recentMs)), Exp.gt(Exp.intBin("priority"), Exp.val(0))));from aerospike_helpers.expressions import GT, IntBin, LT, Or, SinceUpdateTime
recent_ms = 2 * 60 * 60 * 1000
recent_or_high_priority = Or( LT(SinceUpdateTime(), recent_ms), GT(IntBin("priority"), 0),).compile()