---
title: "Query by secondary index"
description: "Implement secondary index queries using Aerospike Expression Language (AEL), the Python SDK, and Voyager."
---

# Query by secondary index

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

In this step you implement the two query methods that power the application UI. You first build a single-condition secondary-index query (`query`), then a multi-condition query (`advanced_search`). Along the way you use Voyager to construct an AEL filter visually and paste it into your code.

The previous step left the `products` set with 200 records, but the home page still shows empty product rows because the listing query is unimplemented.

The product grid is populated by the `query` method, which you implement now. Every code change in this step lives in `python-server/src/aerospikeworkshop/services/key_value_service_new_client.py`.

## Use Voyager to build an AEL filter

The application defines five [secondary indexes](https://aerospike.com/docs/database/manage/namespace/secondary-index) on the `products` set: `articleType`, `subCategory`, `brandName`, `usage`, and `category`. A secondary index lets the database find records by bin value without scanning every record. The home-page dropdowns issue queries through `query(index, filter_value, count)`. Before writing code, build a sample query in Voyager so you can confirm the result and copy the generated AEL.

1.  In Voyager, drill into the `products` set and expand any record by selecting **Show 15 more bins**.
    
     ![Voyager showing all bins on a product record after Show 15 more bins is selected](https://aerospike.com/docs/_astro/voyager-show-bins.B1wNwUEE_Z1lVTpg.png)
    
    Note the `subCategory` bin. You filter on it next.
    
2.  Select the filter (funnel) icon at the top of the set view.
    
3.  In the filter dialog, enter `subCategory` as the field and `Shoes` as the value, then select **Apply**.
    
     ![Voyager filter dialog with subCategory equals Shoes](https://aerospike.com/docs/_astro/voyager-filter-shoes.CDw5K4wS_2mPHeg.png)
    
    The set view updates to show only products whose `subCategory` is `Shoes`. Voyager pages the results 25 rows at a time and does not display a total, so select **\>** to page through the matches.
    
4.  Open the filter again and look at the bottom of the dialog.
    
    The dialog shows the corresponding AEL expression: `$.subCategory == 'Shoes'`. In AEL, `$` refers to the current record, so `$.subCategory` reads the `subCategory` bin.
    
     ![Voyager filter dialog with the AEL expression dollar dot subCategory equals quote Shoes quote at the bottom](https://aerospike.com/docs/_astro/voyager-filter-ael.DcKIelnl_2ftT9Q.png)
    
    AEL is the Aerospike Expression Language. It looks similar to JSONPath and supports both bin values and record metadata such as time to live (TTL) or record size. You use the visually built expression as a starting point in code.
    
5.  Select the copy button to the right of the expression.
    

You can use the same workflow in reverse to debug expressions you write by hand. Whenever you are not sure that an AEL expression is correct, paste it into Voyager’s filter dialog (use the **Expression** tab to enter AEL directly) and confirm that Voyager returns the rows you expect before you put the expression into Python code.

## Implement the single-condition query

1.  In `key_value_service_new_client.py`, find the `query` method.
    
    The current body returns a hard-coded product:
    
    ```python
    product = await self.get_product("13283")
    
    products = [product] if product else []
    ```
    
2.  Replace the two lines above with a hard-coded filter first, to confirm the pattern works. Delete the original lines in full.
    
    ```python
    session = self._require_session()
    
    stream = await (
    
        session.query(self.product_dataset)
    
        .where("$.subCategory == 'Shoes'")
    
        .execute()
    
    )
    
    products: list[Product] = []
    
    async for row in stream:
    
        if row.is_ok and row.record is not None:
    
            products.append(Product.from_bins(row.record.bins))
    
    stream.close()
    ```
    
    In the loop, `row.is_ok and row.record is not None` applies the same success checks as the `get_product` guard, without `result is None` because `async for` only yields rows from the stream.
    
    Leave the existing `return QueryResult(...)` line as it is.
    
3.  Stop the FastAPI application with `Ctrl+C` (if it is running), then restart it from the `python-server` directory.
    
    Terminal window
    
    ```shell
    cd python-server
    
    AEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \
    
      uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080
    ```
    
4.  Reload [http://localhost:8080](http://localhost:8080).
    
    The home page now shows shoes everywhere because the filter is hard-coded.
    
     ![Home page with every category populated by shoe products](https://aerospike.com/docs/_astro/app-shoes-everywhere.DfNzV8Iv_1lWFnj.png)
5.  Replace the query block again, this time using the `index` and `filter_value` parameters.
    
    ```python
    session = self._require_session()
    
    stream = await (
    
        session.query(self.product_dataset)
    
        .where(f"$.{index} == '{filter_value}'")
    
        .bins(["id", "name", "images", "brandName", "price"])
    
        .limit(count)
    
        .execute()
    
    )
    
    products: list[Product] = []
    
    async for row in stream:
    
        if row.is_ok and row.record is not None:
    
            products.append(Product.from_bins(row.record.bins))
    
    stream.close()
    ```
    
    ::: note
    In production, escape or validate `index` and `filter_value` before you build AEL strings from user input.
    :::
    
    Three changes from the previous version:
    
    -   `where` takes an f-string that substitutes the index name and filter value into the AEL expression.
    -   `bins([...])` returns only the bins the home page needs. Returning fewer bins reduces network and memory pressure when the full record is large.
    -   `limit(count)` caps the number of results when the caller asks for a maximum.
6.  Stop the FastAPI application with `Ctrl+C`, then restart it.
    
    Terminal window
    
    ```shell
    cd python-server
    
    AEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \
    
      uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080
    ```
    
7.  Reload [http://localhost:8080](http://localhost:8080).
    
    Each category and article-type dropdown now populates with the correct items.
    

## Implement multi-condition advanced search

The four dropdowns at the top of the home page (`Category`, `Article Type`, `Usage`, `Brand Name`) all feed into `advanced_search(...)`. The method combines up to four `bin == value` predicates with `and`.

At the top of `advanced_search`, helper code builds an `ael` string from the filter parameters. Leave this block in place—you pass `ael` to `.where(ael)` in the SDK call below:

```python
indexes = {

    "category": as_non_null_string(category),

    "articleType": as_non_null_string(article_type),

    "usage": as_non_null_string(usage),

    "brandName": as_non_null_string(brand_name),

}

ael = ""

for field, value in indexes.items():

    if value:

        if ael:

            ael += " and "

        ael += f"$.{field} == '{value}'"

print(f"AEL: {ael}")
```

When a reader selects **Category** Apparel and **Usage** Sports, the log prints:

```plaintext
AEL: $.category == 'Apparel' and $.usage == 'Sports'
```

The current body returns a single hard-coded product:

```python
product = await self.get_product("13283")

products = [product] if product else []
```

Use Voyager once more to confirm that the multi-predicate AEL is correct, then write the SDK call.

1.  In Voyager, open the filter dialog and add two filter rows: `category` equals `Footwear`, and `usage` equals `Sports`. Select **Apply**.
    
     ![Voyager filter dialog with two rows: category equals Footwear and usage equals Sports](https://aerospike.com/docs/_astro/voyager-multi-filter.FLxJBIQG_DGrGo.png)
    
    The set view shows only sports footwear, and the dialog displays the combined AEL: `$.category == 'Footwear' and $.usage == 'Sports'`.
    
2.  Find `advanced_search` in `key_value_service_new_client.py`. Replace the two hard-coded stub lines below the helper code (`product = await self.get_product(...)` through `products = [product] if product else []`) with the following:
    
    ```python
    session = self._require_session()
    
    stream = await (
    
        session.query(self.product_dataset).where(ael).limit(count).execute()
    
    )
    
    products: list[Product] = []
    
    async for row in stream:
    
        if row.is_ok and row.record is not None:
    
            products.append(Product.from_bins(row.record.bins))
    
    stream.close()
    ```
    
    The body is almost identical to the single-condition query. The only differences are that `where` takes the `ael` string the helper code already built, and `bins([...])` is omitted because the search results page renders the full record.
    
    You did not tell the SDK which secondary index to use. It reads your AEL expression and picks the predicate it expects to match the fewest records, then uses that predicate’s index to fetch a small candidate set. The server checks the remaining predicates against those candidates. See [Secondary indexes](https://aerospike.com/docs/database/manage/namespace/secondary-index) for how that selection works with AEL filters. This works the same whether you wrote the AEL by hand or built it visually in Voyager.
    

### Confirm the queries work

1.  Stop the FastAPI application with `Ctrl+C`, then restart it.
    
    Terminal window
    
    ```shell
    cd python-server
    
    AEROSPIKE_CLIENT_PROFILE=new-client AEROSPIKE_PORT=3000 \
    
      uvicorn aerospikeworkshop.main:app --host 0.0.0.0 --port 8080
    ```
    
2.  Reload the home page and try several combinations of category, article type, usage, and brand. The product grid updates to match the selected filters.
    
3.  Select any product to load its detail page. The page renders the product’s full information.
    
     ![Product detail page showing a single product with full bins](https://aerospike.com/docs/_astro/app-product-detail.CreCxrao_2aDVSr.png)

## Outcomes

You now have:

-   A single-condition `query` method that uses any of the five secondary indexes.
-   A multi-condition `advanced_search` method that combines up to four predicates with `and`.
-   A working pattern for testing AEL expressions in Voyager before putting them in code.

You **cannot** yet:

-   Add items to the shopping cart. The cart is empty because `add_to_cart` is not yet implemented.

In the next step you implement the cart methods, including a check-and-set update on a nested map document.

::: undefined
-   I’ve used Voyager to build an AEL filter and copied it into the application.
-   I’ve implemented the single-condition query method.
-   I’ve implemented the multi-condition advanced\_search method.
-   I’ve confirmed the home page lists products and the advanced search filters work.
:::

[Previous  
Store and load products](https://aerospike.com/docs/database/learn/tutorials/get-started-with-aerospike-python-sdk-and-voyager/step/2/part/0/store-and-load-products) [Next  
Implement the shopping cart](https://aerospike.com/docs/database/learn/tutorials/get-started-with-aerospike-python-sdk-and-voyager/step/2/part/2/shopping-cart)