Asking by relationship: ak graph

Graph queries with `ak graph`: fix a name or tag scope, then ask. How to ask, example requests and the queries they become, the grammar, how to read the response, and the limits.

This page gathers and extends the "Asking by relationship" section of Searching. It covers how to ask, example requests by question type and the queries the AI turns them into, the whole grammar, how to read the response, and the limits.

1. What it does, and when to use it

A graph query asks about memories by relationship and by scope. Questions such as "only among memories where this name appears", "only inside this tag", "memories that share the most names with these two", or "what came next" become a single query. It only reads; it changes nothing.

There are three ways to look something up. Pick by the shape of the question.

What you want to ask The way Why
Find by a sentence: "How did we fix the save bug?" Ordinary search (ak …) Finds by meaning and connection, and gives a map of how the results link up. This is the default
Fix a scope, then find by meaning: "among memories where the save format appears, the ones that fixed a load bug" Graph query (ak graph …) Ranks by closeness in meaning inside that scope only. Memories outside the scope do not take the places first
Follow relationships: "the memories that share the most names with these two", "the next records" Graph query Walks shared names, record order, and closeness in meaning
Everything in a scope, a list: "every memory in the balance tag" Graph query, list shape You need a list, not a ranking. Up to the newest 200 at a time
Exact wording, exact counts: "how many memories have 'rollback' in the summary?" The AI counts with the SQL query tool A graph query ranks by meaning, not by matching letters. Partial name matching works on entity and tag names only, not on summary text

A graph query is used only when you ask with ak graph. Without the phrase, even a question about connections starts as an ordinary search, and the AI does not switch to a graph query on its own.

2. How to ask

Put ak graph in front of the question and send it as one message. Write the question the way you normally would.

ak graph among memories where the save format appears, the ones that fixed a load bug
ak graph in the balance tag, the ones that changed enemy health
ak graph memories where both inventory and save format appear
  • The AI translates the question into a graph query and runs it, and alongside the answer it shows the query it ran and what it means, in one line, so you can see what was actually looked up.
  • Names and tags are matched exactly as they were saved. If the spelling is uncertain, the AI checks it first with a name lookup.
  • Send ak graph on its own and the AI shows three example requests to copy, adapted to names in your memory.

3. Example requests by question type, and the queries they become

The request column says the same thing two ways. Ask in your own words. A $name in a query is a parameter slot; its value goes in the params next to it.

Request (in your words) Query it becomes What to look at in the response
① Meaning search inside an entity scope. "among memories where the save format appears, the ones that fixed a load bug" or "in the save format stuff, find where we fixed the load bug" START a = events(entity: $name, text: $q, k: 20) RETURN a, a.score ORDER BY a.score DESC, a.id · params {"name": "save format", "q": "fixed a load bug"} The closest 20. If start.has_more is true the scope holds more: read candidates.count and say "the closest 20 of 64"
② Meaning search inside a tag scope. "in the balance tag, the ones that changed enemy health" or "just the enemy HP tweaks inside the balance tag" START a = events(tag: $tag, text: $q, k: 20) RETURN a, a.score ORDER BY a.score DESC, a.id · params {"tag": "balance", "q": "changed enemy health"} Same as ①. The tag name must match exactly
③ Partial name match. "every name appearing with the save format whose name contains 'bug'" or "entities that show up with the save format and have bug in the name" START a = events(entity: $name, k: 200) MATCH (a)-[:PARTICIPATED_IN]-(n) WHERE n.name CONTAINS $part RETURN DISTINCT n.name LIMIT 200 · params {"name": "save format", "part": "bug"} Searches the newest 200 save format memories. If start.hub is true there were more than 200 and the older ones were not seen
④ Everything, a list. "every memory in the balance tag" or "list the whole balance tag" START a = events(tag: $tag, k: 200) RETURN a LIMIT 200 · params {"tag": "balance"} (for a name scope, entity: $name) A newest-first list. If start.hub is true it passed 200: say "showing only the newest 200"
⑤ Memories where both A and B appear. "memories where both inventory and save format appear" or "inventory records that also mention the save format" START a = events(entity: $a, k: 200) MATCH (a)-[:PARTICIPATED_IN]-(n {name: $b}) RETURN DISTINCT a LIMIT 200 · params {"a": "inventory", "b": "save format"} Collected from the newest 200 inventory memories. If start.hub is true, the rest was not seen
⑥ Neighbours through shared entities. "the other memories that share the most names with these two" or "memories that overlap most with those records" START a = events(ids: $ids) MATCH (a)-[:PARTICIPATED_IN]-(e)-[:PARTICIPATED_IN]-(b) WHERE NOT b.id IN $ids RETURN b, count(DISTINCT e) AS shared ORDER BY shared DESC LIMIT 20 · params {"ids": ["evt_…a", "evt_…b"]} shared is the number of names they have in common. Written with SHARES, s.via carries the names that formed the bridge (section 4, example 4)
⑦ Similar memories, next memories. "memories similar in meaning to this one" or "the records that followed this one, up to three steps" Similar: START a = events(ids: $ids) MATCH (a)-[f:SIMILAR {k: 5}]-(b) RETURN b, f.cos ORDER BY f.cos DESC LIMIT 10 · Next: START a = events(ids: $ids) MATCH (a)-[n:NEXT*1..3]->(b) RETURN b, n.hops ORDER BY n.hops LIMIT 30 f.cos is closeness in meaning (higher is closer); n.hops is how many steps later
⑧ Only new ones, not what I have seen. "not the results I just saw: other memories sharing two or more names with these" or "more connected ones, minus the earlier results" START a = events(ids: $ids) MATCH (a)-[s:SHARES {min: 2}]-(b) WHERE NOT b.id IN $seen RETURN b, s.count, s.via ORDER BY s.weight DESC LIMIT 20 · params {"ids": [...], "seen": [...]} Put the ids already seen in $seen to leave them out. The filter applies after the walk
⑨ Why this order. "why this order? show the scores too" or "how close are they, as numbers" START a = events(tag: $tag, text: $q, k: 10) RETURN a.id, a.summary, a.score ORDER BY a.score DESC, a.id · params {"tag": "balance", "q": "changed enemy health"} a.score is closeness to the start sentence. The gap between first and second place is the reason for the ranking

