Skip to content

feat: add search.node/search.node_all query-module procedures#4460

Merged
imilinovic merged 18 commits into
masterfrom
feat/search-node-parity
Jul 23, 2026
Merged

feat: add search.node/search.node_all query-module procedures#4460
imilinovic merged 18 commits into
masterfrom
feat/search-node-parity

Conversation

@imilinovic

@imilinovic imilinovic commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Adds search.node and search.node_all query-module procedures that find nodes by a {label: property} map, a comparison operator, and a value.

What. CALL search.node(labelPropertyMap, operator, value) YIELD node returns nodes whose property satisfies the operator. A property list per label is a disjunction (results unioned). search.node deduplicates by node id; search.node_all keeps duplicates. Operators: =/exact, <>, <, <=, >, >=, starts with, ends with, contains, =~. The label-property map is accepted as a map or a JSON string (parsed with nlohmann_json); value is a nullable string (null → no rows); operators are trimmed and case-insensitive.

How. Each (label, property) pair is turned into a Cypher query run through the interpreter (mgp_execute_query), so the planner selects the access path (label-property index where available, label scan otherwise) and each search runs in its own transaction. Going through the interpreter also enforces label- and property-level read permissions natively via the evaluator.

Why. Delegating to the interpreter is faster than a hand-rolled scan in nearly every case and inherits future planner improvements. Note: the planner does not yet use the label-property index for STARTS WITH (implemented in #4464) that operator currently falls back to a label scan + filter.

Kernel-free — no changes outside mage/ and config/mappings.json. An earlier revision added a label-property range mgp C API; that has been split out to #4465 and is not required by these procedures.

… mgp API

Add an mgp C API for label-property index range lookups and a MAGE module
that finds nodes by a label->property(s) map plus an operator and value.

Kernel:
- mgp_graph_has_label_property_index: check a ready label-property index exists.
- mgp_label_property_range: opaque builder (make/add_property/destroy) with an
  mgp_bound_type enum; NULL bound = unbounded. Kept opaque for C-ABI stability
  across a future composite-index lookup.
- mgp_graph_vertices_by_label_property_range: index-only range scan returning a
  mgp_vertices_iterator (enforces label-level read auth like plain iteration).
- C++ wrappers in mgp.hpp (Graph::HasLabelPropertyIndex,
  Graph::NodesByLabelPropertyRange) and _mgp.hpp.

Module (search.node, search.node_all):
- Property list per label is a disjunction; results unioned. search.node dedups
  by node id, search.node_all keeps duplicates.
- Index fast path for =, <, <=, >, >=, starts with (prefix successor bound);
  scan fallback for contains, ends with, =~, <> and when no index exists.
- One shared comparison predicate across both paths; only string-typed
  properties match (except <>); value is a nullable string (null -> no rows);
  operators are trimmed and case-insensitive; =~ uses std::regex_match.
- Accepts the label-property map as a map or a JSON string.
- Name-map the equivalent identifiers in config/mappings.json.
- e2e tests under mage/tests/e2e/search_test.
@imilinovic

imilinovic commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Tracking

  • [Link to Epic/Issue]

Standard development

CI Testing Labels

  • Select the appropriate CI test labels (CI -build=build-name -test=test-suite)

Documentation checklist

  • Add the documentation label
  • Add the bug / feature label
  • Add the milestone for which this feature is intended
    • If not known, set for a later milestone
  • Write a release note, including added/changed clauses
    • New search.node and search.node_all procedures find nodes by a {label: property} map, a comparison operator, and a value; a property list per label is OR-ed, search.node de-duplicates by node while search.node_all keeps duplicates. Use them to look up nodes by property value without writing the equivalent MATCH/WHERE; an existing label-property index is used automatically. #4460
  • [ Documentation PR link Add search module documentation (search.node, search.node_all) documentation#1698 ]
    • Is back linked to this development PR

@imilinovic imilinovic self-assigned this Jul 21, 2026
@imilinovic imilinovic added Docs needed Docs needed feature feature labels Jul 21, 2026
@imilinovic imilinovic added this to the mg-v3.13.0 milestone Jul 21, 2026
Address review findings on search.node/search.node_all:
- destroy the range spec exactly once in Graph::NodesByLabelPropertyRange
  (the prior structure could double-free on the null-iterator path)
- guard the index-only range scan with a ready-index check so a missing
  index fails cleanly instead of relying on a DMG_ASSERT compiled out in
  Release
- compile the =~ pattern once per call instead of once per scanned node
- drop a redundant registration comment
- add e2e coverage for the JSON-string map form and an indexed comparison
  operator
@imilinovic
imilinovic force-pushed the feat/search-node-parity branch from 5420773 to a7da8d6 Compare July 21, 2026 20:49
Replace the hand-rolled JSON parser for the label-property map string form
with nlohmann_json (fetched into the MAGE build like fmt/googletest, pinned
to v3.11.3). Behaviour is unchanged for valid input (object of string or
array-of-string values; empty object yields no rows) and malformed input now
reports precise parse errors.
Reimplement search.node and search.node_all to build a query per
(label, property) pair and run it through the interpreter
(mgp_execute_query) instead of driving the label-property index-scan
mgp API directly.

This lets the planner choose the access path, which is faster in almost
every case (label-scoped scans and plan-time-resolved property reads,
versus a full all-nodes scan with a per-node property getter), and runs
each search in its own transaction. Results are unchanged across all
operators, the disjunction/dedup semantics, the null-value and
invalid-operator edge cases, and the map/JSON-string input forms.

The one access path the planner does not yet cover is a prefix range
for `starts with`; it currently falls back to a label scan plus filter.
Teaching the planner to use the label-property index for `starts with`
is left to a separate change.
search.node/node_all now run through the query interpreter and no
longer use the label-property index range mgp API, so remove that API
from this branch; it lives in its own change. Also drop the
nlohmann_json dependency, which nothing links anymore.
…reter path

The move to the query interpreter accidentally reinstated the hand-rolled
JSON parser and dropped nlohmann_json. Restore nlohmann_json parsing for the
label-property map's JSON-string form (and its build dependency) on top of the
interpreter-based execution, so both improvements stand together.
@imilinovic imilinovic changed the title feat: add search.node/search.node_all with label-property index range mgp API feat: add search.node/search.node_all query-module procedures Jul 22, 2026
search.node/node_all ran a sub-query per (label, property) and emitted the
resulting node accessors directly. Those accessors are bound to the sub-query's
transaction, which is destroyed when its result is freed, so reading a returned
node's labels or properties was a use-after-free.

Return id(n) from the sub-query and re-resolve each id against the caller's
graph (guarded by ContainsNode) so emitted nodes live in a transaction that
outlives result consumption. Collapse the duplicate result-field constant.
Add e2e cases for non-string properties under text operators, <> across types,
codepoint string ordering (uppercase before lowercase), multi-label dedup for
node vs node_all, empty map / empty property list, empty value, the JSON
list form, and an unknown-operator error; extend the comparison case with a
name past the lower bound.
STARTS WITH / ENDS WITH / CONTAINS raise on a non-string property, and the
sub-query's error is reported as end-of-results, so a search over a property of
mixed types silently truncated. Guard these operators with a CASE that only
applies them to string values, so a non-string property yields no match rather
than raising. The comparison operators already handle non-string operands
(no match, except <> which matches), so they are left unguarded.

Also drop an unreachable zero-column check and note the intended move to the
caller's transaction.
Cover non-string properties under text operators, <> across types, codepoint
string ordering, multi-label dedup for node vs node_all, empty map / empty
property list, empty value, the JSON list form, and an unknown-operator error.
Condense multi-line comments to single lines where the extra prose was
redundant, keeping the load-bearing rationale (transaction lifetime,
text-operator guard) intact.
Add a >= string case-ordering case (lowercase >= 'a', uppercase sorts before),
matching Neo4j+APOC. Remove test_multi_label_node_all (its coverage is the
intersection of test_node_all_duplicates and test_multi_label_node), and drop
the no-op ORDER BY from single-row tests.
`>= 'Z'` is more illustrative than `>= 'a'`: an uppercase bound pulls in every
lowercase letter (Z=90 < a=97 < z=122) plus Z-prefixed names, while excluding
earlier-uppercase names. Verified against Neo4j+APOC.
A null label-property map is rejected (only a null value is allowed, yielding
0 rows), matching Neo4j+APOC.
Remove comments that just restated the code (the operator-map, identifier-escape
and JSON-parse helpers are self-evident) and drop external-system references
from the remaining rationale comments.
@imilinovic
imilinovic requested a review from as51340 July 22, 2026 21:40
@imilinovic
imilinovic marked this pull request as ready for review July 22, 2026 21:40
Copilot AI review requested due to automatic review settings July 22, 2026 21:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Spell out that STARTS WITH / ENDS WITH / CONTAINS raise on a non-string
property (surfacing here as a silently truncated result), and why the guard
must be a CASE rather than an AND: the planner does not preserve AND operand
order, so the type check is not guaranteed to run before the operator.
@imilinovic
imilinovic force-pushed the feat/search-node-parity branch from d02e9d8 to 920c125 Compare July 22, 2026 21:43
…nter sees both

The query-module count smoke test statically scans source for
module_add_read_procedure call sites. Registering both procedures
through a lambda invoked twice left a single call site, so the scanner
counted one procedure while the runtime registered two, failing the
count assertion.
Comment thread mage/cpp/search_module/algorithm/search.cpp Outdated
Comment thread mage/cpp/search_module/algorithm/search.cpp
Comment thread mage/cpp/search_module/algorithm/search.cpp Outdated
Address review feedback on search.cpp:
- Extract the canonical Cypher operators into an `op::` constant namespace,
  reused instead of scattered string literals.
- Add IsStringOnlyOperator() so the string-only guard lives beside the
  operator constants instead of being duplicated in Run().
- Build the sub-query with fmt::format instead of chained `+` to avoid the
  intermediate temporary strings.
@sonarqubecloud

Copy link
Copy Markdown

@imilinovic
imilinovic added this pull request to the merge queue Jul 23, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 23, 2026
@imilinovic
imilinovic added this pull request to the merge queue Jul 23, 2026
Merged via the queue into master with commit c6e0034 Jul 23, 2026
38 checks passed
@imilinovic
imilinovic deleted the feat/search-node-parity branch July 23, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants