Skip to content

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 syntax
RecordStream 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 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 comparison
session.query(users).where("$.status == 'active'").execute();
// Numeric comparison
session.query(users).where("$.age > 21").execute();
// Boolean check
session.query(users).where("$.verified == true").execute();

Comparison operators

OperatorMeaningExample
==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 valid
session.query(users).where("$.country == 'USA'").execute();
session.query(users).where("$.country == \"USA\"").execute();
session.query(users).where("$.email != 'test@example.com'").execute();
// Strings with spaces
session.query(users).where("$.city == 'New York'").execute();

Numeric values

Use integers or decimals directly:

// Integers
session.query(orders).where("$.quantity > 10").execute();
// Decimals
session.query(products).where("$.price <= 99.99").execute();
// Negative numbers
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();

Logical operators

Combine conditions using and, or, not, and exclusive(...):

OperatorMeaningExample
andBoth conditions must be true"$.age > 21 and $.status == 'active'"
orEither condition can be true"$.role == 'admin' or $.role == 'moderator'"
notNegates 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 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 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:

OperatorUse CaseExample
inMembership 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' 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 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 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 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 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:

ApproachBest ForExample
AELMost queries, readable code"$.age > 21 and $.status == 'active'"
Filter ExpressionsComplex logic, programmatic buildingExp.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();

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 filter
session.query(users.ids(1, 2, 3, 4))
.where("$.score > 90")
.execute();
// Conditional single-record update
session.upsert(users.id("task-1"))
.bin("status").setTo("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 sets
session.query(users)
.where("$.status == 'active'") // Filter applied during scan
.execute();
// With index on 'status': Planner can use the index transparently
// ✅ Fast for selective queries
session.query(users)
.where("$.status == 'active'") // Index lookup + filter
.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();

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();

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();
}
}
}

AEL quick reference

ExpressionMeaning
"$.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();

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();

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();

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();

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();

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();

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();

For full details, see:

Next steps

Query Records

Explore pagination, sorting, and more query options.

Query Records →

Data Model

Understand the data structures AEL filters operate on.

Data Model →

Feedback

Was this page helpful?

What type of feedback are you giving?

What would you like us to know?

+Capture screenshot

Can we reach out to you?