Secondary Indexes

Secondary indexes enable efficient queries on bin values. The SDK provides both manual index management and automatic index discovery.

Creating Indexes

users = DataSet.of("test", "users")

# Numeric index
await (
    session.index(users)
    .on_bin("age")
    .named("users_age_idx")
    .numeric()
    .create()
)

# String index
await (
    session.index(users)
    .on_bin("city")
    .named("users_city_idx")
    .string()
    .create()
)

# Collection index (list elements)
from aerospike_async import CollectionIndexType

await (
    session.index(users)
    .on_bin("tags")
    .named("users_tags_idx")
    .collection(CollectionIndexType.LIST)
    .create()
)

# GEO2DSPHERE index (for GeoJSON bins)
places = DataSet.of("test", "places")
await (
    session.index(places)
    .on_bin("loc")
    .named("places_loc_idx")
    .geo2dsphere()
    .create()
)

Dropping Indexes

await session.index(users).named("users_age_idx").drop()

Auto-Index Discovery

The IndexesMonitor runs as a daemon thread, periodically fetching secondary index metadata from the cluster via PAC’s blocking info APIs. It works identically for the async Client and the synchronous SyncClient — no event loop required.

The monitor starts lazily: the daemon thread spins up the first time an AEL .where() query needs cached secondary-index metadata. Code paths that never use .where() (point reads/writes, batches, dataset scans without AEL filters) pay zero monitor overhead.

When you use .where() with an AEL expression, the client automatically generates an optimal secondary index Filter if a matching index exists. This is transparent — no code changes needed:

# If "users_age_idx" exists on the "age" bin, this query
# automatically uses it as a secondary index filter
stream = await (
    session.query(users)
    .where("$.age > 25")
    .execute()
)

Configuration

The refresh interval defaults to 5 seconds:

client = Client("localhost:3000", index_refresh_interval=2.0)

Explicit Override

Use with_index_context() to explicitly provide index metadata, bypassing auto-discovery:

from aerospike_sdk import IndexContext, Index, IndexTypeEnum

ctx = IndexContext.of("test", [
    Index(
        bin="age",
        index_type=IndexTypeEnum.INTEGER,
        namespace="test",
        name="age_idx",
    ),
])

stream = await (
    session.query(users)
    .with_index_context(ctx)
    .where("$.age > 25")
    .execute()
)

Indexes on Sets

Secondary indexes may be defined on a specific Aerospike set or be cross-set (no set name). When auto-discovery is on, the SDK scopes filter selection to the query’s set automatically — an index on set orders is never used to plan a filter for a query on set customers. Cross-set indexes (those defined without a set name) remain eligible for any query.

To configure this manually, use IndexContext.with_query_set():

from aerospike_sdk import IndexContext, Index, IndexTypeEnum

ctx = IndexContext.with_query_set(
    "test",
    "customers",  # query set
    [
        Index(bin="age", index_type=IndexTypeEnum.INTEGER,
              namespace="test", set_name="customers"),
        Index(bin="total", index_type=IndexTypeEnum.INTEGER,
              namespace="test", set_name="orders"),  # excluded
    ],
)

The total index is on orders and won’t be considered for queries on customers. Only the age index is selectable. Pass query_set=None (or omit it via IndexContext.of) to disable set-based filtering entirely.

Query Hints

Influence which index the server uses with QueryHint:

from aerospike_sdk import QueryHint

# Force a specific index
stream = await (
    session.query(users)
    .where("$.age > 25 and $.city == 'NYC'")
    .with_hint(QueryHint(index_name="users_city_idx"))
    .execute()
)

# Hint by bin name
stream = await (
    session.query(users)
    .where("$.age > 25 and $.city == 'NYC'")
    .with_hint(QueryHint(bin_name="city"))
    .execute()
)