NukeBase

Query Indexes

Without an index, a query is a full scan of the collection: every record is examined, and an ordered query then sorts all of them. That is fine for hundreds of records and wrong for hundreds of thousands. Declare an index and the engine binary-searches instead.

Indexes are declared in server/rules.js, next to the rules for the same path, with an "index" key holding a list of field paths, each itself an array:

rules.js — declaring indexes
module.exports = {
  "posts": {
    // Two separate single-field indexes. The inner arrays are required —
    // ["views","createdAt"] would read as ONE nested path, not two indexes.
    "index": [["views"], ["createdAt"], ["meta", "rating"]],

    "$id": { "read": "true", "write": "admin.uid == data.authorId" }
  },

  "users": {
    "index": [["city"]],
    "$uid": { "read": "true" }
  }
};

Rules for declaring an index:

  • Must sit on a concrete path — not at the root, and not under a wildcard segment. "posts": { "index": … } is valid; "posts": { "$id": { "index": … } } throws at startup.
  • Each entry is a field path array: ["views"], ["meta","rating"]. Max 8 segments deep.
  • At most 4 declared indexes per collection.
  • An invalid declaration is a startup error, not a silent no-op — you will know immediately.

What the planner does with them. The plan field in a query response tells you which path was taken:

  • "index-order" — best case. Binary-search to the start of the window, then walk limit entries. Cost is O(log n + limit) regardless of collection size. Chosen when orderBy matches an index.
  • "index-filter" — an equality or range condition matched an index, so only the candidate keys are examined instead of the whole collection.
  • "index-union" — an || query where every branch hit an index; the branches' key sets are merged.
  • "index-count" — a count: true query answered from index positions alone, without touching a single record.
  • "scan" — no usable index. Every record is examined. Check examined; if it is much larger than the number returned, this is the query to index.

Ordering by record key gets an index automatically. A paged, key-ordered query over a collection of 1,000 records or more builds a $key index on demand — you don't declare it, and $key is not a value you can pass to orderBy (omitting orderBy is how you ask for that ordering). Automatic key indexes are dropped first if the process runs out of index budget, so a declared index always wins.

Indexes are maintained incrementally as records are written, so a write costs a bounded insert or move rather than a rebuild. A write above the indexed collection invalidates it, and it is rebuilt lazily on the next query that needs it. Across the whole process, indexes hold at most 8,000,000 entries (MAX_TOTAL_INDEX_ENTRIES); an index that would take the process past that is disabled with a warning in the server log and its queries fall back to scanning.