Skip to main content

Custom Index Types

An index type is a class the optimizer can use to turn a scan into a lookup. You pass the class to createIndex, and the optimizer applies it wherever it fits.

What an index has to answer​

An Index implementation answers three questions:

  • match(expressions) takes the expressions appearing in a query and returns the ordered positions of the ones this index can serve, or null to decline. This is where you reject predicates you cannot answer: MapIndex, for instance, matches equality and nothing else.
  • createAccessor(expressions) takes the expressions that matched and returns a Calculation that looks up the matching items.
  • reindex(values) takes the source items together with their evaluated index-expression keys and (re)fills your data structure.

The API reference has the exact signatures. An index does not have to store anything: the Cypher ConnectionIndex keeps no data structure of its own. It recognizes join conditions between nodes and relationships and resolves the connected elements through the graph adapter.

Start from MapIndex​

Most of the work in an index is the plan-side plumbing that match and createAccessor do. If your index is a variation on hash lookup, subclass MapIndex and override how keys are formed rather than reimplementing the interface.

MapIndex funnels every index key through a normalizeKey hook, on both the write and the read path, so a case-insensitive index is a two-line override:

import { DortDB, MapIndex } from '@dortdb/core';
import { defaultRules } from '@dortdb/core/optimizer';
import { SQL } from '@dortdb/lang-sql';

class CaseInsensitiveIndex extends MapIndex {
protected override normalizeKey(value: unknown): unknown {
const key = super.normalizeKey(value);
return typeof key === 'string' ? key.toLowerCase() : key;
}
}

const db = new DortDB({
mainLang: SQL(),
optimizer: { rules: defaultRules },
});

db.registerSource(
['users'],
[
{ id: 1, name: 'Alice' },
{ id: 2, name: 'BOB' },
{ id: 3, name: 'carol' },
],
);

db.createIndex(['users'], ['name'], CaseInsensitiveIndex);

db.query("SELECT users.id AS id FROM users WHERE users.name = 'bob'");
// data: [{ id: 2 }], matched through the index despite the case difference

Because normalizeKey runs in reindex as well as in the accessor, both sides of the comparison are lowered and the lookup is still a single hash probe.

Accelerating joins too​

An index can also speed up joins over streams that are not themselves indexed. Implement the HashJoinIndex additions:

  • a static canIndex(expressions), the join-side analogue of match, deciding whether the index applies to a join's expressions; and
  • allValues(), which iterates every stored value. Full outer joins need it to emit non-matching rows.

Then register the class with the executor:

new DortDB({
mainLang: SQL(),
optimizer: { rules: defaultRules },
executor: { hashJoinIndices: [MapIndex, CaseInsensitiveIndex] },
});

MapIndex implements both roles, so the subclass above inherits its hash-join behavior along with everything else. It is the reference implementation to read when you do need to write an index from scratch.