Skip to content

Secondary indexes

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

This page describes how to create and query secondary indexes with the Java and Python Developer SDKs, including index types, AEL predicates, complete examples, and performance considerations. For server-side query types, index filters, and the query optimizer, see Secondary index queries.

What is a secondary index?

By default, Aerospike retrieves records by their primary key (digest). A secondary index (SI) lets you query records by the value of a specific bin.

Query typeBehavior
Single primary key lookupkey → record (O(1), always fast)
Multiple primary key lookupskeys → records (done in parallel across servers, usually fast)
Secondary index querybin value → records (requires index)
Primary index query(queries all records in parallel, O(N), can be slow)

Index types

TypeBin data typeQuery operations
IndexType.STRING (Java) / .string() (Python)StringEquality (==)
IndexType.INTEGER (Java) / .integer() (Python)IntegerEquality and range comparisons (<, <=, >, >=)
IndexType.GEO2DSPHERE (Java) / .geo2dsphere() (Python)GeoJSONWithin radius, within polygon (geoCompare in AEL)

When to use secondary indexes

✅ Good use cases:

  • Filtering by status, category, or type fields
  • Range queries on timestamps or numeric IDs
  • Geospatial queries (find nearby)

❌ Avoid when:

  • High cardinality (millions of unique values)
  • Zero or one result will be returned (for example, an alternate ID for a record)
  • Frequently updated bins
  • Can use primary key lookup instead

Creating an index

Note: Creating indexes in code in production environments is often an anti-pattern due to resource consumption. Use tools like asadm to manage indexes in higher environments instead.

import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.query.IndexCollectionType;
import com.aerospike.client.sdk.query.IndexType;
DataSet users = DataSet.of("test", "users");
// Create an integer index on the "age" bin
session.createIndex(users, "age_idx", "age", IndexType.INTEGER, IndexCollectionType.DEFAULT)
.waitTillComplete();
// Create a string index on the "status" bin
session.createIndex(users, "status_idx", "status", IndexType.STRING, IndexCollectionType.DEFAULT)
.waitTillComplete();
// Create a GEO2DSPHERE index on a GeoJSON bin
DataSet places = DataSet.of("test", "places");
session.createIndex(places, "places_loc_idx", "loc", IndexType.GEO2DSPHERE, IndexCollectionType.DEFAULT)
.waitTillComplete();

📖 API reference: DataSet.of(...) | Session.createIndex(...)

Indexing list and map elements (collection indexes)

The indexes in the previous section are all DEFAULT-type (scalar) indexes that index a bin’s value as a single, opaque unit. A DEFAULT index created on a list or map bin builds without error, but it does not index the collection’s individual elements. It can’t serve a query like “find records where this list contains X” or “find records where this map has key Y”.

To index each element of a list, or each key or value of a map, create a collection index by passing a collection type alongside the scalar index type:

Collection typeIndexesUse for
LISTEach element of a list bin”list contains value X”
MAPKEYS (Java) / MAP_KEYS (Python)Each key of a map bin”map has key Y”
MAPVALUES (Java) / MAP_VALUES (Python)Each value of a map bin”map has a value equal to Z”
DEFAULTThe bin’s value as a whole (default if omitted)Scalar bins (string/integer/GeoJSON)
import com.aerospike.client.sdk.DataSet;
import com.aerospike.client.sdk.query.IndexCollectionType;
import com.aerospike.client.sdk.query.IndexType;
DataSet events = DataSet.of("test", "events");
// Index each element of the "tags" list bin (not the list as a whole).
session.createIndex(events, "events_tags_idx", "tags", IndexType.STRING, IndexCollectionType.LIST)
.waitTillComplete();

📖 API reference: Session.createIndex(...)

To index a nested list or map inside another map (for example, a list stored under a map key), add a CDT context path describing where the nested collection lives. Pass it as the trailing CTX... argument to createIndex (Java) or using .context([CTX...]) on the index builder (Python).

Querying with indexes

