Skip to main content

Indexes & explain

Indexes are created per collection field and are persisted on disk. They speed up queries like:

  • { email: "a@b.com" }
  • { age: { $gte: 18 } }
  • { status: { $in: ["active", "trial"] } }

db.createIndex(collection, field, options?)

createIndex(collection: string, field: string, options?: { unique?: boolean; include?: string[] }): Promise<void>
  • Builds the index from existing documents.
  • After creation, writes keep the index updated.
  • unique: true enforces uniqueness for that field.
  • include: string[] creates a covering index. Included fields (plus _id) are stored in the index so projected queries can avoid primary document reads.

Example: create a unique index

import { LioranManager } from "@liorandb/core";

const manager = new LioranManager({ rootPath: "./.liorandb" });
const db = await manager.db("app");
const users = db.collection("users");

await db.createIndex("users", "email", { unique: true });
await users.insertOne({ email: "a@b.com" });

console.log(await users.findOne({ email: "a@b.com" }));
await manager.close();
Sandbox output (example)
{ _id: "...", email: "a@b.com", __v: 1 }

What happens on duplicates? (unique index)

If you insert a second doc with the same indexed value, the write is rejected.

Sandbox output (example)
Error: Unique constraint violation on index "email"

How LioranDB uses indexes

  • Query execution tries to pick one best index from your query.
  • If no index can be selected, a full scan is used.
  • Even with an index, documents are still re-checked against your full query to ensure correctness.

Text indexes are optional and are used by the $text query operator.

db.createTextIndex(collection, field, options?)

createTextIndex(collection: string, field: string, options?: { normalize?: boolean; stopwords?: string[] }): Promise<void>
  • normalize (default: true): split on non-alphanumeric and lowercase (recommended).
  • stopwords: optional words to ignore.

Querying: $text

$text pre-filters candidates (AND semantics) and then the query continues as usual.

// search all text-indexed fields
await users.find({ $text: { $search: "hello world" } });

// search only specific fields
await users.find({ $text: { $search: "hello world", $fields: ["title", "body"] } });

If you use $text without any text index configured, the query throws UNSUPPORTED_QUERY.

db.explain(collection, query?, options?)

Use explain() to see what the planner did and how much work the query performed.

const plan = await db.explain("users", { age: { $gte: 18 } });
console.log(plan);
Sandbox output (example)
{
indexUsed: "age",
indexType: "btree",
scannedDocuments: 42,
returnedDocuments: 7,
executionTimeMs: 3,
usedFullScan: false,
candidateDocuments: 42
}

Example: compare indexed vs full scan

import { LioranManager } from "@liorandb/core";

const manager = new LioranManager({ rootPath: "./.liorandb" });
const db = await manager.db("app");
const users = db.collection("users");

await users.insertMany([
{ email: "a@b.com", age: 21 },
{ email: "c@d.com", age: 35 },
{ email: "e@f.com", age: 10 },
]);

const before = await db.explain("users", { age: { $gte: 18 } });
await db.createIndex("users", "age");
const after = await db.explain("users", { age: { $gte: 18 } });

console.log({ before: before.usedFullScan, after: after.usedFullScan });
await manager.close();
Sandbox output (example)
{ before: true, after: false }