Loading Data
Load All Instances
Section titled “Load All Instances”@Componentclass PersonService @Autowired constructor( private val graphObjectManager: StatelessGraphObjectManager) { fun getAllPeople(): List<PersonCareer> { return graphObjectManager.loadAll<PersonCareer>() }}Load by ID
Section titled “Load by ID”fun getPerson(uuid: String): PersonCareer? { return graphObjectManager.load<PersonCareer>(uuid)}count returns a Long and is consistent with loadAll — it counts exactly the objects loadAll would return for the same type and filter. There are three overloads, mirroring loadAll/deleteAll:
// 1. Count everything of a type (reified, or pass Issue::class.java)val total: Long = graphObjectManager.count<Issue>()
// 2. Count with a simple WHERE filter (aliases match loadAll: `n` for fragments,// the root fragment field name for views)graphObjectManager.count<Issue>("n.state = 'open'")
// 3. Count with the type-safe DSL (generated per-view extension injects the query object)graphObjectManager.count<RaisedAndAssignedIssue> { where { query.issue.state eq "open" }}The codegen emits INSTANCE-injecting extensions for every DSL-spec method — loadAll<T> { },
deleteAll<T> { }, count<T> { }, and loadNearest<T>(…) { } (the last only for @VectorIndex-ed
views). The reified count<T>() / loadNearest<T>(…) and the explicit ::class.java overloads also
remain (the latter for Java).
Fragments are a straight node count of the fragment’s labels.
Views count only roots that satisfy the view’s required relationships — non-optional, non-collection @GraphRelationships — so the result equals loadAll(...).size, not a naive node count. For example, RaisedAndAssignedIssue requires raisedBy (a single, non-null RAISED_BY):
graphObjectManager.count<Issue>() // 3 — every Issue nodegraphObjectManager.count<RaisedAndAssignedIssue>() // 2 — only Issues with a RAISED_BY edgeAn Issue with no RAISED_BY is a valid Issue node but is not a RaisedAndAssignedIssue, so it is excluded — just as loadAll would exclude it. Optional (nullable) and collection relationships place no such constraint.
From Java, the same three overloads apply (use .count(Issue.class) etc.); the DSL overload takes the generated query object and a lambda.
Vector Search (loadNearest)
Section titled “Vector Search (loadNearest)”loadNearest runs an approximate nearest-neighbour search and returns the hits paired with a
normalized similarity score. Like loadAll, it works on both a @GraphView — searching the
root fragment’s embedding and returning the fully-projected view — and a plain @NodeFragment,
searching and returning the bare nodes:
// Search a view: ranks PropositionViews by their root proposition's embeddingval views: List<Scored<PropositionView>> = graphObjectManager.loadNearest<PropositionView>(queryEmbedding, topK = 20)
// Search a fragment: ranks bare PropositionNodesval nodes: List<Scored<PropositionNode>> = graphObjectManager.loadNearest<PropositionNode>(queryEmbedding, topK = 20)The embedding to search is identified by a @VectorIndex annotation on the searched fragment (the
view’s root fragment, or the fragment itself) —
inferred when the fragment declares one embedding, or named explicitly to pick among
several. @VectorIndex is the query-side declaration of “this embedding is searchable” and is
independent of how the index is created: it works whether you create the index from the annotation
(SchemaCatalog.fromFragments(...)) or from an explicit spec (SchemaCatalog.of(VectorIndexSpec(...)));
in the latter case, add the annotation to the fragment as well — it carries no creation side effects
on its own.
@NodeFragment(labels = ["Proposition"])data class PropositionNode( @NodeId val id: String, val text: String, @VectorIndex(similarity = SimilarityFunction.COSINE) val embedding: List<Float>? = null,)
@GraphViewdata class PropositionView( @Root val proposition: PropositionNode, @GraphRelationship(type = "HAS_MENTION", direction = Direction.OUTGOING) val mentions: List<Mention>,)// Single embedding on the root fragment → inferred, no property argumentval hits: List<Scored<PropositionView>> = graphObjectManager.loadNearest<PropositionView>(queryEmbedding, topK = 20)
hits.forEach { println("${it.score} → ${it.value.proposition.text}") }
// Disambiguate when a node carries several embeddings, and/or floor by similaritygraphObjectManager.loadNearest<PropositionView>("titleEmbedding", queryEmbedding, topK = 20, threshold = 0.8)Each result is a Scored<T>(value, score); the score is normalized to similarity, higher = more
similar on every engine, so ordering and threshold mean the same thing regardless of backend.
topK is the index’s k, not a guaranteed result count. When searching a view, its
required relationships (and the optional threshold) are applied after the K-nearest search,
so a candidate that ranks in the top K but fails the filter is dropped — meaning loadNearest can
return fewer than topK results. A fragment search has no relationship filter, so it returns
the full top K (minus any threshold cut).
k is also the search beam width — this is the surprising part. On Lucene-backed engines the
result queue is the HNSW candidate queue, so k does not merely truncate a ranked list; it decides
how much of the graph the search explores. A small k can miss a vector that is genuinely nearest.
This is measurable, not theoretical: on a 9K-vector index, a vector verified as true global rank 3 was
not returned at any k ≤ 100, and came back at rank 3 at k = 200. Neo4j exposes no separate
ef_search, so raising k is the only query-time lever on recall.
Use searchK to widen the search without widening the result:
// search with a beam of 200, return the best 40 that survive the filtergraphObjectManager.loadNearest<PropositionView>(dsl, queryEmbedding, topK = 40, searchK = 200) { where { proposition.contextId eq ctx }}searchK is what the index is asked for; topK becomes a LIMIT applied after the filter, so
over-fetching actually recovers rows the filter would otherwise have thinned away. Omit it and the
emitted query is unchanged. searchK < topK throws — it can only lose results.
On Neo4j, over-fetching also re-ranks the beam by exact similarity before trimming: where the index quantizes, its own score is computed against quantized vectors and does not order identically to exact similarity, so trimming by it discards true matches the beam already found. Whether an index quantizes depends on the engine build unless you pin it.
Filtered searches dilute. Because predicates apply after the index yields, a scoped caller gets
roughly k × selectivity rows — at 22% selectivity, asking for 40 returns about 9 — and those
survivors are the globally-nearest that happen to be in scope, not the nearest within scope. searchK
mitigates this. See 0.0.79-vector-search-k.md.
Searching a partition (partitionLabel). When the same property is indexed per partition — one
vector index per corpus, tenant, or other scope — name the partition and the search runs inside it,
pre-filtered by construction, so k is no longer diluted by the filter:
persistenceManager.indexes.ensure(VectorIndexSpec("Corpus_abc", "embedding", 1536))
graphObjectManager.loadNearest( PropositionNode::class.java, null, queryEmbedding, topK = 40, threshold = null, searchK = null, partitionLabel = "Corpus_abc",)The label changes only which index is located: the node still carries its own :Proposition label, so
where { } and the projection are unaffected. The index name re-derives as
${label}_${property}_vector, matching what VectorIndexSpec would create — so a fragment whose
@VectorIndex pins an explicit name cannot be partitioned, and says so.
See 0.0.79-vector-partitioning.md.
Backends without a native vector index (Amazon Neptune) throw UnsupportedOperationException.
Pinning the physical index. Only dimensions and similarity_function are always emitted;
everything else about the physical index is the engine’s choice, and engine defaults change between
versions — so the same declaration can produce a different index on different servers. Pin the HNSW
graph shape portably on the annotation, and anything engine-specific on a VectorIndexSpec:
@VectorIndex(similarity = SimilarityFunction.COSINE, hnswM = 32, hnswEfConstruction = 200)val embedding: List<Float>? = null
// engine-specific options — Neo4j quantization, FalkorDB efRuntime — in a catalog specSchemaCatalog.of( VectorIndexSpec("Proposition", "embedding", 1536, engineOptions = listOf(Neo4jVectorOptions(quantizationEnabled = false))),)An unpinned parameter stays the engine’s to choose and is never reported as drift; a pinned one the engine contradicts is. The effective configuration is logged after every index creation either way. See 0.0.79-vector-index-tuning.md.
Filtering with where { }. A where { } block AND-s caller predicates into the same
post-search filter, so you can combine vector similarity with property predicates and
relationship quantifiers in one statement:
// nearest propositions in this context that mention entity X (reified form generated per @VectorIndex view)graphObjectManager.loadNearest<PropositionView>(queryVector, topK = 20) { where { proposition.contextId eq ctx proposition.status eq "active" mentions.any { resolvedId eq entityId } // any{} / none{} over a projected relationship }}The codegen emits this loadNearest<T>(vector, topK, threshold) { where { } } extension for each
@VectorIndex-bearing view (its root fragment). The filtered form works on a bare fragment too,
given the fragment’s generated query object — filter its own properties directly:
// filtered vector search over a bare @NodeFragmentgraphObjectManager.loadNearest<ChunkNode, _>(ChunkNodeQueryDsl.INSTANCE, queryVector, topK = 20) { where { query.containerSectionId eq "sec-1" }}The predicate is applied after the K-nearest search, so it really does prune — a node that ranks
in the top K but fails the predicate is dropped, and topK remains the index’s k (the result may
contain fewer rows). Property predicates filter the projected root; relationship quantifiers
(any{}/none{}) filter the projected relationship collection (any(m IN mentions WHERE …)).
Multiple any{} AND together — “mentions all of these entities” is one any{} per id. Referencing
a relationship the view does not project is an error.
Full-Text Search (loadMatching)
Section titled “Full-Text Search (loadMatching)”loadMatching is the full-text mirror of loadNearest: it finds the topK nodes most relevant to a
text query and returns them as scored, typed results — normalized to a consistent [0, 1] similarity
across engines, with no consumer Cypher and no per-engine score wrangling.
// bare @NodeFragmentval hits: List<Scored<ChunkNode>> = graphObjectManager.loadMatching<ChunkNode>("graph databases", topK = 20)
// with a floor on the normalized relevancegraphObjectManager.loadMatching<ChunkNode>("graph databases", topK = 20, threshold = 0.5)
// a @GraphView searches its root fragment's text index and returns the projected viewgraphObjectManager.loadMatching<ChunkView>("graph databases", topK = 20)The index is resolved from a @FullTextIndex on the searched fragment (property-level for a single
field, or class-level @FullTextIndex(properties = ["title", "body"]) for a multi-property index) — the
same annotation the schema feature uses to create it. threshold defaults to 0.0 (keep everything);
topK is applied as a trailing LIMIT. Each result is a Scored<T>(value, score), most relevant
first, with polymorphic dispatch (a loadMatching<SealedBase> returns each hit as its concrete subtype).
The query string is passed through to the engine’s full-text language (Lucene syntax on Neo4j:
AND/OR/"phrase"/field:term); raw user input is not escaped for you. Backends without a native
full-text index throw UnsupportedOperationException.
Filtering with where { } works exactly like loadNearest — full-text relevance plus property
predicates in one query, on a view or a bare fragment:
graphObjectManager.loadMatching<ChunkNode>("graph databases", topK = 20) { where { query.containerSectionId eq "sec-1" }}Engine note: full-text search runs on Neo4j (
db.index.fulltext), FalkorDB (db.idx.fulltext, by label), and Memgraph (text_search, GA — no experimental flag needed).

