Skip to content

Plan: semantic search without jarvis — strut keeps the graph's vectors - #138

Merged
Evanfeenstra merged 3 commits into
mainfrom
docs/semantic-search-plan
Oct 11, 2026
Merged

Evanfeenstra merged 3 commits into
mainfrom
docs/semantic-search-plan

Conversation

@Evanfeenstra

@Evanfeenstra Evanfeenstra commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

A plan only: plans/semantic-search.md. No code.

What it answers. Why graph/graph-search finds nothing by meaning on swarm38, so a seeded explore answers from grep alone, and what strut needs so semantic search works from strut itself on any Neo4j 5, with no jarvis.

Findings (swarm38, 2026-10-11).

  • Code nodes come from stakgraph's ingest over bolt. They got a Domain_* label and a vector only from one-off jarvis migrations. Nothing has embedded a code node since April: 10,261 of hive's 13,581 Functions have neither. Graph-wide, 206k of 985k nodes have no vector, 132k of them Turn.
  • The vectors that do exist embed the name alone (_search_fields_used: ["name"]; a route handler's text is "POST"). swarm38's live Function Schema has index: "name". jarvis's library, and strut's bundled snapshot of it, have said [name, summary, description] since 2026-04-10. Schema seeding only adds, so 47 live types trail the library.
  • Execution records are embedded too: 410,738 Turn vectors of tool names ("shell"), and strut's own writer embeds every StrutToolCall it projects (37,128).
  • Search reads the domain vector indexes, which select nodes by the Domain_* label. It never reads text_embeddings_vector_index, the index over every node that strut's own seed creates. Neo4j 5.19 has no filtered vector search.

The shape: one source of truth, one rule, one repair job.

  • The live Schema's index decides a node's text. strut brings stale index lists up to the bundled library by writing that one property directly. It only adds fields, and a re-run changes nothing.
  • A background pass in strut embeds every marked node whose vector is missing, or misses a field that now has a value. It writes what strut's writer would have written, including the domain label jarvis's search needs while jarvis is on the graph.
  • Search reads the one vector index, with type, domain and hidden-domain filters in the query, and over-fetches to make up for the filter.

Revised after review (same day).

  • Decided: Turn, StrutToolCall and StrutTurn get no vector; they're too fine-grained. The mark lives in strut's code while jarvis is on a graph, because jarvis reads any Schema property it doesn't know as an attribute. It isn't About.hidden_types, which strips Domain_strut, the label strut's soft delete is scoped by. It isn't removing index either, which would give the catch-all text, not none. The pass also deletes these types' existing vectors (~448k).
  • §2 records why updating index through jarvis's Schema API isn't the route. That API does re-embed the type, but inside the request, and every update rebuilds jarvis's shared keyword index. It does nothing for nodes written afterwards, and adds no labels.

To rule on. Rulings 1, 3, 4 and 5 are proposed, not made; ruling 2 (no vectors for execution records) is decided. Without Turn, the first catch-up on swarm38 is at most ~144k nodes, code (~40k) first.

Findings from swarm38 (2026-10-11), after a seeded explore run never used
the graph: code nodes written by stakgraph's ingest got a Domain label and a
vector only from one-off jarvis migrations (nothing since April: 10,261 of
hive's 13,581 Functions have neither); the vectors that exist embed the name
alone, because the live Function Schema's `index` is "name" while jarvis's
own library (and strut's bundled snapshot) has said [name, summary,
description] since 2026-04-10 — schema seeding is add-only, so 47 live types
trail the library; and search never reads the label-free Data_Bank vector
index strut's own seed creates.

The proposed shape: the Schema's `index` is the mark; a pass in the strut
process embeds every marked node whose vector is missing or misses a field
that now has a value; a widen-only reconcile brings live `index` lists up to
the bundled library; search reads one vector index with the type, domain and
hidden-domain filters in the query, over-fetching for Neo4j 5's post-filter.
Five rulings proposed, none made.
…ass, not jarvis's Schema API

Review the same day. Turn, StrutToolCall and StrutTurn get no vector
(decided): too fine-grained. The mark lives in strut's code while jarvis is
on a graph — jarvis reads any Schema property outside its core list as an
attribute, About.hidden_types withholds Domain_strut (strut's soft-delete
scope), and removing `index` gives the catch-all text, not none. The pass
removes their existing vectors (410,738 on Turn, 37,128 on StrutToolCall)
and writes what strut's writer would, the resolver's domain label included,
so jarvis's search (hive's kgSearch, mcp's graph_agent) sees the code strut
embeds. §2 records why updating `index` through jarvis's Schema API is not
the route: an in-request re-embed of the whole type, a rebuild of jarvis's
shared keyword index on every PUT, nothing for nodes written afterwards, no
labels. First catch-up without Turn: at most ~144k nodes, code first.
…t, the pass in the background, nothing by hand

The pass's first tick no longer holds up boot (today's awaited
backfillEmbeddings in openGraphBackend becomes the start of the timer), the
reconcile is an inline boot step beside the existing migrations, and the
validation reads the pass's log instead of a query run by hand.
@Evanfeenstra
Evanfeenstra merged commit 1c04946 into main Oct 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant