Skip to content

Overview

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

Concept overview: write readable Aerospike Expression Language (AEL) filter strings for queries, batch reads, and conditional writes in the Java and Python Developer SDKs.

Applies to

  • Aerospike Developer SDKs (Java 21+ and Python 3.10+)
  • Aerospike Database 8.2.0.0 or later (AEL filter APIs with .where(...); AEL is compiled and parsed on the server, so there is no client-side AEL parser)

Audience

Application developers new to AEL or migrating from expression builders (Beginner to Intermediate).

Prerequisites

Outcome

You can read and write common AEL filter expressions (comparisons, logic, collections, nested paths) and know when to use indexes and projections.

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 lets you write a query’s filter as a string. On Database 8.2.0 and later, the query optimizer reads that AEL and uses it to select a readable secondary index, then the server evaluates the filter on the records that index returns.

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.0 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. Pin the container type with :LIST or :MAP"$.items:LIST.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:LIST.count() > 5")
.execute();
// Products with at least 3 images
session.query(products)
.where("$.images:LIST.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 expressions

The Developer SDK also supports raw expressions for advanced use cases. Canonical AEL grammar is documented in AEL reference.

ApproachBest ForExample
AELMost queries, readable code"$.age > 21 and $.status == 'active'"
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("user-1", "user-2", "user-3", "user-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 evaluated on candidate records. For large datasets, create secondary indexes on frequently-filtered bins.

The query optimizer reads your AEL and selects a readable secondary index (SI) that can serve it.

import com.aerospike.client.sdk.Record;
import com.aerospike.client.sdk.RecordStream;
// With an index on 'status': the planner uses the index for the lookup,
// then evaluates the remaining condition as a filter.
try (RecordStream stream = session.query(users)
.where("$.status == 'active' and $.tier == 'gold'")
.execute()) {
stream.forEach(result -> {
Record row = result.recordOrThrow();
// Process row
});
}

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

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.0 and $.price <= 50.0")
.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.0"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:LIST.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.0)"Variable binding with let
"when ($.tier == 1 => 'gold', default => 'other')"Conditional with when

Operation expressions (read and write)

Requires Aerospike Database 8.2.0.0 or later.

Beyond filters, pass AEL text for read projection (.bin("out").selectFrom("...") / .select_from(...)) and write-side compute (upsertFrom, insertFrom, updateFrom). Write expressions apply to single-key and batch operate chains, not to query projection, which is read-only. This 8.2.0.0-or-later requirement applies the same way across SDKs. For more information, see:

Advanced AEL syntax

This overview covers common query filters and day-to-day patterns. For filter and operation-expression authoring on single-key, batch, and query commands, see Author AEL expressions. For variable binding (let), conditional when, CDT path patterns, and the full grammar, see AEL reference.

API reference summary

MethodDescriptionAPI reference
.where(...)Attach a Boolean AEL filter to a query or write chainJava | Python
.limit(...)Cap the number of matching records returnedJava | Python
.readingOnlyBins(...) / .bins(...)Project specific bins in query resultsJava | Python

Next steps

Author AEL expressions

Filters, read projection, and write expressions on single-key, batch, and query commands.

Author AEL expressions →

AEL reference

Canonical grammar, CDT paths, metadata functions, and let / when syntax.

AEL reference →

Query optimizer

Learn how the server chooses secondary indexes for AEL queries.

Query optimizer →

Query records

Explore pagination, sorting, and more query options.

Query records →

Data model

Understand the data structures AEL filters operate on.

Data model →

Debug AEL expressions

Parse errors, source coordinates, and filter-decision traces.

Debug AEL expressions →

Error handling

Handle query errors gracefully.

Error handling →

Behaviors

Configure query timeouts and retries.

Behaviors →