# Secondary Indexes

A secondary index (SI) lets you query records by bin value rather than primary key. The server
maintains the index on a per-namespace, per-set, per-bin basis and updates it automatically as
records change.

Secondary indexes are complementary to [`Aerospike.Query`](Aerospike.Query.html) — once an index exists, pass
[`Aerospike.Filter`](Aerospike.Filter.html) predicates in your query to use it.

## Creating an Index

[`Aerospike.create_index/4`](Aerospike.html#create_index/4) sends the creation command to the server. Index building happens
asynchronously in the background:

```elixir
{:ok, task} =
  Aerospike.create_index(:aero, "test", "users",
    bin: "age",
    name: "users_age_idx",
    type: :numeric
  )
```

Required options:

| Option | Type | Description |
|--------|------|-------------|
| `:bin` | `string` | The bin name to index |
| `:name` | `string` | A unique name for this index |
| `:type` | atom | `:numeric`, `:string`, or `:geo2dsphere` |

## Waiting for the Index to Be Ready

[`create_index/4`](Aerospike.html#create_index/4) returns an [`%Aerospike.IndexTask{}`](Aerospike.IndexTask.html). Use [`IndexTask.wait/2`](Aerospike.IndexTask.html#wait/2) to block until
the server reports the index is fully built:

```elixir
:ok = Aerospike.IndexTask.wait(task, timeout: 30_000)
```

Or poll without blocking:

```elixir
case Aerospike.IndexTask.status(task) do
  {:ok, :complete}     -> IO.puts("index ready")
  {:ok, :in_progress}  -> IO.puts("still building...")
  {:error, err}        -> IO.puts("check failed: #{err.message}")
end
```

[`IndexTask.wait/2`](Aerospike.IndexTask.html#wait/2) polls [`status/1`](Aerospike.IndexTask.html#status/1) in a loop. Options:

| Option | Default | Description |
|--------|---------|-------------|
| `:timeout` | no limit | Maximum time to wait in milliseconds |
| `:poll_interval` | 1000 | Milliseconds between status checks |

## Index Types

### Numeric

Index integer or float bins. Use with [`Filter.range/3`](Aerospike.Filter.html#range/3) or [`Filter.equal/2`](Aerospike.Filter.html#equal/2) (integers only):

```elixir
{:ok, task} =
  Aerospike.create_index(:aero, "test", "metrics",
    bin: "score",
    name: "metrics_score_idx",
    type: :numeric
  )

:ok = Aerospike.IndexTask.wait(task)
```

### String

Index string bins. Use with [`Filter.equal/2`](Aerospike.Filter.html#equal/2):

```elixir
{:ok, task} =
  Aerospike.create_index(:aero, "test", "users",
    bin: "email",
    name: "users_email_idx",
    type: :string
  )

:ok = Aerospike.IndexTask.wait(task)
```

### Geo2DSphere

Index GeoJSON point or region bins for geospatial queries:

```elixir
{:ok, task} =
  Aerospike.create_index(:aero, "test", "locations",
    bin: "coords",
    name: "locations_coords_idx",
    type: :geo2dsphere
  )

:ok = Aerospike.IndexTask.wait(task)
```

Once the index is ready, geospatial predicates can use typed
[`Aerospike.Geo`](Aerospike.Geo.html) structs:

```elixir
alias Aerospike.{Filter, Geo, Query}

point = Geo.point(-122.5, 45.5)
region = Geo.circle(-122.5, 45.5, 5_000)

{:ok, matches} =
  Query.new("test", "locations")
  |> Query.where(Filter.geo_within("coords", region))
  |> Query.max_records(10_000)
  |> then(&Aerospike.all(:aero, &1))

{:ok, containing_regions} =
  Query.new("test", "regions")
  |> Query.where(Filter.geo_contains("shape", point))
  |> Query.max_records(10_000)
  |> then(&Aerospike.all(:aero, &1))
```

Raw GeoJSON strings remain supported for backward compatibility:

```elixir
region_json = "{\"type\":\"AeroCircle\",\"coordinates\":[[-122.5,45.5],5000]}"

Query.new("test", "locations")
|> Query.where(Filter.geo_within("coords", region_json))
|> Query.max_records(10_000)
```

## Collection Index Types

When the indexed bin holds a List, Map key, or Map value, pass `:collection` to index the
inner values rather than the bin container itself:

```elixir
# Index every integer in a list bin named "tags"
{:ok, task} =
  Aerospike.create_index(:aero, "test", "articles",
    bin: "tags",
    name: "articles_tags_idx",
    type: :numeric,
    collection: :list
  )

# Index map keys (strings)
{:ok, task} =
  Aerospike.create_index(:aero, "test", "profiles",
    bin: "preferences",
    name: "profiles_prefs_keys_idx",
    type: :string,
    collection: :mapkeys
  )

# Index map values (integers)
{:ok, task} =
  Aerospike.create_index(:aero, "test", "scores",
    bin: "game_scores",
    name: "scores_game_idx",
    type: :numeric,
    collection: :mapvalues
  )
```

| `:collection` value | What is indexed |
|---------------------|----------------|
| `:list` | Every element in a list bin |
| `:mapkeys` | Every key in a map bin |
| `:mapvalues` | Every value in a map bin |

## Querying with an Index

Pair secondary indexes with [`Aerospike.Query`](Aerospike.Query.html) and [`Aerospike.Filter`](Aerospike.Filter.html). Execute with [`Aerospike.all/3`](Aerospike.html#all/3) (or [`stream!/3`](Aerospike.html#stream!/3), [`page/3`](Aerospike.html#page/3), etc.); [`all/3`](Aerospike.html#all/3) requires [`Query.max_records/2`](Aerospike.Query.html#max_records/2).

```elixir
alias Aerospike.{Filter, Query}

# Range query — requires a numeric index on "age"
{:ok, records} =
  Query.new("test", "users")
  |> Query.where(Filter.range("age", 18, 65))
  |> Query.max_records(10_000)
  |> then(&Aerospike.all(:aero, &1))

# Equality query on a string index
{:ok, records} =
  Query.new("test", "users")
  |> Query.where(Filter.equal("email", "user@example.com"))
  |> Query.max_records(10_000)
  |> then(&Aerospike.all(:aero, &1))
```

### Combining Index Filters with Expression Filters

Index filters narrow the server-side record set; expression filters refine it further without
requiring an index:

```elixir
alias Aerospike.{Exp, Filter, Query}

query =
  Query.new("test", "users")
  |> Query.where(Filter.range("age", 18, 65))
  |> Query.filter(Exp.eq(Exp.str_bin("status"), Exp.val("active")))
  |> Query.max_records(10_000)

{:ok, records} = Aerospike.all(:aero, query)
```

The index filter runs server-side first (fast path), then the expression filter applies to
the smaller result set.

## Dropping an Index

```elixir
:ok = Aerospike.drop_index(:aero, "test", "users_age_idx")
```

[`drop_index/3`](Aerospike.html#drop_index/3) returns `:ok` whether or not the index existed.

## Full Example: Build, Query, Drop

```elixir
alias Aerospike.{Filter, IndexTask, Query}

# Insert test data
for i <- 1..1000 do
  key = Aerospike.key("test", "users", "user:#{i}")
  Aerospike.put(:aero, key, %{"age" => i, "name" => "user#{i}"})
end

# Create and wait for index
{:ok, task} =
  Aerospike.create_index(:aero, "test", "users",
    bin: "age",
    name: "users_age_idx",
    type: :numeric
  )

:ok = IndexTask.wait(task, timeout: 60_000)

# Query using the index
{:ok, records} =
  Query.new("test", "users")
  |> Query.where(Filter.range("age", 18, 30))
  |> Query.max_records(10_000)
  |> then(&Aerospike.all(:aero, &1))

IO.puts("Found #{length(records)} users aged 18–30")

# Tear down the index
:ok = Aerospike.drop_index(:aero, "test", "users_age_idx")
```

## Error Handling

```elixir
case Aerospike.create_index(:aero, "test", "users", bin: "age", name: "age_idx", type: :numeric) do
  {:ok, task} ->
    case IndexTask.wait(task, timeout: 30_000) do
      :ok ->
        IO.puts("index ready")

      {:error, %Aerospike.Error{code: :timeout}} ->
        IO.puts("timed out waiting — index may still be building")

      {:error, err} ->
        IO.puts("polling failed: #{err.message}")
    end

  {:error, err} ->
    IO.puts("create_index failed: #{err.message}")
end
```

## Notes

- Index names must be unique within a namespace.
- Creating an index with the same name on a different bin or type returns an error.
- Indexes persist across server restarts.
- Dropping an index that does not exist returns `:ok` (idempotent).
- Background index builds do not block writes — data written during the build is captured.

## Next Steps

- [`Aerospike.IndexTask`](Aerospike.IndexTask.html) — polling task struct reference
- [`Aerospike.Filter`](Aerospike.Filter.html) — index filter predicates ([`equal/2`](Aerospike.Filter.html#equal/2), [`range/3`](Aerospike.Filter.html#range/3))
- [Queries and Scanning](queries-and-scanning.md) — full query API, pagination, partition filters
