---
slug: graph-query
title: "Asking by relationship: ak graph"
description: "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."
lang: en
---


# Asking by relationship: `ak graph`

This page gathers and extends the "Asking by relationship" section of
[Searching](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.

## 8. Related pages

- Ordinary search and reading its results → [Searching](searching)
- The tool map the AI uses → [For AIs connected to AiAkiv](for-ai-agents)
- Graph queries across another team's memory → [Links](links)
- Click-through slides, "Asking by relationship" → [Tips page](https://www.aiakiv.com/learn)
- The same material as a reference for AIs → [llms-graph-query.txt](https://www.aiakiv.com/llms-graph-query.txt)