Once an index exists, the query optimizer can select it: it reads your AEL and picks a readable secondary index (SI) that can serve the filter. If no readable index matches, the query runs as a primary index (PI) query and reads every record in the set, because allowScansWithWhere (allow_scans_with_where in Python) is true by default. Set it to false to make such a query fail with AS_ERR_SINDEX_NOT_FOUND (201) instead. Always verify that indexes exist and are RW before production. See Query optimizer.

import com.aerospike.client.sdk.Record;
import com.aerospike.client.sdk.RecordStream;
// Server optimizer may select age_idx for $.age > 21
try (RecordStream stream = session.query(users)
.where("$.age > 21")
.execute()) {
stream.forEach(result -> {
Record row = result.recordOrThrow();
// Process row (for example, row.getString("name"))
});
}

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

Complete example

import com.aerospike.client.sdk.AerospikeException;
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 com.aerospike.client.sdk.query.IndexCollectionType;
import com.aerospike.client.sdk.query.IndexType;
public class SecondaryIndexExample {
public static void main(String[] args) {
try (Cluster cluster = new ClusterDefinition("localhost", 3000).connect()) {
Session session = cluster.createSession(Behavior.DEFAULT);
DataSet users = DataSet.of("test", "users");
String k1 = "sidx-example-1";
String k2 = "sidx-example-2";
String k3 = "sidx-example-3";
String ageIndex = "age_idx_demo";
String statusIndex = "status_idx_demo";
// Cleanup so the example is repeatable.
session.delete(users.ids(k1, k2, k3)).execute().close();
try {
session.dropIndex(users, ageIndex).waitTillComplete();
} catch (AerospikeException ignored) {
// Index may not exist yet.
}
try {
session.dropIndex(users, statusIndex).waitTillComplete();
} catch (AerospikeException ignored) {
// Index may not exist yet.
}
// Seed sample data.
session.insert(users)
.bins("name", "age", "status")
.id(k1).values("Alice", 28, "sidx_active")
.id(k2).values("Bob", 42, "sidx_active")
.id(k3).values("Carol", 19, "sidx_inactive")
.execute();
// Create secondary indexes.
session.createIndex(users, ageIndex, "age", IndexType.INTEGER, IndexCollectionType.DEFAULT)
.waitTillComplete();
session.createIndex(users, statusIndex, "status", IndexType.STRING, IndexCollectionType.DEFAULT)
.waitTillComplete();
// Compound predicate; server optimizer selects the most selective readable index
System.out.println("sidx_active users age >= 30:");
try (RecordStream ageQuery = session.query(users)
.where("$.status == 'sidx_active' and $.age >= 30")
.readingOnlyBins("name", "age")
.execute()) {
ageQuery.forEach(result -> {
Record user = result.recordOrThrow();
System.out.println(" - " + user.getString("name") + " (" + user.getInt("age") + ")");
});
}
// Server optimizer selects an SI for $.status == 'sidx_active'
System.out.println("\nsidx_active users:");
try (RecordStream statusQuery = session.query(users)
.where("$.status == 'sidx_active'")
.readingOnlyBins("name", "status")
.execute()) {
statusQuery.forEach(result -> {
Record user = result.recordOrThrow();
System.out.println(" - " + user.getString("name"));
});
}
}
}
}

📖 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.createIndex(...) | Session.dropIndex(...) | Session.query(DataSet) | OperationObjectBuilder.bins(...) | IdValuesBuilder.id(...) | IdValuesRowBuilder.values(...) | ChainableQueryBuilder.where(...) | ChainableQueryBuilder.readingOnlyBins(...) | ChainableQueryBuilder.execute() | ChainableNoBinsBuilder.execute() | RecordStream.forEach(...) | RecordStream.close() | RecordResult.recordOrThrow() | Record.getString(...) | Record.getInt(...) | AerospikeException

Performance considerations

  • Indexes consume memory on every node
  • Index updates add write latency
  • Queries without matching indexes run as PI queries (slow)
  • A missing or dropped index sends the query to the primary index, where it reads every record. Monitor show jobs queries alongside PI metrics to catch this.
  • Monitor index memory usage in production

Next steps