Skip to content

3.17 Text into the graph, and one query as a pipeline — ai.fromText

CALL ai.fromText($notes,
'(t:Attendee)-[:RAISED]->(i:Issue), (t)-[:WORKS_FOR]->(a:CustomerAccount)')
YIELD t, i, a
WITH DISTINCT i, a
MATCH (a)-[:HAS_CASE]->(c:SupportCase) WHERE c.status IN ['open', 'pending']
WITH i, relevant(c, 'the same problem as: ' + i.summary) AS tracked
UNWIND tracked AS tc
CREATE (i)-[:TRACKED_BY]->(tc)
WITH count(*) AS linked
MATCH (t2:Attendee)-[:RAISED]->(untracked:Issue)
WHERE NOT EXISTS { (untracked)-[:TRACKED_BY]->() }
RETURN untracked.summary, untracked.severity, t2.name, t2.role

Means: “From these meeting notes, what did the customer raise that we have no open ticket for?”

Four things happen in one ordinary query:

  • The notes become a graph. ai.fromText reads the text into the pattern’s shape. Attendee and Issue are realm types marked projected: true — nothing fetches or stores them — and their descriptions are what the model is shown, so “a problem, not a request or a wish” keeps “they want annual billing” out of Issue. CustomerAccount is a world type, so it is resolved through its spine, never created: a company the books do not know comes back as a warning, not a fake account.
  • The model judges nodes, per group. relevant(c, …) keeps the cases that are the same problem as this issue — the criterion reads the grouping key — and what it keeps are the group’s own nodes.
  • The query writes, and nothing is stored. CREATE of a projected relationship lands in the run’s scratch: the rest of the query sees it, no other run does, and it is gone when the run ends.
  • The label is the isolation. MATCH (untracked:Issue) reads exactly what this run projected, because Issue has no stored instances.

Habits: use fresh names across stages (tc, t2, untracked); write only projected types; and read the warnings — a declined personal address or an unresolved company is said, never silently empty. See §7.8 and §7.9 of the spec.