Overview
Learn how to query Aerospike using the Developer SDK’s Aerospike Expression Language (AEL), a readable, intuitive syntax for filtering records.
What is AEL?
AEL lets you write filter expressions using natural, readable syntax instead of complex builder patterns:
import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;
// AEL: Clean, readable syntaxRecordStream stream = session.query(users) .where("$.status == 'active' and $.age >= 21") .execute();stream.forEach(result -> { Record row = result.recordOrThrow(); // Process row});stream.close();
// Compare to traditional expression builders (more verbose)// Expression filter = Exp.and(// Exp.eq(Exp.stringBin("status"), Exp.val("active")),// Exp.ge(Exp.intBin("age"), Exp.val(21))// );# AEL: Clean, readable syntaxstream = await session.query(users).where( "$.status == 'active' and $.age >= 21").execute()async for row in stream: record = row.record_or_raise() pass # Process recordstream.close()
# Compare to traditional expression builders (more verbose)# filter = Exp.and_([# Exp.eq(Exp.string_bin("status"), val("active")),# Exp.ge(Exp.int_bin("age"), val(21)),# ])AEL parses your string into an optimized filter expression at query time, giving you the best of both worlds: developer ergonomics and Aerospike performance.
Basic syntax
AEL expressions are infix expressions:
left_operand operator right_operand// String comparisonsession.query(users).where("$.status == 'active'").execute();
// Numeric comparisonsession.query(users).where("$.age > 21").execute();
// Boolean checksession.query(users).where("$.verified == true").execute();# String comparisonawait session.query(users).where("$.status == 'active'").execute()
# Numeric comparisonawait session.query(users).where("$.age > 21").execute()
# Boolean checkawait session.query(users).where("$.verified == true").execute()Comparison operators
| Operator | Meaning | Example |
|---|---|---|
== | Equal to | "$.status == 'active'" |
!= | Not equal to | "$.status != 'deleted'" |
> | Greater than | "$.age > 21" |
>= | Greater than or equal | "$.age >= 18" |
< | Less than | "$.score < 100" |
<= | Less than or equal | "$.score <= 50" |
String values
You can use either single or double quotes:
// Single or double quotes are both validsession.query(users).where("$.country == 'USA'").execute();session.query(users).where("$.country == \"USA\"").execute();session.query(users).where("$.email != 'test@example.com'").execute();
// Strings with spacessession.query(users).where("$.city == 'New York'").execute();# Single or double quotes are both validawait session.query(users).where("$.country == 'USA'").execute()await session.query(users).where("$.country == \"USA\"").execute()await session.query(users).where("$.email != 'test@example.com'").execute()
# Strings with spacesawait session.query(users).where("$.city == 'New York'").execute()Numeric values
Use integers or decimals directly:
// Integerssession.query(orders).where("$.quantity > 10").execute();
// Decimalssession.query(products).where("$.price <= 99.99").execute();
// Negative numberssession.query(accounts).where("$.balance >= -100").execute();# Integersawait session.query(orders).where("$.quantity > 10").execute()
# Decimalsawait session.query(products).where("$.price <= 99.99").execute()
# Negative numbersawait session.query(accounts).where("$.balance >= -100").execute()Boolean values
Use true or false (lowercase):
session.query(users).where("$.verified == true").execute();session.query(users).where("$.deleted == false").execute();await session.query(users).where("$.verified == true").execute()await session.query(users).where("$.deleted == false").execute()Logical operators
Combine conditions using and, or, not, and exclusive(...):
| Operator | Meaning | Example |
|---|---|---|
and | Both conditions must be true | "$.age > 21 and $.status == 'active'" |
or | Either condition can be true | "$.role == 'admin' or $.role == 'moderator'" |
not | Negates a condition | "not ($.status == 'deleted')" |
exclusive(...) | Exactly one argument expression evaluates to true | "exclusive($.tier == 1, $.tier == 2, $.tier == 3)" |
Combining conditions
// AND: Both conditions requiredsession.query(users) .where("$.age >= 18 and $.status == 'active'") .execute();
// OR: Either condition matchessession.query(users) .where("$.role == 'admin' or $.role == 'superuser'") .execute();
// NOT: Exclude matchessession.query(users) .where("not ($.status == 'banned')") .execute();
// EXCLUSIVE: exactly one condition can matchsession.query(users) .where("exclusive($.tier == 1, $.tier == 2, $.tier == 3)") .execute();# AND: Both conditions required( session.query(users) .where("$.age >= 18 and $.status == 'active'") .execute())
# OR: Either condition matches( session.query(users) .where("$.role == 'admin' or $.role == 'superuser'") .execute())
# NOT: Exclude matches( session.query(users) .where("not ($.status == 'banned')") .execute())
# EXCLUSIVE: exactly one condition can match( session.query(users) .where("exclusive($.tier == 1, $.tier == 2, $.tier == 3)") .execute())Complex expressions with parentheses
Use parentheses to control evaluation order:
// Without parentheses: AND has higher precedence than OR// "a or b and c" evaluates as "a or (b and c)"
// With parentheses: Explicit groupingsession.query(users) .where("($.role == 'admin' or $.role == 'moderator') and $.verified == true") .execute();
// Multiple levelssession.query(orders) .where("($.status == 'shipped' or $.status == 'delivered') and ($.total > 100 or $.priority == 'high')") .execute();# Without parentheses: AND has higher precedence than OR# "a or b and c" evaluates as "a or (b and c)"
# With parentheses: Explicit grouping( session.query(users) .where("($.role == 'admin' or $.role == 'moderator') and $.verified == true") .execute())
# Multiple levels( session.query(orders) .where("($.status == 'shipped' or $.status == 'delivered') and ($.total > 100 or $.priority == 'high')") .execute())Collection operators
Work with lists and maps using special operators:
| Operator | Use Case | Example |
|---|---|---|
in | Membership test | "'sale' in $.tags" |
count() | Collection size/cardinality | "$.items.count() > 5" |
List membership
Check whether a value is present in a list:
// Find users with the 'premium' tagsession.query(users) .where("'premium' in $.tags") .execute();
// Find orders containing a specific productsession.query(orders) .where("'SKU-12345' in $.product_ids") .execute();# Find users with the 'premium' tag( session.query(users) .where("'premium' in $.tags") .execute())
# Find orders containing a specific product( session.query(orders) .where("'SKU-12345' in $.product_ids") .execute())Collection size
Filter by the size of a list or map:
// Users with more than 5 orderssession.query(users) .where("$.order_history.count() > 5") .execute();
// Products with at least 3 imagessession.query(products) .where("$.images.count() >= 3") .execute();# Users with more than 5 orders( session.query(users) .where("$.order_history.count() > 5") .execute())
# Products with at least 3 images( session.query(products) .where("$.images.count() >= 3") .execute())Working with nested data
Access nested map values using dot notation:
// Given record:// { "address": { "city": "San Francisco", "zip": "94102" } }
// Query nested fieldsession.query(users) .where("$.address.city == 'San Francisco'") .execute();
// Multiple nesting levels// { "profile": { "settings": { "theme": "dark" } } }session.query(users) .where("$.profile.settings.theme == 'dark'") .execute();# Given record:# { "address": { "city": "San Francisco", "zip": "94102" } }
# Query nested field( session.query(users) .where("$.address.city == 'San Francisco'") .execute())
# Multiple nesting levels# { "profile": { "settings": { "theme": "dark" } } }( session.query(users) .where("$.profile.settings.theme == 'dark'") .execute())Combining nested access with other operators
// Nested field with comparisonsession.query(users) .where("$.profile.age >= 21 and $.address.country == 'USA'") .execute();
// Nested list containssession.query(users) .where("'email' in $.preferences.notifications") .execute();# Nested field with comparison( session.query(users) .where("$.profile.age >= 21 and $.address.country == 'USA'") .execute())
# Nested list contains( session.query(users) .where("'email' in $.preferences.notifications") .execute())Existence checks
Use exists() to test whether a bin/path is present:
// Find records where email is not setsession.query(users) .where("not($.email.exists())") .execute();
// Find records where email existssession.query(users) .where("$.email.exists()") .execute();# Find records where email is not set( session.query(users) .where("not($.email.exists())") .execute())
# Find records where email exists( session.query(users) .where("$.email.exists()") .execute())AEL vs filter expressions
The Developer SDK also supports raw filter expressions for advanced use cases:
| Approach | Best For | Example |
|---|---|---|
| AEL | Most queries, readable code | "$.age > 21 and $.status == 'active'" |
| Filter Expressions | Complex logic, programmatic building | Exp.and(Exp.gt(...), Exp.eq(...)) |
When to use filter expressions
Use raw expressions when you need:
- Programmatic expression composition/reuse in application code
- Compatibility with existing expression code
import com.aerospike.client.sdk.exp.Exp;
// AEL (recommended for most cases)session.query(users).where("$.age > 21").execute();
// Raw Exp API (for advanced cases)Exp exp = Exp.gt(Exp.intBin("age"), Exp.val(21));session.query(users).where(exp).execute();from aerospike_sdk import Exp, val
# AEL (recommended for most cases)await session.query(users).where("$.age > 21").execute()
# Raw expression (for advanced cases)filter = Exp.gt(Exp.int_bin("age"), val(21))await session.query(users).where(filter).execute()AEL beyond full-set queries
AEL is not limited to session.query(dataSet) over an entire set. You can apply .where(...) anywhere the fluent API accepts expressions, including batch key reads and conditional writes.
// Batch key read with AEL filtersession.query(users.ids(1, 2, 3, 4)) .where("$.score > 90") .execute();
// Conditional single-record updatesession.upsert(users.id("task-1")) .bin("status").setTo("COMPLETED") .where("$.status == 'PENDING' and $.terminated == false") .execute();# Batch key read with AEL filterawait ( session.query(users.ids(1, 2, 3, 4)) .where("$.score > 90") .execute())
# Conditional single-record updateawait ( session.upsert(users.id("task-1")) .bin("status").set_to("COMPLETED") .where("$.status == 'PENDING' and $.terminated == false") .execute())Performance considerations
Use secondary indexes
AEL expressions are parsed once and compiled before execution, then evaluated on candidate records. For large datasets, create secondary indexes on frequently-filtered bins.
Index choice is transparent to your code: keep the same AEL query, and the SDK/query planner can use an index when available (or scan when none exists).
// Without index: Scans ALL records, applies filter// ⚠️ Slow for large setssession.query(users) .where("$.status == 'active'") // Filter applied during scan .execute();
// With index on 'status': Planner can use the index transparently// ✅ Fast for selective queriessession.query(users) .where("$.status == 'active'") // Index lookup + filter .execute();# Without index: Scans ALL records, applies filter# ⚠️ Slow for large sets( session.query(users) .where("$.status == 'active'") .execute())
# With index on 'status': Planner can use the index transparently# ✅ Fast for selective queries( session.query(users) .where("$.status == 'active'") .execute())Limit results
Always limit results when you don’t need everything:
import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;
// Get only the first 100 matching records (server-side limit)RecordStream stream = session.query(users) .where("$.status == 'active'") .limit(100) .execute();stream.forEach(result -> { Record row = result.recordOrThrow(); // Process row});stream.close();# Get only the first 100 matching records (server-side limit)stream = await session.query(users).where("$.status == 'active'").limit(100).execute()async for row in stream: record = row.record_or_raise() pass # Process recordstream.close()Keep expressions readable
Complex expressions are valid, but readability and maintainability matter:
Hard to maintain:($.a and $.b) or ($.c and $.d) or ($.e and $.f) and not ($.g or $.h)
Prefer clear grouping and intent:($.status == 'active' and $.age > 21) or $.priority == 'high'The primary performance factor is index selectivity and record volume, not whether you wrote the filter as AEL text or an expression builder tree.
Use projections
Request only the bins you need:
import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;
// Only retrieve 'name' and 'email' bins (projection)RecordStream stream = session.query(users) .where("$.status == 'active'") .readingOnlyBins("name", "email") .execute();stream.forEach(result -> { Record row = result.recordOrThrow(); // Process row});stream.close();# Only retrieve 'name' and 'email' bins (projection)stream = await session.query(users).where("$.status == 'active'").bins( ["name", "email"]).execute()async for row in stream: record = row.record_or_raise() pass # record.bins.get("name"), etc.stream.close()Complete example
import com.aerospike.client.sdk.Cluster;import com.aerospike.client.sdk.ClusterDefinition;import com.aerospike.client.sdk.DataSet;import com.aerospike.client.sdk.Record;import com.aerospike.client.sdk.RecordStream;import com.aerospike.client.sdk.Session;import com.aerospike.client.sdk.policy.Behavior;import java.util.concurrent.atomic.AtomicInteger;
public class AELQueryExample { public static void main(String[] args) { try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) { Session session = cluster.createSession(Behavior.DEFAULT); DataSet products = DataSet.of("test", "products"); String p1 = "ael-product-1"; String p2 = "ael-product-2"; String p3 = "ael-product-3"; String p4 = "ael-product-4";
// Seed sample data so the example is repeatable. session.delete(products.ids(p1, p2, p3, p4)).execute().close(); session.insert(products) .bins("name", "category", "price", "rating", "tags", "metadata") .id(p1).values("TV", "electronics", 499.99, 4.6, java.util.List.of("sale"), java.util.Map.of("featured", true)) .id(p2).values("Headphones", "electronics", 39.99, 4.2, java.util.List.of("clearance"), java.util.Map.of("featured", false)) .id(p3).values("Notebook", "office", 12.50, 3.9, java.util.List.of("stationery"), java.util.Map.of("featured", false)) .id(p4).values("Monitor", "electronics", 199.99, 4.8, java.util.List.of("sale", "new"), java.util.Map.of("featured", true)) .execute();
// Simple equality AtomicInteger electronicsCount = new AtomicInteger(0); RecordStream s1 = session.query(products) .where("$.category == 'electronics'") .execute(); s1.forEach(result -> { Record row = result.recordOrThrow(); electronicsCount.incrementAndGet(); }); s1.close();
// Range query AtomicInteger affordableCount = new AtomicInteger(0); RecordStream s2 = session.query(products) .where("$.price >= 10 and $.price <= 50") .execute(); s2.forEach(result -> { Record row = result.recordOrThrow(); affordableCount.incrementAndGet(); }); s2.close();
// Complex filter with nested access AtomicInteger featuredCount = new AtomicInteger(0); RecordStream s3 = session.query(products) .where("$.metadata.featured == true and $.rating >= 4.0") .readingOnlyBins("name", "price", "rating") .limit(10) .execute(); s3.forEach(result -> { Record row = result.recordOrThrow(); featuredCount.incrementAndGet(); }); s3.close();
// List contains AtomicInteger taggedCount = new AtomicInteger(0); RecordStream s4 = session.query(products) .where("'sale' in $.tags or 'clearance' in $.tags") .execute(); s4.forEach(result -> { Record row = result.recordOrThrow(); taggedCount.incrementAndGet(); }); s4.close();
System.out.println("Found " + electronicsCount.get() + " electronics"); System.out.println("Found " + affordableCount.get() + " affordable products"); System.out.println("Found " + featuredCount.get() + " featured products"); System.out.println("Found " + taggedCount.get() + " sale/clearance products");
// Cleanup session.delete(products.ids(p1, p2, p3, p4)).execute().close(); } }}import asyncio
from aerospike_sdk import Behavior, ClusterDefinition, DataSet
async def main(): cluster_def = ClusterDefinition("localhost", 3000) # If your Dockerized server advertises an internal bridge IP, # map it back to localhost for host-based examples. cluster_def = cluster_def.with_ip_map({"172.17.0.2": "127.0.0.1"})
async with await cluster_def.connect() as cluster: session = cluster.create_session(Behavior.DEFAULT) products = DataSet.of("test", "products") p1 = products.id("ael-product-1") p2 = products.id("ael-product-2") p3 = products.id("ael-product-3") p4 = products.id("ael-product-4")
# Seed sample data so the example is repeatable. stream = await session.batch().delete(p1).delete(p2).delete(p3).delete(p4).execute() stream.close() await ( session.batch() .insert(p1).put({ "name": "TV", "category": "electronics", "price": 499.99, "rating": 4.6, "tags": ["sale"], "metadata": {"featured": True}, }) .insert(p2).put({ "name": "Headphones", "category": "electronics", "price": 39.99, "rating": 4.2, "tags": ["clearance"], "metadata": {"featured": False}, }) .insert(p3).put({ "name": "Notebook", "category": "office", "price": 12.50, "rating": 3.9, "tags": ["stationery"], "metadata": {"featured": False}, }) .insert(p4).put({ "name": "Monitor", "category": "electronics", "price": 199.99, "rating": 4.8, "tags": ["sale", "new"], "metadata": {"featured": True}, }) .execute() )
# Simple equality electronics_count = 0 stream = await session.query(products).where("$.category == 'electronics'").execute() async for row in stream: row.record_or_raise() electronics_count += 1 stream.close()
# Range query # Float bins need float literals in AEL — "$.price >= 10" against a # float bin returns no rows because the comparison types don't align. affordable_count = 0 stream = await session.query(products).where("$.price >= 10.0 and $.price <= 50.0").execute() async for row in stream: row.record_or_raise() affordable_count += 1 stream.close()
# Complex filter with nested access featured_count = 0 stream = await session.query(products).where( "$.metadata.featured == true and $.rating >= 4.0" ).bins(["name", "price", "rating"]).limit(10).execute() async for row in stream: row.record_or_raise() featured_count += 1 stream.close()
# List contains tagged_count = 0 stream = await session.query(products).where( "'sale' in $.tags or 'clearance' in $.tags" ).execute() async for row in stream: row.record_or_raise() tagged_count += 1 stream.close()
print(f"Found {electronics_count} electronics") print(f"Found {affordable_count} affordable products") print(f"Found {featured_count} featured products") print(f"Found {tagged_count} sale/clearance products")
# Cleanup stream = await session.batch().delete(p1).delete(p2).delete(p3).delete(p4).execute() stream.close()
asyncio.run(main())AEL quick reference
| Expression | Meaning |
|---|---|
"$.age == 25" | Age equals 25 |
"$.price > 100" | Price greater than 100 |
"$.status != 'deleted'" | Status is not “deleted” |
"$.age >= 18 and $.age <= 65" | Age between 18 and 65 |
"$.role == 'admin' or $.role == 'mod'" | Role is admin or mod |
"not($.banned == true)" | Not banned |
"'vip' in $.tags" | Tags list contains “vip” |
"$.items.count() > 0" | Items list is not empty |
"$.address.city == 'NYC'" | Nested city equals NYC |
"$.email.exists()" | Email is set |
"let (total = $.price * $.qty) then (${total} > 1000)" | Variable binding with let |
"when ($.tier == 1 => 'gold', default => 'other')" | Conditional with when |
More AEL capabilities
This page focuses on common query filters, but AEL also supports more expressive constructs from the Java AEL reference and post-preview language design notes.
Variable binding with let (...) then (...)
Use let to define reusable intermediate values so long expressions stay readable.
// Reuse intermediate calculations in a single predicate.session.query(orders) .where("let (subtotal = $.price * $.qty, tax = ${subtotal} * 0.1) then (${subtotal} + ${tax} > 1000)") .execute();# Reuse intermediate calculations in a single predicate.await ( session.query(orders) .where("let (subtotal = $.price * $.qty, tax = ${subtotal} * 0.1) then (${subtotal} + ${tax} > 1000)") .execute())Conditional logic with when (..., default => ...)
Use when for inline branching inside an expression.
// Compare a bin against a conditionally-computed value.session.query(users) .where("$.badge == (when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze'))") .execute();# Compare a bin against a conditionally-computed value.await ( session.query(users) .where("$.badge == (when ($.tier == 1 => 'gold', $.tier == 2 => 'silver', default => 'bronze'))") .execute())String and map literals
String literals use single quotes. In map literals, keys and string values should also be quoted.
session.query(users) .where("$.name == 'Tim' and $.labels == {'role': 'user', 'tier': 2}") .execute();await ( session.query(users) .where("$.name == 'Tim' and $.labels == {'role': 'user', 'tier': 2}") .execute())Arithmetic and bitwise operators
AEL supports arithmetic expressions (+, -, *, /) and integer bitwise operators (&, |, ^, <<, >>, >>>) in predicates.
// Arithmetic comparison.session.query(items) .where("($.unitPrice * $.quantity) - $.discount > 500") .execute();
// Bitmask check: confirm enabled flag bit is set.session.query(items) .where("(($.flags >> 2) & 1) == 1") .execute();# Arithmetic comparison.await ( session.query(items) .where("($.unitPrice * $.quantity) - $.discount > 500") .execute())
# Bitmask check: confirm enabled flag bit is set.await ( session.query(items) .where("(($.flags >> 2) & 1) == 1") .execute())Record metadata functions
You can filter by record metadata such as remaining TTL, on-device size, and tombstone status.
session.query(events) .where("$.ttl() < 3600 and $.recordSize() > 1024") .execute();
session.query(events) .where("$.isTombstone() == true") .execute();await ( session.query(events) .where("$.ttl() < 3600 and $.recordSize() > 1024") .execute())
await ( session.query(events) .where("$.isTombstone() == true") .execute())Richer CDT path reads and typed returns
Beyond simple dot navigation, AEL design work includes deeper path forms and typed getters, so complex nested list/map content can be filtered more precisely.
// Example of typed path access used inside a predicate.session.query(profiles) .where("$.prefs.theme.get(type: STRING) == 'dark' and $.prefs.flags.get(type: INT) > 0") .execute();# Example of typed path access used inside a predicate.await ( session.query(profiles) .where("$.prefs.theme.get(type: STRING) == 'dark' and $.prefs.flags.get(type: INT) > 0") .execute())Post-preview roadmap examples
The post-preview AEL roadmap adds additional language areas. The examples below are syntax previews from the reference docs, not guaranteed to be available in every SDK release today.
// Regex filtering (ICU syntax).session.query(users).where("$.email =~ /@example\\.com$/i").execute();
// Deeper CDT path filtering/mutation forms.session.query(catalog).where("$.listbin.[=a:z].remove(return: NONE)").execute();session.query(catalog).where("$.mapbin.{a,c}.get(return: VALUE).[].count() == 2").execute();
// Method-style function families (string / blob-bit / HLL / geo).session.query(metrics).where("$.name.uppercase() == 'CPU'").execute();session.query(metrics).where("$.blob.bitGet(offset: 0, size: 8) > 0").execute();session.query(metrics).where("$.hll.hllCount() > 100").execute();session.query(metrics).where("geoCompare($.region, geoJson('{\"type\":\"Point\",\"coordinates\":[-122.4,37.8]}'))").execute();# Regex filtering (ICU syntax).await session.query(users).where("$.email =~ /@example\\.com$/i").execute()
# Deeper CDT path filtering/mutation forms.await session.query(catalog).where("$.listbin.[=a:z].remove(return: NONE)").execute()await session.query(catalog).where("$.mapbin.{a,c}.get(return: VALUE).[].count() == 2").execute()
# Method-style function families (string / blob-bit / HLL / geo).await session.query(metrics).where("$.name.uppercase() == 'CPU'").execute()await session.query(metrics).where("$.blob.bitGet(offset: 0, size: 8) > 0").execute()await session.query(metrics).where("$.hll.hllCount() > 100").execute()await session.query(metrics).where("geoCompare($.region, geoJson('{\"type\":\"Point\",\"coordinates\":[-122.4,37.8]}'))").execute()For full details, see:
Next steps
Query Records
Explore pagination, sorting, and more query options.
Data Model
Understand the data structures AEL filters operate on.
Error Handling
Handle query errors gracefully.
Behaviors
Configure query timeouts and retries.