Skip to content

Context for operations on nested elements

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

Every List and Map operation targets a specific element within a list or map bin. For elements at the top level of the collection, no extra addressing is needed. For nested elements, a context provides the path from the bin to the target, one selector per nesting level.

  • Top-level elements: no context is needed.
  • Nested elements: supply a context with one selector per nesting level.
  • Missing intermediate elements: use MAP_KEY_CREATE or LIST_INDEX_CREATE to create the path as you go. See Map context examples.
  • Depth limit: as of Aerospike Database 8.2.0, Lists and Maps support up to 64 levels of nesting. Keep a context path within that depth, including one that creates missing levels. See Nesting depth limit for how depth is counted.

CDT context API

The following describes the CDT context API in generic terms. Each language client might have slightly different terms to express the same concepts, such as the Java client’s CTX class.

The context is a list of element selectors targeting a specific nested element. Each element selector includes a context type and a value.

Which operations take a context

Most operations in the List, Map, and String APIs take an optional context parameter, on both reads and writes. See String context examples.

Blob and HLL operations do not. They act on the value of the bin itself, so a blob or an HLL stored inside a list or a map cannot be modified in place — read the element out, change it, and write it back.

Element selectors

The following element selectors can be applied starting from the top level of the collection, forming an increasingly deeper path into it.

  • BY_LIST_INDEX(index)
  • BY_LIST_RANK(rank)
  • BY_LIST_VALUE(value)
  • BY_MAP_INDEX(index)
  • BY_MAP_RANK(rank)
  • BY_MAP_KEY(key)
  • BY_MAP_VALUE(value)

Each element selector must identify exactly one element. BY_LIST_VALUE and BY_MAP_VALUE require an exact value, so WILDCARD cannot be used here because it might match multiple elements. WILDCARD is only valid in list and map *_by_value and *_by_value_list operations. To select and operate on multiple elements at once, use path expression contexts.

Create-if-missing selectors

The following selectors create an element if it does not exist, then select it. This is similar to how mkdir -p creates intermediate directories.

  • MAP_KEY_CREATE(key)
  • LIST_INDEX_CREATE(index)

See the map context example for a worked example.

List context examples

Consider the following list stored in bin ‘l’:

[0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]

This list can be visualized as:

[0, 1, [ ], 7, [ ]] depth 0 (top level)
2, [ ], 5, 6 8, 9 depth 1
3, 4 depth 2

We can operate on the list element [3, 4] by identifying a context for the operation using [BY_LIST_INDEX(2), BY_LIST_INDEX(1)].

  • The first selector BY_LIST_INDEX(2) selects the third element of the top level list. The element selected by it is the list [2, [3, 4], 5, 6].
  • The second selector BY_LIST_INDEX(1) selects the element at index position 1. The element selected by it is the list [3, 4].
  • A list API operation can now be applied to one of the elements within this nested list.

At the top level we have five elements, three of them scalar integer values, two of them are list values.

The list value at index position 2 has four elements: the integer value 2, a list element, then the integer values 5 and 6. Its list element at index position 1 has two elements, the integers 3 and 4.


Append at depth 1

Append the value 100 to the list nested at the last element of the top level.

# Pseudocode — not runnable
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(-1)])
try (RecordStream rs = session.upsert(key)
.bin("l").onListIndex(-1).listAppend(100)
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Result:

[0, 1, [2, [3, 4], 5, 6], 7, [8, 9, 100]]

Without the context we are appending to the top level list.

# Pseudocode — not runnable
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100)
try (RecordStream rs = session.upsert(key)
.bin("l").listAppend(100)
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Result:

[0, 1, [2, [3, 4], 5, 6], 7, [8, 9], 100]

Error: selector targets wrong type

Append the value 100 to a list element at index position 0. There is an integer value at index 0, not a list.

# Pseudocode — not runnable
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(0)])
# Error 12 — no change
try (RecordStream rs = session.upsert(key)
.bin("l").onListIndex(0).listAppend(100)
.execute()) {
Record rec = rs.next().recordOrThrow();
}
// recordOrThrow() throws AerospikeException: result code 12 (ResultCode.BIN_TYPE_ERROR)

Error: error code 12 AS_ERR_INCOMPATIBLE_TYPE

BY_LIST_INDEX(0) selects the integer 0; the append fails because it needs a list.


Append at depth 2

Append the value 100 to the deepest list, which is at depth 2. This requires a context with two selectors to navigate to that list.

