Skip to main content

Hybrid Retrieval: Filters Plus Semantic Search

Pure semantic search ranks by similarity to a query embedding. Pure keyword or filter search matches exact values. Hybrid retrieval combines both: narrowing the candidate set by business constraints first, then ranking by semantic relevance within that set.

RushDB supports hybrid retrieval natively — db.ai.search accepts a where clause that is applied before the similarity ranking step.


How it works

db.ai.search runs structured filtering and semantic ranking in one call:

  1. The where clause restricts which records are eligible (tenant, date range, category, status, etc.)
  2. The query string (or queryVector, see below) is compared against embeddings on the specified propertyName
  3. Results are returned ordered by __score — a 0–1 cosine similarity value

This means you get tenant isolation and time-scoping for free, without a separate pre-filter step.


Prerequisites

Before calling db.ai.search, a managed embedding index must exist and be ready for the label and property you want to search. See Semantic Search for Multi-Tenant Products for setup instructions.


Step 1: Ingest records

from rushdb import RushDB
import os

db = RushDB(os.environ["RUSHDB_API_KEY"], base_url="https://api.rushdb.com/api/v1")

db.records.import_json({
"label": "ARTICLE",
"data": [
{
"tenantId": "acme",
"title": "Deploying Postgres on Kubernetes",
"body": "A practical guide to running stateful workloads on K8s using StatefulSets and persistent volumes.",
"category": "infrastructure",
"publishedAt": "2025-01-15",
"status": "published"
},
{
"tenantId": "acme",
"title": "Backpressure Patterns in Event Queues",
"body": "How to handle consumer lag and apply backpressure in Kafka and RabbitMQ setups.",
"category": "messaging",
"publishedAt": "2025-02-10",
"status": "published"
},
{
"tenantId": "acme",
"title": "Zero-Downtime Schema Migrations",
"body": "Techniques for running database migrations without taking down production traffic.",
"category": "infrastructure",
"publishedAt": "2025-03-01",
"status": "draft"
}
]
})

Step 2: Semantic search with no filters (baseline)

Start with pure semantic search to verify the index is working.

results = db.ai.search({
"query": "stateful storage in container orchestration",
"propertyName": "body",
"labels": ["ARTICLE"]
})

for article in results.data:
print(f"{article.data.get('title')} — score: {article.__score:.3f}")

Step 3: Add structural filters (hybrid mode)

Narrow to published articles within a given tenant. The where clause runs before the similarity step — only eligible records are ranked.

hybrid_results = db.ai.search({
"query": "stateful storage in container orchestration",
"propertyName": "body",
"labels": ["ARTICLE"],
"where": {
"tenantId": "acme",
"status": "published"
}
})

for article in hybrid_results.data:
print(f"[{article.data.get('category')}] {article.data.get('title')} — score: {article.__score:.3f}")

The draft migration article is excluded even if it has a high semantic similarity score, because status: 'published' removes it from the candidate set before scoring.


Step 4: Add date range and category filters

Compound filters are plain where clauses. Combine them freely — the semantics are AND by default.

filtered = db.ai.search({
"query": "handling operational failures",
"propertyName": "body",
"labels": ["ARTICLE"],
"where": {
"tenantId": "acme",
"status": "published",
"category": "infrastructure",
"publishedAt": {"$gte": "2025-01-01"}
},
"limit": 5
})

Step 5: Paginate semantic results

Use skip and limit for result pagination. Consistent ordering is guaranteed as long as the same query and where are used.

def search_articles(query: str, tenant_id: str, page: int = 0, page_size: int = 10):
results = db.ai.search({
"query": query,
"propertyName": "body",
"labels": ["ARTICLE"],
"where": {"tenantId": tenant_id, "status": "published"},
"skip": page * page_size,
"limit": page_size
})
return {
"results": [
{"id": r.id, "title": r.data.get("title"), "score": r.__score}
for r in results.data
],
"page": page,
"pageSize": page_size
}

page0 = search_articles("container storage performance", "acme")
page1 = search_articles("container storage performance", "acme", page=1)

Step 6: Filtering by relationship (graph-aware hybrid retrieval)

Combine semantic search with a graph traversal filter. Here, only articles tagged with a specific TAG record are eligible for ranking.

tagged_results = db.ai.search({
"query": "storage class and persistent volumes",
"propertyName": "body",
"labels": ["ARTICLE"],
"where": {
"tenantId": "acme",
"status": "published",
"TAG": {
"$relation": {"type": "TAGGED_WITH", "direction": "out"},
"slug": "kubernetes"
}
}
})

Step 7: External vectors (BYOV)

If you generate embeddings outside RushDB (e.g., with your own model), pass queryVector instead of query. RushDB skips embedding generation and uses your vector directly.

external_embedding = my_embedding_model.embed("container storage performance")

external_results = db.ai.search({
"queryVector": external_embedding,
"propertyName": "body",
"labels": ["ARTICLE"],
"where": {"tenantId": "acme", "status": "published"}
})

When to use pure search vs. hybrid

ScenarioApproach
Open-ended exploration ("find anything about X")Pure semantic search, no where
Tenant-scoped retrievalAlways add tenantId to where
Time-bounded retrieval (last 30 days)Add date filter to where
Category/status filtering before rankingAdd structured fields to where
Graph-relationship scoping (tagged with, authored by)Add relationship traversal to where
Custom embedding modelUse queryVector instead of query

Performance characteristics

  • The where clause runs as a graph filter before the vector similarity step. A highly selective filter (eliminating most records) makes semantic search faster because fewer records are ranked.
  • The limit parameter controls how many top-ranked records are returned. Use values between 5–50 for typical UX use cases.
  • skip is available for pagination but deep pagination over large result sets (skip > 500) may perform worse than re-querying with an updated where filter.

Next steps