Aerospike Expression Language (AEL)¶
AEL lets you write Aerospike filter expressions as strings.
Pass an AEL string to .where() on any query or write builder.
stream = await session.query(users).where("$.age > 18").execute()
The examples on this page run fastest when a secondary index covers the bin
being filtered. A .where() query that no index can satisfy falls back to a
full-set scan; see Secondary Indexes to create one or to reject
the primary-index fallback.
How string AEL is executed¶
The SDK does not parse AEL strings locally. String AEL is sent to the
server for compilation (field 43 via
FilterExpression.from_server_compiled_ael) when every node can compile it.
The check is re-derived from the cluster’s node list once per tend interval, so a node joining with a build older than 8.2.0 (during a rolling upgrade, for example) closes the capability within one tend rather than at the next reconnect, and it reopens once that node leaves.
Dataset queries on clusters that also support query selection (field 44)
use server-led index selection: the server explains the AEL string, picks an
index/plan, and the SDK executes with that plan. Use
QueryHint to influence index choice.
If a cluster mid-upgrade may still include an older node, guard string AEL and
fall back to the programmatic Exp builder:
async with ClusterDefinition("localhost", 3000).connect() as cluster:
if cluster.supports_ael():
stream = await session.query(users).where("$.age > 18").execute()
else:
from aerospike_sdk import Exp
stream = await (
session.query(users)
.where(Exp.gt(Exp.int_bin("age"), Exp.int_val(18)))
.execute()
)
Unguarded, string AEL against a cluster with such a node raises AerospikeError with
ResultCode.OP_NOT_APPLICABLE:
from aerospike_sdk import ResultCode
from aerospike_sdk.exceptions import AerospikeError
try:
stream = await session.query(users).where("$.age > 18").execute()
except AerospikeError as exc:
if exc.result_code == ResultCode.OP_NOT_APPLICABLE:
... # fall back to Exp
Syntax Reference¶
The grammar below describes valid AEL text accepted by the server compiler. Invalid syntax or reserved names are rejected at query time on the server, not by a local parser.
Bin Access¶
Prefix bin names with $:
$.age
$.name
$.settings
Bin names accept a wider character set than plain identifiers:
$.name@host # @ is permitted in bin names
$.@attr # leading or trailing @
$."my-bin" # quoting allows otherwise-illegal characters (-, space, $, ...)
$.'my bin' # single quotes work too
$.true # reserved keywords are valid bin names
$.when # so are keywords like 'when', 'and', 'or', 'let', etc.
The substring null (case-insensitive) is reserved in bin names: $.null,
$.my_null_bin, and $."NULL" are invalid.
Every bin reference needs a resolvable type¶
A bin carries no type the expression can see, so the server has to work one out
before it can read the bin. It resolves the type from either an explicit pin
or a literal somewhere in the expression — and rejects the expression with
PARAMETER_ERROR when it has neither. One literal is enough for every bin in
the expression:
session.query(users).where("$.age == 25") # the literal 25 resolves $.age
session.query(users).bin("n").select_from("$.age:INT") # pinned
session.query(users).bin("n").select_from("$.age + 0") # the literal 0 resolves it
session.query(users).bin("n").select_from("$.age + $.score + 0") # resolves both
session.query(users).bin("n").select_from("$.age") # PARAMETER_ERROR
session.query(users).where("$.age > $.score") # PARAMETER_ERROR
This is one rule, not a difference between where() and select_from(): the
last two fail because nothing in them names a type, and pinning either operand
($.age:INT > $.score) makes both legal. A method call resolves its receiver
the same way — $.rate.toInt() is rejected, $.rate:FLOAT.toInt() is not.
Failures here arrive as a bare PARAMETER_ERROR. Raise
error_detail_verbosity to ErrorDetailVerbosity.MESSAGE on the session’s
Behavior to have the server’s own explanation returned with the error.
Comparison Operators¶
$.age == 30
$.age != 30
$.age > 18
$.age >= 18
$.age < 65
$.age <= 65
Logical Operators¶
$.age > 18 and $.status == "active"
$.role == "admin" or $.role == "superadmin"
not($.deleted:BOOL)
exclusive($.role == "admin", $.suspended:BOOL)
not is a call: not($.age == 30) parses, a bare not $.age == 30 does not.
exclusive is true when exactly one of its two or more arguments is true;
Exp.exclusive is the builder form.
Arithmetic¶
$.price * $.quantity > 1000
$.score + $.bonus >= 100
$.total - $.discount > 0
$.value % 2 == 0
$.rate:FLOAT ** 2.0 > 100.0
** takes FLOAT operands only; pin the bin and write the exponent as a float.
Arithmetic functions:
abs($.balance) > 100
ceil($.rating)
floor($.rating)
max($.a, $.b) > 10
min($.a, $.b) < 0
log(value: $.rating:FLOAT, base: 2.0)
pow(base: $.rating:FLOAT, exponent: 2.0)
log and pow take named arguments only.
Bitwise Operators¶
$.flags & 0xFF
$.mask | 0x01
$.value ^ 0xAA
~$.mask
$.bits << 4
$.bits >> 2
$.bits >>> 2
Type Casting¶
$.count:INT.toFloat() > 3.14
$.rating:FLOAT.toInt()
$.numstr:STRING.toInt()
(5).toFloat()
A cast needs its receiver’s type, so pin the bin; a numeric literal receiver goes in parentheses.
String Values¶
Use double or single quotes:
$.name == "Alice"
$.name == 'Alice'
Embed dynamic values either with an f-string or by passing params to
where(), which interpolates them with printf syntax:
min_age = 18
stream = await session.query(users).where(f"$.age > {min_age}").execute()
stream = await session.query(users).where("$.age > %d", min_age).execute()
The printf form uses standard printf template syntax. Both forms are plain
interpolation — neither quotes nor escapes the value, so never pass untrusted
input. When the value is not trusted, use the Exp builder, which never
round-trips through text.
Two things to know about the printf form. Booleans are lowered to AEL’s
true / false rather than Python’s True. And AEL’s % (modulo) operator
must be written %% whenever you pass params, since the template is a format
string only then:
session.query(users).where("$.id % 100 == 0") # no params, plain %
session.query(users).where("$.id %% 100 == 0 and $.age > %d", min_age)
String Methods¶
String methods chain onto a string-typed receiver, and work in where() and
select_from() alike:
$.name:STRING.upper() == 'ALICE'
$.name:STRING.startsWith('Al')
$.email:STRING.endsWith('@aerospike.com')
$.name:STRING.contains(needle: 'lic')
$.name:STRING.strlen() > 3
$.padded:STRING.trim()
$.name:STRING.replace(find: 'A', replace: 'a')
$.name:STRING =~ /^al/i
The =~ operator matches a regular expression; flags such as i follow the
closing slash. Whether an argument is named is part of each method’s signature,
not a matter of how many it takes: contains(needle: 'lic') must name its one
argument, while startsWith('Al') and repeat(2) must not, and the server
rejects the other form. For the builder-side equivalents, including string
writes, see String Operations.
List Membership (IN)¶
$.status in ["active", "pending", "review"]
"gold" in $.tiers
CDT Paths¶
Access nested data with bracket notation:
$.settings.theme == "dark"
$.scores.[0] > 90
$.matrix.[0].[1] == 42
$.users.alice.age > 30
Map keys can be typed at parse time:
$.bin.42 == 100 # integer map key (decimal)
$.bin.0xff == 100 # integer map key (hex)
$.bin.0b101 == 100 # integer map key (binary)
$.bin.-3 == 100 # negative integer map key
$.bin."42" == "x" # string map key (quoting forces string type)
A digit-only segment after the dot ($.bin.42) becomes an integer map key;
quote it ($.bin."42") to force string interpretation. The two compile to
distinct expressions and match different keys at runtime.
Selecting several elements at once returns a list, so these read naturally in
select_from() or under .count():
$.m:MAP.{@alpha,gamma} # values for a key list
$.m:MAP.{@alpha:gamma} # values for a key range (end-exclusive)
$.l:LIST.[0:2] # values for an index range
$.l:LIST.[#-1]:INT # element by rank (highest)
See Path Expressions for wildcards, filters and writes.
CDT Functions¶
$.scores:LIST.count() > 5
$.tags:LIST.count() == 0
count() needs to know whether the bin is a list or a map, so pin it.
GeoJSON¶
Compare a GeoJSON bin to a literal value with geoCompare(a, b). Either side
can be a bin path or a geoJson('...') literal — pick whichever reads more
naturally. The match semantics are server-side GEO2DSPHERE: a Point matches
any AeroCircle or Polygon containing it, and vice versa.
geoCompare($.loc, geoJson('{"type":"Point","coordinates":[-122.349,47.620]}'))
geoCompare(geoJson('{"type":"AeroCircle","coordinates":[[-122.0,37.4],3000.0]}'), $.loc)
Bins typed as GEO are recognized automatically when referenced inside
geoCompare(...); an explicit cast like $.loc.get(type: GEO) is accepted
but not required.
HyperLogLog¶
Seven read-side HLL path functions are available on HLL bins. Each operates on
$.binName as the receiver:
$.h.hllCount() > 1000000
$.h.hllDescribe() == [14, 0]
$.h.hllMayContain(['alice', 'bob']) == 1
$.h.hllUnionCount($.a) > 50000
$.h.hllIntersectCount($.a) > 100
$.h.hllSimilarity($.a) >= 0.8
$.h.hllUnion($.a) == x'00040c00...'
hllDescribe() returns a two-element list [index_bit_count, min_hash_bit_count];
the server reports 0 for a sketch without minhash (the -1 sentinel used
internally to mean “inherit / no minhash” is normalized away on the wire).
The multi-sketch functions (hllUnion, hllUnionCount, hllIntersectCount,
hllSimilarity) take their multi-sketch argument in one of two shapes:
A single HLL bin reference —
$.a. The server treats a bare HLL value as an implicit single-element list, so$.h.hllUnionCount($.a)evaluates cleanly.A list-typed expression of HLL byte blobs — an inline literal list of AEL blob literals,
[x'00040c00...', x'00040c00...']. Build these in Python withsketch.hex()and interpolate them into the template.
[$.a, $.b] (a list literal containing bin references) is not supported
— the server’s HLL ops can’t recursively evaluate scalar bin sub-expressions
inside a composed list. If you need to combine multiple bins in one
expression without pre-fetching, drop down to the programmatic Exp.* API
or open multiple bin-pair queries.
Create and update sketches with the builder API
(session.upsert(key).bin("h").hll_init(HllConfig.of(14)), .hll_add(...));
the server compiler does not accept hllInit.
Hex and Binary Literals¶
$.flags == 0xFF
$.mask == 0b10101010
Variables (let/then)¶
Bind intermediate values:
let (total = $.price * $.qty) then (${total} > 1000)
Bind in let (...), then refer to each variable as ${name} inside then (...).
Unknown and Error¶
The unknown and error keywords compile to a sentinel that the server
treats as an evaluator-unknown result — useful as a when action when no
sensible value can be returned:
when ($.role == "admin" => $.tier:STRING, default => unknown)
error is an alias for unknown and produces the same expression. Both
short-circuit any enclosing comparison or logical operator.
Query hints and index selection¶
On clusters with query selection (field 44), the server picks the secondary
index and query plan from the AEL string. Influence that choice with
QueryHint:
from aerospike_sdk import QueryHint
stream = await (
session.query(users)
.where("$.age > 25 and $.city == 'NYC'")
.with_hint(QueryHint(index_name="age_idx"))
.execute()
)
See the Secondary Indexes guide for creating indexes and listing
them with session.list_indexes().
Programmatic Expressions¶
For cases where a string AEL expression is insufficient, use the Exp builder
(Exp is Aerospike’s expression type, re-exported from aerospike_sdk):
from aerospike_sdk import Exp
expr = Exp.and_([
Exp.gt(Exp.int_bin("age"), Exp.int_val(18)),
Exp.eq(Exp.string_bin("status"), Exp.string_val("active")),
])
stream = await session.query(users).where(expr).execute()
Use Exp on all clusters; use string AEL when supports_ael() is true.
Path Expressions¶
A path walks into a collection bin, fans out over its elements with *, and
narrows them with a filter [?(...)]. Inside the filter, @ is the current
element’s value, @key its map key and @index its list index. Paths work in
all three AEL entry points:
# select_from: project the matching elements, or a count of them
stream = await (
session.query(key).bin("big").select_from("$.l:LIST.*[?(@:INT > 200)]").execute()
)
# where: filter records on a path
stream = await (
session.query(users)
.where("$.tags:LIST.*[?(@.upper() == 'VIP')].count() > 0")
.execute()
)
# upsert_from: write a modified copy of the collection into another bin
await (
session.upsert(key)
.bin("bumped").upsert_from("$.l:LIST.*[?(@:INT > 200)].modify(@:INT + 1)")
.execute()
)
A path write never changes the bin it reads: upsert_from stores the modified
collection in the target bin (bumped above) and leaves $.l as it was. The
write methods are append(...), appendItems([...]), setTo(...) on a
selected element, and modify(...) on filtered elements; flags follow a colon,
as in appendItems([600, 700]):ADD_UNIQUE.
A loop variable carries no type of its own, so pin it before a cast or
arithmetic: (@:INT).toString() and @:INT + 1 compile and evaluate. Before a
method call the pinned variable must be parenthesized; unlike the bin form
$.rate:FLOAT.toInt(), @:INT.toString() is a syntax error. An
unpinned @.toString() parses but fails at evaluation with
ResultCode.OP_NOT_APPLICABLE. Methods that only make sense on one type, such
as @key.upper(), need no pin.
Building paths programmatically¶
The same walks are available as values, for use with Exp or when the path is
assembled at runtime: select_by_path / modify_by_path, the SelectFlags
and ModifyFlags enums, CTX.all_children() /
CTX.all_children_with_filter(), and the loop-variable family
(Exp.int_loop_var, .string_loop_var, .map_loop_var, etc.):
from aerospike_sdk import (
CTX,
CdtOperation,
Exp,
LoopVarPart,
ModifyFlags,
SelectFlags,
)
in_stock = Exp.eq(
Exp.map_loop_var(LoopVarPart.VALUE),
Exp.bool_val(True),
)
op = CdtOperation.select_by_path(
"store",
SelectFlags.VALUE,
[CTX.map_key("books"), CTX.all_children_with_filter(in_stock)],
)
Path expressions can also remove the elements they match. The dedicated
factory is CdtOperation.remove(bin, ctx) — equivalent to
CdtOperation.modify_by_path with an Exp.remove_result() modify expression —
and Exp.exp_remove() is the expression-level counterpart:
over_5 = Exp.gt(Exp.int_loop_var(LoopVarPart.VALUE), Exp.val(5))
op = CdtOperation.remove("nums", [CTX.all_children_with_filter(over_5)])
examples/cdt_path_expression_example.py in the repository runs this removal
end to end. Mind the name collision: the chainable
.on_map_key(...).remove() is the older, unrelated CDT
removal — only the path-based CdtOperation.remove takes a CTX path with
filters.