Context for operations on nested elements
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_CREATEorLIST_INDEX_CREATEto create the path as you go. See map context examples.
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.
Most operations in the List and Map API take an optional context parameter.
The context is a list of element selectors targeting a specific nested element. Each element selector includes a context type and a value.
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 2We 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)])Record record = client.operate(null, key, ListOperation.append("l", Value.get(100), CTX.listIndex(-1)), Operation.get("l"));ops = [ list_operations.list_append("l", 100, None, [cdt_ctx.cdt_ctx_list_index(-1)])]_, _, bins = client.operate(key, ops)as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 1);as_cdt_ctx_add_list_index(&ctx, -1);
as_operations ops;as_operations_inita(&ops, 2);
as_integer v;as_integer_init(&v, 100);as_operations_list_append(&ops, "l", &ctx, NULL, (as_val*)&v);as_operations_add_read(&ops, "l");
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.ListAppendWithPolicyContextOp(as.DefaultListPolicy(), "l", []*as.CDTContext{as.CtxListIndex(-1)}, 100), as.GetBinOp("l"),)Record record = client.Operate(null, key, ListOperation.Append("l", Value.Get(100), CTX.ListIndex(-1)), Operation.Get("l"));const ops = [ lists.append('l', 100).withContext(ctx => ctx.addListIndex(-1)), Aerospike.operations.read('l')]const result = await client.operate(key, ops)Result:
[0, 1, [2, [3, 4, 100], 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)Record record = client.operate(null, key, ListOperation.append("l", Value.get(100)), Operation.get("l"));ops = [ list_operations.list_append("l", 100)]_, _, bins = client.operate(key, ops)as_operations ops;as_operations_inita(&ops, 2);
as_integer v;as_integer_init(&v, 100);as_operations_list_append(&ops, "l", NULL, NULL, (as_val*)&v);as_operations_add_read(&ops, "l");
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.ListAppendOp("l", 100), as.GetBinOp("l"),)Record record = client.Operate(null, key, ListOperation.Append("l", Value.Get(100)), Operation.Get("l"));const ops = [ lists.append('l', 100), Aerospike.operations.read('l')]const result = await client.operate(key, ops)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 26 — no changeRecord record = client.operate(null, key, ListOperation.append("l", Value.get(100), CTX.listIndex(0)), Operation.get("l"));// throws AerospikeException: Error 26 OP_NOT_APPLICABLEops = [ list_operations.list_append("l", 100, None, [cdt_ctx.cdt_ctx_list_index(0)])]# raises exception: error 26 OP_NOT_APPLICABLE_, _, bins = client.operate(key, ops)as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 1);as_cdt_ctx_add_list_index(&ctx, 0);
as_operations ops;as_operations_inita(&ops, 2);
as_integer v;as_integer_init(&v, 100);as_operations_list_append(&ops, "l", &ctx, NULL, (as_val*)&v);as_operations_add_read(&ops, "l");
as_record* rec = NULL;// returns AEROSPIKE_ERR_OP_NOT_APPLICABLE (error 26)aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.ListAppendWithPolicyContextOp(as.DefaultListPolicy(), "l", []*as.CDTContext{as.CtxListIndex(0)}, 100), as.GetBinOp("l"),)// err: OP_NOT_APPLICABLE (error 26)Record record = client.Operate(null, key, ListOperation.Append("l", Value.Get(100), CTX.ListIndex(0)), Operation.Get("l"));// throws AerospikeException: Error 26 OP_NOT_APPLICABLEconst ops = [ lists.append('l', 100).withContext(ctx => ctx.addListIndex(0)), Aerospike.operations.read('l')]// throws AerospikeError: error 26 OP_NOT_APPLICABLEconst result = await client.operate(key, ops)Error: error code 26 OP_NOT_APPLICABLE
A list context selector must target a list element, and a map context selector must target a map element. Applying a list operation to a scalar value fails.
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)])Record record = client.operate(null, key, ListOperation.append("l", Value.get(100), CTX.listIndex(2), CTX.listIndex(1)), Operation.get("l"));ops = [ list_operations.list_append("l", 100, None, [cdt_ctx.cdt_ctx_list_index(2), cdt_ctx.cdt_ctx_list_index(1)])]_, _, bins = client.operate(key, ops)as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 2);as_cdt_ctx_add_list_index(&ctx, 2);as_cdt_ctx_add_list_index(&ctx, 1);
as_operations ops;as_operations_inita(&ops, 2);
as_integer v;as_integer_init(&v, 100);as_operations_list_append(&ops, "l", &ctx, NULL, (as_val*)&v);as_operations_add_read(&ops, "l");
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.ListAppendWithPolicyContextOp(as.DefaultListPolicy(), "l", []*as.CDTContext{as.CtxListIndex(2), as.CtxListIndex(1)}, 100), as.GetBinOp("l"),)Record record = client.Operate(null, key, ListOperation.Append("l", Value.Get(100), CTX.ListIndex(2), CTX.ListIndex(1)), Operation.Get("l"));const ops = [ lists.append('l', 100) .withContext(ctx => ctx.addListIndex(2).addListIndex(1)), Aerospike.operations.read('l')]const result = await client.operate(key, ops)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 1We 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])])List<Value> matchValue = List.of(Value.get(2), Value.get(2));
Record record = client.operate(null, key, ListOperation.append("l", Value.get(3), CTX.listValue(Value.get(matchValue))), Operation.get("l"));ops = [ list_operations.list_append("l", 3, None, [cdt_ctx.cdt_ctx_list_value([2, 2])])]_, _, bins = client.operate(key, ops)as_arraylist match_val;as_arraylist_inita(&match_val, 2);as_arraylist_append_int64(&match_val, 2);as_arraylist_append_int64(&match_val, 2);
as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 1);as_cdt_ctx_add_list_value(&ctx, (as_val*)&match_val);
as_operations ops;as_operations_inita(&ops, 2);
as_integer v;as_integer_init(&v, 3);as_operations_list_append(&ops, "l", &ctx, NULL, (as_val*)&v);as_operations_add_read(&ops, "l");
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.ListAppendWithPolicyContextOp(as.DefaultListPolicy(), "l", []*as.CDTContext{ as.CtxListValue(as.NewValue([]int{2, 2})), }, 3), as.GetBinOp("l"),)IList<Value> matchValue = new List<Value> { Value.Get(2), Value.Get(2) };
Record record = client.Operate(null, key, ListOperation.Append("l", Value.Get(3), CTX.ListValue(Value.Get(matchValue))), Operation.Get("l"));const ops = [ lists.append('l', 3) .withContext(ctx => ctx.addListValue([2, 2])), Aerospike.operations.read('l')]const result = await client.operate(key, ops)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 runnablemap_increment('m', 'jokes', 317, context=[MAP_KEY_CREATE('stats'), MAP_KEY_CREATE('accolades')])Record record = client.operate(null, key, MapOperation.increment(MapPolicy.Default, "m", Value.get("jokes"), Value.get(317), CTX.mapKeyCreate(Value.get("stats"), MapOrder.UNORDERED), CTX.mapKeyCreate(Value.get("accolades"), MapOrder.UNORDERED)), Operation.get("m"));ops = [ map_operations.map_increment("m", "jokes", 317, None, [cdt_ctx.cdt_ctx_map_key_create("stats", aerospike.MAP_UNORDERED), cdt_ctx.cdt_ctx_map_key_create("accolades", aerospike.MAP_UNORDERED)])]_, _, bins = client.operate(key, ops)as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 2);as_cdt_ctx_add_map_key_create(&ctx, (as_val*)as_string_new("stats", false), AS_MAP_UNORDERED);as_cdt_ctx_add_map_key_create(&ctx, (as_val*)as_string_new("accolades", false), AS_MAP_UNORDERED);
as_operations ops;as_operations_inita(&ops, 2);
as_string mk;as_string_init(&mk, "jokes", false);as_integer mv;as_integer_init(&mv, 317);as_operations_map_increment(&ops, "m", &ctx, NULL, (as_val*)&mk, (as_val*)&mv);as_operations_add_read(&ops, "m");
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);record, err := client.Operate(nil, key, as.MapIncrementOp(as.DefaultMapPolicy(), "m", "jokes", 317, as.CtxMapKeyCreate(as.StringValue("stats"), as.MapOrder.UNORDERED), as.CtxMapKeyCreate(as.StringValue("accolades"), as.MapOrder.UNORDERED)), as.GetBinOp("m"),)Record record = client.Operate(null, key, MapOperation.Increment(MapPolicy.Default, "m", Value.Get("jokes"), Value.Get(317), CTX.MapKeyCreate(Value.Get("stats"), MapOrder.UNORDERED), CTX.MapKeyCreate(Value.Get("accolades"), MapOrder.UNORDERED)), Operation.Get("m"));const context = new Aerospike.cdt.Context() .addMapKeyCreate('stats') .addMapKeyCreate('accolades')
const ops = [ maps.increment('m', 'jokes', 317) .withContext(context), Aerospike.operations.read('m')]const result = await client.operate(key, ops)Result:
{"name": "chuck norris", "stats": {"accolades": {"jokes": 317}}}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 totrue. 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"));
Record record = client.operate(null, key, CdtOperation.selectByPath("vehicles", SelectFlags.VALUE, CTX.allChildrenWithFilter(isTesla), CTX.mapKey(Value.get("license"))));is_tesla = exp.Eq( exp.MapGetByKey( ctx=None, return_type=aerospike.MAP_RETURN_VALUE, value_type=exp.ResultType.STRING, key=exp.Val("make"), bin=exp.LoopVarMap(aerospike.EXP_LOOPVAR_VALUE) ), exp.Val("Tesla")).compile()
ctx = [ cdt_ctx.cdt_ctx_all_children_with_filter(is_tesla), cdt_ctx.cdt_ctx_map_key("license"),]ops = [ operations.select_by_path( bin_name="vehicles", ctx=ctx, flags=aerospike.EXP_PATH_SELECT_VALUE)]_, _, bins = client.operate(key, ops)isTesla := as.ExpEq( as.ExpMapGetByKey( as.MapReturnType.VALUE, as.ExpTypeSTR, as.ExpStringVal("make"), as.ExpMapLoopVar(as.VALUE), ), as.ExpStringVal("Tesla"),)
record, err := client.Operate(nil, key, as.SelectByPath("vehicles", as.EXP_PATH_SELECT_VALUE, as.CtxAllChildrenWithFilter(isTesla), as.CtxMapKey(as.NewStringValue("license"))),)as_exp_build(is_tesla, as_exp_cmp_eq( as_exp_map_get_by_key(NULL, AS_MAP_RETURN_VALUE, AS_EXP_TYPE_STR, as_exp_str("make"), as_exp_loopvar_map(AS_EXP_LOOPVAR_VALUE)), as_exp_str("Tesla")));
as_cdt_ctx ctx;as_cdt_ctx_inita(&ctx, 2);as_cdt_ctx_add_all_children_with_filter(&ctx, is_tesla);as_cdt_ctx_add_map_key(&ctx, (as_val*)as_string_new("license", false));
as_operations ops;as_operations_inita(&ops, 1);as_operations_select_by_path(&err, &ops, "vehicles", &ctx, AS_EXP_PATH_SELECT_VALUE);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);Exp isTesla = Exp.EQ( MapExp.GetByKey(MapReturnType.VALUE, Exp.Type.STRING, Exp.Val("make"), Exp.MapLoopVar(LoopVarPart.VALUE)), Exp.Val("Tesla"));
Record record = client.Operate(null, key, CDTOperation.SelectByPath("vehicles", SelectFlag.VALUE, CTX.AllChildrenWithFilter(isTesla), CTX.MapKey(Value.Get("license"))));const isTesla = exp.eq( exp.maps.getByKey( exp.loopVarMap(exp.loopVarPart.VALUE), exp.str('make'), exp.type.STR, maps.returnType.VALUE ), exp.str('Tesla'))
const context = new Context() .addAllChildrenWithFilter(isTesla) .addMapKey('license')
const ops = [ op.selectByPath('vehicles', exp.pathSelectFlags.VALUE, context)]const result = await client.operate(key, ops)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 SQLWHERE 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. You can only have one expression at any level, soAND_FILTER:- Cannot be chained after a previous
AND_FILTER(exp). - Cannot be used after an
ALL_CHILDRENorALL_CHILDREN_WITH_FILTERcontext. - Not supported inside expression-wrapped operations (
CdtExp.selectByPath). Use direct CDT operations (CdtOperation.selectByPath/CdtOperation.modifyByPath) instead.
- Cannot be chained after a previous
CdtOperation.selectByPath("myBin", SelectFlags.MATCHING_TREE | SelectFlags.NO_FAIL, CTX.mapKeysIn(10001L, 10003L), // select rooms by ID CTX.andFilter(Exp.and(timeFilter, deletedFilter)), // additional room-level filters CTX.allChildren(), // descend into each room's children CTX.allChildrenWithFilter(rateFilter)); // filter at the rates levelFor usage examples and performance comparisons, see path expressions performance.