How we reviewed this: we ran it in Docker on Python 3.13 against the public Neo4j “Companies KG” demo database the project targets by default, with a real Jev key. We ran the unit tests offline, the integration tests against the live graph and the live API, and then drove the beam search ourselves from a node we picked.
What it does
neo4jev, by Michael Hunger — Neo4j’s community caretaker, per his profile — navigates a knowledge graph one hop at a time by asking Jev which relationship to follow next.
At each node it lists the outgoing (or incoming, or both) relationships, presents them as Choice options with their type, properties and target, and asks two questions in a single request:
- Choice: which relationship gets you closer to the goal? The probability distribution is the ranking.
- Noul: has the goal already been reached at this node, before following anything?
That combination is the whole design. Top-k and cutoff over the probabilities turn one hop into a beam search: several branches can be explored, and paths are ranked by the sum of log-probabilities, which avoids both float underflow and a bias toward short paths. The Noul is the stop signal.
Nothing about the schema is hard-coded. Labels, relationship types, property names and available indexes are read from the database at run time, so it points at any Neo4j instance. There are three goal modes: free text (“find a company that competed with Apple”), an explicit target node, and a path-intent pattern that returns several distinct paths rather than one.
Running it
The unit tests — 161 of them — passed offline. The integration tests, which need both the live graph and a live key, all passed: 11 of them, including a real hop and the step that asks Jev which properties of a label are its human-readable identity.
Then we drove the navigator ourselves, starting from Apple Inc. in the demo graph, with the goal “find a company that competes with Apple”:
start = Apple Inc.
hop 0: goal_reached_here=0.05 -[HAS_COMPETITOR]-> Organization Amazon.com, Inc. p=0.590
hop 1: goal_reached_here=0.86 (stopped, no edge followed)
That is what you want it to do: at Apple the stop signal is near zero, HAS_COMPETITOR wins the Choice over 30 candidate edges, and at Amazon the same Noul reads 0.86 and the search stops and says why. Every hop keeps its full distribution and its candidate list, so the trace explains itself — for a graph tool, that auditability is worth as much as the answer.
Where it gets rough
Each hop only sees a sample of a busy node. Relationships are capped per type (10) and in total (60 by default), filled round-robin across types so one chatty type can’t crowd out the rest. The README is upfront that this means a supernode is sampled and that the left-out edges can never be chosen. We saw exactly that: with the cap lowered to 30 and both directions enabled — so the same budget had to cover incoming edges too — the same query no longer found HAS_COMPETITOR at all, and followed HAS_CUSTOMER to Microsoft instead. The cap is a parameter, not a constant; on a dense graph, raise it or narrow the direction, and know that the answer depends on what the sample contained.
It can walk into a dead end, and says so. Our path-intent run (“from the company to the cities where its investors are based”) confidently hopped onto an Investment node, which has nowhere onward in that graph, and both branches terminated with NO_CANDIDATES. No fabricated path, no made-up city — the result says where it stopped and why. Correct behaviour, but a reminder that a one-hop-at-a-time search is greedy: it can’t know the branch is a cul-de-sac before it commits.
One sharp edge for library users. resolve_display_value() gives you a node’s readable name, but only after the label’s display properties have been derived — itself one Jev call per label. Skip that step and it falls back to “the longest short string property”, which in our first run labelled Apple Inc. as “Nasdaq (NASDAQ)” and a competitor as “American multinational technology company”. The Streamlit app derives first and is unaffected; we hit it by calling the helper directly. Derive before you display.
Smaller notes: Jupyter isn’t a declared dependency, so the notebooks need an ad-hoc environment (the README says so), and the vector index on the demo graph is filled by a placeholder embedder, so vector start-node search works dimensionally but doesn’t mean anything until you supply a real one. Both are documented.
Permissions and data
- Node properties go to TypeSafe. Each hop sends the current node, its candidate relationships with their properties and targets, and the live graph schema. Long text is truncated at 200 characters and vector properties are dropped outright — a sensible default that keeps article bodies and embeddings out of the request, and one worth checking against your own data before pointing this at a private graph.
- The default target is a public demo database. The Companies KG credentials in the README are public and not secret. Your own instance goes in
.env, which is gitignored.
- Spending is bounded per run.
max_calls (24 by default) caps the requests a navigation can make, and max_depth caps the hops. The Streamlit app exposes both as controls.
- It refuses to fake model output. Without a key, the notebooks and app still run: each call is attempted, the API’s error is displayed verbatim, and the pipeline continues on stand-in answers that are explicitly labelled as such. We wish more demos did this.
Maintenance
39 commits through September 18, MIT licensed, with a written PRD, a task list, an agents file, and a test suite that separates offline unit tests from live integration tests. It is presented as a demo, and the README’s status section says precisely which parts run end to end. Treat src/neo4jev/ as reusable code and the rest as the demonstration it says it is.
Verdict
This is the clearest worked example we’ve seen of using Jev to steer a traversal rather than to classify a document: one request per hop carrying both the ranking and the stop signal, exact rules (capping, de-duplication, scoring, beam selection) kept in code, and a trace that shows its work at every step. It found the answer we asked for on a real graph, and when we sent it somewhere with no onward edges it stopped and told us.
Before using it on your own graph, decide what the edge cap should be — that’s the parameter that decides what the model is even allowed to consider — and check what your node properties would be sending.
For other search tools, see the Search & Knowledge Graphs category, and our Best Jev Tools roundup for the wider picture.
See how it compares with other tools in Best Jev tools, tested hands-on.
Review updated Sep 20, 2026. Numbers quoted from the project are its author's own; we don't publish our own measurements of Jev.