4. Worked examples

Each one follows a request from start to the AI's answer. The data is a made-up game project, and the responses are shortened to their gist.

Example 1. Meaning search inside a scope: "the closest k of N"

Request:  ak graph among memories where the save format appears, the ones that fixed a load bug

The line the AI shows:
  Query run: START a = events(entity: $name, text: $q, k: 20) RETURN a, a.score ORDER BY a.score DESC, a.id
  (meaning: the 20 memories mentioning "save format" closest in meaning to "fixed a load bug")

Response gist:
  rows: 20 (evt_…, a.score from 0.62 down to 0.41)
  start: scoped_mode "exact", candidates {count: 64, exact: true}, filled true, has_more true
  partial: false, truncated: []
  notes: "start: top-20 of 64 candidates; 44 more exist in scope (not a budget cut)"

What the AI says: "These are the 20 closest in meaning out of 64 memories where the save format appears. The other 44 were not returned, so this is not a comparison of all 64. The first (0.62) is the record that fixed the order in which the version field is read on load; it is well ahead of the second (0.55), so it is the most direct answer."

exact means "the distance of all 64 candidates was measured", not "all 64 were returned". That is why the AI does not say it checked everything.

Example 2. Everything, a list: past 200

Request:  ak graph show me every memory in the balance tag

The line the AI shows:
  Query run: START a = events(tag: $tag, k: 200) RETURN a LIMIT 200
  (meaning: memories in the balance tag, newest first, up to 200)

Response gist:
  rows: 200
  start: hub true, has_more true
  partial: false, truncated: []
  notes: "start: top-200 of 201+ candidates; 1+ more exist in scope (not a budget cut)"

What the AI says: "The balance tag has more than 200 memories. I am showing only the newest 200; older records were not seen this time. If you need one topic, add a sentence such as 'enemy health' and it becomes a meaning search inside the scope."

A + after a number means a lower bound: "at least 201", not an exact count.

Example 3. 0 results: "not in the top k" versus "could not confirm"

The same query shape returned 0 twice. The two mean different things.

(a) 0 with nothing cut
Request:  ak graph among inventory memories close to the item duplication bug, the ones where the save format also appears
  Query run: START a = events(entity: $a, text: $q, k: 20) MATCH (a)-[:PARTICIPATED_IN]-(n {name: $b}) RETURN DISTINCT a
Response gist:
  rows: 0
  start: scoped_mode "exact", candidates {count: 90, exact: true}, filled true, has_more true
  partial: false, truncated: []
  notes: "start: top-20 of 90 candidates; 70 more exist in scope (not a budget cut)"

(b) 0 without covering the whole scope
Request:  ak graph among combat memories close to the corrupted save file incident, the ones where the save format also appears
  Query run: same shape as (a), with $a = "combat"
