---
title: "Secondary index"
description: "Master secondary index management and queries in the Aerospike Rust client, including filters and pagination."
---

# Secondary index

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

This guide provides a comprehensive overview of managing secondary indexes with the Aerospike Rust client. It covers creating and dropping indexes, defining query statements with `Filter::equal` and `Filter::range`, and executing queries using partition filters. Additionally, it demonstrates how to implement cursor-based pagination for large result sets through a complete code example.

## Creating a secondary index

You can create and delete secondary indexes from the database using the Aerospike Rust client.

To create a secondary index, invoke either `Client::create_index_on_bin()` or `Client::create_index_using_expression()`. Secondary indexes are created asynchronously, so each method returns an `IndexTask` before the index propagates to the cluster. Call `IndexTask::wait_till_complete()` to block until the index is fully built.

The following example creates a numeric index `idx_test_bar_baz` in namespace `test` within set `bar` and bin `baz`, then waits for it to be ready:

```rust
use std::time::Duration;

use aerospike::Task;

match client

    .create_index_on_bin(

        &AdminPolicy::default(),

        "test",

        "bar",

        "baz",

        "idx_test_bar_baz",

        IndexType::Numeric,

        CollectionIndexType::Default,

        None,

    )

    .await

{

    Ok(index_task) => {

        index_task

            .wait_till_complete(Some(Duration::from_secs(30)))

            .await?;

        println!("Index created.");

    }

    Err(err) => println!("Failed to create index: {}", err),

}
```

::: note
`wait_till_complete` is defined in the `Task` trait. You must import it (`use aerospike::Task;`) for the method to be available on `IndexTask`.
:::