# Pseudocode — not runnable
# [0, 1, [2, [3, 4], 5, 6], 7, [8, 9]]
list_append('l', 100, context=[BY_LIST_INDEX(2), BY_LIST_INDEX(1)])
try (RecordStream rs = session.upsert(key)
.bin("l").onListIndex(2).onListIndex(1).listAppend(100)
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Result:

[0, 1, [2, [3, 4, 100], 5, 6], 7, [8, 9]]

Select by value with duplicates

In this example bin ‘l’ contains a list of tuples, including duplicates:

[[1, 1], [2, 2], [2, 2, 2], [2, 2], [1, 1]]

This list can be visualized as:

[ [ ], [ ], [ ], [ ], [ ] ] depth 0 (top level)
1, 1 2, 2 2, 2, 2 2, 2 1, 1 depth 1

We select the sub-list [2, 2] using BY_LIST_VALUE with an exact value. Because there are multiple matching elements, the selector picks the first one encountered in list order.

# Pseudocode — not runnable
# [[1, 1], [2, 2], [2, 2, 2], [2, 2], [1, 1]]
list_append('l', 3, context=[BY_LIST_VALUE([2, 2])])
try (RecordStream rs = session.upsert(key)
.bin("l").onListValue(List.of(2, 2)).listAppend(3)
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Result:

The sub-list [2, 2] at index 1 is selected and gets the appended element. The duplicate [2, 2] at index 3 is not modified.

[[1, 1], [2, 2, 3], [2, 2, 2], [2, 2], [1, 1]]

Map context examples

Create a nested map path

The MAP_KEY_CREATE selector creates intermediate map entries that do not yet exist, then selects them. This is useful when building up nested structures incrementally.

In the following example we want to add accolades to the stats of an actor. The data is in bin ‘m’:

{"name": "chuck norris"}

We want to increment a jokes accolade by 317, but neither "stats", "accolades", nor "jokes" exists yet. Using MAP_KEY_CREATE, the operation creates the full path and succeeds.

# Pseudocode — not runnable
map_increment('m', 'jokes', 317, context=[MAP_KEY_CREATE('stats'), MAP_KEY_CREATE('accolades')])
try (RecordStream rs = session.upsert(key)
.bin("m").onMapKey("stats", MapOrder.UNORDERED)
.onMapKey("accolades", MapOrder.UNORDERED)
.onMapKey("jokes").add(317)
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Result:

{"name": "chuck norris", "stats": {"accolades": {"jokes": 317}}}

String context examples

Uppercase a nested string

String operations reach a String nested in a List or Map through the same context path as List and Map operations. Every string operation except to_string accepts one.

The data is in bin ‘v’:

[{"license": "abc-123"}]

We want to upper the license of the first vehicle. LIST_INDEX selects the element, MAP_KEY selects the field, and the string operation transforms the value in place.

# Pseudocode — not runnable
# [{"license": "abc-123"}]
string_upper('v', context=[LIST_INDEX(0), MAP_KEY('license')])
try (RecordStream rs = session.upsert(key)
.bin("v").onListIndex(0).onMapKey("license").upper()
.execute()) {
Record rec = rs.next().recordOrThrow();
}

The bin now holds [{"license": "ABC-123"}].

If the path does not resolve — no element at index 0, or no license key — the operation returns AS_ERR_OP_NOT_APPLICABLE. Unlike a missing bin, a missing nested path is not created, even by the string operations that create a missing bin. Set NO_FAIL to turn an unresolved path into a success that writes nothing.

Path expression contexts

The contexts described above select a single element at each level. Starting with Aerospike Database 8.1.1, path expressions extend this model to select and filter multiple elements at once, using selectByPath and modifyByPath instead of traditional single-element CDT operations.

Matching and filtering (8.1.1+)

  • ALL_CHILDREN - matches all children of the current Map or List without filtering.
  • ALL_CHILDREN_WITH_FILTER(exp) - matches children of the current Map or List where the filter expression evaluates to true. The filter can assign an aspect of the current element (its value, key or index) to a loop variable.

The following example uses the vehicles data model, a list of maps within a bin called vehicles. It reads the license plates of all vehicles where make is "Tesla":

Exp isTesla = Exp.eq(
MapExp.getByKey(MapReturnType.VALUE, Exp.Type.STRING,
Exp.val("make"), Exp.mapLoopVar(LoopVarPart.VALUE)),
Exp.val("Tesla"));
try (RecordStream rs = session.query(key)
.bin("vehicles").onEachChild(isTesla).onMapKey("license").collectValues()
.execute()) {
Record rec = rs.next().recordOrThrow();
}

Expected output: ["6ABC123"]

Key selection and combined filtering (8.1.2+)

  • MAP_KEYS_IN(keys...) - select map entries whose keys match any of the provided values. This is equivalent to a SQL WHERE key IN (k1, k2, ...) clause and uses the map’s internal index for efficient lookup.

  • AND_FILTER(exp) - apply an additional filter expression at the same level as the preceding context. Entries must satisfy both the preceding context and this filter to be included. AND_FILTER:

    • Cannot be the first context, because it filters what the preceding context selects.
    • Cannot be chained after a previous AND_FILTER(exp), because you can only have one expression at any level.
    • Cannot be used after an ALL_CHILDREN or ALL_CHILDREN_WITH_FILTER context, which already carries that level’s expression.

    The server rejects each of these with error 4 (AS_ERR_PARAMETER), or with error 26 (AS_ERR_OP_NOT_APPLICABLE) when the path runs inside an expression.

The following example uses the booking data model, a map bin doc keyed by room ID. It selects rooms 10001 and 10003, keeps the ones that are not deleted and have a time after 1780000000, and returns each one’s rates whose beta is above 0:

from aerospike_sdk import Exp, ExpType, LoopVarPart, MapReturnType
def field(name, value_type):
return Exp.map_get_by_key(MapReturnType.VALUE, value_type, Exp.val(name),
Exp.map_loop_var(LoopVarPart.VALUE), [])
time_filter = Exp.gt(field("time", ExpType.INT), Exp.val(1780000000))
deleted_filter = Exp.eq(field("isDeleted", ExpType.BOOL), Exp.val(False))
rate_filter = Exp.gt(field("beta", ExpType.FLOAT), Exp.val(0.0))
stream = (session.query(key)
.bin("doc").on_map_keys_in([10001, 10003]) # select rooms by ID
.and_filter(Exp.and_([time_filter, deleted_filter])) # additional room-level filters
.on_each_child() # descend into each room's children
.on_each_child_where(rate_filter) # filter at the rates level
.collect_matching_tree(no_fail=True)
.execute())
rec = stream.first_or_raise().record_or_raise()

Expected output: room 10001, with only its first rate.

For usage examples and performance comparisons, see path expressions performance.

Language-specific client APIs