Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/odd-drinks-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@rushdb/javascript-sdk': patch
'@rushdb/mcp-server': patch
'rushdb-dashboard': patch
'@rushdb/skills': patch
'rushdb-core': patch
'rushdb-docs': patch
---

Rework `$cycle` and stabilize relationship/query intelligence

- **`$cycle` is now a record-level operator**: `where: { $cycle: { type, direction, hops } }` — the value IS the traversal spec. Replaces the previous `KEY: { $cycle: true, $relation: {...} }` block form entirely (breaking: the old form now throws). Compiles to an `EXISTS` subquery so the engine stops at the first cycle found per record instead of enumerating every path — avoids exponential blowup on densely connected graphs. Default traversal hop cap (`RUSHDB_MAX_TRAVERSAL_HOPS`) lowered from 25 to 10 to keep worst-case queries within the transaction budget.
- **Relationship pattern suggestions are now verified against live data**: candidates are proposed from schema names/types and LLM semantic judgment, then confirmed with a real graph probe before being surfaced — sampled schema values (which carry no signal on high-cardinality data) no longer gate or reject a suggestion.
- SmartSearch prompts and specs (core, MCP server, skills, docs) made more data-agnostic and kept in sync across all four DSL mirrors.
- Fix: CSV import no longer fails when a column parses to a JS `Date` (folded back to ISO string).
- Fix: dashboard record search box text no longer leaks between project switches.
9 changes: 3 additions & 6 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,14 @@
db.records.find({
labels: ['ACCOUNT'],
where: {
RING: {
$cycle: true,
$relation: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
$cycle: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
})
```

A `$cycle` block requires `$relation` with `hops` (`min` ≥ 2, the default) and accepts no other keys; its label key is a display name. Combine with `$not` to select records not on a cycle.
`$cycle` is a record-level predicate: its value is the traversal spec itself (`type`, `direction`, `hops` — `hops` mandatory, `min` ≥ 2, defaulting to 2). A cycle has no separate endpoint, so it accepts no `$alias`, no property criteria, and no nested labels. Combine with `$not` to select records not on a cycle.

**Traversal depth policy** — `hops.max` is capped per deployment via `RUSHDB_MAX_TRAVERSAL_HOPS` (default 25) on the shared cloud connection. Self-hosted deployments (`RUSHDB_SELF_HOSTED=true`) and projects with a custom external Neo4j allow unbounded traversal (`max` omitted), guarded by the existing transaction timeout.
**Traversal depth policy** — `hops.max` is capped per deployment via `RUSHDB_MAX_TRAVERSAL_HOPS` (default 10) on the shared cloud connection. Self-hosted deployments (`RUSHDB_SELF_HOSTED=true`) and projects with a custom external Neo4j allow unbounded traversal (`max` omitted), guarded by the existing transaction timeout.

Also in this release:

Expand Down
10 changes: 5 additions & 5 deletions docs/docs/connect/skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ displayed_sidebar: docs

Install RushDB Agent Skills to teach compatible AI assistants how to query RushDB correctly, model graph data, build faceted search, and use RushDB as persistent memory.

Skills complement the [MCP server](./mcp): MCP gives an agent runtime tools; Skills teach the agent when and how to use them.
Skills complement the [MCP server](/connect/mcp): MCP gives an agent runtime tools; Skills teach the agent when and how to use them.

## Install

Expand Down Expand Up @@ -46,11 +46,11 @@ An empty schema is valid for a new project. The important result is that the age

## OpenClaw

OpenClaw uses workspace-level skill installation. Follow the [OpenClaw walkthrough](../learn/tutorials/agent-memory/agent-skills-with-openclaw#part-2--install-the-rushdb-skills-pack) for native `openclaw` and ClawHub commands.
OpenClaw uses workspace-level skill installation. Follow the [OpenClaw walkthrough](/learn/tutorials/agent-memory/agent-skills-with-openclaw#part-2--install-the-rushdb-skills-pack) for native `openclaw` and ClawHub commands.

## Next Steps

- [Connect the MCP server](./mcp)
- [Bootstrap persistent agent memory](../learn/agent-memory/quickstart)
- [Use RushDB with AI agents](./agents)
- [Connect the MCP server](/connect/mcp)
- [Bootstrap persistent agent memory](/learn/agent-memory/quickstart)
- [Use RushDB with AI agents](/connect/agents)
- [Browse the skills source](https://github.com/rush-db/rushdb/tree/main/packages/skills)
18 changes: 9 additions & 9 deletions docs/docs/get-started/quick-tutorial.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,20 +197,20 @@ curl -X POST "https://api.rushdb.com/api/v1/records/import/json" \
</TabItem>
</Tabs>

→ [TypeScript SDK](/learn/reference/typescript) · [Python SDK](/learn/reference/python) · [REST API](../learn/reference/rest-api/introduction)
→ [TypeScript SDK](/learn/reference/typescript) · [Python SDK](/learn/reference/python) · [REST API](/learn/reference/rest-api/introduction)

---

## What to build

| Use case | Guide |
| ------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Agent memory — sessions, decisions, context | [Episodic Memory for Multi-Step Agents](../learn/tutorials/agent-memory/episodic-memory) |
| GraphRAG — graph + vector search | [GraphRAG Tutorial](../learn/tutorials/ai-and-rag/graphrag) |
| Semantic search over your data | [Semantic Search in 5 Minutes](../learn/tutorials/ai-and-rag/ai-semantic-search) |
| Hybrid filter + semantic search | [Hybrid Retrieval](../learn/tutorials/ai-and-rag/hybrid-retrieval) |
| Explore an unknown dataset | [Discovery Queries](../learn/tutorials/search-and-queries/discovery-queries) |
| Safe agent query planning | [Agent-Safe Query Planning](../learn/tutorials/agent-memory/agent-safe-query-planning) |
| Use case | Guide |
| ------------------------------------------- | -------------------------------------------------------------------------------------- |
| Agent memory — sessions, decisions, context | [Episodic Memory for Multi-Step Agents](/learn/tutorials/agent-memory/episodic-memory) |
| GraphRAG — graph + vector search | [GraphRAG Tutorial](/learn/tutorials/ai-and-rag/graphrag) |
| Semantic search over your data | [Semantic Search in 5 Minutes](/learn/tutorials/ai-and-rag/ai-semantic-search) |
| Hybrid filter + semantic search | [Hybrid Retrieval](/learn/tutorials/ai-and-rag/hybrid-retrieval) |
| Explore an unknown dataset | [Discovery Queries](/learn/tutorials/search-and-queries/discovery-queries) |
| Safe agent query planning | [Agent-Safe Query Planning](/learn/tutorials/agent-memory/agent-safe-query-planning) |

---

Expand Down
13 changes: 3 additions & 10 deletions docs/docs/learn/records-and-queries/find-and-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ curl -X POST https://api.rushdb.com/api/v1/records/search \

### Multi-hop and cycles

Add `hops` to `$relation` to traverse a variable number of hops (`type`/`direction` apply to every hop; criteria filter only the final record), and `$cycle: true` to find records on a closed path back to themselves:
Add `hops` to `$relation` to traverse a variable number of hops (`type`/`direction` apply to every hop; criteria filter only the final record), and use the `$cycle` operator — its value is the traversal spec itself, with `hops` mandatory (`min` ≥ 2) — to find records on a closed path back to themselves:

<Tabs groupId="programming-language">
<TabItem value="python" label="Python" default>
Expand All @@ -343,10 +343,7 @@ result = db.records.find({
result = db.records.find({
"labels": ["ACCOUNT"],
"where": {
"RING": { # display name, not matched as a label
"$cycle": True,
"$relation": {"type": "TRANSFERRED_TO", "direction": "out", "hops": {"min": 2, "max": 6}}
}
"$cycle": {"type": "TRANSFERRED_TO", "direction": "out", "hops": {"min": 2, "max": 6}}
}
})
```
Expand All @@ -370,11 +367,7 @@ const { data } = await db.records.find({
const { data: rings } = await db.records.find({
labels: ['ACCOUNT'],
where: {
RING: {
// display name, not matched as a label
$cycle: true,
$relation: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
$cycle: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
})
```
Expand Down
15 changes: 6 additions & 9 deletions docs/docs/learn/reference/python/SearchQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ result = db.records.find({

##### Variable-length traversal (`hops`)

Add `hops` to `$relation` to match records up to N hops away. A number means exactly N hops; a `{"min": ..., "max": ...}` dict is a range (`min` defaults to `1`). `type` and `direction` apply to every hop; property criteria and the block's label constrain only the final record — intermediates are anonymous. `max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 25); omitting it requests unbounded traversal, allowed only on self-hosted deployments or projects with a custom Neo4j instance:
Add `hops` to `$relation` to match records up to N hops away. A number means exactly N hops; a `{"min": ..., "max": ...}` dict is a range (`min` defaults to `1`). `type` and `direction` apply to every hop; property criteria and the block's label constrain only the final record — intermediates are anonymous. `max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 10); omitting it requests unbounded traversal, allowed only on self-hosted deployments or projects with a custom Neo4j instance:

```python
# Employees whose management chain (up to 4 hops) contains Alice
Expand All @@ -259,20 +259,17 @@ result = db.records.find({

##### Cycle detection (`$cycle`)

`"$cycle": True` matches records on a closed path back to themselves (rings, circular ownership). It requires a dict `$relation` with `hops` (`min` ≥ 2, defaults to 2) and accepts nothing else — no `$alias`, no property criteria, no nested labels. The block's key is a display name, not matched as a label:
`$cycle` is a record-level predicate that matches records on a closed path back to themselves (rings, circular ownership). Its value is the traversal spec itself — `type`, `direction`, `hops` — with `hops` mandatory (`min` ≥ 2, defaults to 2). It accepts nothing else — no `$alias`, no property criteria, no nested labels; intermediate node labels are unconstrained. It composes under `$not`/`$or`/`$and`, and can be placed inside a label block to anchor the cycle at a related record:

```python
# Accounts on a directed transfer ring of 2–6 hops
result = db.records.find({
"labels": ["ACCOUNT"],
"where": {
"RING": {
"$cycle": True,
"$relation": {
"type": "TRANSFERRED_TO",
"direction": "out",
"hops": { "min": 2, "max": 6 }
}
"$cycle": {
"type": "TRANSFERRED_TO",
"direction": "out",
"hops": { "min": 2, "max": 6 }
}
}
})
Expand Down
14 changes: 7 additions & 7 deletions docs/docs/learn/reference/typescript/SearchQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ export type Related<M extends Record<string, Model['schema']> = Models> =
[Key in keyof M]?: {
$alias?: string
$relation?: TraversalRelationOptions | string
$cycle?: boolean
} & Where<M[Key]>
}

Expand All @@ -395,6 +394,10 @@ export type TraversalHops = number | { min?: number; max?: number }
export type TraversalRelationOptions = RelationOptions & {
hops?: TraversalHops
}

export type CycleExpression = RelationOptions & {
hops: TraversalHops
}
```

Defines conditions on related records. The key of the nested object **is** the label name (case-sensitive). Use `$alias` to name the traversal for later use in `select`/`groupBy`, and `$relation` to constrain the relationship type or direction:
Expand All @@ -421,7 +424,7 @@ Defines conditions on related records. The key of the nested object **is** the l
}
```

`hops` turns the traversal into a variable-length one: a number means exactly N hops, `{ min?, max? }` a range (`min` defaults to `1`). `type` and `direction` apply to every hop; property criteria and the nested label constrain only the final record — intermediates are anonymous. `max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 25); omitting it requests unbounded traversal, allowed only on self-hosted deployments or projects with a custom Neo4j instance:
`hops` turns the traversal into a variable-length one: a number means exactly N hops, `{ min?, max? }` a range (`min` defaults to `1`). `type` and `direction` apply to every hop; property criteria and the nested label constrain only the final record — intermediates are anonymous. `max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 10); omitting it requests unbounded traversal, allowed only on self-hosted deployments or projects with a custom Neo4j instance:

```typescript
// Employees whose management chain (1–4 hops) contains Alice
Expand All @@ -436,17 +439,14 @@ Defines conditions on related records. The key of the nested object **is** the l
}
```

`$cycle: true` matches records that sit on a closed path back to themselves. It requires an object `$relation` with `hops` (`min` ≥ 2, defaults to 2) and accepts nothing else — no `$alias`, no property criteria, no nested labels. The block's key is a display name, not matched as a label:
`$cycle` is a record-level predicate that matches records sitting on a closed path back to themselves. Its value is the traversal spec itself — `type`, `direction`, `hops` — with `hops` mandatory (`min` ≥ 2, defaults to 2). It accepts nothing else — no `$alias`, no property criteria, no nested labels; intermediate node labels are unconstrained. It composes under `$not`/`$or`/`$and`, and can be placed inside a label block to anchor the cycle at a related record:

```typescript
// Accounts on a directed transfer ring of 2–6 hops
{
labels: ['ACCOUNT'],
where: {
RING: {
$cycle: true,
$relation: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
$cycle: { type: 'TRANSFERRED_TO', direction: 'out', hops: { min: 2, max: 6 } }
}
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/learn/search-query/search-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ For optimal performance when using the Search API:
1. **Be Specific**: Filter by labels when possible to narrow the search scope
2. **Use Indexed Properties**: Prioritize filtering on properties that have indexes
3. **Limit Results**: Use pagination to retrieve only the records you need
4. **Bound Your Traversals**: Variable-length traversal (`$relation.hops`) is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 25) — keep `max` as small as your use case allows, and constrain `type` and `direction` to prune the search
4. **Bound Your Traversals**: Variable-length traversal (`$relation.hops`) is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 10) — keep `max` as small as your use case allows, and constrain `type` and `direction` to prune the search
5. **Use Aliases Efficiently**: Define aliases only for records you need to reference in aggregations

## Next Steps
Expand Down
21 changes: 9 additions & 12 deletions docs/docs/learn/search-query/where-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,7 @@ Key semantics:

- `type` and `direction` constrain **every hop**; the nested label constrains only the **endpoint** record. Intermediate records are anonymous and unconstrained.
- Omitting `type` means "any relationship between records" — each hop may even use a different relationship type. RushDB automatically excludes its internal property metadata edges, so untyped traversal never leaves your data model.
- `hops.max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 25). Omitting `max` requests **unbounded** traversal (`*min..`), which is only allowed on self-hosted deployments and projects with a custom Neo4j instance — bounded there by the transaction timeout.
- `hops.max` is capped per deployment (`RUSHDB_MAX_TRAVERSAL_HOPS`, default 10). Omitting `max` requests **unbounded** traversal (`*min..`), which is only allowed on self-hosted deployments and projects with a custom Neo4j instance — bounded there by the transaction timeout.
- Deeper nesting composes naturally: a nested label block under a `hops` endpoint continues from that endpoint.

> **Performance:** variable-length traversal explores every matching path. Keep `max` as small as your use case allows, and prefer setting `direction` — an undirected, untyped `hops` query is the most expensive shape.
Expand All @@ -719,19 +719,16 @@ Key semantics:

### Cycle Detection (`$cycle`)

Use `$cycle: true` to find records that sit on a **closed path back to themselves** — fraud rings, circular ownership, dependency cycles:
`$cycle` is a record-level predicate that finds records sitting on a **closed path back to themselves** — fraud rings, circular ownership, dependency cycles. Its value is the traversal spec itself:

```typescript
{
labels: ["ACCOUNT"],
where: {
RING: { // Key is a display name — NOT matched as a label
$cycle: true,
$relation: {
type: "TRANSFERRED_TO",
direction: "out",
hops: { min: 2, max: 6 } // Ring length; min defaults to 2 for cycles
}
$cycle: {
type: "TRANSFERRED_TO",
direction: "out",
hops: { min: 2, max: 6 } // Ring length; min defaults to 2 for cycles
}
}
}
Expand All @@ -741,10 +738,10 @@ This returns every ACCOUNT participating in a directed transfer ring of 2–6 ho

Rules and semantics:

- A `$cycle` block requires `$relation` with `hops` (`min` ≥ 2 — a 1-hop "cycle" would be a self-loop) and accepts **nothing else**: no `$alias`, no property criteria, no nested labels. A cycle has no separate endpoint to filter or alias — filter the root record instead.
- The block's key is ignored as a label (both ends of the pattern are the root record); it just needs to be unique among its siblings.
- `$cycle`'s value is the traversal spec — `type`, `direction`, `hops` — with `hops` mandatory (`min` ≥ 2, defaults to 2 — a 1-hop "cycle" would be a self-loop). It accepts **nothing else**: no `$alias`, no property criteria, no nested labels. A cycle has no separate endpoint to filter or alias — filter the root record instead. Intermediate node labels are unconstrained.
- Set `direction` for flow-like semantics (money, ownership, dependencies). An undirected cycle also matches innocent back-and-forth pairs like mutual transfers.
- Combine with logical operators: `$not: { RING: { $cycle: true, ... } }` finds records **not** on a cycle.
- Combine with logical operators: `$not: { $cycle: { ... } }` finds records **not** on a cycle.
- `$cycle` can also be placed inside a label block to anchor the cycle at a **related** record: `ACCOUNT: { country: "US", $cycle: { ... } }`.
- Cypher trail semantics apply: each _relationship_ is used once per path, but a path may revisit a record through a different relationship — figure-eight shapes count as cycles.

### Aliasing for Aggregations
Expand Down
Loading
Loading