Response gist:
  rows: 0
  start: scoped_mode "filtered_ann", candidates {count: 3001, exact: false}, filled false
  partial: true, truncated: [{stage: "start", reason: "filled_false"}]
  notes: "scoped START did not cover its whole scope (filled false, names truncated, or cut by
          a statement timeout or the deadline) - rows may be missing; 0 rows means
          'not confirmed here', not 'none exist'"

What the AI says for (a): "Among the closest 20 of 90 inventory memories, none also mentions the save format. The other 70 were not looked at, so I cannot say there are none. To check all of them, ask for memories where both names appear, in list shape."

What the AI says for (b): "Combat has more than 3,000 memories, so a fast approximate search was used, and it could not fill all 20 places. So 0 here means 'not confirmed this time', not 'none'. Narrowing the scope, by adding a tag or using a narrower name, lets us look again."

Saying "none" takes evidence: a 0 after covering the whole scope, or a check by another route that sees everything, such as a name lookup.

Example 4. Walking relationships: reading an aggregate column

Request:  ak graph the other memories that share the most names with the top two records from example 1

The line the AI shows:
  Query run: START a = events(ids: $ids) MATCH (a)-[:PARTICIPATED_IN]-(e)-[:PARTICIPATED_IN]-(b)
             WHERE NOT b.id IN $ids RETURN b, count(DISTINCT e) AS shared ORDER BY shared DESC LIMIT 20
  (meaning: other memories reached through the names in the two records, most shared names first, 20 of them)

Response gist:
  columns: [b, shared]
  rows: 20. row 1 {b: {id: evt_…, summary: "Wrote the save slot migration script"}, shared: 3}
            row 2 {b: {id: evt_…, summary: "Changed the load screen progress display"}, shared: 2} …
  partial: false, truncated: []

What the AI says: "The memory sharing three names with the two records is 'Wrote the save slot migration script'. To see which names formed the bridge, ask again with SHARES and read s.via."

shared is not a rank; it is the number of names in common. Memories reached by walking have no a.score (it is empty). a.score is closeness to the start sentence, so only the start carries it.

5. The grammar at a glance

A read-only subset of Cypher, shaped START … [MATCH … [WHERE …]] RETURN ….

Start (START a = events(…)). The start is always memories (events).

Start Meaning k default · max
text: $q Memories closest in meaning to the sentence, across everything 5 · 20
entity: $name Memories where that name appears, newest first 5 · 200
tag: $t Memories in that tag, newest first 5 · 200
ids: [$id1, $id2] or ids: $ids The given memories (up to 20; k is ignored) -
entity: $name, text: $q Closest in meaning, inside the memories where that name appears 5 · 20
tag: $t, text: $q Closest in meaning, inside that tag 5 · 20

Relations (MATCH (a)-[:RELATION]-(b))

Relation Joins Meaning and usable values
PARTICIPATED_IN entity and memory That name appears in that memory. Walking memory→name→memory gives "memories where the same name appears"
SHARES {min, min_w} memory and memory They share at least min names. s.count (how many), s.weight (rarer names weigh more), s.via (the names that form the bridge)
SIMILAR {k, min} memory and memory Neighbours close in meaning. f.cos
FAR {max} memory and memory Keeps only the far ones (cos < max). With SHARES: "names overlap, meaning is far"
NEXT {source} memory → memory Record order (thread, conversation). Directed. Several steps with *1..3, n.hops
MEMBER_OF {kind} memory → tag Tag membership. Directed
RESOLVED_BY {relation} memory → memory A plan or expectation leads to the record where it was carried out. Directed. r.asserted_at
CONNECTED entity and entity Names that have appeared together. c.event_count
SAME_AS entity and entity An alias registered as the same thing. x.confidence

