Search indexes bolted on the side — secondary indexes for full-text, range and vector seeks

Index-free adjacency only serves EXPAND, so finding start nodes by value — full-text, range, geo, vector — is served by classic secondary indexes (Lucene, B-tree) maintained as separate structures riding shotgun, with the usual write-amplification and staleness costs.

Previously

Sharding placed the edges; it never solved finding the anchor by value. That job goes to classic indexes — full-text, B-tree, vector — bolted on the side, feeding start nodes into the native pointer-chase, with their own staleness costs. So now we have the complete picture for LOCAL work: SEEK an anchor, then EXPAND cheaply. But every query we've built lights up only a few nodes. What about the questions that need the WHOLE graph?

Scene 12

Search indexes bolted on the side

  1. Watch
  2. Try it
  3. Predict
  4. Capture
SECONDARY INDEXES (bolted on the side)NATIVE CORE (EXPAND only)native pointer-chasestore (EXPAND)SEEK ← anchor idindex-free adjacency · O(1)/hopEach search box hands ONE start-node id to the core — then the native pointer-chase takes over (EXPAND).
What to watch for

The native pointer-chase store sits in the center — that's the index-free adjacency you built: follow edges you already hold, O(1) per hop. But it can ONLY expand from a node you already have. To start 'from the product whose description says cordless drill', or 'from users aged 30-40', or 'from the photo most similar to this embedding', the engine asks a SEPARATE box on the side. Watch each search box light up and hand exactly one start-node id into the core's SEEK — then the native walk takes over.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Query.run
value-search SEEKs an anchor (side index), then EXPANDs natively
def run(query):
# SEEK: find the start node BY VALUE — not possible natively
index = pick_secondary_index(query.predicate) # lucene | btree | vector
start_id = index.lookup(query.value) # value -> node id
# EXPAND: index-free adjacency, O(1) per hop
node = store.load(start_id)
return traverse(node, query.pattern) # follow pointers
Store.write
a write must fan out to every secondary index (amp / staleness)
def write(node, change):
store.apply(change) # the core mutation
for index in secondary_indexes:
if index.sync:
index.reindex(node) # on the write path -> write-amp
else:
enqueue_async(index, node) # lags -> stale window
Planner.pickIndex
each value predicate routes to its OWN side structure — none of it is the native walk
def pick_index(predicate):
# the core can only EXPAND, so route by value-kind
if predicate.kind == TEXT:
return lucene # words -> node ids (free-text)
if predicate.kind == RANGE:
return btree # value/range -> node ids
if predicate.kind == VECTOR:
return vector # nearest embedding -> node ids
raise NoIndex # else: full label scan
Index.lookup
a side index returns whatever it currently maps — fresh only if it re-applied the last write
def lookup(value):
# this structure is maintained SEPARATELY from the core
node_id = self.map.get(value) # value -> node id
# if a write hasn't been re-applied here yet,
# self.map still holds the pre-write entry
return node_id # may point at a since-changed node

Where this sits in Build a graph database (Neo4j / Dgraph-style)

Scene 12 of 16, in the Scale & ACID act — ACID on one box; the partition cut turns hops to RPCs.. Index-free adjacency only serves EXPAND, so finding start nodes by value — full-text, range, geo, vector — is served by classic secondary indexes (Lucene, B-tree) maintained as separate structures riding shotgun, with the usual write-amplification and staleness costs.

Up next. Every mechanism so far — index-free adjacency, anchors, sharding, bolted-on search — optimizes touching a FEW nodes near a start point. But PageRank, connected components, global counts must touch EVERY vertex and edge, repeatedly. There's no locality to exploit, so index-free adjacency buys nothing. The payoff inverts, and a completely different model takes over: think like a vertex.

All 16 scenes in Build a graph database (Neo4j / Dgraph-style) · Every curriculum

Built with Arqly
Every scene in Build a graph database (Neo4j / Dgraph-style) builds on the one before it.All 16 Build a graph database (Neo4j / Dgraph-style) scenes