Skip to main content

Reusable SearchQuery

RushDB uses one core query object shape across multiple APIs.

You can reuse the same query logic across five perspectives on data:

  • Records
  • Properties
  • Labels
  • Relationships
  • Values

Once you learn SearchQuery once, you can apply it almost everywhere.

Canonical SearchQuery Shape

# SearchQuery is a plain dict in Python
search_query = {
"labels": ["..."], # optional
"where": {}, # optional
"select": {}, # optional
"groupBy": ["..."], # optional
"orderBy": "asc" | {"field": "asc"}, # optional
"limit": 100, # optional
"skip": 0 # optional
}

Supported by:

  • records.find / /api/v1/records/search
  • records.delete / /api/v1/records/delete
  • relationships.find / /api/v1/relationships/search
  • labels.find / /api/v1/labels/search
  • properties.find / /api/v1/properties/search

And paired with values exploration via /api/v1/properties/:id/values.

Why It Matters

  • Reduced learning curve
  • Reusable filtering logic across endpoints
  • Predictable behavior for filtering, aggregation, and pagination
  • Easier AI-assisted query generation because the shape stays stable

One Intent, Five Perspectives

SearchQuery's ultimate feature is intent reuse.

You can express one business question and view it from different perspectives:

  • Records: return matching entities
  • Properties: discover which fields participate in that slice
  • Labels: discover which entity types match
  • Relationships: inspect graph links within the same slice
  • Values: enumerate canonical values for a chosen property in that slice

This means your app can move from listing to discovery to analytics without changing mental model.

Where Clause Essentials

Primitive and operator filters

"where": {
"status": {"$in": ["active", "pending"]},
"amount": {"$gte": 1000, "$lt": 5000},
"name": {"$contains": "acme"},
"isArchived": {"$ne": True}
}

Logical composition

"where": {
"$or": [
{"status": "active"},
{
"$and": [
{"status": "pending"},
{"priority": {"$gte": 8}}
]
}
]
}

Relationship traversal

In SearchQuery, relationship traversal uses label keys directly.

"where": {
"DEPARTMENT": {
"$alias": "$department",
"name": "Engineering",
"PROJECT": {
"$alias": "$project",
"EMPLOYEE": {
"$alias": "$employee",
"role": {"$in": ["Developer", "Lead"]}
}
}
}
}

Important:

  • Use the label key itself (DEPARTMENT, PROJECT, EMPLOYEE) to traverse
  • Use $alias to reference traversed nodes in select and groupBy
  • Do not use unsupported traversal fields such as $label, $as, $through, $of

Date range rule

For range operators on datetimes, use component objects.

"where": {
"createdAt": {
"$gte": {"$year": 2025, "$month": 1, "$day": 1},
"$lt": {"$year": 2026, "$month": 1, "$day": 1}
}
}

GroupBy

SearchQuery supports grouped analytics using the select and groupBy clauses.

Per-record flat metrics

rows = db.records.find({
"labels": ["PROJECT"],
"where": {
"EMPLOYEE": {"$alias": "$employee"}
},
"select": {
"projectName": "$record.name",
"headcount": {"$count": "$employee"},
"totalSalary": {"$sum": "$employee.salary"}
},
"limit": 100
})

Dimensional groupBy

by_status = db.records.find({
"labels": ["ORDER"],
"select": {
"count": {"$count": "*"},
"revenue": {"$sum": "$record.total"}
},
"groupBy": ["$record.status"],
"orderBy": {"revenue": "desc"}
})

Self-group (single-row KPI)

totals = db.records.find({
"labels": ["ORDER"],
"select": {
"totalRevenue": {"$sum": "$record.total"}
},
"groupBy": ["totalRevenue"],
"orderBy": {"totalRevenue": "asc"}
})

Critical Limit Rules

  • Do not use limit in self-group KPI queries
  • Do not use limit in dimensional groupBy unless intentionally asking for top N groups
  • limit is valid for listing/browsing and per-record flat aggregation
  • For self-group KPI queries, include orderBy on the select key to force full-scan aggregation behavior

Reusable Patterns Across APIs

1. Search records

result = db.records.find({
"labels": ["PRODUCT"],
"where": {
"active": True,
"price": {"$gte": 10, "$lte": 50},
"tags": {"$in": ["featured"]}
},
"orderBy": {"price": "asc"},
"limit": 20,
"skip": 0
})
products = result.data

2. Delete records with the same where logic

db.records.delete({
"labels": ["PRODUCT"],
"where": {
"discontinued": True,
"inventory": 0
}
})

3. Search relationships

created_relationships = db.relationships.find({
"where": {
"type": "CREATED",
"weight": {"$gte": 0.5}
},
"limit": 50
})

4. Find labels by data constraints

labels = db.labels.find({
"where": {
"region": {"$in": ["US", "CA", "MX"]}
}
})

5. Discover properties for a label

product_props = db.properties.find({
"labels": ["PRODUCT"],
"where": {
"type": "number"
}
})

6. Explore values for a selected property

Use values as the final perspective when you need canonical filter options for UI facets, prompts, and dynamic query builders.

Typical flow:

  1. Use properties.find to locate a property and get its id
  2. Call /api/v1/properties/:id/values for value discovery in the same data context
  3. Feed returned canonical values back into your next SearchQuery ($in, $nin, exact match)

Common Pitfalls to Avoid

  • Using limit on KPI/self-group queries
  • Using traversal keys like $label instead of real label keys (EMPLOYEE, PROJECT)
  • Treating groupBy self-group keys as property paths instead of select key names
  • Using plain date strings with $gt/$gte/$lt/$lte instead of datetime component objects
EndpointDocs
/api/v1/records/searchRecords API
/api/v1/records/deleteDelete Records
/api/v1/relationships/searchRelationships API
/api/v1/labels/searchLabels API
/api/v1/properties/searchProperties API
/api/v1/properties/:id/valuesProperties API

Next Step

If you want advanced examples (nested collect, multi-hop traversal, time buckets, KPI patterns), continue with the deep-dive tutorial:

SearchQuery Deep Dive: Advanced Patterns