@rubytech/create-maxy 1.0.715 → 1.0.716
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +33 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +27 -11
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +29 -13
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +267 -0
- package/payload/platform/lib/graph-search/src/index.ts +27 -12
- package/payload/platform/neo4j/schema.cypher +53 -7
- package/payload/platform/plugins/docs/references/internals.md +4 -4
- package/payload/platform/plugins/memory/references/graph-primitives.md +9 -3
- package/payload/platform/scripts/embed-backfill.sh +370 -0
- package/payload/platform/scripts/seed-neo4j.sh +10 -4
- package/payload/server/server.js +1 -1
package/dist/index.js
CHANGED
|
@@ -1451,6 +1451,39 @@ function setupAccount() {
|
|
|
1451
1451
|
if (existsSync(seedScript)) {
|
|
1452
1452
|
shell("bash", [seedScript], { cwd: INSTALL_DIR });
|
|
1453
1453
|
}
|
|
1454
|
+
// Task 748 — universal embedding coverage backfill. Run after seed so the
|
|
1455
|
+
// entity_search index is in place and any pre-Task-748 nodes (e.g. the
|
|
1456
|
+
// 5096 LinkedIn-imported Persons on existing Pis that bulk-import skipped
|
|
1457
|
+
// embedding for) get a vector populated. Idempotent — instant no-op when
|
|
1458
|
+
// nothing is pending, so re-running on every install is harmless.
|
|
1459
|
+
//
|
|
1460
|
+
// Failure-mode policy: WARN, do not abort. The fulltext index is already
|
|
1461
|
+
// applied above, so BM25 search works end-to-end without embeddings; the
|
|
1462
|
+
// only gap is vector ranking quality on legacy nodes. Aborting the
|
|
1463
|
+
// installer on an Ollama hiccup would block every install for a
|
|
1464
|
+
// strictly-degradable feature. The script's own loud-failure output
|
|
1465
|
+
// tells the operator how to re-run.
|
|
1466
|
+
const backfillScript = join(INSTALL_DIR, "platform/scripts/embed-backfill.sh");
|
|
1467
|
+
if (existsSync(backfillScript)) {
|
|
1468
|
+
const start = Date.now();
|
|
1469
|
+
logFile(`> bash ${backfillScript} (warn-not-abort)`);
|
|
1470
|
+
const result = spawnSync("bash", [backfillScript], {
|
|
1471
|
+
stdio: "inherit",
|
|
1472
|
+
timeout: 30 * 60_000,
|
|
1473
|
+
cwd: INSTALL_DIR,
|
|
1474
|
+
});
|
|
1475
|
+
const dur = ((Date.now() - start) / 1000).toFixed(1);
|
|
1476
|
+
if (result.status !== 0 || result.signal) {
|
|
1477
|
+
const reason = result.signal ? `signal=${result.signal}` : `exit=${result.status}`;
|
|
1478
|
+
logFile(` WARN: embed-backfill non-zero (${reason}) after ${dur}s`);
|
|
1479
|
+
console.warn(`\n WARNING: embed-backfill did not complete (${reason}) — BM25 search works,\n` +
|
|
1480
|
+
` but vector ranking on legacy nodes will be sparse until you re-run:\n` +
|
|
1481
|
+
` bash ${backfillScript}\n`);
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
logFile(` OK embed-backfill in ${dur}s`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1454
1487
|
}
|
|
1455
1488
|
// ---------------------------------------------------------------------------
|
|
1456
1489
|
// Tunnel script shortcuts
|
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared hybrid search primitive over the Neo4j knowledge graph.
|
|
3
3
|
*
|
|
4
|
-
* Pre-Task-675 there were two BM25 implementations over the same
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Pre-Task-675 there were two BM25 implementations over the same fulltext
|
|
5
|
+
* index — one in the memory MCP tool, one in the admin Hono route — with
|
|
6
|
+
* divergent filter semantics (soft-delete primitive) and divergent ranking
|
|
7
|
+
* (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
|
|
8
|
+
* into this lib; memory MCP + admin route now share a single code path so
|
|
9
|
+
* `/graph` UI ranking matches what the agent sees.
|
|
10
|
+
*
|
|
11
|
+
* Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
|
|
12
|
+
* and its label union expanded from 3 labels (KnowledgeDocument | Section |
|
|
13
|
+
* Chunk) to the full operator-meaningful label set (~40 labels) on every
|
|
14
|
+
* textual property the schema's writers assign. The lib references the new
|
|
15
|
+
* name via FULLTEXT_INDEX_NAME below; the doctrine test at
|
|
16
|
+
* `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
|
|
17
|
+
* and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
|
|
18
|
+
* label declared in the schema.
|
|
10
19
|
*
|
|
11
20
|
* QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
|
|
12
21
|
* │ ├--> MERGE --> EXPAND --> RESULTS
|
|
@@ -30,6 +39,13 @@
|
|
|
30
39
|
* line with the prefix and fields it wants.
|
|
31
40
|
*/
|
|
32
41
|
import { type Session } from "neo4j-driver";
|
|
42
|
+
/**
|
|
43
|
+
* Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
|
|
44
|
+
* INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
|
|
45
|
+
* (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
|
|
46
|
+
* matches this constant — drift between the two breaks BM25 silently.
|
|
47
|
+
*/
|
|
48
|
+
export declare const FULLTEXT_INDEX_NAME = "entity_search";
|
|
33
49
|
export interface SearchHit {
|
|
34
50
|
nodeId: string;
|
|
35
51
|
labels: string[];
|
|
@@ -118,10 +134,10 @@ export declare function discoverIndexes(session: Session): Promise<Map<string, s
|
|
|
118
134
|
/** Clear the index cache — call after schema changes. */
|
|
119
135
|
export declare function clearIndexCache(): void;
|
|
120
136
|
/**
|
|
121
|
-
* BM25 full-text search against the `
|
|
137
|
+
* BM25 full-text search against the universal `entity_search` index (Task 748).
|
|
122
138
|
* Returns [] when the index doesn't exist — matches memory-search.ts
|
|
123
|
-
* graceful-fallback semantics so a fresh account
|
|
124
|
-
* doesn't 500 the caller.
|
|
139
|
+
* graceful-fallback semantics so a fresh account or a Pi mid-migration
|
|
140
|
+
* (POPULATING window) doesn't 500 the caller.
|
|
125
141
|
*/
|
|
126
142
|
export declare function bm25Only(session: Session, params: Bm25OnlyParams): Promise<SearchHit[]>;
|
|
127
143
|
/**
|
|
@@ -134,8 +150,8 @@ export declare function bm25Only(session: Session, params: Bm25OnlyParams): Prom
|
|
|
134
150
|
* 2. Vector search per label (one query per vector index discovered at
|
|
135
151
|
* boot). Nodes-by-label filter short-circuits when the requested
|
|
136
152
|
* labels have no index.
|
|
137
|
-
* 3. BM25 search on
|
|
138
|
-
* vector half (scope/agent/keyword/trashed).
|
|
153
|
+
* 3. BM25 search on the universal `entity_search` index (Task 748) —
|
|
154
|
+
* same filter semantics as vector half (scope/agent/keyword/trashed).
|
|
139
155
|
* 4. Keyword subscriptions (when set): BM25 per keyword + property
|
|
140
156
|
* lookup against `node.keywords` array. Both bypass the agentSlug
|
|
141
157
|
* filter — subscriptions are scope-inclusive by design (matches
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared hybrid search primitive over the Neo4j knowledge graph.
|
|
4
4
|
*
|
|
5
|
-
* Pre-Task-675 there were two BM25 implementations over the same
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Pre-Task-675 there were two BM25 implementations over the same fulltext
|
|
6
|
+
* index — one in the memory MCP tool, one in the admin Hono route — with
|
|
7
|
+
* divergent filter semantics (soft-delete primitive) and divergent ranking
|
|
8
|
+
* (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
|
|
9
|
+
* into this lib; memory MCP + admin route now share a single code path so
|
|
10
|
+
* `/graph` UI ranking matches what the agent sees.
|
|
11
|
+
*
|
|
12
|
+
* Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
|
|
13
|
+
* and its label union expanded from 3 labels (KnowledgeDocument | Section |
|
|
14
|
+
* Chunk) to the full operator-meaningful label set (~40 labels) on every
|
|
15
|
+
* textual property the schema's writers assign. The lib references the new
|
|
16
|
+
* name via FULLTEXT_INDEX_NAME below; the doctrine test at
|
|
17
|
+
* `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
|
|
18
|
+
* and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
|
|
19
|
+
* label declared in the schema.
|
|
11
20
|
*
|
|
12
21
|
* QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
|
|
13
22
|
* │ ├--> MERGE --> EXPAND --> RESULTS
|
|
@@ -31,6 +40,7 @@
|
|
|
31
40
|
* line with the prefix and fields it wants.
|
|
32
41
|
*/
|
|
33
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.FULLTEXT_INDEX_NAME = void 0;
|
|
34
44
|
exports.escapeLucene = escapeLucene;
|
|
35
45
|
exports.normaliseBm25Scores = normaliseBm25Scores;
|
|
36
46
|
exports.discoverIndexes = discoverIndexes;
|
|
@@ -41,7 +51,13 @@ const neo4j_driver_1 = require("neo4j-driver");
|
|
|
41
51
|
const index_js_1 = require("../../graph-trash/dist/index.js");
|
|
42
52
|
const VECTOR_WEIGHT = 0.7;
|
|
43
53
|
const BM25_WEIGHT = 0.3;
|
|
44
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
|
|
56
|
+
* INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
|
|
57
|
+
* (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
|
|
58
|
+
* matches this constant — drift between the two breaks BM25 silently.
|
|
59
|
+
*/
|
|
60
|
+
exports.FULLTEXT_INDEX_NAME = "entity_search";
|
|
45
61
|
/**
|
|
46
62
|
* Lucene-escape special characters per Neo4j's fulltext query grammar.
|
|
47
63
|
* `/[+\-&|!(){}[\]^"~*?:\\/]/g` — matches memory-search.ts:127 and
|
|
@@ -97,10 +113,10 @@ function buildKeywordFilter(keywords, keywordMatch = "any") {
|
|
|
97
113
|
};
|
|
98
114
|
}
|
|
99
115
|
/**
|
|
100
|
-
* BM25 full-text search against the `
|
|
116
|
+
* BM25 full-text search against the universal `entity_search` index (Task 748).
|
|
101
117
|
* Returns [] when the index doesn't exist — matches memory-search.ts
|
|
102
|
-
* graceful-fallback semantics so a fresh account
|
|
103
|
-
* doesn't 500 the caller.
|
|
118
|
+
* graceful-fallback semantics so a fresh account or a Pi mid-migration
|
|
119
|
+
* (POPULATING window) doesn't 500 the caller.
|
|
104
120
|
*/
|
|
105
121
|
async function bm25Only(session, params) {
|
|
106
122
|
const { query, accountId, limit, allowedScopes, agentSlug, keywords, keywordMatch, labels } = params;
|
|
@@ -128,7 +144,7 @@ async function bm25Only(session, params) {
|
|
|
128
144
|
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
129
145
|
ORDER BY score DESC
|
|
130
146
|
LIMIT $limit`, {
|
|
131
|
-
indexName: FULLTEXT_INDEX_NAME,
|
|
147
|
+
indexName: exports.FULLTEXT_INDEX_NAME,
|
|
132
148
|
query: escaped,
|
|
133
149
|
accountId,
|
|
134
150
|
limit: (0, neo4j_driver_1.int)(limit),
|
|
@@ -167,8 +183,8 @@ async function bm25Only(session, params) {
|
|
|
167
183
|
* 2. Vector search per label (one query per vector index discovered at
|
|
168
184
|
* boot). Nodes-by-label filter short-circuits when the requested
|
|
169
185
|
* labels have no index.
|
|
170
|
-
* 3. BM25 search on
|
|
171
|
-
* vector half (scope/agent/keyword/trashed).
|
|
186
|
+
* 3. BM25 search on the universal `entity_search` index (Task 748) —
|
|
187
|
+
* same filter semantics as vector half (scope/agent/keyword/trashed).
|
|
172
188
|
* 4. Keyword subscriptions (when set): BM25 per keyword + property
|
|
173
189
|
* lookup against `node.keywords` array. Both bypass the agentSlug
|
|
174
190
|
* filter — subscriptions are scope-inclusive by design (matches
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { FULLTEXT_INDEX_NAME } from "../index.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Doctrine test (Task 748) — pins the universal-coverage invariant for the
|
|
9
|
+
* fulltext index so future schema edits cannot silently re-narrow.
|
|
10
|
+
*
|
|
11
|
+
* The pre-Task-748 fulltext index `knowledge_fulltext` covered 3 of ~40
|
|
12
|
+
* registered labels (`KnowledgeDocument | Section | Chunk`); BM25 silently
|
|
13
|
+
* returned zero hits for Person/Organization/Task/Conversation/etc. queries
|
|
14
|
+
* regardless of term. The doctrine: search is "find any node in my graph
|
|
15
|
+
* that mentions this term" — there is no allowlist of search-eligible labels.
|
|
16
|
+
*
|
|
17
|
+
* This test reads `platform/neo4j/schema.cypher` (the schema source of truth)
|
|
18
|
+
* and `platform/ui/app/lib/graph-labels.ts` (the canvas label registry) at
|
|
19
|
+
* runtime, parses both, and asserts:
|
|
20
|
+
*
|
|
21
|
+
* 1. The fulltext index name in `schema.cypher` matches the
|
|
22
|
+
* `FULLTEXT_INDEX_NAME` constant in the lib. Drift between the two
|
|
23
|
+
* breaks BM25 silently — the lib queries an index name that doesn't
|
|
24
|
+
* exist, hits the graceful-fallback `[]` branch, and search returns
|
|
25
|
+
* no BM25 hits.
|
|
26
|
+
* 2. The label union in the index ⊇ union of:
|
|
27
|
+
* - `Object.keys(GRAPH_LABEL_COLOURS)` (canvas-registered labels)
|
|
28
|
+
* - every label declared via `CREATE CONSTRAINT|INDEX FOR (n:Label)`
|
|
29
|
+
* in `schema.cypher` (write-targeted labels)
|
|
30
|
+
* minus a small explicit excluded set (`Trashed`, `GraphPreference`)
|
|
31
|
+
* that carry no operator-meaningful textual content.
|
|
32
|
+
* 3. The property union in the index ⊇ the canonical text-property list
|
|
33
|
+
* (the properties that writers across the codebase assign to nodes).
|
|
34
|
+
* New writers extending the canon must add their property here.
|
|
35
|
+
*
|
|
36
|
+
* Failing assertions point the next person at exactly the schema drift to
|
|
37
|
+
* close — not "search is mysteriously broken."
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const SCHEMA_PATH = resolve(__dirname, "../../../../neo4j/schema.cypher");
|
|
42
|
+
const LABELS_PATH = resolve(__dirname, "../../../../ui/app/lib/graph-labels.ts");
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Labels declared in `schema.cypher` or `GRAPH_LABEL_COLOURS` that legitimately
|
|
46
|
+
* carry no searchable textual content. The doctrine test allows these to be
|
|
47
|
+
* absent from the fulltext index union without failing.
|
|
48
|
+
*
|
|
49
|
+
* - `Trashed` — soft-delete flag label; nodes carrying it are
|
|
50
|
+
* already excluded from every search via `notTrashed()`
|
|
51
|
+
* in the lib.
|
|
52
|
+
* - `GraphPreference`— per-(accountId, userId) UI metadata for /graph;
|
|
53
|
+
* carries no textual content, never operator-visible
|
|
54
|
+
* as a search hit.
|
|
55
|
+
*/
|
|
56
|
+
const EXCLUDED_FROM_INDEX = new Set(["Trashed", "GraphPreference"]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Canonical text-property list — every property the platform's writers
|
|
60
|
+
* assign that the operator might search for. Extending the canon in a new
|
|
61
|
+
* writer means extending this list AND the schema's `ON EACH [...]` clause.
|
|
62
|
+
* Discovered via codebase sweep during Task 748 across `memory-write`,
|
|
63
|
+
* `memory-archive-write`, `memory-ingest`, `email-store`, `neo4j-store`,
|
|
64
|
+
* `workflow-create`, `tool-call-writer`, `review-detector/writer`.
|
|
65
|
+
*/
|
|
66
|
+
const CANONICAL_TEXT_PROPERTIES = [
|
|
67
|
+
// Generic
|
|
68
|
+
"name", "title", "summary", "body", "content", "text", "description",
|
|
69
|
+
"headline", "abstract", "note", "label", "value", "message", "preview",
|
|
70
|
+
"tagline",
|
|
71
|
+
// Person
|
|
72
|
+
"firstName", "lastName", "givenName", "familyName", "email",
|
|
73
|
+
// Email
|
|
74
|
+
"subject", "bodyPreview", "fromName", "fromAddress", "screeningReason",
|
|
75
|
+
// EmailAccount
|
|
76
|
+
"agentAddress",
|
|
77
|
+
// Credential
|
|
78
|
+
"authority",
|
|
79
|
+
// AccessGrant
|
|
80
|
+
"contactValue",
|
|
81
|
+
// ToolCall
|
|
82
|
+
"toolName",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
interface IndexDeclaration {
|
|
86
|
+
name: string;
|
|
87
|
+
labels: Set<string>;
|
|
88
|
+
properties: Set<string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extract the `CREATE FULLTEXT INDEX entity_search ... ;` block from
|
|
93
|
+
* `schema.cypher` and parse its label and property sets.
|
|
94
|
+
*
|
|
95
|
+
* Uses a paren-balanced scan from the matched `CREATE FULLTEXT INDEX <name>`
|
|
96
|
+
* to the next semicolon — survives whitespace/comment/line-break formatting
|
|
97
|
+
* drift that a single monolithic regex would not. Failing here means the
|
|
98
|
+
* declaration's shape diverged from `CREATE FULLTEXT INDEX <name> IF NOT EXISTS
|
|
99
|
+
* FOR (n:A|B|...) ON EACH [n.x, n.y, ...];` in a way the parser does not
|
|
100
|
+
* recognise — extend this function before the test.
|
|
101
|
+
*/
|
|
102
|
+
function parseFulltextIndex(rawCypher: string, indexName: string): IndexDeclaration {
|
|
103
|
+
// Strip line comments first so a historical note like
|
|
104
|
+
// `// pre-Task-748: CREATE FULLTEXT INDEX entity_search FOR (n:OldLabel)`
|
|
105
|
+
// does not fool the extractor into parsing the comment instead of the
|
|
106
|
+
// live declaration.
|
|
107
|
+
const cypher = stripLineComments(rawCypher);
|
|
108
|
+
const declStart = cypher.search(
|
|
109
|
+
new RegExp(`CREATE\\s+FULLTEXT\\s+INDEX\\s+${indexName}\\b`, "m"),
|
|
110
|
+
);
|
|
111
|
+
if (declStart < 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`parseFulltextIndex: could not find 'CREATE FULLTEXT INDEX ${indexName}' in schema.cypher. ` +
|
|
114
|
+
`Drift between schema and FULLTEXT_INDEX_NAME constant in graph-search/src/index.ts.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const declEnd = cypher.indexOf(";", declStart);
|
|
118
|
+
if (declEnd < 0) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`parseFulltextIndex: no terminating ';' after 'CREATE FULLTEXT INDEX ${indexName}' — schema.cypher is malformed.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const block = cypher.slice(declStart, declEnd);
|
|
124
|
+
|
|
125
|
+
const forMatch = block.match(/FOR\s+\(n:([^\)]+)\)/);
|
|
126
|
+
if (!forMatch) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`parseFulltextIndex: could not extract FOR (n:...) clause from index ${indexName}. Block: ${block.slice(0, 200)}...`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const labels = new Set(
|
|
132
|
+
forMatch[1]
|
|
133
|
+
.split("|")
|
|
134
|
+
.map((s) => s.trim())
|
|
135
|
+
.filter((s) => s.length > 0),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const onEachMatch = block.match(/ON\s+EACH\s+\[([^\]]+)\]/);
|
|
139
|
+
if (!onEachMatch) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`parseFulltextIndex: could not extract ON EACH [...] clause from index ${indexName}. Block: ${block.slice(0, 200)}...`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const properties = new Set(
|
|
145
|
+
onEachMatch[1]
|
|
146
|
+
.split(",")
|
|
147
|
+
.map((s) => s.trim().replace(/^n\./, ""))
|
|
148
|
+
.filter((s) => s.length > 0),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { name: indexName, labels, properties };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Strip Cypher `// line comments` before regex-parsing so an example or
|
|
156
|
+
* migration note inside a comment block — e.g. `// previously: CREATE
|
|
157
|
+
* INDEX FOR (g:Goal) ON (g.embedding)` — does not inject a phantom label
|
|
158
|
+
* into the schema-declared set. Block comments `/* ... */` are not
|
|
159
|
+
* stripped because schema.cypher does not use them today.
|
|
160
|
+
*/
|
|
161
|
+
function stripLineComments(cypher: string): string {
|
|
162
|
+
return cypher.replace(/^\s*\/\/.*$/gm, "");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extract every label declared in `schema.cypher` via `FOR (alias:Label)`.
|
|
167
|
+
* Each `CREATE CONSTRAINT|INDEX|VECTOR INDEX|FULLTEXT INDEX` clause that
|
|
168
|
+
* targets a label produces one entry. The fulltext index itself uses
|
|
169
|
+
* `FOR (n:A|B|...)` — we extract the union of all alternatives.
|
|
170
|
+
*/
|
|
171
|
+
function parseSchemaDeclaredLabels(cypher: string): Set<string> {
|
|
172
|
+
const labels = new Set<string>();
|
|
173
|
+
const stripped = stripLineComments(cypher);
|
|
174
|
+
const matches = stripped.matchAll(/FOR\s+\(\w+:([^\)]+)\)/g);
|
|
175
|
+
for (const m of matches) {
|
|
176
|
+
for (const label of m[1].split("|")) {
|
|
177
|
+
const trimmed = label.trim();
|
|
178
|
+
// Filter out anything that doesn't look like a Neo4j label (defensive
|
|
179
|
+
// against future schema patterns that might inject non-label tokens).
|
|
180
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(trimmed)) {
|
|
181
|
+
labels.add(trimmed);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return labels;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extract `Object.keys(GRAPH_LABEL_COLOURS)` from `graph-labels.ts` by
|
|
190
|
+
* matching the export's object-literal keys. Tolerates trailing commas,
|
|
191
|
+
* inline comments between keys, and leading whitespace.
|
|
192
|
+
*/
|
|
193
|
+
function parseGraphLabelColours(source: string): Set<string> {
|
|
194
|
+
const exportMatch = source.match(
|
|
195
|
+
/export\s+const\s+GRAPH_LABEL_COLOURS\s*:\s*[^=]+=\s*\{([\s\S]+?)\n\}/m,
|
|
196
|
+
);
|
|
197
|
+
if (!exportMatch) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
"parseGraphLabelColours: could not find 'export const GRAPH_LABEL_COLOURS' object literal in graph-labels.ts.",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const labels = new Set<string>();
|
|
203
|
+
// Match keys that are bare identifiers (camelCase or PascalCase). Skip
|
|
204
|
+
// commented-out lines and string-quoted keys (none today, but defensive).
|
|
205
|
+
for (const m of exportMatch[1].matchAll(/^\s*([A-Z][A-Za-z0-9]*)\s*:/gm)) {
|
|
206
|
+
labels.add(m[1]);
|
|
207
|
+
}
|
|
208
|
+
return labels;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
describe("entity_search fulltext index — universal-coverage doctrine (Task 748)", () => {
|
|
212
|
+
const schemaCypher = readFileSync(SCHEMA_PATH, "utf-8");
|
|
213
|
+
const labelsSource = readFileSync(LABELS_PATH, "utf-8");
|
|
214
|
+
|
|
215
|
+
const indexDecl = parseFulltextIndex(schemaCypher, FULLTEXT_INDEX_NAME);
|
|
216
|
+
const schemaDeclaredLabels = parseSchemaDeclaredLabels(schemaCypher);
|
|
217
|
+
const colourLabels = parseGraphLabelColours(labelsSource);
|
|
218
|
+
|
|
219
|
+
const requiredLabels = new Set<string>();
|
|
220
|
+
for (const l of colourLabels) if (!EXCLUDED_FROM_INDEX.has(l)) requiredLabels.add(l);
|
|
221
|
+
for (const l of schemaDeclaredLabels) if (!EXCLUDED_FROM_INDEX.has(l)) requiredLabels.add(l);
|
|
222
|
+
|
|
223
|
+
it("schema declares the index under the FULLTEXT_INDEX_NAME constant", () => {
|
|
224
|
+
// The lib's BM25 path queries `db.index.fulltext.queryNodes($indexName)`.
|
|
225
|
+
// If `schema.cypher` declares a different name, the lib hits its
|
|
226
|
+
// graceful-fallback `[]` branch and BM25 silently returns no hits —
|
|
227
|
+
// the precise drift Task 748's rename was meant to delete.
|
|
228
|
+
expect(indexDecl.name).toBe(FULLTEXT_INDEX_NAME);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("index labels ⊇ Object.keys(GRAPH_LABEL_COLOURS) − {excluded}", () => {
|
|
232
|
+
const missing = [...colourLabels]
|
|
233
|
+
.filter((l) => !EXCLUDED_FROM_INDEX.has(l))
|
|
234
|
+
.filter((l) => !indexDecl.labels.has(l));
|
|
235
|
+
expect(missing).toEqual([]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("index labels ⊇ every schema-declared label − {excluded}", () => {
|
|
239
|
+
// Position, Credential, etc. — labels written by the platform but not
|
|
240
|
+
// (yet) registered in GRAPH_LABEL_COLOURS. The doctrine: every label
|
|
241
|
+
// the platform writes is searchable on day 1. Registering for canvas
|
|
242
|
+
// colour is a separate concern.
|
|
243
|
+
const missing = [...schemaDeclaredLabels]
|
|
244
|
+
.filter((l) => !EXCLUDED_FROM_INDEX.has(l))
|
|
245
|
+
.filter((l) => !indexDecl.labels.has(l));
|
|
246
|
+
expect(missing).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("index properties ⊇ canonical text-property list", () => {
|
|
250
|
+
// Adding a new text property in a writer (e.g. `Position.companyName`
|
|
251
|
+
// hypothetically) without extending the schema's `ON EACH [...]` clause
|
|
252
|
+
// means BM25 cannot match on that property. Catch it here.
|
|
253
|
+
const missing = CANONICAL_TEXT_PROPERTIES.filter(
|
|
254
|
+
(p) => !indexDecl.properties.has(p),
|
|
255
|
+
);
|
|
256
|
+
expect(missing).toEqual([]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("required-label set is non-trivial — sanity check that the parsers ran", () => {
|
|
260
|
+
// Defensive: if the parsers above silently regressed to "no labels
|
|
261
|
+
// found", the supersets above would vacuously pass. Assert a meaningful
|
|
262
|
+
// floor (we expect ~40 labels post-Task-748).
|
|
263
|
+
expect(requiredLabels.size).toBeGreaterThanOrEqual(30);
|
|
264
|
+
expect(indexDecl.labels.size).toBeGreaterThanOrEqual(30);
|
|
265
|
+
expect(indexDecl.properties.size).toBeGreaterThanOrEqual(20);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared hybrid search primitive over the Neo4j knowledge graph.
|
|
3
3
|
*
|
|
4
|
-
* Pre-Task-675 there were two BM25 implementations over the same
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Pre-Task-675 there were two BM25 implementations over the same fulltext
|
|
5
|
+
* index — one in the memory MCP tool, one in the admin Hono route — with
|
|
6
|
+
* divergent filter semantics (soft-delete primitive) and divergent ranking
|
|
7
|
+
* (agent ran hybrid vector+BM25, UI ran BM25-only). Task 675 collapses both
|
|
8
|
+
* into this lib; memory MCP + admin route now share a single code path so
|
|
9
|
+
* `/graph` UI ranking matches what the agent sees.
|
|
10
|
+
*
|
|
11
|
+
* Task 748 — fulltext index `knowledge_fulltext` was renamed to `entity_search`
|
|
12
|
+
* and its label union expanded from 3 labels (KnowledgeDocument | Section |
|
|
13
|
+
* Chunk) to the full operator-meaningful label set (~40 labels) on every
|
|
14
|
+
* textual property the schema's writers assign. The lib references the new
|
|
15
|
+
* name via FULLTEXT_INDEX_NAME below; the doctrine test at
|
|
16
|
+
* `__tests__/fulltext-coverage.test.ts` parses `platform/neo4j/schema.cypher`
|
|
17
|
+
* and asserts the index covers the union of `GRAPH_LABEL_COLOURS` and every
|
|
18
|
+
* label declared in the schema.
|
|
10
19
|
*
|
|
11
20
|
* QUERY --> EMBED --> VECTOR SEARCH (per index) --> ┐
|
|
12
21
|
* │ ├--> MERGE --> EXPAND --> RESULTS
|
|
@@ -35,7 +44,13 @@ import { notTrashed } from "../../graph-trash/dist/index.js";
|
|
|
35
44
|
|
|
36
45
|
const VECTOR_WEIGHT = 0.7;
|
|
37
46
|
const BM25_WEIGHT = 0.3;
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Name of the universal fulltext index (Task 748). Mirrors the `CREATE FULLTEXT
|
|
49
|
+
* INDEX` declaration in `platform/neo4j/schema.cypher`. The doctrine test
|
|
50
|
+
* (`__tests__/fulltext-coverage.test.ts`) asserts the schema's index name
|
|
51
|
+
* matches this constant — drift between the two breaks BM25 silently.
|
|
52
|
+
*/
|
|
53
|
+
export const FULLTEXT_INDEX_NAME = "entity_search";
|
|
39
54
|
|
|
40
55
|
export interface SearchHit {
|
|
41
56
|
nodeId: string;
|
|
@@ -182,10 +197,10 @@ function buildKeywordFilter(
|
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
/**
|
|
185
|
-
* BM25 full-text search against the `
|
|
200
|
+
* BM25 full-text search against the universal `entity_search` index (Task 748).
|
|
186
201
|
* Returns [] when the index doesn't exist — matches memory-search.ts
|
|
187
|
-
* graceful-fallback semantics so a fresh account
|
|
188
|
-
* doesn't 500 the caller.
|
|
202
|
+
* graceful-fallback semantics so a fresh account or a Pi mid-migration
|
|
203
|
+
* (POPULATING window) doesn't 500 the caller.
|
|
189
204
|
*/
|
|
190
205
|
export async function bm25Only(
|
|
191
206
|
session: Session,
|
|
@@ -259,8 +274,8 @@ export async function bm25Only(
|
|
|
259
274
|
* 2. Vector search per label (one query per vector index discovered at
|
|
260
275
|
* boot). Nodes-by-label filter short-circuits when the requested
|
|
261
276
|
* labels have no index.
|
|
262
|
-
* 3. BM25 search on
|
|
263
|
-
* vector half (scope/agent/keyword/trashed).
|
|
277
|
+
* 3. BM25 search on the universal `entity_search` index (Task 748) —
|
|
278
|
+
* same filter semantics as vector half (scope/agent/keyword/trashed).
|
|
264
279
|
* 4. Keyword subscriptions (when set): BM25 per keyword + property
|
|
265
280
|
* lookup against `node.keywords` array. Both bypass the agentSlug
|
|
266
281
|
* filter — subscriptions are scope-inclusive by design (matches
|
|
@@ -258,13 +258,59 @@ OPTIONS {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
// Universal full-text BM25 index for hybrid keyword search (Task 748).
|
|
262
|
+
//
|
|
263
|
+
// Every operator-meaningful label written by the platform is in the index union;
|
|
264
|
+
// every textual property a writer assigns is in the property union. Neo4j silently
|
|
265
|
+
// ignores absent properties on a given label, so over-inclusion is harmless.
|
|
266
|
+
//
|
|
267
|
+
// **Doctrine.** Search is "find any node in my graph that mentions this term" —
|
|
268
|
+
// not "find a knowledge document". Pre-Task-748 the index name `knowledge_fulltext`
|
|
269
|
+
// covered only `KnowledgeDocument | Section | Chunk` (3 of ~40 written labels), so
|
|
270
|
+
// BM25 silently returned zero hits for Person/Organization/Task/Conversation/etc.
|
|
271
|
+
// regardless of query. Universal coverage is the doctrine; the doctrine test at
|
|
272
|
+
// `platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts` parses this
|
|
273
|
+
// declaration and asserts label-set ⊇ union(GRAPH_LABEL_COLOURS, schema-declared)
|
|
274
|
+
// so future label additions cannot silently re-narrow.
|
|
275
|
+
//
|
|
276
|
+
// Label union — every operator-meaningful label:
|
|
277
|
+
// - Business identity: LocalBusiness, Service, PriceSpecification, OpeningHoursSpecification, Organization
|
|
278
|
+
// - People: Person, UserProfile, Preference, AdminUser, AccessGrant
|
|
279
|
+
// - Knowledge: KnowledgeDocument, Section, Chunk (legacy), DigitalDocument, CreativeWork,
|
|
280
|
+
// Question, FAQPage, DefinedTerm, Review, ImageObject
|
|
281
|
+
// - Conversational: Conversation, AdminConversation, PublicConversation, Message,
|
|
282
|
+
// UserMessage, AssistantMessage, ToolCall
|
|
283
|
+
// - Tasks/projects/events: Task, Project, Event
|
|
284
|
+
// - Workflows: Workflow, WorkflowStep, WorkflowRun, StepResult
|
|
285
|
+
// - Onboarding: OnboardingState
|
|
286
|
+
// - Email: Email, EmailAccount
|
|
287
|
+
// - Review signals: ReviewAlert
|
|
288
|
+
// - CV/career sublabels: Position, Credential
|
|
289
|
+
//
|
|
290
|
+
// Property union — every textual property the schema's writers assign:
|
|
291
|
+
// - Generic: name, title, summary, body, content, text, description, headline, abstract,
|
|
292
|
+
// note, label, value, message, preview, tagline
|
|
293
|
+
// - Person: firstName, lastName, givenName, familyName, email
|
|
294
|
+
// - Email: subject, bodyPreview, fromName, fromAddress
|
|
295
|
+
// - EmailAccount: agentAddress
|
|
296
|
+
// - Email: screeningReason
|
|
297
|
+
// - Credential: authority
|
|
298
|
+
// - AccessGrant: contactValue
|
|
299
|
+
// - ToolCall: toolName
|
|
300
|
+
CREATE FULLTEXT INDEX entity_search IF NOT EXISTS
|
|
301
|
+
FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
|
|
302
|
+
|Person|UserProfile|Preference|AdminUser|AccessGrant
|
|
303
|
+
|KnowledgeDocument|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
|
|
304
|
+
|Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
|
|
305
|
+
|Task|Project|Event
|
|
306
|
+
|Workflow|WorkflowStep|WorkflowRun|StepResult
|
|
307
|
+
|OnboardingState|Email|EmailAccount|ReviewAlert
|
|
308
|
+
|Position|Credential)
|
|
309
|
+
ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
|
|
310
|
+
n.title, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
|
|
311
|
+
n.email, n.note, n.label, n.value, n.message, n.preview, n.tagline,
|
|
312
|
+
n.subject, n.bodyPreview, n.fromName, n.fromAddress, n.agentAddress, n.screeningReason,
|
|
313
|
+
n.authority, n.contactValue, n.toolName];
|
|
268
314
|
|
|
269
315
|
// Project node (Task 740) — a standalone creative-output node distinct from
|
|
270
316
|
// :Section. Anchored via (:UserProfile)-[:CREATED]->(:Project), with optional
|
|
@@ -18,7 +18,7 @@ QUERY
|
|
|
18
18
|
│ ├──► MERGE ──► EXPAND ──► RESULTS
|
|
19
19
|
│ │
|
|
20
20
|
└── ESCAPE (Lucene special chars) ──────► BM25 FULL-TEXT ──┘
|
|
21
|
-
(
|
|
21
|
+
(entity_search index — universal coverage)
|
|
22
22
|
|
|
23
23
|
Merge formula: combined = 0.7 × vector_score + 0.3 × normalised_bm25_score
|
|
24
24
|
Deduplication: by nodeId — when a node appears in both paths, keep the max score from each method independently, then combine.
|
|
@@ -29,7 +29,7 @@ Fallback: if the full-text index doesn't exist, vector-only results are returned
|
|
|
29
29
|
|
|
30
30
|
**Vector path:** The query is embedded via Ollama (model per `EMBED_MODEL` env var, default `nomic-embed-text`). The resulting vector is compared against Neo4j's HNSW cosine indexes — one per indexed label. Dimensions are configured at install time (default 768). The search runs against all discovered indexes (or a subset if the caller specifies label filters). Scores are in [0, 1] (cosine similarity).
|
|
31
31
|
|
|
32
|
-
**BM25 path:** The raw query text is escaped for Lucene special characters and run against the `
|
|
32
|
+
**BM25 path:** The raw query text is escaped for Lucene special characters and run against the `entity_search` full-text index (Task 748 — universal coverage), which spans every operator-meaningful label written by the platform on the canonical text-property union (~28 properties: `name`, `firstName`, `lastName`, `givenName`, `familyName`, `title`, `summary`, `body`, `content`, `description`, `headline`, `email`, `subject`, `bodyPreview`, etc.). Pre-Task-748 the index was named `knowledge_fulltext` and covered only `KnowledgeDocument | Section | Chunk` — that gap silently hid Person/Organization/Task/Event/etc. from BM25 regardless of query. Raw BM25 scores are in [0, infinity) — they are normalised to [0, 1] via min-max scaling within the result set before merging. When all scores are equal (or a single result), all normalise to 1.0.
|
|
33
33
|
|
|
34
34
|
**Merge:** Results from both paths are collected in a single map keyed by `nodeId`. A node appearing in both paths accumulates the max vector score and max BM25 score independently. The combined score is `0.7 * vectorScore + 0.3 * bm25Score`. Results are sorted descending by combined score, then sliced to the requested limit (default 10).
|
|
35
35
|
|
|
@@ -59,7 +59,7 @@ Indexed labels: `Question`, `DefinedTerm`, `Review`, `Service`, `Person`, `Local
|
|
|
59
59
|
|
|
60
60
|
| Index name | Labels | Properties | Purpose |
|
|
61
61
|
|---|---|---|---|
|
|
62
|
-
| `
|
|
62
|
+
| `entity_search` | All operator-meaningful labels (~40, see [`schema.cypher`](../../../neo4j/schema.cypher)) | Canonical text-property union (~28) | Universal BM25 keyword matching across the whole graph (Task 748) |
|
|
63
63
|
|
|
64
64
|
### Embedding lifecycle
|
|
65
65
|
|
|
@@ -282,7 +282,7 @@ Each public agent can subscribe to up to 5 keywords via `knowledgeKeywords` in i
|
|
|
282
282
|
|
|
283
283
|
For each subscription keyword, two complementary searches run:
|
|
284
284
|
|
|
285
|
-
1. **BM25 full-text search** — queries the `
|
|
285
|
+
1. **BM25 full-text search** — queries the universal `entity_search` index (Task 748) with the keyword as the search term. Catches content that mentions the keyword in its text across every operator-meaningful label.
|
|
286
286
|
|
|
287
287
|
2. **Property-based search** — finds nodes whose `keywords` array property contains the subscription keyword (case-insensitive). Catches nodes explicitly tagged with that keyword topic. These matches are boosted to maximum BM25 score (1.0) since they are exact tag matches.
|
|
288
288
|
|
|
@@ -292,11 +292,13 @@ Or use `maxy-graph-get_neo4j_schema` for a richer one-shot structural summary.
|
|
|
292
292
|
|
|
293
293
|
### Fulltext
|
|
294
294
|
|
|
295
|
-
Use the `
|
|
296
|
-
|
|
295
|
+
Use the universal `entity_search` index (Task 748) for keyword-style search
|
|
296
|
+
across every operator-meaningful label — Person, Organization, Task, Event,
|
|
297
|
+
Conversation, KnowledgeDocument, Email, etc. — on every textual property
|
|
298
|
+
the platform's writers assign:
|
|
297
299
|
|
|
298
300
|
```cypher
|
|
299
|
-
CALL db.index.fulltext.queryNodes('
|
|
301
|
+
CALL db.index.fulltext.queryNodes('entity_search', $query)
|
|
300
302
|
YIELD node, score
|
|
301
303
|
WHERE score > 0.5
|
|
302
304
|
RETURN labels(node)[0] AS type,
|
|
@@ -306,6 +308,10 @@ RETURN labels(node)[0] AS type,
|
|
|
306
308
|
LIMIT 20
|
|
307
309
|
```
|
|
308
310
|
|
|
311
|
+
Pre-Task-748 the index was named `knowledge_fulltext` and covered only
|
|
312
|
+
`KnowledgeDocument | Section | Chunk`. Existing Pis pick up the rename on
|
|
313
|
+
the next install via `seed-neo4j.sh`.
|
|
314
|
+
|
|
309
315
|
### Filter by status or category
|
|
310
316
|
|
|
311
317
|
Events that are cancelled:
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================
|
|
3
|
+
# embed-backfill.sh — populate embeddings on legacy nodes (Task 748)
|
|
4
|
+
#
|
|
5
|
+
# Walks the Neo4j graph for nodes carrying any registered Maxy label that
|
|
6
|
+
# lack `n.embedding` and have at least one populated text property. For
|
|
7
|
+
# each such node the script builds a text representation from the same
|
|
8
|
+
# property union the fulltext index covers (`name`, `title`, `summary`,
|
|
9
|
+
# `headline`, `body`, `content`, `text`), POSTs it to Ollama's `/api/embed`
|
|
10
|
+
# endpoint, and writes the resulting vector back to the node.
|
|
11
|
+
#
|
|
12
|
+
# Why it exists. Pre-Task-748 bulk-import paths (notably `memory-archive-write`
|
|
13
|
+
# for LinkedIn Connections.csv, ~5096 Persons per import) skipped per-row
|
|
14
|
+
# embedding to keep import latency under five minutes. With Task 748's
|
|
15
|
+
# universal fulltext coverage in place, BM25 catches those nodes immediately
|
|
16
|
+
# but vector ranking is sparse until embeddings exist. This script heals
|
|
17
|
+
# both the legacy backlog and any future bulk-imported population.
|
|
18
|
+
#
|
|
19
|
+
# Idempotent. Re-running picks up exactly where a prior run left off because
|
|
20
|
+
# the gating predicate is `n.embedding IS NULL` — nodes embedded by the
|
|
21
|
+
# previous run are excluded from the next batch query.
|
|
22
|
+
#
|
|
23
|
+
# Loud failure (per feedback_loud_failures.md). Any Ollama HTTP failure or
|
|
24
|
+
# cypher-shell error aborts the script with a non-zero exit and prints a
|
|
25
|
+
# precise re-run instruction. Partial-state-on-abort is safe: nodes whose
|
|
26
|
+
# embedding was committed before the abort stay embedded; the rest fall back
|
|
27
|
+
# into the next run's batch.
|
|
28
|
+
#
|
|
29
|
+
# Concurrent-run safety. flock-guarded — a second concurrent invocation
|
|
30
|
+
# exits immediately with a clear message, no work attempted. Protects
|
|
31
|
+
# against operator double-clicks and against the installer running it
|
|
32
|
+
# while a manual run is in flight.
|
|
33
|
+
#
|
|
34
|
+
# Usage. Stand-alone re-run: `bash platform/scripts/embed-backfill.sh`.
|
|
35
|
+
# Installer-driven: invoked automatically post-`seed-neo4j.sh` on every
|
|
36
|
+
# install (the no-op fast path returns in milliseconds when nothing is
|
|
37
|
+
# pending, so re-running on every install is harmless).
|
|
38
|
+
# ============================================================
|
|
39
|
+
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
|
|
42
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
43
|
+
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
44
|
+
|
|
45
|
+
NEO4J_URI="${NEO4J_URI:-bolt://localhost:7687}"
|
|
46
|
+
NEO4J_USER="${NEO4J_USER:-neo4j}"
|
|
47
|
+
OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
|
|
48
|
+
EMBED_MODEL="${EMBED_MODEL:-nomic-embed-text}"
|
|
49
|
+
BATCH_SIZE="${EMBED_BACKFILL_BATCH_SIZE:-50}"
|
|
50
|
+
|
|
51
|
+
# Lock file is brand-scoped via the install directory hash so concurrent
|
|
52
|
+
# Maxy + Real Agent installs (or any two brand installs sharing the device)
|
|
53
|
+
# do not block each other unnecessarily — they target separate Neo4j
|
|
54
|
+
# instances under separate INSTALL_DIRs and have zero shared state. The
|
|
55
|
+
# explicit env var override stays for operator-driven workflows.
|
|
56
|
+
INSTALL_DIR_HASH="$(echo -n "$PROJECT_DIR" | shasum | cut -c1-12)"
|
|
57
|
+
LOCK_FILE="${EMBED_BACKFILL_LOCK_FILE:-/tmp/maxy-embed-backfill-${INSTALL_DIR_HASH}.lock}"
|
|
58
|
+
|
|
59
|
+
# Resolve Neo4j password the same way seed-neo4j.sh does. Explicit env var
|
|
60
|
+
# takes precedence so the installer can pass it through without writing the
|
|
61
|
+
# file twice.
|
|
62
|
+
NEO4J_PASSWORD_FILE="$PROJECT_DIR/config/.neo4j-password"
|
|
63
|
+
if [ -z "${NEO4J_PASSWORD:-}" ]; then
|
|
64
|
+
if [ -f "$NEO4J_PASSWORD_FILE" ]; then
|
|
65
|
+
NEO4J_PASSWORD=$(cat "$NEO4J_PASSWORD_FILE")
|
|
66
|
+
else
|
|
67
|
+
echo "[embed-backfill] FAILED: NEO4J_PASSWORD env var unset and $NEO4J_PASSWORD_FILE missing"
|
|
68
|
+
echo "[embed-backfill] re-run after the seed step writes the password file, or set NEO4J_PASSWORD explicitly"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
export NEO4J_URI NEO4J_USER NEO4J_PASSWORD OLLAMA_URL EMBED_MODEL BATCH_SIZE
|
|
73
|
+
|
|
74
|
+
if ! command -v cypher-shell >/dev/null 2>&1; then
|
|
75
|
+
echo "[embed-backfill] FAILED: cypher-shell not on PATH; install Neo4j or add cypher-shell to PATH"
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
79
|
+
echo "[embed-backfill] FAILED: python3 not on PATH; the installer requires it"
|
|
80
|
+
exit 1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# flock guard — second concurrent invocation exits cleanly. The exec on
|
|
84
|
+
# fd 200 keeps the lock held for the lifetime of this process; flock -n
|
|
85
|
+
# is non-blocking so a busy lock returns immediately rather than queueing.
|
|
86
|
+
exec 200>"$LOCK_FILE"
|
|
87
|
+
if ! flock -n 200; then
|
|
88
|
+
echo "[embed-backfill] another instance is already running (lock=$LOCK_FILE), skipping"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# The python heredoc owns the per-batch loop. It uses subprocess to call
|
|
93
|
+
# cypher-shell (avoids re-implementing Bolt) and urllib to call Ollama
|
|
94
|
+
# (no extra deps). cypher-shell `--format plain` returns CSV; the csv
|
|
95
|
+
# module handles quoting/escaping reliably so node text containing commas,
|
|
96
|
+
# quotes, or newlines round-trips correctly.
|
|
97
|
+
#
|
|
98
|
+
# Cypher contract:
|
|
99
|
+
# READ: one row per unembedded node — { id: elementId, text: coalesced }
|
|
100
|
+
# gated by `n.embedding IS NULL` AND `any(label IN labels(n)
|
|
101
|
+
# WHERE label IN $registered)` AND a non-empty coalesce of the
|
|
102
|
+
# text property union. Nodes carrying an :Trashed label are
|
|
103
|
+
# excluded explicitly. READ params (`registered` list of strings,
|
|
104
|
+
# `batchSize` int) are passed via cypher-shell `--param` as plain
|
|
105
|
+
# Cypher expressions (string list literals + integer literal).
|
|
106
|
+
# WRITE: one batched UNWIND per chunk — pairs of (id, embedding[])
|
|
107
|
+
# interpolated into the Cypher payload as bare-key map literals
|
|
108
|
+
# (`{id: '...', embedding: [...]}`). Cypher does NOT accept
|
|
109
|
+
# double-quoted-string map keys, so JSON-serialised values cannot
|
|
110
|
+
# be passed via `--param` for the WRITE side; the inline literal
|
|
111
|
+
# path is the apoc-free alternative.
|
|
112
|
+
#
|
|
113
|
+
# The script does NOT shell out to the existing TS embed() helper because
|
|
114
|
+
# that would require booting Node + the platform/lib build. Calling the
|
|
115
|
+
# Ollama HTTP endpoint directly preserves the same behaviour with zero
|
|
116
|
+
# build dependency.
|
|
117
|
+
exec python3 - <<'PYEOF'
|
|
118
|
+
import json
|
|
119
|
+
import os
|
|
120
|
+
import sys
|
|
121
|
+
import time
|
|
122
|
+
import urllib.error
|
|
123
|
+
import urllib.request
|
|
124
|
+
from subprocess import PIPE, Popen
|
|
125
|
+
from io import StringIO
|
|
126
|
+
import csv
|
|
127
|
+
|
|
128
|
+
NEO4J_URI = os.environ["NEO4J_URI"]
|
|
129
|
+
NEO4J_USER = os.environ["NEO4J_USER"]
|
|
130
|
+
NEO4J_PASSWORD = os.environ["NEO4J_PASSWORD"]
|
|
131
|
+
OLLAMA_URL = os.environ["OLLAMA_URL"]
|
|
132
|
+
EMBED_MODEL = os.environ["EMBED_MODEL"]
|
|
133
|
+
BATCH_SIZE = int(os.environ["BATCH_SIZE"])
|
|
134
|
+
|
|
135
|
+
# Mirrors the FOR (n:...) clause of `entity_search` in schema.cypher.
|
|
136
|
+
# Doctrine: every label written by the platform is searchable AND embeddable.
|
|
137
|
+
# Future label additions must extend BOTH this list and schema.cypher; the
|
|
138
|
+
# fulltext-coverage doctrine test catches the schema half but not this list.
|
|
139
|
+
REGISTERED_LABELS = [
|
|
140
|
+
"LocalBusiness", "Service", "PriceSpecification", "OpeningHoursSpecification", "Organization",
|
|
141
|
+
"Person", "UserProfile", "Preference", "AdminUser", "AccessGrant",
|
|
142
|
+
"KnowledgeDocument", "Section", "Chunk", "DigitalDocument", "CreativeWork",
|
|
143
|
+
"Question", "FAQPage", "DefinedTerm", "Review", "ImageObject",
|
|
144
|
+
"Conversation", "AdminConversation", "PublicConversation", "Message",
|
|
145
|
+
"UserMessage", "AssistantMessage", "ToolCall",
|
|
146
|
+
"Task", "Project", "Event",
|
|
147
|
+
"Workflow", "WorkflowStep", "WorkflowRun", "StepResult",
|
|
148
|
+
"OnboardingState", "Email", "EmailAccount", "ReviewAlert",
|
|
149
|
+
"Position", "Credential",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
# Properties to coalesce for the embedding text. Ordered: most identifying
|
|
153
|
+
# property first. Matches the canonical text-property list pinned by the
|
|
154
|
+
# fulltext-coverage doctrine test.
|
|
155
|
+
EMBED_TEXT_PROPS = ["name", "title", "summary", "headline", "body", "content", "text"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cypher(query: str, params: dict | None = None) -> str:
|
|
159
|
+
"""Run a Cypher statement via cypher-shell --format plain.
|
|
160
|
+
Returns stdout as a single string. Aborts the script on non-zero exit
|
|
161
|
+
so a Cypher syntax error or a Neo4j outage surfaces immediately."""
|
|
162
|
+
cmd = [
|
|
163
|
+
"cypher-shell", "-u", NEO4J_USER, "-p", NEO4J_PASSWORD, "-a", NEO4J_URI,
|
|
164
|
+
"--format", "plain",
|
|
165
|
+
]
|
|
166
|
+
if params:
|
|
167
|
+
for key, value in params.items():
|
|
168
|
+
cmd.extend(["--param", f"{key} => {json.dumps(value)}"])
|
|
169
|
+
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
|
170
|
+
out, err = proc.communicate(query.encode("utf-8"))
|
|
171
|
+
if proc.returncode != 0:
|
|
172
|
+
sys.stderr.write(f"[embed-backfill] FAILED: cypher-shell exited {proc.returncode}\n")
|
|
173
|
+
sys.stderr.write(err.decode("utf-8", errors="replace"))
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
return out.decode("utf-8", errors="replace")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parse_csv_rows(stdout: str) -> list[dict]:
|
|
179
|
+
"""cypher-shell --format plain emits a CSV header + rows. The csv module
|
|
180
|
+
handles quoting reliably even when text contains commas/quotes/newlines."""
|
|
181
|
+
if not stdout.strip():
|
|
182
|
+
return []
|
|
183
|
+
reader = csv.DictReader(StringIO(stdout))
|
|
184
|
+
return list(reader)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def ollama_embed(text: str, *, timeout: int = 30, retry_on_timeout: bool = True) -> list[float]:
|
|
188
|
+
"""POST text to Ollama /api/embed.
|
|
189
|
+
|
|
190
|
+
Cold-start tolerance: when nomic-embed-text is not yet loaded into Ollama's
|
|
191
|
+
process memory, the first request for the model after a fresh boot can
|
|
192
|
+
exceed 30s while the model loads. Subsequent requests are fast. We retry
|
|
193
|
+
ONCE on TimeoutError with a longer (180s) timeout so a cold model load
|
|
194
|
+
does not abort the entire backfill at the first node. Retry is OFF by
|
|
195
|
+
default for the warmup probe to avoid recursion.
|
|
196
|
+
|
|
197
|
+
Aborts the script (non-zero exit) on any non-recoverable HTTP failure
|
|
198
|
+
with a precise message + re-run instruction so the operator never thinks
|
|
199
|
+
the backfill silently completed.
|
|
200
|
+
"""
|
|
201
|
+
body = json.dumps({"model": EMBED_MODEL, "input": text}).encode("utf-8")
|
|
202
|
+
req = urllib.request.Request(
|
|
203
|
+
f"{OLLAMA_URL}/api/embed",
|
|
204
|
+
data=body,
|
|
205
|
+
headers={"Content-Type": "application/json"},
|
|
206
|
+
method="POST",
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
210
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
211
|
+
except TimeoutError as e:
|
|
212
|
+
if retry_on_timeout:
|
|
213
|
+
sys.stderr.write(
|
|
214
|
+
f"[embed-backfill] WARN: Ollama timeout after {timeout}s — likely cold-start; retrying with 180s timeout\n"
|
|
215
|
+
)
|
|
216
|
+
return ollama_embed(text, timeout=180, retry_on_timeout=False)
|
|
217
|
+
sys.stderr.write(f"[embed-backfill] FAILED: Ollama timeout after {timeout}s ({e})\n")
|
|
218
|
+
sys.stderr.write(
|
|
219
|
+
f"[embed-backfill] re-run via: bash {os.path.dirname(os.path.realpath(__file__))}/embed-backfill.sh\n"
|
|
220
|
+
)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
except (urllib.error.URLError, urllib.error.HTTPError) as e:
|
|
223
|
+
sys.stderr.write(f"[embed-backfill] FAILED: Ollama unreachable ({e})\n")
|
|
224
|
+
sys.stderr.write(
|
|
225
|
+
f"[embed-backfill] re-run via: bash {os.path.dirname(os.path.realpath(__file__))}/embed-backfill.sh\n"
|
|
226
|
+
)
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
embeddings = payload.get("embeddings", [])
|
|
229
|
+
if not embeddings or not embeddings[0]:
|
|
230
|
+
sys.stderr.write(f"[embed-backfill] FAILED: Ollama returned no embedding for text length={len(text)}\n")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
return embeddings[0]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def cypher_string_literal(s: str) -> str:
|
|
236
|
+
"""Format a Python string as a Cypher single-quoted string literal.
|
|
237
|
+
|
|
238
|
+
Escapes the two characters Cypher requires escaping inside single-quoted
|
|
239
|
+
strings: backslash and single quote. elementId values from Neo4j 5 are
|
|
240
|
+
typically `<dbprefix>:<uuid>:<recordId>` (alphanumeric + colon + dash) and
|
|
241
|
+
will not normally contain either, but escape defensively so a future
|
|
242
|
+
elementId format change cannot break the WRITE batch with a syntax error.
|
|
243
|
+
"""
|
|
244
|
+
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def cypher_float_list(values: list[float]) -> str:
|
|
248
|
+
"""Format a list of floats as a Cypher list literal `[v1, v2, ...]`.
|
|
249
|
+
|
|
250
|
+
repr() on a Python float emits a decimal that Cypher accepts as a number
|
|
251
|
+
literal — including the negative sign, scientific notation, and infinity
|
|
252
|
+
edge cases. nomic-embed-text returns finite cosine-bounded floats so
|
|
253
|
+
inf/nan are not expected, but Python's repr is stable for any case that
|
|
254
|
+
does occur.
|
|
255
|
+
"""
|
|
256
|
+
return "[" + ",".join(repr(v) for v in values) + "]"
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Build the WHERE clause once. The $registered parameter is interpolated
|
|
260
|
+
# into Cypher as a list literal; cypher-shell --param gives us a typed pass.
|
|
261
|
+
COALESCE_TEXT = "coalesce(" + ", ".join(f"n.{p}" for p in EMBED_TEXT_PROPS) + ", '')"
|
|
262
|
+
COUNT_QUERY = f"""
|
|
263
|
+
MATCH (n) WHERE n.embedding IS NULL
|
|
264
|
+
AND NOT n:Trashed
|
|
265
|
+
AND any(label IN labels(n) WHERE label IN $registered)
|
|
266
|
+
AND {COALESCE_TEXT} <> ''
|
|
267
|
+
RETURN count(n) AS remaining;
|
|
268
|
+
"""
|
|
269
|
+
BATCH_QUERY = f"""
|
|
270
|
+
MATCH (n) WHERE n.embedding IS NULL
|
|
271
|
+
AND NOT n:Trashed
|
|
272
|
+
AND any(label IN labels(n) WHERE label IN $registered)
|
|
273
|
+
AND {COALESCE_TEXT} <> ''
|
|
274
|
+
RETURN elementId(n) AS id,
|
|
275
|
+
labels(n)[0] AS firstLabel,
|
|
276
|
+
{COALESCE_TEXT} AS text
|
|
277
|
+
LIMIT $batchSize;
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
count_out = cypher(COUNT_QUERY, {"registered": REGISTERED_LABELS})
|
|
281
|
+
total_remaining = 0
|
|
282
|
+
for row in parse_csv_rows(count_out):
|
|
283
|
+
total_remaining = int(row["remaining"])
|
|
284
|
+
|
|
285
|
+
print(f"[embed-backfill] start total={total_remaining} model={EMBED_MODEL}")
|
|
286
|
+
|
|
287
|
+
if total_remaining == 0:
|
|
288
|
+
print("[embed-backfill] done remaining=0 (nothing to backfill)")
|
|
289
|
+
sys.exit(0)
|
|
290
|
+
|
|
291
|
+
# Pre-warm Ollama so the first per-node call doesn't pay the model-load
|
|
292
|
+
# latency. The cold-start window for nomic-embed-text on a Pi 5 can exceed
|
|
293
|
+
# 30s; calling once with a tiny throwaway input loads the weights into
|
|
294
|
+
# memory before the loop begins. Failure here is treated identically to
|
|
295
|
+
# any other Ollama failure — loud abort with re-run instruction.
|
|
296
|
+
print(f"[embed-backfill] pre-warm model={EMBED_MODEL} timeout=180s")
|
|
297
|
+
ollama_embed("warmup", timeout=180, retry_on_timeout=False)
|
|
298
|
+
|
|
299
|
+
processed_total = 0
|
|
300
|
+
batch_index = 0
|
|
301
|
+
while True:
|
|
302
|
+
batch_start = time.time()
|
|
303
|
+
batch_out = cypher(
|
|
304
|
+
BATCH_QUERY,
|
|
305
|
+
{"registered": REGISTERED_LABELS, "batchSize": BATCH_SIZE},
|
|
306
|
+
)
|
|
307
|
+
rows = parse_csv_rows(batch_out)
|
|
308
|
+
if not rows:
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
# Compute embeddings serially. Ollama on a Pi 5 handles ~3-10 embeds
|
|
312
|
+
# per second with nomic-embed-text; concurrent requests just queue
|
|
313
|
+
# behind the GPU/CPU bottleneck so parallelism wouldn't help.
|
|
314
|
+
pairs: list[tuple[str, list[float]]] = []
|
|
315
|
+
label_counts: dict[str, int] = {}
|
|
316
|
+
for row in rows:
|
|
317
|
+
node_id = row["id"]
|
|
318
|
+
text = row["text"]
|
|
319
|
+
first_label = row["firstLabel"]
|
|
320
|
+
if not text:
|
|
321
|
+
continue
|
|
322
|
+
embedding = ollama_embed(text)
|
|
323
|
+
pairs.append((node_id, embedding))
|
|
324
|
+
label_counts[first_label] = label_counts.get(first_label, 0) + 1
|
|
325
|
+
|
|
326
|
+
if not pairs:
|
|
327
|
+
# Defensive: query said rows exist but all text was empty after
|
|
328
|
+
# the python read — means the COALESCE_TEXT predicate is wider
|
|
329
|
+
# than the python check. Stop to avoid an infinite loop.
|
|
330
|
+
sys.stderr.write("[embed-backfill] WARN: batch returned rows with empty text — stopping to avoid infinite loop\n")
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
# Build the WRITE batch as a Cypher literal payload rather than a
|
|
334
|
+
# `--param` map. cypher-shell's `--param` parses the value as a Cypher
|
|
335
|
+
# expression, and Cypher map keys must be bare identifiers (or backtick-
|
|
336
|
+
# quoted) — NOT double-quoted strings as JSON would emit. Interpolating
|
|
337
|
+
# bare-key map literals directly avoids the question entirely:
|
|
338
|
+
#
|
|
339
|
+
# UNWIND [{id: '4:abc:1', embedding: [0.1, 0.2, ...]}, ...] AS pair
|
|
340
|
+
# MATCH (n) WHERE elementId(n) = pair.id
|
|
341
|
+
# SET n.embedding = pair.embedding;
|
|
342
|
+
#
|
|
343
|
+
# cypher_string_literal escapes any backslash/quote in elementIds
|
|
344
|
+
# defensively; cypher_float_list serialises the embedding via repr()
|
|
345
|
+
# which Cypher accepts as a number literal.
|
|
346
|
+
pair_literals = ",".join(
|
|
347
|
+
f"{{id: {cypher_string_literal(node_id)}, embedding: {cypher_float_list(embedding)}}}"
|
|
348
|
+
for node_id, embedding in pairs
|
|
349
|
+
)
|
|
350
|
+
cypher(
|
|
351
|
+
f"""
|
|
352
|
+
UNWIND [{pair_literals}] AS pair
|
|
353
|
+
MATCH (n) WHERE elementId(n) = pair.id
|
|
354
|
+
SET n.embedding = pair.embedding;
|
|
355
|
+
"""
|
|
356
|
+
)
|
|
357
|
+
elapsed_ms = int((time.time() - batch_start) * 1000)
|
|
358
|
+
batch_index += 1
|
|
359
|
+
processed_total += len(pairs)
|
|
360
|
+
label_summary = ", ".join(f"{k}={v}" for k, v in sorted(label_counts.items()))
|
|
361
|
+
print(f"[embed-backfill] batch={batch_index} processed={len(pairs)} elapsed-ms={elapsed_ms} labels={label_summary}")
|
|
362
|
+
|
|
363
|
+
# Final remaining check — should be zero or the diff between original
|
|
364
|
+
# total and processed_total (e.g. if new writes landed mid-run).
|
|
365
|
+
final_out = cypher(COUNT_QUERY, {"registered": REGISTERED_LABELS})
|
|
366
|
+
final_remaining = 0
|
|
367
|
+
for row in parse_csv_rows(final_out):
|
|
368
|
+
final_remaining = int(row["remaining"])
|
|
369
|
+
print(f"[embed-backfill] done processed={processed_total} remaining={final_remaining}")
|
|
370
|
+
PYEOF
|
|
@@ -411,13 +411,19 @@ fi
|
|
|
411
411
|
|
|
412
412
|
echo "==> Connecting to Neo4j at $NEO4J_URI as $NEO4J_USER"
|
|
413
413
|
|
|
414
|
-
#
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
|
|
414
|
+
# Schema migrations run before the main schema apply so renames don't collide
|
|
415
|
+
# with the new declarations. Each statement is idempotent (`IF EXISTS`):
|
|
416
|
+
# - Task 249: `user_profile_account_unique` replaced by composite (accountId, userId).
|
|
417
|
+
# - Task 249: `preference_category` index replaced by (accountId, userId, category).
|
|
418
|
+
# - Task 748: `knowledge_fulltext` (3 labels) replaced by `entity_search` (~40 labels)
|
|
419
|
+
# with the universal label/property union. The new index is created by the
|
|
420
|
+
# schema apply below; dropping the old name here is what lets cypher-shell
|
|
421
|
+
# run both in one pass without conflict.
|
|
422
|
+
echo "==> Migrating schema: dropping renamed/obsolete constraints + indexes..."
|
|
418
423
|
"$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" << 'MIGRATE_EOF'
|
|
419
424
|
DROP CONSTRAINT user_profile_account_unique IF EXISTS;
|
|
420
425
|
DROP INDEX preference_category IF EXISTS;
|
|
426
|
+
DROP INDEX knowledge_fulltext IF EXISTS;
|
|
421
427
|
MIGRATE_EOF
|
|
422
428
|
|
|
423
429
|
# Vector index dimensions — configurable at install time via --embed-model.
|
package/payload/server/server.js
CHANGED
|
@@ -10127,7 +10127,7 @@ var import_dist = __toESM(require_dist());
|
|
|
10127
10127
|
import { int } from "neo4j-driver";
|
|
10128
10128
|
var VECTOR_WEIGHT = 0.7;
|
|
10129
10129
|
var BM25_WEIGHT = 0.3;
|
|
10130
|
-
var FULLTEXT_INDEX_NAME = "
|
|
10130
|
+
var FULLTEXT_INDEX_NAME = "entity_search";
|
|
10131
10131
|
function escapeLucene(query) {
|
|
10132
10132
|
return query.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, "\\$&");
|
|
10133
10133
|
}
|