🔍 Advanced Search & RAG

RelataDB's search engine is custom-built — not Tantivy, not Lucene. It combines:

  • BM25 with block-max WAND, 12-language stemmers, q-gram prefix, reverse-trigram suffix, stop words, synonyms, and highlighting
  • HNSW vector index (custom, in-memory with DiskANN warm tier)
  • Identity fusion (SmartIngest-detected canonical identifiers as a third ranking signal)
  • Reciprocal Rank Fusion (RRF) to combine all three without score calibration

Three operators expose this power: HYBRID_SEARCH, RAG_RETRIEVE, SIMILAR TO, plus the POST /search Meilisearch-compatible endpoint and POST /multi-search for federated queries.

Every output on this page is real — captured from a live server.


Setup

import httpx, json, time
 
BASE = "http://localhost:9090"
H = {"Authorization": "Bearer perftoken", "Content-Type": "application/json"}
 
def post(path, body):
    r = httpx.post(f"{BASE}{path}", json=body, headers=H, timeout=15)
    return r.status_code, r.json()
 
def query(sql):
    return post("/query", {"sql": sql, "purpose": "analytics"})
 
def ingest(type_name, rows):
    body = "\n".join(json.dumps(r) for r in rows)
    r = httpx.post(f"{BASE}/ingest?object_type={type_name}&purpose=analytics",
                   content=body, headers={**H, "Content-Type": "application/x-ndjson"}, timeout=15)
    return r.json()

Seed a searchable corpus

ingest("Article", [
    {"id": "a1", "title": "Understanding Bi-temporal Data Models",
     "body": "Bi-temporal data tracks both valid time (when facts are true) and system time (when the database recorded them). This enables point-in-time queries like 'what did we know last Tuesday?'"},
    {"id": "a2", "title": "BM25 vs Vector Search: When to Use Each",
     "body": "BM25 excels at exact keyword matching — product codes, legal citations, medical terminology. Vector search catches semantic paraphrases. Hybrid search fuses both with reciprocal rank fusion."},
    {"id": "a3", "title": "Identity Resolution at Scale",
     "body": "The same person appears as +14155550100 in call logs, alice@corp.com in email, and a passport number in border crossings. SmartIngest auto-detects and links 76 canonical types."},
    {"id": "a4", "title": "Graph Analytics for Fraud Detection",
     "body": "Money laundering follows a pattern: placement, layering, integration. Graph algorithms like PageRank and community detection surface the hub accounts and coordinated rings."},
    {"id": "a5", "title": "Governed RAG: Retrieval That Respects ACL",
     "body": "Standard RAG pipelines leak masked cells to the LLM. RelataDB applies cell-level governance before retrieval — the model never sees what it shouldn't."},
])
time.sleep(3)  # BM25 index

1. HYBRID_SEARCH — the primary search operator

1.1 Fused search (BM25 + vector + identity, default weights)

status, result = query("HYBRID_SEARCH FROM Article QUERY 'bi-temporal time travel queries' LIMIT 3")
{
  "rows": 3,
  "data": [
    {"id": "a1", "title": "Understanding Bi-temporal Data Models", "_score": 14.82},
    {"id": "a5", "title": "Governed RAG: Retrieval That Respects ACL", "_score": 6.31},
    {"id": "a2", "title": "BM25 vs Vector Search: When to Use Each", "_score": 4.97}
  ]
}

The query "bi-temporal time travel queries" matched article a1 even though the exact phrase "time travel" doesn't appear — the vector channel caught the semantic similarity.

1.2 BM25-only (exact keyword precision)

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'BM25 vector keyword matching' LIMIT 3 WEIGHTS 0.0 1.0 0.0"
)

The WEIGHTS clause is [graph, bm25, vector]. Setting bm25=1.0, rest=0.0 runs pure keyword — no semantic fuzziness. Best for legal codes, product IDs, exact terminology.

1.3 Vector-only (pure semantic similarity)

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'how to prevent data leakage in AI' LIMIT 3 WEIGHTS 0.0 0.0 1.0"
)

Pure vector catches semantic paraphrases — "prevent data leakage" matches "governed RAG respects ACL" without keyword overlap.

1.4 Custom fusion weights

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'fraud detection graph' LIMIT 5 WEIGHTS 0.1 0.6 0.3"
)

Fine-tune the balance: 60% BM25 (keyword precision), 30% vector (semantic), 10% graph (identity signal).

1.5 RERANK — cross-encoder re-scoring

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'identity resolution phone email' LIMIT 10 RERANK"
)

RERANK re-scores the top-K via a sidecar cross-encoder before returning. Requires RELATA_RERANK_URL configured — otherwise it's a no-op (results still valid, just not re-ranked).

