Map operations examples
Aerospike maps can be used to implement use cases such as:
- Event History Containers
- Document Store
- Leaderboards
Modeling concepts
Nested lists and maps may be combined to model complex use cases. The examples below use the map operations available in each Aerospike language client.
Developers familiar with other NoSQL document stores will see the flexibility in applying map and list operations on bins with nested CDTs. Combined with (operate) single record transactions, Aerospike provides powerful functionality for modeling a wide range of use cases.
Examples
Event containers with unique timestamps
In this example we want to store and query user event data. Each record contains the recent N events of a specific user, keyed by that user’s unique identifier.
Assuming that events will not occur at the same millisecond, we’ll use millisecond timestamps as map keys for distinct events.
Each event’s data will be a tuple [ event-type, { attr1: v1, attr2: v2, ... } ].
Our sample data will be the following events of a single user:
{ 1523474230000: ['fav', {'sku':1, 'b':2}], 1523474231001: ['comment', {'sku':2, 'b':22}], 1523474236006: ['viewed', {'foo':'bar', 'sku':3, 'zz':'top'}], 1523474235005: ['comment', {'sku':1, 'c':1234}], 1523474233003: ['viewed', {'sku':3, 'z':26}], 1523474234004: ['viewed', {'sku':1, 'ff':'hhhl'}]}Retrieving data for specific event types
We’ll retrieve all the events of a specific event type, using a get_all_by_value map operation.
The argument for the operation is the tuple ['comment', *]. The wildcard singleton
(*) matches any remaining elements in the tuple from that position onward,
so all tuples starting with 'comment' as their first element are matched.
// events = {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474233003: ["viewed", {sku: 3, z: 26}],// 1523474234004: ["viewed", {sku: 1, ff: "hhhl"}],// 1523474235005: ["comment", {sku: 1, c: 1234}],// 1523474236006: ["viewed", {foo: "bar", sku: 3, zz: "top"}]}
List<Value> pattern = Arrays.asList(Value.get("comment"), Value.WILDCARD);Record record = client.operate(null, key, MapOperation.getByValue("events", Value.get(pattern), MapReturnType.KEY_VALUE));// {1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}# events = {1523474230000: ["fav", {"sku": 1, "b": 2}],# 1523474231001: ["comment", {"sku": 2, "b": 22}],# ...# 1523474235005: ["comment", {"sku": 1, "c": 1234}], ...}
_, _, bins = client.operate(key, [ map_operations.map_get_by_value( "events", ["comment", aerospike.CDTWildcard()], aerospike.MAP_RETURN_KEY_VALUE, )])# {1523474231001: ["comment", {"sku": 2, "b": 22}],# 1523474235005: ["comment", {"sku": 1, "c": 1234}]}// events = {1523474230000: ["fav", {sku: 1, b: 2}], ...}
pattern := []any{"comment", as.NewWildCardValue()}record, err := client.Operate(nil, key, as.MapGetByValueOp("events", pattern, as.MapReturnType.KEY_VALUE),)// {1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}// events = {1523474230000: ["fav", {sku: 1, b: 2}], ...}
as_arraylist pattern;as_arraylist_inita(&pattern, 2);as_arraylist_append_str(&pattern, "comment");as_arraylist_append(&pattern, (as_val*)&as_cmp_wildcard);
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_value(&ops, "events", NULL, (as_val*)&pattern, AS_MAP_RETURN_KEY_VALUE);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);// {1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}// events = {1523474230000: ["fav", {sku: 1, b: 2}], ...}
IList pattern = new List<Value> { Value.Get("comment"), Value.WILDCARD };Record record = client.Operate(null, key, MapOperation.GetByValue("events", Value.Get(pattern), MapReturnType.KEY_VALUE));// {1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}// events = {1523474230000: ["fav", {sku: 1, b: 2}], ...}
const maps = Aerospike.maps
const result = await client.operate(key, [ maps.getByValue("events", ["comment", new Aerospike.Wildcard()], maps.returnType.KEY_VALUE)])// {1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}Expanding on that example, we will retrieve all the events for multiple event types
using get_all_by_value_list:
List<Value> commentPattern = Arrays.asList(Value.get("comment"), Value.WILDCARD);List<Value> favPattern = Arrays.asList(Value.get("fav"), Value.WILDCARD);
List<Value> patterns = Arrays.asList(Value.get(commentPattern), Value.get(favPattern));Record record = client.operate(null, key, MapOperation.getByValueList("events", patterns, MapReturnType.KEY_VALUE));// {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}wildcard = aerospike.CDTWildcard()_, _, bins = client.operate(key, [ map_operations.map_get_by_value_list( "events", [["comment", wildcard], ["fav", wildcard]], aerospike.MAP_RETURN_KEY_VALUE, )])# {1523474230000: ["fav", {"sku": 1, "b": 2}],# 1523474231001: ["comment", {"sku": 2, "b": 22}],# 1523474235005: ["comment", {"sku": 1, "c": 1234}]}commentPat := []any{"comment", as.NewWildCardValue()}favPat := []any{"fav", as.NewWildCardValue()}
record, err := client.Operate(nil, key, as.MapGetByValueListOp("events", []any{commentPat, favPat}, as.MapReturnType.KEY_VALUE),)// {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}as_arraylist comment_pat;as_arraylist_inita(&comment_pat, 2);as_arraylist_append_str(&comment_pat, "comment");as_arraylist_append(&comment_pat, (as_val*)&as_cmp_wildcard);
as_arraylist fav_pat;as_arraylist_inita(&fav_pat, 2);as_arraylist_append_str(&fav_pat, "fav");as_arraylist_append(&fav_pat, (as_val*)&as_cmp_wildcard);
as_arraylist patterns;as_arraylist_inita(&patterns, 2);as_arraylist_append(&patterns, (as_val*)&comment_pat);as_arraylist_append(&patterns, (as_val*)&fav_pat);
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_value_list(&ops, "events", NULL, (as_list*)&patterns, AS_MAP_RETURN_KEY_VALUE);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);// {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}IList commentPattern = new List<Value> { Value.Get("comment"), Value.WILDCARD };IList favPattern = new List<Value> { Value.Get("fav"), Value.WILDCARD };
IList patterns = new List<Value> { Value.Get(commentPattern), Value.Get(favPattern) };Record record = client.Operate(null, key, MapOperation.GetByValueList("events", patterns, MapReturnType.KEY_VALUE));// {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}const wc = new Aerospike.Wildcard()const maps = Aerospike.maps
const result = await client.operate(key, [ maps.getByValueList("events", [["comment", wc], ["fav", wc]], maps.returnType.KEY_VALUE)])// {1523474230000: ["fav", {sku: 1, b: 2}],// 1523474231001: ["comment", {sku: 2, b: 22}],// 1523474235005: ["comment", {sku: 1, c: 1234}]}Counting events
We will get a count of a specific event type against the sample data above by
specifying a returnType=count.
The default behavior would be that of returnType=KeyValue:
List<Value> viewedPattern = Arrays.asList(Value.get("viewed"), Value.WILDCARD);Record viewedCount = client.operate(null, key, MapOperation.getByValue("events", Value.get(viewedPattern), MapReturnType.COUNT));// 3
List<Value> commentPattern = Arrays.asList(Value.get("comment"), Value.WILDCARD);Record commentCount = client.operate(null, key, MapOperation.getByValue("events", Value.get(commentPattern), MapReturnType.COUNT));// 2wildcard = aerospike.CDTWildcard()
_, _, bins = client.operate(key, [ map_operations.map_get_by_value( "events", ["viewed", wildcard], aerospike.MAP_RETURN_COUNT )])# bins["events"] == 3
_, _, bins = client.operate(key, [ map_operations.map_get_by_value( "events", ["comment", wildcard], aerospike.MAP_RETURN_COUNT )])# bins["events"] == 2viewedPat := []any{"viewed", as.NewWildCardValue()}record, err := client.Operate(nil, key, as.MapGetByValueOp("events", viewedPat, as.MapReturnType.COUNT),)// record.Bins["events"] == 3
commentPat := []any{"comment", as.NewWildCardValue()}record, err = client.Operate(nil, key, as.MapGetByValueOp("events", commentPat, as.MapReturnType.COUNT),)// record.Bins["events"] == 2as_arraylist viewed_pat;as_arraylist_inita(&viewed_pat, 2);as_arraylist_append_str(&viewed_pat, "viewed");as_arraylist_append(&viewed_pat, (as_val*)&as_cmp_wildcard);
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_value(&ops, "events", NULL, (as_val*)&viewed_pat, AS_MAP_RETURN_COUNT);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);// rec bins "events" == 3
as_arraylist comment_pat;as_arraylist_inita(&comment_pat, 2);as_arraylist_append_str(&comment_pat, "comment");as_arraylist_append(&comment_pat, (as_val*)&as_cmp_wildcard);
as_operations ops2;as_operations_inita(&ops2, 1);as_operations_map_get_by_value(&ops2, "events", NULL, (as_val*)&comment_pat, AS_MAP_RETURN_COUNT);
as_record* rec2 = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops2, &rec2);// rec2 bins "events" == 2IList viewedPattern = new List<Value> { Value.Get("viewed"), Value.WILDCARD };Record viewedCount = client.Operate(null, key, MapOperation.GetByValue("events", Value.Get(viewedPattern), MapReturnType.COUNT));// 3
IList commentPattern = new List<Value> { Value.Get("comment"), Value.WILDCARD };Record commentCount = client.Operate(null, key, MapOperation.GetByValue("events", Value.Get(commentPattern), MapReturnType.COUNT));// 2const wc = new Aerospike.Wildcard()const maps = Aerospike.maps
const viewedResult = await client.operate(key, [ maps.getByValue("events", ["viewed", wc], maps.returnType.COUNT)])// viewedResult.bins.events == 3
const commentResult = await client.operate(key, [ maps.getByValue("events", ["comment", wc], maps.returnType.COUNT)])// commentResult.bins.events == 2Trimming the map
Often we want to cap the number of events captured within a map. We will use
the remove_by_index_range
map operation to keep the last 1000 events. The INVERTED flag removes everything
outside the specified range — effectively keeping only the 1000 highest-indexed
(most recent) entries and discarding the rest.
client.operate(null, key, MapOperation.removeByIndexRange("events", -1000, 1000, MapReturnType.NONE | MapReturnType.INVERTED));client.operate(key, [ map_operations.map_remove_by_index_range( "events", -1000, aerospike.MAP_RETURN_NONE, 1000, inverted=True )])client.Operate(nil, key, as.MapRemoveByIndexRangeCountOp("events", -1000, 1000, as.MapReturnType.NONE|as.MapReturnType.INVERTED),)as_operations ops;as_operations_inita(&ops, 1);as_operations_map_remove_by_index_range(&ops, "events", NULL, -1000, 1000, AS_MAP_RETURN_NONE | AS_MAP_RETURN_INVERTED);
aerospike_key_operate(&as, &err, NULL, &key, &ops, NULL);client.Operate(null, key, MapOperation.RemoveByIndexRange("events", -1000, 1000, MapReturnType.NONE | MapReturnType.INVERTED));const maps = Aerospike.maps
await client.operate(key, [ maps.removeByIndexRange("events", -1000, 1000) .invertSelection() .andReturn(maps.returnType.NONE)])Event containers with unique UUIDs
In some use cases a timestamp would result in frequent collisions. We can model using a unique identifier as the map key.
In this example a conversation thread is stored in a single record. The map keys
are message UUIDs, and the map values are list tuples [timestamp, msg-string, username].
Our sample data will be the messages in a single conversation thread:
{ '0edf5b73-535c-4be7-b653-c0513dc79fb4': [1523474230, "Billie Jean is not my lover", "MJ"], '29342a0b-e20f-4676-9ecf-dfdf02ef6683': [1523474241, "She's just a girl who", "MJ"], '31a8ba1b-8415-aab7-0ecc-56ee659f0a83': [1523474245, "claims that I am the one", "MJ"], '9f54b4f8-992e-427f-9fb3-e63348cd6ac9': [1523474249, "...", "Tito"], '1ae56b18-7a3c-4f64-adb7-2e845eb5094e': [1523474257, "But the kid is not my son", "MJ"], '08785e96-eb1b-4a74-a767-7b56e8f13ea9': [1523474306, "ok...", "Tito"], '319fa1a6-0640-4354-a426-10c4d3459f0a': [1523474316, "Hee-hee!", "MJ"]}We will retrieve all the messages in a range of timestamps, using the get_by_value_interval map operation.
The arguments for the operation are minimum and maximum tuples of [timestamp, nil]. The NIL singleton is lower in value than a string. The get_by_value_interval checks if each map value (a list) is between the two list arguments of the operation.
// Retrieve messages with timestamps in [1523474240, 1523474246)List<Value> rangeStart = Arrays.asList(Value.get(1523474240), Value.getAsNull());List<Value> rangeEnd = Arrays.asList(Value.get(1523474246), Value.getAsNull());
Record record = client.operate(null, key, MapOperation.getByValueRange("msgs", Value.get(rangeStart), Value.get(rangeEnd), MapReturnType.KEY_VALUE));// {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],// 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}# Retrieve messages with timestamps in [1523474240, 1523474246)_, _, bins = client.operate(key, [ map_operations.map_get_by_value_range( "msgs", [1523474240, None], [1523474246, None], aerospike.MAP_RETURN_KEY_VALUE, )])# {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],# 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}// Retrieve messages with timestamps in [1523474240, 1523474246)rangeStart := []any{1523474240, nil}rangeEnd := []any{1523474246, nil}
record, err := client.Operate(nil, key, as.MapGetByValueRangeOp("msgs", rangeStart, rangeEnd, as.MapReturnType.KEY_VALUE),)// {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],// 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}// Retrieve messages with timestamps in [1523474240, 1523474246)as_arraylist range_start;as_arraylist_inita(&range_start, 2);as_arraylist_append_int64(&range_start, 1523474240);as_arraylist_append(&range_start, (as_val*)&as_nil);
as_arraylist range_end;as_arraylist_inita(&range_end, 2);as_arraylist_append_int64(&range_end, 1523474246);as_arraylist_append(&range_end, (as_val*)&as_nil);
as_operations ops;as_operations_inita(&ops, 1);as_operations_map_get_by_value_range(&ops, "msgs", NULL, (as_val*)&range_start, (as_val*)&range_end, AS_MAP_RETURN_KEY_VALUE);
as_record* rec = NULL;aerospike_key_operate(&as, &err, NULL, &key, &ops, &rec);// {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],// 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}// Retrieve messages with timestamps in [1523474240, 1523474246)IList rangeStart = new List<Value> { Value.Get(1523474240), Value.AsNull };IList rangeEnd = new List<Value> { Value.Get(1523474246), Value.AsNull };
Record record = client.Operate(null, key, MapOperation.GetByValueRange("msgs", Value.Get(rangeStart), Value.Get(rangeEnd), MapReturnType.KEY_VALUE));// {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],// 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}// Retrieve messages with timestamps in [1523474240, 1523474246)const maps = Aerospike.maps
const result = await client.operate(key, [ maps.getByValueRange("msgs", [1523474240, null], [1523474246, null], maps.returnType.KEY_VALUE)])// {29342a0b-...: [1523474241, "She's just a girl who", "MJ"],// 31a8ba1b-...: [1523474245, "claims that I am the one", "MJ"]}By the interval comparison rules the following is evaluated:
[1523474240, nil] ≰ [1523474230, "Billie Jean is not my lover", "MJ"] < [1523474246, nil] (false)[1523474240, nil] ≤ [1523474241, "She's just a girl who", "MJ"] < [1523474246, nil] (true)[1523474240, nil] ≤ [1523474245, "claims that I am the one", "MJ"] < [1523474246, nil] (true)[1523474240, nil] ≤ [1523474249, "...", "Tito"] ≮ [1523474246, nil] (false)[1523474240, nil] ≤ [1523474257, "But the kid is not my son", "MJ"] ≮ [1523474246, nil] (false)[1523474240, nil] ≤ [1523474306, "ok...", "Tito"] ≮ [1523474246, nil] (false)[1523474240, nil] ≤ [1523474316, "Hee-hee!", "MJ"] ≮ [1523474246, nil] (false)