> 📖 **API reference**: [`Client::create_index_on_bin`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.create_index_on_bin) | [`IndexTask::wait_till_complete`](https://docs.rs/aerospike/latest/aerospike/struct.IndexTask.html#method.wait_till_complete)

Secondary indexes can only be created once on the server as a combination of namespace, set, and bin name with either integer or string data types. For example, if you define a secondary index to index bin `x` that contains integer values, then only records containing bins named `x` with integer values are indexed. Other records with a bin named `x` that contain non-integer values are not indexed.

When an index management call is made to any node in the Aerospike Server cluster, the information automatically propagates to the remaining nodes.

## Removing a secondary index

To remove a secondary index using `Client::drop_index()`:

```rust
match client.drop_index(&AdminPolicy::default(), "test", "bar", "idx_test_bar_baz").await {

    Err(err) => println!("Couldn't drop index: {}", err),

    _ => {}

}
```

> 📖 **API reference**: [`Client::drop_index`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.drop_index)

## Defining the query

Use `query::Statement` to define a query.

First, create an instance of `Statement` by specifying the namespace _test_ and set _bar_ to query. Optionally, you can specify a list of bin names to restrict which record bins are returned by the query. If you wish to return all bins, use `Bins::All`.

```rust
use aerospike::Statement;

let mut stmt = Statement::new("test", "bar", Bins::Some(vec!["name".into(), "age".into()]));
```

> 📖 **API reference**: [`Statement::new`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html)

::: note
Namespace is required when querying using secondary indexes; however, sets are optional, depending on the secondary index.
:::

### Applying filters

To query a secondary index, specify a filter on the query. It appears that multiple filters are allowed, but the server currently restricts queries to a single filter.

Use the [`Filter`](https://docs.rs/aerospike/latest/aerospike/query/struct.Filter.html) type. Common constructors:

-   `Filter::equal(bin, value)`: equality filter for integer or string values.
-   `Filter::range(bin, begin, end)`: inclusive numeric range filter.
-   `Filter::contains(bin, value, collection_index_type)` and `Filter::contains_range(...)`: filters for collection secondary indexes.
-   `Filter::geo_within_region`, `Filter::geo_within_radius`, and `Filter::geo_contains`: geospatial filters.

```rust
use aerospike::query::Filter;

stmt.add_filter(Filter::equal("status", "active"));
```

This example uses a numeric index on bin `baz` in namespace `test` within set `bar` to find all records with values from 0 to 100 inclusive:

```rust
stmt.add_filter(Filter::range("baz", 0_i64, 100_i64));
```

::: note
In the `aerospike` crate 2.1, the legacy filter macros (`as_eq!`, `as_range!`, and others) are deprecated and may fail to compile. Use the `Filter` constructors listed above instead.
:::

> 📖 **API reference**: [`Statement::add_filter`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.add_filter)

::: note
Filters are optional. If not specified, the scan runs over the entire namespace and/or set.
:::

## Executing the query

To execute the query, invoke `Client::query`:

```rust
pub async fn query(

        &self,

        policy: &QueryPolicy,

        partition_filter: PartitionFilter,

        statement: Statement,

    ) -> Result<Arc<Recordset>>
```

> 📖 **API reference**: [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query)

Where,

-   `policy`: Query behavior definition.
-   `partition_filter`: Query partition filter, which acts something like a cursor. For the purposes of this tutorial, we will set this to `PartitionFilter::all()`.
-   `statement`: Query to execute.

`RecordSet` lets you iterate over the results of the query.

To execute the query and iterate over the results:

```rust
let qpolicy = QueryPolicy::default();

let pf = PartitionFilter::all();

match client.query(&qpolicy, pf, stmt).await {

    Ok(rs) => {

        let mut rs = rs.into_stream();

        while let Some(res) = rs.next().await {

            match res {

                Ok(rec) => {

                    // .. process record

                }

                Err(err) => println!("Error fetching record: {}", err),

            }

        }

    }

    Err(err) => println!("Error with query: {}", err),

}
```

> 📖 **API reference**: [`QueryPolicy::default`](https://docs.rs/aerospike/latest/aerospike/struct.QueryPolicy.html) | [`PartitionFilter::all`](https://docs.rs/aerospike/latest/aerospike/struct.PartitionFilter.html#method.all) | [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query)

## Cursor-based pagination

::: note
`PartitionFilter::clone()` creates a fresh copy that does NOT preserve cursor state. Always obtain the updated filter from the completed stream. Using `.clone()` causes an infinite loop re-reading the same page.
:::

```rust
// Paginate query results in pages of 100 records.

let mut pf = PartitionFilter::all();

let page_size: u64 = 100;

loop {

    let mut stmt = Statement::new("test", "bar", Bins::All);

    stmt.add_filter(Filter::range("baz", 0_i64, 100_i64));

    let mut qpolicy = QueryPolicy::default();

    qpolicy.max_records = page_size;

    let rs = client.query(&qpolicy, pf, stmt).await?;

    let mut stream = rs.into_stream();

    let mut count: u64 = 0;

    while let Some(result) = stream.next().await {

        match result {

            Ok(record) => count += 1,

            Err(err) => println!("Error: {err}"),

        }

    }

    pf = stream.partition_filter().await

        .expect("partition filter should be available after query");

    if pf.done() || count < page_size {

        break;

    }

}
```

## Complete example

The following complete program connects to an Aerospike instance on localhost, writes sample records, creates a secondary index on bin `baz`, runs a query using that index, and then closes the client connection.

```rust
use std::time::Duration;

use aerospike::{

    as_bin, as_key,

    AdminPolicy, Bins, Client, ClientPolicy,

    CollectionIndexType, IndexType,

    PartitionFilter, QueryPolicy, Statement,

    Task, WritePolicy,

};

use aerospike::query::Filter;

use futures::stream::StreamExt;

#[tokio::main]

async fn main() -> Result<(), Box<dyn std::error::Error>> {

    // Connect to localhost Aerospike.

    let client = Client::new(&ClientPolicy::default(), &"127.0.0.1:3000").await?;

    println!("Connected to Aerospike.");

    // Write a few records so the query has data to return.

    for (id, baz_value) in [(1, 10), (2, 55), (3, 120)] {

        let key = as_key!("test", "bar", id);

        let bins = vec![

            as_bin!("name", format!("item-{id}")),

            as_bin!("baz", baz_value),

        ];

        client.put(&WritePolicy::default(), &key, &bins).await?;

    }

    // Create a secondary index on test.bar.baz (ignore if it already exists).

    let index_name = "idx_test_bar_baz_complete_example";

    match client

        .create_index_on_bin(

            &AdminPolicy::default(),

            "test",

            "bar",

            "baz",

            index_name,

            IndexType::Numeric,

            CollectionIndexType::Default,

            None,

        )

        .await

    {

        Ok(index_task) => {

            index_task

                .wait_till_complete(Some(Duration::from_secs(30)))

                .await?;

            println!("Secondary index created: {index_name}");

        }

        Err(e) => {

            println!("Index already exists or creation skipped: {e}");

        }

    }

    // Query the secondary index.

    let mut stmt = Statement::new("test", "bar", Bins::All);

    stmt.add_filter(Filter::range("baz", 0_i64, 100_i64));

    let rs = client

        .query(&QueryPolicy::default(), PartitionFilter::all(), stmt)

        .await?;

    let mut stream = rs.into_stream();

    println!("Query results (baz between 0 and 100):");

    while let Some(result) = stream.next().await {

        match result {

            Ok(record) => println!("{:?}", record.bins),

            Err(err) => eprintln!("Error fetching record: {err}"),

        }

    }

    // Close the connection.

    client.close().await?;

    println!("Connection closed.");

    Ok(())

}
```

> 📖 **API reference**: [`Client::new`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.new) | [`Client::put`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.put) | [`Client::create_index_on_bin`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.create_index_on_bin) | [`IndexTask::wait_till_complete`](https://docs.rs/aerospike/latest/aerospike/struct.IndexTask.html#method.wait_till_complete) | [`Statement::new`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.new) | [`Statement::add_filter`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.add_filter) | [`PartitionFilter::all`](https://docs.rs/aerospike/latest/aerospike/struct.PartitionFilter.html#method.all) | [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query) | [`Client::close`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.close)

### Expected results

When run against a local Aerospike instance with namespace `test` and set `bar`, output should be similar to:

```text
Connected to Aerospike.

Secondary index created: idx_test_bar_baz_complete_example

Query results (baz between 0 and 100):

{"name": String("item-1"), "baz": Int(10)}

{"name": String("item-2"), "baz": Int(55)}

Connection closed.
```