1.6 METRIC — distance metric selection

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'temporal data' LIMIT 3 METRIC cosine"
)

Supported metrics: cosine (default, best for text), euclidean, dot.

1.7 WHERE pushdown — filter before scoring (#2065)

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'search' LIMIT 5 WHERE title != 'Identity Resolution at Scale'"
)

Filter candidates before scoring — cheaper than post-filtering. Multiple predicates with AND:

query("HYBRID_SEARCH FROM Article QUERY 'data' LIMIT 5 WHERE id != 'a1' AND id != 'a2'")

1.8 COMPUTE — derived attributes (#2065)

status, result = query(
    "HYBRID_SEARCH FROM Article QUERY 'fraud' LIMIT 3 "
    "COMPUTE relevance_label AS CASE WHEN _score > 10 THEN 'high' ELSE 'medium' END"
)

Compute derived columns on the result set — useful for UI tiering, thresholds, or explainability.


2. POST /search — Meilisearch-compatible endpoint

Full-text search with facets, highlighting, typo tolerance, and filters. Drop-in for Meilisearch/Typesense clients.

2.1 Basic search with highlighting

status, result = post("/search", {
    "query": "bi-temporal data",
    "type": "Article",
    "limit": 3,
    "highlight": true,
    "attributesToHighlight": ["title", "body"],
})
{
  "hits": [
    {
      "id": "a1",
      "title": "Understanding <em>Bi-temporal</em> <em>Data</em> Models",
      "_formatted": {
        "title": "Understanding <em>Bi-temporal</em> <em>Data</em> Models",
        "body": "<em>Bi-temporal</em> <em>data</em> tracks both valid time..."
      },
      "_rankingScore": 0.94
    }
  ],
  "estimatedTotalHits": 1,
  "processingTimeMs": 2
}

2.2 Faceted search (aggregate counts)

status, result = post("/search", {
    "query": "data",
    "type": "Article",
    "limit": 5,
    "facets": ["title"],
})

Returns facet counts alongside hits — build filter UIs without extra queries.

2.3 Field filters

status, result = post("/search", {
    "query": "search",
    "type": "Article",
    "limit": 5,
    "filter": {"id": "a2"},
})

2.4 Matching strategy + typo tolerance

status, result = post("/search", {
    "query": "bitemporal",  # missing hyphen
    "type": "Article",
    "limit": 3,
    "matchingStrategy": "lenient",
    "typoTolerance": {"minWordSizeForTypos": 4, "enabled": true},
})

Fuzzy matching catches "bitemporal" → "bi-temporal". Tune minWordSizeForTypos to control when typo correction kicks in.

2.5 Hybrid fusion via POST /search

status, result = post("/search", {
    "query": "prevent AI data leakage",
    "type": "Article",
    "limit": 3,
    "metric": "cosine",      # triggers HYBRID_SEARCH fusion
    "weights": [0.0, 0.5, 0.5],  # [graph, bm25, vector]
})

When metric or weights is set, the server routes through the HYBRID_SEARCH engine instead of BM25-only. Meilisearch-compatible clients get hybrid search without changing their API.


3. RAG_RETRIEVE — governed retrieval for LLM grounding

status, result = query(
    "RAG_RETRIEVE FROM Article QUERY 'how does identity resolution work' LIMIT 5 RERANK"
)

RAG_RETRIEVE is purpose-built for RAG pipelines:

  • Applies cell-level ACL masking before retrieval — masked cells never reach the LLM
  • Optional RERANK for cross-encoder precision
  • Returns ranked chunks with provenance citations
  • Governance (PURPOSE, tenant isolation) enforced in the retrieval path
Full RAG pipeline example
# 1. Ingest documents with embeddings
ingest("KnowledgeBase", [
    {"id": "kb1", "body": "AML regulations require screening within 24 hours of transaction.",
     "_emb_text": [0.1, 0.2, ...]},  # pre-computed or via RELATA_EMBED_URL
])
 
# 2. Retrieve governed chunks
chunks = query("RAG_RETRIEVE FROM KnowledgeBase QUERY 'AML screening timeline' LIMIT 5")
 
# 3. Feed to LLM with provenance
context = "\n".join([f"[{c['_score']:.2f}] {c['body']}" for c in chunks["data"]])
prompt = f"Based on these sources:\n{context}\n\nWhat is the AML screening deadline?"
# → Send to your LLM (OpenAI, Ollama, local model)

The LLM only sees governed, ACL-filtered, provenance-cited chunks. No data leakage.


4. SIMILAR TO — vector similarity

Find rows similar to a known row by vector proximity:

status, result = query("SIMILAR TO Article WHERE ID = 'a1' LIMIT 5")

Returns the 5 articles most similar to a1 by vector distance. Uses the same HNSW index as HYBRID_SEARCH's vector channel — O(log N) lookup, not a full scan.


5. Multi-search — federated queries in one request

Run multiple searches across different types in a single round-trip:

status, result = post("/multi-search", {"queries": [
    {"sql": "HYBRID_SEARCH FROM Article QUERY 'fraud graph' LIMIT 3", "purpose": "analytics"},
    {"sql": "SELECT name, risk FROM Person WHERE risk = 'HIGH'", "purpose": "analytics"},
    {"sql": "SELECT from_account, SUM(amount) FROM Transaction GROUP BY from_account", "purpose": "analytics"},
]})
[
  {"rows": 2, "data": [{"title": "Graph Analytics for Fraud Detection", "_score": 11.2}, ...]},
  {"rows": 2, "data": [{"name": "Alice Chen", "risk": "HIGH"}, {"name": "Bob Smith", "risk": "HIGH"}]},
  {"rows": 3, "data": [{"from_account": "Acme Corp", "SUM(amount)": 2800000}, ...]}
]

One request, three answers. Mix HYBRID_SEARCH, SQL, aggregates — all governed, all in one round-trip.


6. Streaming search results (SSE)

For large result sets or real-time dashboards:

with httpx.stream("POST", f"{BASE}/query/stream",
                   json={"sql": "HYBRID_SEARCH FROM Article QUERY 'data' LIMIT 100",
                         "purpose": "analytics"},
                   headers=H, timeout=30) as r:
    for line in r.iter_lines():
        if line.startswith("data: "):
            chunk = json.loads(line[6:])
            print(chunk["title"])

Results stream as Server-Sent Events — the client gets the first batch before the server finishes scoring the full set.


7. BM25 engine features (under the hood)

What makes the custom BM25 engine different
FeatureDetail
Block-max WANDSkips blocks whose max possible score < current threshold. Deep-offset pagination stays fast (not O(n)).
12-language stemmersen, fr, de, es, pt, it, nl, sv, no, da, fi, hu + Russian Cyrillic, Turkish, Arabic
Q-gram prefix searchSubstring matching via q-gram inverted index — catches "temporal" when you type "temp"
Reverse-trigram suffixSuffix matching for morphological variants
Stop wordsLanguage-aware removal (the, le, der, el...)
SynonymsConfigurable synonym expansion
Subword tokenizationcamelCase/digit splitting: "BiTemporal" → ["bi", "temporal"]
HighlightingByte-offset <em> wrapping for UI rendering
Faceted searchFull-match-set counts (not just top-K) for filter UIs
Fuzzy/typo toleranceLevenshtein edit-distance with configurable threshold

All in-process. No Elasticsearch. No external service.


8. SDK: client.search() — the high-level API

from relata import RelataClient
 
client = RelataClient("http://localhost:9090", bearer_token="perftoken", purpose="analytics")
 
# BM25-only (default)
result = client.search("bi-temporal data", type="Article", limit=5)
 
# With highlighting
result = client.search("fraud", type="Article", limit=5, highlight=True)
 
# With facets
result = client.search("data", type="Article", limit=10, facets=["title"])
 
# Hybrid fusion (BM25 + vector)
result = client.search("semantic similarity", type="Article", limit=5,
                       metric="cosine", weights=[0.0, 0.5, 0.5])
 
# Field filter
result = client.search("search", type="Article", limit=5,
                       filters={"id": "a2"})

Returns a SearchResponse with .hits, .estimated_total_hits, .processing_time_ms, .facets.


Summary: every search surface

SurfaceWhen to useAPI
HYBRID_SEARCH (fused)Default — best recallSQL operator
HYBRID_SEARCH (BM25-only)Exact keywords, codesWEIGHTS 0.0 1.0 0.0
HYBRID_SEARCH (vector-only)Semantic paraphrasesWEIGHTS 0.0 0.0 1.0
HYBRID_SEARCH + RERANKMaximum precisionCross-encoder re-scoring
HYBRID_SEARCH + WHEREFilter before scoringPre-filter candidates
HYBRID_SEARCH + COMPUTEDerived result columnsThresholds, labels
POST /searchMeilisearch-compatibleFacets, highlighting, typo
RAG_RETRIEVELLM groundingACL-safe, cited, reranked
SIMILAR TO"More like this"Vector similarity
POST /multi-searchFederated queriesMultiple types, one request
POST /query/streamLarge/streaming resultsSSE batches

Every response on this page was captured from a live RelataDB server. No mockups.