Search UX Patterns
Most user-facing search combines two things:
- Structured filtering — only show results matching the user's explicit constraints (role, date range, status)
- Relevance ranking — sort by how closely each result matches the user's intent in natural language
RushDB provides both surfaces: records.find() for structured queries and ai.search() for semantic ranking. The patterns below show how to combine them, surface enough context for explainable results, and paginate reliably.
Pattern 1: Instant structured search
For search over categorical or exact fields, records.find() with a where clause is immediate.
- Python
- TypeScript
- shell
from rushdb import RushDB
import os
db = RushDB(os.environ["RUSHDB_API_KEY"], base_url="https://api.rushdb.com/api/v1")
def search_articles(params: dict) -> dict:
where = {"status": "published"}
if params.get("category"):
where["category"] = params["category"]
if params.get("authorId"):
where["AUTHOR"] = {
"$relation": {"type": "AUTHORED_BY", "direction": "out"},
"authorId": params["authorId"]
}
if params.get("publishedAfter"):
where["publishedAt"] = {"$gte": params["publishedAfter"]}
result = db.records.find({
"labels": ["ARTICLE"],
"where": where,
"orderBy": {"publishedAt": "desc"},
"limit": params.get("limit", 20),
"skip": params.get("skip", 0)
})
return {
"items": [{"id": a.id, "title": a.get("title"), "category": a.get("category")} for a in result.data],
"total": result.total
}
import RushDB from '@rushdb/javascript-sdk'
const db = new RushDB(process.env.RUSHDB_API_KEY!)
interface ArticleSearchParams {
category?: string
authorId?: string
publishedAfter?: string
limit?: number
skip?: number
}
async function searchArticles(params: ArticleSearchParams) {
const where: Record<string, unknown> = { status: 'published' }
if (params.category) where.category = params.category
if (params.authorId) where.AUTHOR = { $relation: { type: 'AUTHORED_BY', direction: 'out' }, authorId: params.authorId }
if (params.publishedAfter) where.publishedAt = { $gte: params.publishedAfter }
const result = await db.records.find({
labels: ['ARTICLE'],
where,
orderBy: { publishedAt: 'desc' },
limit: params.limit ?? 20,
skip: params.skip ?? 0
})
return {
items: result.data.map(a => ({
id: a.__id,
title: a.title,
category: a.category,
publishedAt: a.publishedAt
})),
total: result.total
}
}
curl -s -X POST "https://api.rushdb.com/api/v1/records/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $RUSHDB_API_KEY" \
-d '{
"labels": ["ARTICLE"],
"where": {"status": "published", "category": "engineering"},
"orderBy": {"publishedAt": "desc"},
"limit": 20,
"skip": 0
}'
Pattern 2: Semantic search with structured post-filter
Use ai.search() when the user types a free-text query. The where clause filters before semantic ranking, so only records matching all structural constraints are scored.
- Python
- TypeScript
- shell
def semantic_article_search(params: dict) -> list[dict]:
search_params = {
"query": params["query"],
"propertyName": "content",
"labels": ["ARTICLE"],
"limit": params.get("limit", 10)
}
where = {"status": "published"}
if params.get("category"):
where["category"] = params["category"]
if params.get("publishedAfter"):
where["publishedAt"] = {"$gte": params["publishedAfter"]}
if len(where) > 1:
search_params["where"] = where
result = db.ai.search(search_params)
return [
{"id": a.id, "title": a.get("title"), "score": a.score, "category": a.get("category")}
for a in result.data
]
interface SemanticSearchParams {
query: string
category?: string
publishedAfter?: string
limit?: number
}
async function semanticArticleSearch(params: SemanticSearchParams) {
const searchParams: Parameters<typeof db.ai.search>[0] = {
query: params.query,
propertyName: 'content',
labels: ['ARTICLE'],
limit: params.limit ?? 10
}
if (params.category || params.publishedAfter) {
const where: Record<string, unknown> = { status: 'published' }
if (params.category) where.category = params.category
if (params.publishedAfter) where.publishedAt = { $gte: params.publishedAfter }
searchParams.where = where
}
const result = await db.ai.search(searchParams)
return result.data.map(a => ({
id: a.__id,
title: a.title,
score: a.__score, // 0–1 cosine similarity
category: a.category
}))
}
curl -s -X POST "https://api.rushdb.com/api/v1/ai/search" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $RUSHDB_API_KEY" \
-d '{
"query": "how to reduce latency in distributed systems",
"propertyName": "content",
"labels": ["ARTICLE"],
"where": {"status": "published", "category": "engineering"},
"limit": 10
}'
Pattern 3: Contextual result enrichment
Raw records often lack display context. Enrich results with related data after retrieval — author name, category breadcrumb, or related tags.
- Python
- TypeScript
from concurrent.futures import ThreadPoolExecutor
def enriched_search(user_query: str) -> list[dict]:
results = db.ai.search({
"query": user_query,
"propertyName": "content",
"labels": ["ARTICLE"],
"limit": 10
})
def enrich(article):
author_result = db.records.find({
"labels": ["AUTHOR"],
"where": {
"ARTICLE": {
"$relation": {"type": "AUTHORED_BY", "direction": "in"},
"__id": article.id
}
}
})
return {
"id": article.id,
"title": article.get("title"),
"score": article.score,
"author": author_result.data[0].get("name") if author_result.data else None
}
with ThreadPoolExecutor(max_workers=5) as pool:
return list(pool.map(enrich, results.data))
async function enrichedSearch(userQuery: string) {
// 1. Semantic ranking
const results = await db.ai.search({
query: userQuery,
propertyName: 'content',
labels: ['ARTICLE'],
limit: 10
})
// 2. Enrich in parallel — fetch author names via relationship traversal
const enriched = await Promise.all(
results.data.map(async (article) => {
const authorResult = await db.records.find({
labels: ['AUTHOR'],
where: {
ARTICLE: {
$relation: { type: 'AUTHORED_BY', direction: 'in' },
__id: article.__id
}
}
})
return {
id: article.__id,
title: article.title,
score: article.__score,
author: authorResult.data[0]?.name ?? null,
category: article.category
}
})
)
return enriched
}
Per-result enrichment (a sub-query per item) multiplies network calls. For 10 results, that is 10 additional queries. Prefer storing denormalized display fields on the primary record where possible, and reserve traversal enrichment for fields that must be kept in sync with the related record.
Pattern 4: Pagination with stable ordering
Pagination is only stable when results are ordered by an immutable field. Avoid paginating over semantic search — scores change with model updates. Paginate structured queries instead.
- Python
- TypeScript
def paginated_articles(category: str, page: int, page_size: int = 20) -> dict:
result = db.records.find({
"labels": ["ARTICLE"],
"where": {"status": "published", "category": category},
"orderBy": {"publishedAt": "desc"},
"limit": page_size,
"skip": page * page_size
})
return {
"items": [{"id": a.id, "title": a.get("title")} for a in result.data],
"total": result.total,
"hasNext": (page + 1) * page_size < result.total
}
interface Page<T> {
items: T[]
total: number
hasNext: boolean
}
async function paginatedArticles(category: string, page: number, pageSize = 20): Promise<Page<object>> {
const result = await db.records.find({
labels: ['ARTICLE'],
where: { status: 'published', category },
orderBy: { publishedAt: 'desc' },
limit: pageSize,
skip: page * pageSize
})
return {
items: result.data.map(a => ({ id: a.__id, title: a.title, publishedAt: a.publishedAt })),
total: result.total,
hasNext: (page + 1) * pageSize < result.total
}
}
Pattern 5: Zero-results fallback with semantic widening
If a structured query returns nothing, fall back to semantic search without the structural constraints — then surface a "Did you mean?" style hint.
- Python
- TypeScript
def search_with_fallback(query: str, category: str) -> dict:
structured = db.records.find({
"labels": ["ARTICLE"],
"where": {"status": "published", "category": category, "title": {"$contains": query}},
"limit": 10
})
if structured.total > 0:
return {"results": structured.data, "mode": "exact"}
semantic = db.ai.search({
"query": query,
"propertyName": "content",
"labels": ["ARTICLE"],
"where": {"status": "published"},
"limit": 5
})
return {"results": semantic.data, "mode": "semantic"}
async function searchWithFallback(query: string, category: string) {
// Try structured first
const structured = await db.records.find({
labels: ['ARTICLE'],
where: { status: 'published', category, title: { $contains: query } },
limit: 10
})
if (structured.total > 0) {
return { results: structured.data, mode: 'exact' as const }
}
// Widen to semantic across all categories
const semantic = await db.ai.search({
query,
propertyName: 'content',
labels: ['ARTICLE'],
where: { status: 'published' },
limit: 5
})
return { results: semantic.data, mode: 'semantic' as const }
}
Choosing between structured and semantic search
| Situation | Use |
|---|---|
| User selects filters from UI facets | records.find() with where |
| User types a free-text query box | ai.search() |
| User types AND applies filters | ai.search() with where |
| Result count matters (pagination) | records.find() — ai.search() does not return total |
| Query is empty / filters only | records.find() — skip semantic scoring |
| Exact match required | records.find() |
Next steps
- Hybrid Retrieval — two-phase structured-filter + semantic-rank for AI pipelines
- Explainable Results — surface evidence for every search result
- Query Optimization — shape queries for throughput and cost efficiency