Quickstart
This page walks you through a first session with RushDB: start a node, write
records (including a nested JSON document), query them, connect two records
with a relationship, run a vector search, and read the same data from psql.
It takes about ten minutes. Every request goes to the native HTTP protocol
with curl.
1. Install and start a node
Install RushDB (other methods are in Installation):
curl -fsSL https://raw.githubusercontent.com/rush-db/rushdb/main/install.sh | sh
Start a persistent node in a fresh directory. Leave it running in its own terminal.
export RUSHDB_TOKEN=local-token # the default; pick your own for real data
rushdb up --data-dir ./quickstart-data
If port 3000 or 5432 is taken, add --port 3100 --pgwire-port 5433 and use
those ports below. In a second terminal, confirm the node is healthy:
rushdb health
{"status":"ok","engine":"rushdb-lmpg","protocolVersion":1,"queryLanguages":["gremlin","mongo","searchQuery","sql","sqlPgq"]}
Every data request is a JSON envelope posted to
/protocol/v1/execute. To keep the examples short, define a helper in the
second terminal:
export RUSHDB_TOKEN=local-token
rdb() {
curl -s http://127.0.0.1:3000/protocol/v1/execute \
-H "Authorization: Bearer $RUSHDB_TOKEN" \
-H 'Content-Type: application/json' \
--data "$1"
}
An envelope always has protocolVersion (currently 1) and one
operation, whose kind is query, mutate, describe, ingest, index,
transaction, purgeTenant or backup. Successful responses echo
protocolVersion, usually add serverTimingMs, and carry the result in
response; the responses below are abbreviated.
2. Create records
Create a Person record. Supplying an id (any UUID) is optional; it lets
the later steps refer to the record without copying ids around.
rdb '{
"protocolVersion": 1,
"operation": {
"kind": "mutate",
"mutation": {
"kind": "putRecord",
"mode": "create",
"id": "019a0000-0000-7000-8000-000000000001",
"label": "Person",
"properties": {
"name": "Ada Lovelace",
"born": 1815,
"birthdate": "1815-12-10",
"languages": ["en", "fr"],
"nickname": null
}
}
}
}'
The response carries the stored record (abbreviated):
{
"protocolVersion": 1,
"serverTimingMs": 1,
"response": {
"kind": "record",
"record": {
"id": "019a0000-0000-7000-8000-000000000001",
"label": "Person",
"properties": {
"name": "Ada Lovelace",
"born": 1815,
"birthdate": "1815-12-10",
"languages": ["en", "fr"],
"nickname": null
},
"propertyTypes": {
"name": "string",
"born": "number",
"birthdate": "datetime",
"languages": "string"
}
}
}
}
Notice what happened without any schema:
birthdatewas recognised as a date and stored as a datetime.languagesis one property holding two string values.nicknameis stored as an explicitnull— present, but empty — which is different from not having the property at all.
Add two more people:
rdb '{"protocolVersion":1,"operation":{"kind":"mutate","mutation":{"kind":"putRecord","mode":"create",
"id":"019a0000-0000-7000-8000-000000000002","label":"Person",
"properties":{"name":"Grace Hopper","born":1906,"languages":["en"]}}}}'
rdb '{"protocolVersion":1,"operation":{"kind":"mutate","mutation":{"kind":"putRecord","mode":"create",
"id":"019a0000-0000-7000-8000-000000000003","label":"Person",
"properties":{"name":"Alan Turing","born":1912,"languages":["en","de"]}}}}'
3. Ingest a nested JSON document
putRecord writes one flat record. To store a nested document, use
ingest: every nested object (and every object inside an array) becomes its
own record, linked to its parent by a relationship.
rdb '{
"protocolVersion": 1,
"operation": {
"kind": "ingest",
"ingest": {
"kind": "documents",
"label": "Company",
"documents": {
"name": "Analytical Engines Ltd",
"founded": 1834,
"address": { "city": "London", "street": "Dorset Street" },
"department": [
{ "name": "Engineering", "headcount": 12 },
{ "name": "Research", "headcount": 4 }
]
},
"options": {
"returnResult": true,
"relationshipTypes": { "address": "HAS_ADDRESS", "department": "HAS_DEPARTMENT" }
}
}
}
}'
The response reports four records and three relationships:
{
"protocolVersion": 1,
"response": {
"kind": "ingested",
"count": 4,
"relationships": 3,
"records": [
{ "id": "…", "label": "Company", "properties": { "founded": 1834, "name": "Analytical Engines Ltd" }, "propertyTypes": { "…": "…" } },
{ "id": "…", "label": "address", "properties": { "city": "London", "street": "Dorset Street" }, "propertyTypes": { "…": "…" } },
{ "id": "…", "label": "department", "properties": { "headcount": 12, "name": "Engineering" }, "propertyTypes": { "…": "…" } },
{ "id": "…", "label": "department", "properties": { "headcount": 4, "name": "Research" }, "propertyTypes": { "…": "…" } }
]
}
}
Child records take their label from the JSON key (address, department).
The Nested import page explains every option,
including upserts.
4. Query records
Find people born in 1900 or later, sorted by name:
rdb '{
"protocolVersion": 1,
"operation": {
"kind": "query",
"query": {
"kind": "records",
"query": {
"labels": ["Person"],
"predicate": { "born": { "$gte": 1900 } },
"order": { "name": "asc" },
"limit": 10
}
}
}
}'
{
"protocolVersion": 1,
"response": {
"kind": "records",
"total": 2,
"records": [
{ "id": "019a0000-0000-7000-8000-000000000003", "label": "Person", "properties": { "name": "Alan Turing", "…": "…" }, "propertyTypes": { "…": "…" } },
{ "id": "019a0000-0000-7000-8000-000000000002", "label": "Person", "properties": { "name": "Grace Hopper", "…": "…" }, "propertyTypes": { "…": "…" } }
]
}
}
total counts every match before limit is applied. A filter can also reach
across relationships: put a related label as a key in the predicate. This
finds companies that have a department with at least ten people:
rdb '{"protocolVersion":1,"operation":{"kind":"query","query":{"kind":"records","query":{
"labels": ["Company"],
"predicate": { "department": { "headcount": { "$gte": 10 } } }
}}}}'
Multi-value properties match when any value matches, so
{"languages": "de"} finds Alan Turing. See
Filtering for every operator.
5. Add a relationship
Link Ada to the other two people with an INFLUENCED relationship that
carries a property of its own:
rdb '{
"protocolVersion": 1,
"operation": {
"kind": "mutate",
"mutation": {
"kind": "linkRecordsById",
"command": {
"relationType": "INFLUENCED",
"sourceIds": ["019a0000-0000-7000-8000-000000000001"],
"targetIds": ["019a0000-0000-7000-8000-000000000002",
"019a0000-0000-7000-8000-000000000003"],
"properties": { "via": "the Analytical Engine notes" }
}
}
}
}'
{"protocolVersion":1,"response":{"kind":"linked","created":2,"skippedExisting":0}}
Linking by id is idempotent: repeating the request creates no duplicate
edges and reports "created": 0, "skippedExisting": 2. Now traverse the relationship — find every
person that Ada influenced:
rdb '{"protocolVersion":1,"operation":{"kind":"query","query":{"kind":"records","query":{
"labels": ["Person"],
"predicate": {
"Person": {
"$relation": { "type": "INFLUENCED", "direction": "in" },
"name": "Ada Lovelace"
}
}
}}}}'
The direction is seen from the records you are looking for: Grace and Alan
have an incoming INFLUENCED edge from a Person named Ada. List the
edges themselves with a relationship query:
rdb '{"protocolVersion":1,"operation":{"kind":"query","query":{"kind":"relationships","query":{
"relationType": "INFLUENCED"
}}}}'
Each row has the relationship's type, its sourceId and targetId, the
endpoint labels, and its properties.
6. A vector search teaser
RushDB stores and searches vectors you compute; it does not generate embeddings. Create a three-dimensional vector index, write two notes with vectors attached, and search:
rdb '{"protocolVersion":1,"operation":{"kind":"index","index":{"kind":"createVector","definition":{
"name": "note-embedding", "label": "Note", "property": "embedding",
"dimensions": 3, "similarity": "cosine"
}}}}'
rdb '{"protocolVersion":1,"operation":{"kind":"mutate","mutation":{"kind":"putRecord","mode":"create",
"label":"Note","properties":{"text":"engines and looms"},
"vectors":[{"propertyName":"embedding","vector":[0.9,0.1,0.0],"index":"note-embedding"}]}}}'
rdb '{"protocolVersion":1,"operation":{"kind":"mutate","mutation":{"kind":"putRecord","mode":"create",
"label":"Note","properties":{"text":"compilers and ships"},
"vectors":[{"propertyName":"embedding","vector":[0.0,0.2,0.9],"index":"note-embedding"}]}}}'
rdb '{"protocolVersion":1,"operation":{"kind":"query","query":{"kind":"vector","query":{
"labels": ["Note"], "property": "embedding", "index": "note-embedding",
"vector": [1.0, 0.0, 0.0], "limit": 2
}}}}'
The response is a records list ordered by similarity; each record carries a
score, and "engines and looms" comes first. Vector search can also be
filtered with a predicate and combined with full-text search; see
Vector search and
Hybrid search.
7. Connect with psql
The PostgreSQL wire listener is bound whenever the sql language is
enabled, which it is by default, on port 5432. Connect with psql. The password is the token; the user name is ignored, and in
single-tenant mode so is the database name.
PGPASSWORD="$RUSHDB_TOKEN" psql "host=127.0.0.1 port=5432 user=rushdb dbname=rushdb sslmode=disable"
SELECT name, born FROM Person ORDER BY born;
name | born
--------------+------
Ada Lovelace | 1815
Grace Hopper | 1906
Alan Turing | 1912
(3 rows)
Labels appear as tables and properties as columns.
Note: The PostgreSQL wire protocol is qualified against the
tokio-postgresRust driver. Other clients,psqlincluded, are outside the qualified surface: stick to plainSELECT/INSERT/UPDATE/DELETEstatements, and do not expect catalog-introspection commands (such as\d) or ORMs to work. See PostgreSQL wire protocol.
Clean up
Stop the node and delete ./quickstart-data when you are done.