Other rules

  • MATCH can be left out. When the start itself is the answer, end with START … RETURN … (①②④⑨).
  • WHERE: =, <>, <, <=, >, >=, IN, CONTAINS, AND·OR·NOT, and the closeness in meaning of two memory variables, cos(a, b). WHERE comes only after a MATCH.
  • CONTAINS works on the name of an entity or tag only. It ignores letter case and full-width forms. A piece that is all ASCII (letters, digits, punctuation) needs 2 characters or more; one Hangul or Han character is enough.
  • RETURN [DISTINCT], the aggregates count·collect·min·max·sum·avg, AS aliases, ORDER BY, LIMIT. With aggregates, ORDER BY sorts by a returned column (its alias).
  • a.score: closeness to the start sentence. It is empty on a start without a sentence and on memories reached by walking, and empty values sort last. For a stable order use ORDER BY a.score DESC, a.id.
  • $name is a parameter. The user's sentence is not written into the query; it goes through params.
  • If the arrow is left off a directed relation and the two ends allow only one direction, it is filled in and noted in notes ((a)-[:MEMBER_OF]-(t) → ->). NEXT and RESOLVED_BY, which join memory to memory, need the arrow written out. A wrong arrow is rejected, not flipped.
  • Returned rows are shrunk: a memory to {id, summary, timestamp, order_index}, an entity to {id, name, type}, a tag to {id, name}. Open the full text separately with a content lookup.

Not available

  • No WITH. Conditions go into relation values (SHARES {min: 3}).
  • No writes (CREATE, SET and so on). It is read-only.
  • No "starts with" (STARTS WITH). The AI gets the names with CONTAINS, filters them itself, and says it did.
  • No letter matching on summary text. That question belongs to the SQL query.
  • A question that is only a sentence, with no relationship in it, is better as an ordinary search than as a graph query.

6. Reading the response

The response is {columns, rows, row_count, start, partial, truncated, plan, budget, notes}. Completeness signals come in two kinds.

Not a budget cut (they do not go into partial or truncated)

  • start.has_more: true: the scope holds more candidates than came back. Carried by meaning search inside a scope and by tag: alone.
  • start.candidates.count: the number of candidates in the scope. With exact: false it is a lower bound. "The closest 20 of 224" is not "all 224".
  • start.hub: true: entity: or tag: alone found more than the newest k it returned. For entity: alone this is the only signal.
  • Default LIMIT: without LIMIT, rows stop at 20 (up to 200).

Budget cuts (partial: true, an entry in truncated)

  • filled: false: a large scope was searched approximately and fewer than k were found.
  • Name cap: more than 20 entities or tags matched the name and only some were used (start.entities_truncated / tags_truncated).
  • Timeout or deadline: the walk was too wide and was cut part way. The feature is not broken. Rather than repeating the same query, narrow it: lower k or LIMIT, or raise SHARES {min}.

Notes that can appear (they come in English, verbatim)

  • More candidates: start: top-20 of 64 candidates; 44 more exist in scope (not a budget cut)
  • Cut at the default LIMIT: rows cut to the default LIMIT 20; add LIMIT to return more (max 200)
  • Scope not fully covered: scoped START did not cover its whole scope (filled false, names truncated, or cut by a statement timeout or the deadline) - rows may be missing; 0 rows means 'not confirmed here', not 'none exist'
  • Walk cut after a partial name match: CONTAINS filtered after the walk and the walk was truncated - 0 rows means 'not confirmed here', not 'none exist'

When the scope was not fully covered, the third note appears instead of the first. The first note is also left off a 0 result that a budget cut emptied.

0 results means "could not confirm", not "none". If something was cut, it was not confirmed. Even with nothing cut, if only the top k was seen (has_more, hub), the rest was not looked at. Saying "none" takes a 0 after covering the whole scope, or evidence from another route that sees everything, such as a name lookup.

A rejection comes back as {error, blocked_by: syntax|grammar|params, hint, allowed_*}. The AI reads the hint and the allowed lists, fixes the query once, and calls again.

7. Limits

Item Value
k default 5
k max (text:, scope plus sentence) 20. Higher values are lowered to 20 and noted in notes
k max (entity: / tag: alone) 200
Number of ids: up to 20
Entities or tags one name can point to up to 20
LIMIT default 20, max 200
Several steps (* on NEXT / SHARES) 1 to 3
CONTAINS piece length 2 or more if all ASCII, 1 or more with Hangul or Han
Summary length Shortened in each row. Open the full text with a content lookup
  • Memories outside your permissions are never visible. They do not show in the rows or in the candidate counts. What you can read is the scope of the query.
  • Meaning search inside a scope returns at most the closest 20 at a time. To see more, ask in list shape (scope alone with k: 200) or narrow further with a sentence or a tag.
  • Some servers do not offer this feature, or have part of it off. If query_memory_graph is not in the tool list, graph queries are not available. A server with only the new syntax off (scope plus sentence, tag:, leaving out MATCH, a.score, CONTAINS, arrow filling) rejects it with … is not available on this server (scoped match is off). The AI then switches to text: or entity: alone, or to an ordinary search, and tells you it is off.
Full index → Docs View as Markdown