Skip to content

Loading Data

const everyone = await gom.loadAll(PersonCareer);
const one = await gom.load(PersonCareer, uuid); // undefined when there is none
const 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 n

count 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 relationships

A 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.

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 filter
await 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.

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.