Skip to content

Overview

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

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

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute() | RecordStream.forEach(...) | RecordStream.close() | RecordResult.recordOrThrow()

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

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.execute()

Boolean values

Use true or false (lowercase):

session.query(users).where("$.verified == true").execute();
session.query(users).where("$.deleted == false").execute();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: DataSet.ids(...) | DataSet.id(...) | Session.upsert(DataSet) | Session.upsert(Key) | Session.query(List) | ChainableOperationBuilder.bin(...) | ChainableQueryBuilder.bin(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.limit(...) | ChainableQueryBuilder.execute() | RecordStream.forEach(...) | RecordStream.close() | RecordResult.recordOrThrow()

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

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.readingOnlyBins(...) | ChainableQueryBuilder.execute() | RecordStream.forEach(...) | RecordStream.close() | RecordResult.recordOrThrow()

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

📖 API reference: ClusterDefinition(String,int) | ClusterDefinition.connect() | Cluster.createSession(Behavior) | Cluster.close() | DataSet.of(...) | DataSet.ids(...) | Session.insert(DataSet) | Session.delete(List) | Session.delete(Key) | Session.query(DataSet) | OperationObjectBuilder.bins(...) | IdValuesBuilder.id(...) | IdValuesRowBuilder.values(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.readingOnlyBins(...) | ChainableQueryBuilder.limit(...) | ChainableQueryBuilder.execute() | ChainableNoBinsBuilder.execute() | RecordStream.forEach(...) | RecordStream.close() | RecordResult.recordOrThrow()

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

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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();

📖 API reference: Session.query(DataSet) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.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?