Expressions
Aerospike expressions are a specialized, strictly typed, functional language. They are designed specifically for manipulating and comparing data fields (bins) and record metadata.
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 operations: Determine if a record operation (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.
For working with nested collection data, see:
- Querying collection data types - 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
Operation expressions are bin operations that atomically compute a value from data already in the record or supplied by the expression itself.
- 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.
Filter XDR records with expressions
With Aerospike, 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.
Record filtering with expressions
Record filtering selects only the records that satisfy a boolean expression, meaning the expression must evaluate to true.
Example: read policy filter
You attach a compiled boolean expression to the read policy (for example filterExp in the Java and C# clients, FilterExpression in Go, base.filter_exp in C, the expressions entry in the Python policy dict, or filterExpression on a Node.js ReadPolicy). The server evaluates it when the record exists; see Execution timing below. For single-record commands, batched reads, and queries, see your language’s guide (for example Expressions - Java).
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)#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;
/* aerospike_key_get(&as, &err, &policy, &key, &rec); *//* as_exp_destroy(read_filter); */// Requires: import aerospike "github.com/aerospike/aerospike-client-go/v6"policy := aerospike.NewPolicy()policy.FilterExpression = aerospike.ExpGreater( aerospike.ExpIntBin("age"), aerospike.ExpIntVal(18))
record, err := client.Get(policy, key)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);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)Supported functions
Record filter expressions are powerful because they support a wide range of functions, including:
- A variety of metadata functions.
- All applicable data type functions, such as:
- The full List and Map APIs (even in a nested context).
- Bitwise functions for binary data (blobs).
- Geo-spatial queries for GeoJSON data.
- HyperLogLog (HLL) functions.
You can use filters with the following commands:
Syntax and behavior
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 operation expects; for example, a comparison expression requires both operands to be the same type. See the client API reference for your language for the specific type signatures.
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 their input is unknown, except for logical expressions which use trilean logic (true, false, or unknown) and can sometimes return a definite answer.
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 true or false answer.
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.
The description above applies to record filter evaluation (queries, scans, batch reads, policies that take a filter). Operation and XDR expressions follow the same trilean rules; see each feature’s documentation for how results are applied.
Example: metadata first, trilean short-circuit
since_update() 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.
- If the record is stale (not updated within the window),
since_updateis at least the window length, solt(since_update, window)isfalsein phase 1. The wholeandisfalse—the server does not read binpriorityfrom storage. - If the record is recent, that conjunct is
truein phase 1, butintBin("priority")is unknown there. The conjunction is unknown, so phase 2 loads the record and re-evaluates.
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()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))));// Requires: import as "github.com/aerospike/aerospike-client-go/v6"recentMs := int64(2 * 60 * 60 * 1000)_ = as.ExpAnd( as.ExpLess(as.ExpSinceUpdate(), as.ExpIntVal(recentMs)), as.ExpGreater(as.ExpIntBin("priority"), as.ExpIntVal(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))));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)),)Disjunction (or)
Keep records that are either recently updated or have priority greater than zero.
- If the record is recent, the first disjunct is
truein phase 1, so the wholeoristruewithout readingpriority. - If the record is stale, the first disjunct is
falsein phase 1 and the second is unknown until bin data is available—so the filter may proceed to phase 2.
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()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))));// Requires: import as "github.com/aerospike/aerospike-client-go/v6"recentMs := int64(2 * 60 * 60 * 1000)_ = as.ExpOr( as.ExpLess(as.ExpSinceUpdate(), as.ExpIntVal(recentMs)), as.ExpGreater(as.ExpIntBin("priority"), as.ExpIntVal(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))));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)),)For query tuning (ordering metadata predicates to skip storage I/O), see Two-phase execution for filter expressions.