SapixDBSapixDB/Docs
Home

Indexes

Single-Field Indexes

Without an index, every filtered query performs a full strand scan — O(n) over every record in the strand. That is fine for small datasets, but once a strand grows past 100k records, unindexed filters become the dominant latency source. A single-field index reduces that to an O(k) lookup where k is the number of matching records.

When to Add an Index

Add a single-field index when you have a strand with more than a few thousand records and you frequently filter on a specific field using equality or prefix operators. Good candidates are fields like plan, status, region, tenant_id, or any foreign-key-style reference field that your queries filter on in a tight loop.

You do not need an index if the strand is small, if you only ever retrieve records by their content hash directly, or if your filters always use operators that cannot use indexes (see the table below).

Indexes have a write costEvery insert or update that touches an indexed field requires the agent to update the index entry in addition to writing the strand record. For write-heavy workloads with very low read latency requirements, benchmark with and without the index before committing to it in production.

Creating an Index

Send a POST request to the indexes endpoint for the agent that owns the strand. The name field is a stable identifier you choose — it must be unique within the agent. The field value is the JSON key inside each strand record that you want to index.

HTTP
POST /v1/agents/users/indexes
Authorization: Bearer spx_root_YOUR_ROOT_KEY
Content-Type: application/json

{
  "name":  "idx_plan",
  "field": "plan"
}
Response (202 Accepted)
{
  "name":   "idx_plan",
  "field":  "plan",
  "type":   "single",
  "status": "building"
}

The index build runs asynchronously in the background. The agent continues serving queries and writes normally while the build progresses. You can check status at any time with the list endpoint.

Checking Build Status

HTTP
GET /v1/agents/users/indexes
Authorization: Bearer spx_root_YOUR_ROOT_KEY
Response
{
  "indexes": [
    {
      "name":   "idx_plan",
      "field":  "plan",
      "type":   "single",
      "status": "ready"
    }
  ]
}

Possible status values:

StatusMeaning
buildingThe agent is scanning existing strand records to populate the index. New writes are indexed in real-time during this phase.
readyThe index is fully built and active. Queries on the indexed field now use it automatically.
errorThe build failed. Delete the index entry and recreate it to retry.
Queries run during the buildWhile the index is in building state, queries still work — they fall back to a full scan for records not yet indexed. Once the build completes, all subsequent queries use the index. There is no read window where results could be incorrect.

Querying with an Index

You do not need to name the index in your query. The query engine inspects the filter fields and automatically selects the best available index. If an index exists for the filtered field and the operator is index-compatible, the engine uses an index scan instead of a full scan.

Query (automatic index selection)
POST /v1/agents/users/query
Authorization: Bearer spx_root_YOUR_ROOT_KEY
Content-Type: application/json

{
  "type":   "scan",
  "limit":  50,
  "filter": {
    "field": "plan",
    "op":    "eq",
    "value": "pro"
  }
}

To confirm whether a query is using the index, run an explain query first — see the Query Explain page.

Operator Compatibility

Not all filter operators can use an index. The engine uses an index only when the operator is one of the index-compatible set.

OperatorUses index?Notes
eqYesMost selective — exact match on the indexed value.
inYesPerforms one index lookup per value in the list, then unions results.
starts_withYesPrefix range scan on the index — efficient for string prefixes.
containsNoSubstring match requires scanning all values; index cannot help.
ends_withNoSuffix match requires a full scan.
likeNoPattern match — full scan only.
is_nullNoNull checks scan all records.
betweenNoRange queries require a sorted index structure not yet supported.
gt / ltNoInequality range — full scan.
Tip — use eq when you canIf your query uses gt/lt for a field with a small number of distinct values (e.g. numeric tier), consider rewriting it as an in filter with explicit values. That turns a full scan into an index scan.

Deleting an Index

Deleting an index is immediate. Queries on the affected field fall back to full scans from the moment of deletion. Write overhead for that field is also removed instantly.

HTTP
DELETE /v1/agents/users/indexes/idx_plan
Authorization: Bearer spx_root_YOUR_ROOT_KEY
Response (200 OK)
{
  "deleted": "idx_plan"
}

Multiple Indexes on One Strand

You can create multiple single-field indexes on the same strand — one per frequently-filtered field. Each index is independent. If a query filters on two indexed fields simultaneously, consider a composite index instead, which can be more efficient than two separate single-field lookups.

Create two indexes on the same strand
# Index on 'plan'
POST /v1/agents/users/indexes
{ "name": "idx_plan", "field": "plan" }

# Index on 'region'
POST /v1/agents/users/indexes
{ "name": "idx_region", "field": "region" }
Rule of thumbStart with no indexes. Use explain to identify which fields produce full_scan on your most frequent queries, then add indexes for those fields only.