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, ornullto 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 aCalculationthat 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 ofmatch, 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.