---
title: "Verify collocation and partial upserts"
description: "Read features from two feature views, inspect the shared Aerospike record, and observe a targeted Map CDT update."
---

# Verify collocation and partial upserts

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

With `driver_stats` and `pricing` materialized into Aerospike, you now verify the shared-record layout in three steps: read both feature views through Feast, inspect the underlying Aerospike record, and materialize only `driver_stats` to observe a targeted Map CDT update.

## Read both feature views in one call

Call `get_online_features` with features from both `driver_stats` and `pricing` and confirm that a single response contains all three values.

1.  Create `read_features.py`:
    
    read\_features.py
    
    ```python
    from feast import FeatureStore
    
    store = FeatureStore(repo_path=".")
    
    features = store.get_online_features(
    
        features=[
    
            "driver_stats:rating",
    
            "driver_stats:trips_last_7d",
    
            "pricing:surge_multiplier",
    
        ],
    
        entity_rows=[{"driver_id": 1001}],
    
    ).to_dict()
    
    print(features)
    ```
    
    `FeatureStore(repo_path=".")` loads the registry location from `feature_store.yaml` in the current directory. The registry is Feast’s metadata store for entity, feature view, and service definitions.
    
    Each string in `features` uses the `"feature_view_name:feature_name"` format. This lets you mix features from different views in a single call. Feast resolves each reference to the correct feature view and issues one `online_read` per view. Both views share the same entity record key, so each read targets the same physical record in Aerospike.
    
    `entity_rows` is a list of dicts, one per entity lookup. Each dict must contain the entity’s join key column and value. Feast returns one result row per entry.
    
2.  Run it:
    
    Terminal window
    
    ```shell
    python3 read_features.py
    ```
    
    Expected result
    
    ```text
    {'driver_id': [1001], 'rating': [4.91], 'trips_last_7d': [132], 'surge_multiplier': [1.2]}
    ```
    
    Note that a single record includes values from both `driver_stats` and `pricing`.
    
3.  Confirm the multi-view lookup:
    
    -   `rating` and `trips_last_7d` are present (from `driver_stats`).
    -   `surge_multiplier` is present (from `pricing`).
    -   `driver_id` is `[1001]`.

## Inspect the Aerospike record directly

Use the Aerospike Python client to scan the Feast set and confirm that both feature views are stored together on one record per driver.

1.  Create `inspect_records.py` to read every record in the Feast set directly from Aerospike:
    
    The Aerospike Python client’s `scan.foreach` callback receives a three-element tuple: `(key, metadata, bins)`. The key tuple includes a record digest, which is a stable identifier even when scan order changes. The script sorts records by that digest.
    
    inspect\_records.py
    
    ```python
    import aerospike
    
    client = aerospike.client({"hosts": [("127.0.0.1", 3000)]}).connect()
    
    records = []
    
    def collect(record_tuple):
    
        key, _, bins = record_tuple  # key, metadata, bins
    
        digest = bytes(key[3]).hex()
    
        records.append((digest, bins))
    
    scan = client.scan("test", "driver_features_latest")
    
    scan.foreach(collect)
    
    client.close()
    
    print(f"records in set: {len(records)}")
    
    for digest, bins in sorted(records):
    
        print(f"record digest: {digest}")
    
        views = sorted(bins["features"].keys())
    
        print(f"feature views on this record: {views}")
    
        for view_name in views:
    
            values = {
    
                k: round(v, 4) if isinstance(v, float) else v
    
                for k, v in bins["features"][view_name].items()
    
            }
    
            print(f"  {view_name}: {values}")
    ```
    
2.  Run it:
    
    Terminal window
    
    ```shell
    python3 inspect_records.py
    ```
    
    Expected result
    
    ```text
    records in set: 3
    
    record digest: RECORD_DIGEST_1
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': RATING_1, 'trips_last_7d': TRIPS_1}
    
      pricing: {'surge_multiplier': SURGE_1}
    
    record digest: RECORD_DIGEST_2
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': RATING_2, 'trips_last_7d': TRIPS_2}
    
      pricing: {'surge_multiplier': SURGE_2}
    
    record digest: RECORD_DIGEST_3
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': RATING_3, 'trips_last_7d': TRIPS_3}
    
      pricing: {'surge_multiplier': SURGE_3}
    ```
    
