Loading Data
const everyone = await gom.loadAll(PersonCareer);const one = await gom.load(PersonCareer, uuid); // undefined when there is noneconst open = await gom.loadAll(Issue, (q) => ({ where: q.state.eq('open') }));const raw = await gom.loadAll(Issue, 'n.state = $state', { state: 'open' }); // a predicate of your own, over ncount counts exactly what loadAll would return for the same type and filter:
await gom.count(Issue);await gom.count(Issue, (q) => ({ where: q.state.eq('open') }));await gom.count(RaisedAndAssignedIssue); // only the issues that meet the view's required relationshipsA fragment is counted by its labels. A view counts only the roots that meet its required relationships and paths, so
the number equals loadAll(...).length, not a count of nodes.
Vector Search (loadNearest)
Section titled “Vector Search (loadNearest)”loadNearest runs an approximate nearest-neighbour search and returns each hit with a similarity score. It works on a
view, by its root’s embedding, and on a fragment:
@NodeFragment({ labels: ['Proposition'] })export class PropositionNode { @NodeId() id: string; text: string; @VectorIndex({ similarity: 'COSINE' }) embedding?: number[];}
const hits: Scored<PropositionView>[] = await gom.loadNearest(PropositionView, queryEmbedding, { topK: 20 });hits.forEach((it) => console.log(it.score, it.value.proposition.text));The searched embedding is the fragment’s @VectorIndex field: inferred when there is one, and named with property
when there are several. The score is a similarity, higher meaning more similar, on every engine, so threshold means
the same thing everywhere: (1 + cos) / 2 for a cosine index, from 1 for the same direction down to 0 for the opposite
one, and 1 / (1 + d²) for a Euclidean index.
Memgraph’s cosine scores changed after 3.2.0 for vectors more than 90 degrees from the query. Drivine read Memgraph’s
similarity, which for a cosine index is the absolute value of 1 - distance (memgraph/memgraph#5199), so a cosine of
-0.6 scored 0.8, as 0.6 does, and opposite vectors scored 1. It now reads the distance, so they score 0.2 and 0, as on
every other store. Scores for a positive cosine, and every Euclidean score, are unchanged.
| Option | Meaning |
|---|---|
topK |
How many to return. Without searchK, also how many the index is asked for. |
searchK |
How many the index is asked for, at least topK. topK then trims after the filter. |
threshold |
The least score a hit may have. |
where |
A condition applied after the search: references, or a predicate over n with bindings. |
partitionLabel |
Search the index of another label over the same property: one index per corpus or tenant. |
property |
Which @VectorIndex field, when the fragment has several. |
depth |
maxDepth overrides for a view. |
topK is the index’s k, not a promised count. A view’s required relationships, threshold and where filter
after the search, so a search can return fewer than topK.
k is also the search’s beam. On Lucene-backed engines the result queue is the HNSW candidate queue, so a small
k can miss a vector that really is nearest. searchK widens the search without widening the result:
// search with a beam of 200, and return the best 40 that pass the filterawait gom.loadNearest(PropositionView, queryEmbedding, { topK: 40, searchK: 200, where: (q) => [q.proposition.contextId.eq(ctx), q.mentions.any((m) => m.resolvedId.eq(entityId))]});A filtered search dilutes: at 22% selectivity, 40 asked for returns about 9, the nearest overall that happen to pass
rather than the nearest of those that pass. searchK mitigates that. A per-partition index avoids it: with one vector
index for each partition label, partitionLabel searches inside the partition, which is filtered by construction.
Full-Text Search (loadMatching)
Section titled “Full-Text Search (loadMatching)”loadMatching is the full-text mirror of loadNearest. The relevance is scaled to [0, 1], the best hit being 1:
@NodeFragment({ labels: ['Chunk'] })export class ChunkNode { @NodeId() id: string; @FullTextIndex() text: string;}
await gom.loadMatching(ChunkNode, 'graph databases', { topK: 20 });await gom.loadMatching(ChunkView, 'graph databases', { topK: 20, threshold: 0.5, where: (q) => q.chunk.sectionId.eq('s1') });The index is the fragment’s @FullTextIndex: on a property for one field, or on the class,
@FullTextIndex({ properties: ['title', 'body'] }), for several. Name one with property when the fragment declares
more than one. The text is the engine’s full-text language (Lucene on Neo4j): escape user input yourself.
Full-text scores on Postgres, SQLite and Oracle without Oracle Text changed after 3.4.1. A hit scored by how often
the query’s terms occur in it. It now scores by BM25, as Neo4j’s index scores it (see
Postgres): a rare term outweighs a common one, and a short text outranks a long one
that holds a term as often. A term written again at once, kiwi kiwi kiwi, is counted each time, where it was counted
every other time. A prefix* adds the same to every hit that holds it, as on Neo4j. The hits can come in another
order, and a threshold can keep others.
topK bounds the index too. The scores are scaled over the hits the index yields that have every one of the
root’s labels, and each hit is held to do it, so on Neo4j, Memgraph and FalkorDB the index is asked for its best topK
hits: a common word that matches most of a corpus costs no more than those. A node with the indexed label alone is no
root, so it is dropped before the scores are scaled, and the best hit a search can return scores 1 on every store.
threshold keeps the best hits, so it changes nothing here. A where, like a view’s required relationships and paths,
or a label beyond the first (the index is over the first), is applied after the index yields; the labels are checked
before the scores are scaled, the rest after. Where any of them filters, the index is asked for
topK × FULL_TEXT_FILTERED_OVER_FETCH (10) hits and the filter keeps the best topK of those. A filter that passes
fewer than one in ten of the best hits can then return fewer than topK while the index holds more that pass it.
Memgraph’s index is asked for no more than 1,000,000 hits, however large topK is.

