feat(engram): semantically seed the graph leg (Will's HippoRAG pass, SEED_K=8)

engram_assoc_leg previously took its seeds only from the top-3 LEXICAL hits.
For a paraphrase query the lexical hits are noise by construction, so the walk
never reached the neighbourhood that holds the answer. This adds the seeding
pass Will documents at el_runtime.c l.6082 — "Semantic seeding (HippoRAG
pattern, use similarity twice): the query is embedded, the top-K nodes by
cosine join the seed set" — using his own ENGRAM_EMBED_SEED_K (8).

Similarity is now used twice, coherently: cosine picks where to STAND in the
graph, the structural-relation walk decides what is REACHABLE, and cosine
orders what was reached (iteration 2's finding, unchanged).

The seed list is deliberately NOT floored at ENGRAM_EMBED_SEED_MIN. Measured
over all 38 gold queries: true paraphrase targets score cosine 0.46-0.66 and
the three nonsense controls' own nearest neighbours score 0.55/0.60/0.62 —
the distributions OVERLAP, so no absolute cosine floor separates signal from
gibberish. The gate that works is reachability: gibberish's nearest neighbours
carry no structural edge, so its graph leg is empty and the controls hold.

The raw top-K is selected inside the existing scoring pass, so the cosine is
computed exactly once per node: no extra corpus pass, no extra embed
round-trip, latency flat (p50 1220 -> 1227 ms, 1.01x).

Measured vs the certified baseline feat/hybrid-semantic-recall, embedded
corpus, 2 runs each, zero run-to-run drift on both sides:
  hit@5 51.4% -> 68.6%   MRR@10 0.387 -> 0.461
  paraphrase 38.5% -> 61.5%   associative 0% -> 66.7%
  exact_rare 100% held, nonsense 2/3 held, superseded 2/3 held
  phrase 85.7% -> 71.4% (q11, the known rank-5 rotation tax)
  net +6 queries (7 fixed / 1 broken), McNemar p=0.0703
This commit is contained in:
Tim Lingo
2026-08-07 15:35:08 -05:00
parent 059ce02003
commit 6f3a048f36
8 changed files with 2455 additions and 4 deletions
+62 -4
View File
@@ -7531,8 +7531,30 @@ static int eg_assoc_excluded(const EngramNode* n) {
* function of hop count and ranks the relay hub above all of its own
* children. Composing the two is mine, not Will's the description ranks the
* activation result set by strength (l.78). */
/* Semantic seeding of the graph leg — the HippoRAG pass Will documents at
* l.6082: "the query is embedded, the top-K nodes by cosine >= SEED_MIN join
* the seed set", using his own ENGRAM_EMBED_SEED_K (8). It is "similarity used
* twice, coherently": cosine picks where to STAND in the graph, the structural
* walk decides what is REACHABLE from there, and cosine then ORDERS what was
* reached (iteration-2's finding, kept intact).
*
* Why the seed list is NOT floored at ENGRAM_EMBED_SEED_MIN here. That
* constant is calibrated for a cosine scale this corpus does not have: with
* nomic-embed-text every true paraphrase target measures 0.46-0.66, and the
* three nonsense controls' own nearest neighbours measure 0.55/0.60/0.62
* they OVERLAP, so no absolute cosine floor separates signal from gibberish
* (measured, all 38 queries). Iteration 2 established the same thing one step
* later in the pipeline: applying the floor to graph CANDIDATES removed every
* gain, because within a structurally-reached neighbourhood relative cosine
* still discriminates below the absolute threshold. The gate that actually
* works is reachability eg_rel_is_structural() plus the firing threshold.
* A semantically-near node with no structural attachment expands to nothing
* and contributes nothing, which is exactly what happens to gibberish: the
* nearest neighbours of q33/q34 are unattached, so their graph leg is empty.
*/
static int64_t engram_assoc_leg(EngramStore* g,
const EngramRankEntry* L, int64_t nL,
const int64_t* semseed, int64_t nsemseed,
const float* qv, int32_t qdim,
EngramSemEntry* out, int64_t out_cap) {
if (!g || nL <= 0 || !qv || qdim <= 0 || out_cap <= 0) return 0;
@@ -7553,6 +7575,14 @@ static int64_t engram_assoc_leg(EngramStore* g,
act[idx] = 1.0; seen[idx] = 2; /* 2 = seed: never a result */
if (qt < qcap) { q[qt] = idx; hop[qt] = 0; qt++; }
}
/* ...and the semantic seeds, on the same footing (strength 1.0, hop 0). */
for (int64_t s = 0; s < nsemseed; s++) {
int64_t idx = semseed[s];
if (idx < 0 || idx >= g->node_count) continue;
if (seen[idx]) continue;
act[idx] = 1.0; seen[idx] = 2;
if (qt < qcap) { q[qt] = idx; hop[qt] = 0; qt++; }
}
const double SPREAD_DECAY = 0.7;
while (qh < qt) {
@@ -9524,6 +9554,13 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
EngramSemEntry* sem = qv ? malloc((size_t)g->node_count * sizeof(EngramSemEntry)) : NULL;
int64_t nsem = 0;
int64_t nhits = 0;
/* Raw (unfloored) cosine top-K, kept for the graph leg's
* semantic seeds. Selected in THIS pass so the cosine is
* computed exactly once per node the seeding costs no extra
* pass over the corpus and no extra embed round-trip. */
int64_t semseed[ENGRAM_EMBED_SEED_K];
double semseedc[ENGRAM_EMBED_SEED_K];
int64_t nsemseed = 0;
for (int64_t i = 0; i < g->node_count; i++) {
EngramNode* n = &g->nodes[i];
/* Filter transparent layers — same as engram_search. */
@@ -9535,9 +9572,29 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
hits[nhits].salience = n->salience;
nhits++;
}
if (sem) {
double sv = eg_sem_term(n, qv, qdim);
if (sv > 0.0) { sem[nsem].idx = i; sem[nsem].sem = sv; nsem++; }
if (sem && n->emb && n->emb_dim == qdim) {
double c = eg_cosine(n->emb, qv, qdim);
/* Semantic leg: identical to eg_sem_term(), which is
* left in place and still used by engram_search().
* Inlined here only so one cosine serves both uses. */
if (c > ENGRAM_EMBED_SEED_MIN) {
double sv = (c - ENGRAM_EMBED_SEED_MIN) / (1.0 - ENGRAM_EMBED_SEED_MIN);
if (sv > 1.0) sv = 1.0;
sem[nsem].idx = i; sem[nsem].sem = sv; nsem++;
}
/* Graph seeds: top-K by RAW cosine, insertion-ordered. */
if (c > 0.0 && (nsemseed < ENGRAM_EMBED_SEED_K
|| c > semseedc[nsemseed - 1])) {
int64_t p = nsemseed < ENGRAM_EMBED_SEED_K
? nsemseed : ENGRAM_EMBED_SEED_K - 1;
while (p > 0 && semseedc[p - 1] < c) {
semseedc[p] = semseedc[p - 1];
semseed[p] = semseed[p - 1];
p--;
}
semseedc[p] = c; semseed[p] = i;
if (nsemseed < ENGRAM_EMBED_SEED_K) nsemseed++;
}
}
}
qsort(hits, (size_t)nhits, sizeof(EngramRankEntry), engram_rank_cmp);
@@ -9549,7 +9606,8 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
* lexical ordering safe. */
EngramSemEntry* assoc = qv ? malloc((size_t)ENGRAM_ASSOC_MAX * sizeof(EngramSemEntry)) : NULL;
int64_t nassoc = assoc
? engram_assoc_leg(g, hits, nhits, qv, qdim, assoc, ENGRAM_ASSOC_MAX)
? engram_assoc_leg(g, hits, nhits, semseed, nsemseed,
qv, qdim, assoc, ENGRAM_ASSOC_MAX)
: 0;
int64_t* order = malloc((size_t)lim * sizeof(int64_t));
if (order) {