feat(engram): claim 24 without a threshold, and an embedding substrate that knows query from document
Two changes to the ONE leg that generalises. Iteration 8 measured that out of sample the semantic leg contributes 100% of the stack's gain and the graph leg contributes nothing, so this is where the remaining headroom is. 1. THE 0.60 FLOOR IS A PER-QUERY LOTTERY, AND CLAIM 24 HAS NO THRESHOLD IN IT. 06-claims.md l.148: "respond to embedding search queries by returning the node records whose embedding vectors have the HIGHEST COSINE SIMILARITY to a query vector, independently of the spreading activation traversal." A ranking. ENGRAM_EMBED_SEED_MIN is defined at el_runtime.c l.6094 as the HippoRAG seed-JOIN threshold and l.6102 admits the read-path leg merely "reuses" it. Measured on the 30 held-out paraphrases: the query's own top-1 cosine ranges 0.564-0.680, so the constant keeps a rank-1 answer for one query and discards a rank-1 answer for the next. Six golds sit at global cosine rank 1-2 scoring 0.564-0.589 - discarded by nothing but the constant. What holds the nonsense controls is the corpus-vocabulary gate (nhits == 0), not this floor. Cosine clamped to [0,1] per 05-detailed-description l.69. 2. THE VECTORS THEMSELVES ANSWER THE WRONG QUESTION. EL_EMBED_MODEL defaults to nomic-embed-text, an ASYMMETRIC retrieval encoder trained with task prefixes. Embedding query and document bare - as this file did on both sides - measures topical similarity rather than answer-hood. eg_embed_fetch now takes the task prefix: EL_EMBED_QUERY_PREFIX on the three query call sites, EL_EMBED_DOC_PREFIX on the two backfill sites. Restores no claim, and says so: Will specifies only "computed by an embedding model over the node's content" (l.17), so the model is his and its correct use is ours. It is the substrate under claim 24 - the index is only as good as the vectors in it. Reproducer for the derived corpus: tools/retrieval-eval/embed-corpus-prefixed.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+49
-15
@@ -6226,12 +6226,30 @@ static void eg_ctx_blend(const float* e, int32_t dim) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Asymmetric task prefixes. The configured model (EL_EMBED_MODEL, default
|
||||
* nomic-embed-text) is an ASYMMETRIC retrieval encoder: it is trained with a
|
||||
* task prefix and places a question and its answer in different regions of the
|
||||
* space unless told which role each text is playing. Embedding both sides
|
||||
* bare — as this file did — measures topical similarity rather than
|
||||
* answer-hood, which is the wrong quantity for claim 24's index.
|
||||
* These restore no claim: Will's description specifies only "computed by an
|
||||
* embedding model over the node's content" (05-detailed-description l.17), so
|
||||
* the model is his choice and this is correct USE of it, not his design.
|
||||
* They are the substrate under claim 24, whose "vector similarity index over
|
||||
* the semantic embedding vectors" is only as good as the vectors in it. */
|
||||
#define EL_EMBED_DOC_PREFIX "search_document: "
|
||||
#define EL_EMBED_QUERY_PREFIX "search_query: "
|
||||
|
||||
/* Fetch an embedding from Ollama. Returns malloc'd float[dim] or NULL.
|
||||
* `pfx` is the asymmetric task prefix (document or query); it is prepended
|
||||
* verbatim and is NOT counted against the content truncation budget.
|
||||
* Truncates input to ENGRAM_EMBED_MAX_CHARS and JSON-escapes it. Honors the
|
||||
* circuit breaker; a NULL return is always safe to ignore (fail-soft). */
|
||||
static float* eg_embed_fetch(const char* text, int32_t* out_dim) {
|
||||
static float* eg_embed_fetch_pfx(const char* pfx, const char* text,
|
||||
int32_t* out_dim) {
|
||||
*out_dim = 0;
|
||||
if (!text || !*text) return NULL;
|
||||
if (!pfx) pfx = "";
|
||||
int64_t now = engram_now_ms();
|
||||
if (now < _eg_embed_breaker_until) return NULL;
|
||||
/* Build request body with escaped, truncated prompt. */
|
||||
@@ -6250,11 +6268,11 @@ static float* eg_embed_fetch(const char* text, int32_t* out_dim) {
|
||||
else esc[w++] = (char)c;
|
||||
}
|
||||
esc[w] = '\0';
|
||||
size_t blen = w + strlen(eg_embed_model()) + 64;
|
||||
size_t blen = w + strlen(pfx) + strlen(eg_embed_model()) + 64;
|
||||
char* body = malloc(blen);
|
||||
if (!body) { free(esc); return NULL; }
|
||||
snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s\"}",
|
||||
eg_embed_model(), esc);
|
||||
snprintf(body, blen, "{\"model\":\"%s\",\"prompt\":\"%s%s\"}",
|
||||
eg_embed_model(), pfx, esc);
|
||||
free(esc);
|
||||
struct curl_slist* h = curl_slist_append(NULL, "Content-Type: application/json");
|
||||
el_val_t resp = http_do_t("POST", eg_embed_url(), body, h,
|
||||
@@ -7774,7 +7792,7 @@ el_val_t engram_search(el_val_t query, el_val_t limit) {
|
||||
/* Claim-24 semantic leg: one query embedding, fetched once per search.
|
||||
* NULL (embedder down / circuit breaker open) => pure lexical, as before. */
|
||||
int32_t qdim = 0;
|
||||
float* qv = eg_embed_fetch(q, &qdim);
|
||||
float* qv = eg_embed_fetch_pfx(EL_EMBED_QUERY_PREFIX, q, &qdim);
|
||||
EngramSemEntry* sem = qv ? malloc((size_t)g->node_count * sizeof(EngramSemEntry)) : NULL;
|
||||
int64_t nsem = 0;
|
||||
int64_t nhits = 0;
|
||||
@@ -8196,7 +8214,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->emb || !eg_embed_eligible(n)) continue;
|
||||
int32_t d = 0;
|
||||
float* v = eg_embed_fetch(n->content, &d);
|
||||
float* v = eg_embed_fetch_pfx(EL_EMBED_DOC_PREFIX, n->content, &d);
|
||||
if (!v) break; /* embedder down / breaker open — stop this call */
|
||||
n->emb = v; n->emb_dim = d;
|
||||
backfilled++;
|
||||
@@ -8214,7 +8232,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) {
|
||||
q_emb = _eg_qcache_emb; q_dim = _eg_qcache_dim;
|
||||
} else {
|
||||
int32_t d = 0;
|
||||
float* v = eg_embed_fetch(q, &d);
|
||||
float* v = eg_embed_fetch_pfx(EL_EMBED_QUERY_PREFIX, q, &d);
|
||||
if (v) {
|
||||
free(_eg_qcache_text); free(_eg_qcache_emb);
|
||||
_eg_qcache_text = strdup(q);
|
||||
@@ -9646,7 +9664,7 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
* so the semantic half of the retrieval surface has to land HERE
|
||||
* to be observable to the MCP wrapper and the app. */
|
||||
int32_t qdim = 0;
|
||||
float* qv = eg_embed_fetch(q, &qdim);
|
||||
float* qv = eg_embed_fetch_pfx(EL_EMBED_QUERY_PREFIX, q, &qdim);
|
||||
EngramSemEntry* sem = qv ? malloc((size_t)g->node_count * sizeof(EngramSemEntry)) : NULL;
|
||||
int64_t nsem = 0;
|
||||
int64_t nhits = 0;
|
||||
@@ -9687,12 +9705,28 @@ el_val_t engram_search_json(el_val_t query, el_val_t limit) {
|
||||
}
|
||||
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;
|
||||
/* Claim-24 semantic leg, restored verbatim: "returning
|
||||
* the node records whose embedding vectors have the
|
||||
* HIGHEST COSINE SIMILARITY to a query vector" — a
|
||||
* ranking, with no threshold anywhere in the claim.
|
||||
* ENGRAM_EMBED_SEED_MIN is defined at l.6094 as the
|
||||
* HippoRAG SEED-JOIN threshold; using it as a RESULT
|
||||
* filter here was never authorised, and it is a
|
||||
* per-query lottery rather than a quality gate: the
|
||||
* query's own top-1 cosine ranges 0.56-0.68 across the
|
||||
* held-out gold set, so 0.60 keeps a rank-1 answer for
|
||||
* one query and discards a rank-1 answer for the next.
|
||||
* Measured on the 30 held-out paraphrases: six golds
|
||||
* sit at global cosine rank 1-2 and score 0.564-0.589,
|
||||
* discarded by nothing but this constant.
|
||||
* What holds the nonsense controls is NOT this floor
|
||||
* but the corpus-vocabulary gate below (nhits == 0):
|
||||
* gibberish has no lexical seeds, so no leg reports.
|
||||
* Cosine is clamped to [0,1] per 05-detailed-description
|
||||
* l.69 ("clamped to [0,1] to prevent anti-correlated
|
||||
* embeddings from producing negative activation"). */
|
||||
double sv = c < 0.0 ? 0.0 : (c > 1.0 ? 1.0 : c);
|
||||
if (sv > 0.0) {
|
||||
sem[nsem].idx = i; sem[nsem].sem = sv; nsem++;
|
||||
}
|
||||
/* Graph seeds: top-K by RAW cosine, insertion-ordered. */
|
||||
@@ -10208,7 +10242,7 @@ el_val_t engram_embed_backfill(el_val_t count) {
|
||||
EngramNode* n = &g->nodes[i];
|
||||
if (n->emb || !eg_embed_eligible(n)) continue;
|
||||
int32_t d = 0;
|
||||
float* v = eg_embed_fetch(n->content, &d);
|
||||
float* v = eg_embed_fetch_pfx(EL_EMBED_DOC_PREFIX, n->content, &d);
|
||||
if (!v) break; /* embedder down / breaker open — stop this call */
|
||||
n->emb = v; n->emb_dim = d;
|
||||
done++;
|
||||
|
||||
Reference in New Issue
Block a user