3.  Confirm both feature views share each record:
    
    Three drivers produced three Aerospike records, one per driver. Each record holds both feature views inside its `features` bin.
    
    -   `records in set` is `3`.
    -   Every record lists both `driver_stats` and `pricing` under `feature views on this record`.
    -   Each record has a unique digest. Your digest values differ from the placeholders in this example.

## Observe a targeted Map CDT update

1.  Create `update_driver_stats.py` with new `driver_stats` values and a later timestamp:
    
    update\_driver\_stats.py
    
    ```python
    import pandas as pd
    
    driver_ids = [1001, 1002, 1003]
    
    event_timestamp = pd.Timestamp.now(tz="UTC").floor("s")
    
    pd.DataFrame({
    
        "driver_id": driver_ids,
    
        "event_timestamp": [event_timestamp] * len(driver_ids),
    
        "rating": [4.95, 4.80, 4.90],
    
        "trips_last_7d": [140, 101, 215],
    
    }).to_parquet("data/driver_stats.parquet")
    
    print("Updated data/driver_stats.parquet")
    ```
    
    `event_timestamp` is set to the current time, not ten minutes in the past like in `generate_data.py`. Feast uses the timestamp to decide which rows are newer than the previous materialization run. A timestamp in the past could fall outside the incremental window and be skipped. Using the current time guarantees the rows are picked up.
    
    The DataFrame contains only `driver_stats` columns. This Parquet file is the offline source for `driver_stats` only. The `pricing` feature view has its own source file and is unaffected by this update.
    
2.  Run it:
    
    Terminal window
    
    ```shell
    python3 update_driver_stats.py
    ```
    
    Expected result
    
    ```text
    Updated data/driver_stats.parquet
    ```
    
3.  Materialize `driver_stats` only:
    
    Run incremental materialization again, but limit it to `driver_stats` with `--views`. Feast re-reads the updated Parquet file and partial-upserts only the `driver_stats` map key on each driver record. `pricing` is not part of this run.
    
    Terminal window
    
    ```shell
    feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S") --views driver_stats
    ```
    
    Expected result
    
    ```text
    Materializing 1 feature views to 2026-07-14 21:58:00+00:00 into the aerospike online store.
    
    driver_stats from 2026-07-14 21:54:28+00:00 to 2026-07-14 21:58:00+00:00:
    ```
    
    A `DeprecationWarning` about `meta["ttl"]` can appear and does not block the write.
    
4.  Run `inspect_records.py` again:
    
    Terminal window
    
    ```shell
    python3 inspect_records.py
    ```
    
    Expected result
    
    ```text
    records in set: 3
    
    record digest: RECORD_DIGEST_1
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': UPDATED_RATING_1, 'trips_last_7d': UPDATED_TRIPS_1}
    
      pricing: {'surge_multiplier': SURGE_1}
    
    record digest: RECORD_DIGEST_2
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': UPDATED_RATING_2, 'trips_last_7d': UPDATED_TRIPS_2}
    
      pricing: {'surge_multiplier': SURGE_2}
    
    record digest: RECORD_DIGEST_3
    
    feature views on this record: ['driver_stats', 'pricing']
    
      driver_stats: {'rating': UPDATED_RATING_3, 'trips_last_7d': UPDATED_TRIPS_3}
    
      pricing: {'surge_multiplier': SURGE_3}
    ```
    
    Match records between the two runs by digest. For each digest, the `driver_stats` values change while the `pricing` values match exactly.
    
5.  Confirm the targeted update:
    
    -   Each digest is present in both runs.
    -   `driver_stats` values changed to match the update.
    -   `pricing` values for each digest are identical to the first inspect run.

You read both feature views through Feast, confirmed they share one Aerospike record per driver, and observed that materializing `driver_stats` only updated the `driver_stats` map entries inside each record’s `features` bin. That targeted write is how the integration keeps independently updated feature views on a shared record.

::: undefined
-   I’ve read features from both feature views in one `get_online_features` call.
-   I’ve inspected the Aerospike record and confirmed both feature views are on it.
-   I’ve re-materialized driver\_stats alone and confirmed only its map entries changed.
:::

[Previous  
Define and materialize feature views](https://aerospike.com/docs/develop/feast-aerospike-online-store/step/3/part/0/define-and-materialize) [Next  
Wrap up](https://aerospike.com/docs/develop/feast-aerospike-online-store/step/4/part/0/wrap-up)