# Operate, CDT, And Geo
`Aerospike.operate/4` executes a list of server-side operations against one
record. Use it for atomic primitive updates, complex data type operations,
expression operations, bit operations, HyperLogLog operations, and readbacks.
## Primitive Operations
Primitive operation builders live in `Aerospike.Op`.
```elixir
key = Aerospike.key("test", "sessions", "session:1")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{
"visits" => 1,
"status" => "new"
})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.add("visits", 1),
Aerospike.Op.put("status", "active"),
Aerospike.Op.get("visits")
])
record.bins["visits"]
```
## List Operations
List operations live in `Aerospike.Op.List`. Selector operations can choose
return shapes with return-type helpers.
```elixir
key = Aerospike.key("test", "sessions", "session:events")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{"events" => ["created"]})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.List.append("events", "opened"),
Aerospike.Op.List.size("events")
])
record.bins["events"]
```
Selector helpers can address list entries by index, rank, or value. Index is
position in the list. Rank is position in value order, so `-1` means the
highest-ranked value. `return_type:` controls whether the server returns
values, indexes, ranks, counts, or an existence flag.
```elixir
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.List.get_by_index("events", 0,
return_type: :value
),
Aerospike.Op.List.get_by_rank("events", -1,
return_type: :value
),
Aerospike.Op.List.get_by_value("events", "opened",
return_type: :exists
)
])
```
List write policies use mnemonic order and flag values. Defaults are the
safest choice; set policy values only when your application needs ordered-list
or write-flag behavior. Advanced compatibility callers can still pass integer
values, including `{:raw, integer}` for unnamed server values.
```elixir
Aerospike.Op.List.append("events", "clicked", policy: [order: :ordered, flags: :add_unique])
```
## Map Operations
Map operations live in `Aerospike.Op.Map`.
```elixir
key = Aerospike.key("test", "profiles", "user:stats")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{"stats" => %{"views" => 1}})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.Map.increment("stats", "views", 1),
Aerospike.Op.Map.put("stats", "updated_by", "worker-1"),
Aerospike.Op.Map.get_by_key("stats", "views",
return_type: :value
)
])
record.bins["stats"]
```
Map selectors can return keys, values, key/value pairs, indexes, ranks, counts,
or existence flags. Key selectors default to keys, value/rank selectors default
to values or key/value pairs depending on the operation; pass `return_type:`
when the desired shape matters.
```elixir
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.Map.get_by_key("stats", "views",
return_type: :value
),
Aerospike.Op.Map.get_by_rank("stats", -1,
return_type: :key_value
),
Aerospike.Op.Map.get_by_value("stats", 1,
return_type: :key
)
])
record.bins["stats"]
```
Map write policies use mnemonic order and flag values. `:order` controls map
ordering and `:flags` applies write flags when the operation supports them.
Advanced compatibility callers can still pass integer values, including
`{:raw, integer}` for unnamed server values.
```elixir
Aerospike.Op.Map.put_items("stats", %{"likes" => 2},
policy: [order: :key_ordered, flags: :update_only]
)
```
## Nested CDT Paths
Nested CDT operations use `Aerospike.Ctx` path steps through the `:ctx` option.
```elixir
key = Aerospike.key("test", "profiles", "user:nested")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{
"profile" => %{"events" => []}
})
{:ok, _record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.List.append("profile", "signed-in",
ctx: [Aerospike.Ctx.map_key("events")]
)
])
```
Context paths can move through maps and lists by key, value, index, or rank.
The operation bin is the top-level CDT bin; each context step navigates inside
that value before the operation runs.
```elixir
key = Aerospike.key("test", "profiles", "user:nested-scores")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{
"profile" => %{
"teams" => [
%{"name" => "red", "scores" => [10, 20]},
%{"name" => "blue", "scores" => [15]}
]
}
})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.List.append("profile", 25,
ctx: [
Aerospike.Ctx.map_key("teams"),
Aerospike.Ctx.list_index(0),
Aerospike.Ctx.map_key("scores")
]
),
Aerospike.Op.List.get_by_rank("profile", -1,
ctx: [
Aerospike.Ctx.map_key("teams"),
Aerospike.Ctx.list_index(0),
Aerospike.Ctx.map_key("scores")
],
return_type: :value
)
])
record.bins["profile"]
```
See `Aerospike.Op.List`, `Aerospike.Op.Map`, and `Aerospike.Ctx` for the full
operation reference.
## Bit Operations
Bit operations require Aerospike blob bins. Plain Elixir binaries are encoded
as strings by this client, so seed bit bins with `{:blob, binary}`.
```elixir
key = Aerospike.key("test", "profiles", "user:flags")
{:ok, _metadata} =
Aerospike.put(:aerospike, key, %{"flags" => {:blob, <<0>>}})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.Bit.set("flags", 0, 8, <<0b1010_0000>>),
Aerospike.Op.Bit.count("flags", 0, 8)
])
record.bins["flags"]
```
## HyperLogLog Operations
HyperLogLog operations live in `Aerospike.Op.HLL` and require server support
for HLL bins.
```elixir
key = Aerospike.key("test", "profiles", "visitors")
{:ok, _metadata} = Aerospike.put(:aerospike, key, %{"seed" => 0})
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.HLL.init("visitors", 14, 0),
Aerospike.Op.HLL.add("visitors", ["ada", "grace"], 14, 0),
Aerospike.Op.HLL.get_count("visitors")
])
record.bins["visitors"]
```
## Expression Operations
Expression operate helpers live in `Aerospike.Op.Exp` and return or write
server-side expression values.
```elixir
{:ok, record} =
Aerospike.operate(:aerospike, key, [
Aerospike.Op.Exp.read("projected", Aerospike.Exp.int_bin("score")),
Aerospike.Op.Exp.write("score_copy", Aerospike.Exp.int_bin("score"))
])
record.bins["projected"]
```
## Geo Values And Queries
Use `Aerospike.Geo` typed values for GeoJSON bins. Geo secondary-index queries
use `Aerospike.Filter.geo_within/2` or `Aerospike.Filter.geo_contains/2`.
```elixir
key = Aerospike.key("test", "places", "pdx")
point = Aerospike.Geo.point(-122.6765, 45.5231)
{:ok, _metadata} = Aerospike.put(:aerospike, key, %{"loc" => point})
{:ok, task} =
Aerospike.create_index(:aerospike, "test", "places",
bin: "loc",
name: "places_loc_geo_idx",
type: :geo2dsphere
)
:ok = Aerospike.IndexTask.wait(task)
region = Aerospike.Geo.circle(-122.6765, 45.5231, 10_000.0)
query =
Aerospike.Query.new("test", "places")
|> Aerospike.Query.where(Aerospike.Filter.geo_within("loc", region))
|> Aerospike.Query.max_records(100)
{:ok, records} = Aerospike.query_all(:aerospike, query)
```
Geo filters require matching secondary indexes. `query_all/3` and
`query_page/3` require `query.max_records`.