Skip to content

Use case: hybrid retrieval, filters & facets

Hybrid search is little big brain’s headline capability. This guide covers how to get the most out of it: choosing sources and targets, narrowing with filters, bucketing with facets, and fusing several queries with reciprocal-rank fusion.

A single search.hybrid call combines, over one snapshot:

  1. lexical matching (exact and near-exact terms)
  2. ontology resolution and concept expansion (canonicalize + widen the query)
  3. BM25 full-text scoring
  4. vector / ANN semantic similarity
  5. graph-neighborhood signal (entities near strong matches score higher)

You don’t wire these together — the engine does. Add explain: true to see each leg’s contribution.

const res = await lbb.search.hybrid("customer identity storage", {
topK: 10,
source: "persisted", // "persisted" (index runs) or "ephemeral" (built on the fly)
consistency: "strong", // overlay WAL committed after the index run
targets: ["entities", "assertions", "observations"],
});
  • sourcepersisted uses the durable index runs (fast, scalable); ephemeral builds an index on the fly (handy right after a write with no persisted run yet). On the hosted product, search never 404s: it falls back to ephemeral while a base run is built.
  • consistencystrong (default) overlays facts committed after the last index run so results match the head; eventual skips the overlay for lower cost.
  • targets — which document kinds to return: entities, assertions, observations, relation types, ontology terms/concepts, neighborhoods.

Filters are structured JSON expressions applied across BM25, vector, and hybrid paths. Supported ops: equality, set membership (in), existence, numeric comparison, token containment, glob, regex, and boolean and/or/not.

const res = await lbb.search.hybrid("identity storage", {
topK: 10,
filter: {
and: [
{ field: "entity_type", op: "in", value: ["DATABASE", "DATASET"] },
{ field: "tier", op: "eq", value: "prod" },
],
},
});

Filters can prune candidates early, but final results always re-verify the filter against graph-derived metadata — a candidate that slips through approximate generation is still dropped if it doesn’t truly match.

Facets count metadata buckets after filters — great for building a faceted UI or letting an agent see the shape of the result set before drilling in.

const res = await lbb.search.hybrid("identity storage", {
topK: 10,
facets: ["target_kind", "entity_type", "relation"],
});
// res.facets => { entity_type: { DATABASE: 3, DATASET: 1 }, relation: { STORES: 2, ... } }

Common facet fields: target_kind, entity_type, relation, source_id, label, target, text.

When a question decomposes into several angles, run them as subqueries and fuse with reciprocal-rank fusion — this beats a single query for recall on multi-faceted questions.

const res = await lbb.search.multi({
queries: [
"systems that store identity data",
"services that read customer records",
"databases classified as prod tier",
],
topK: 10,
});

Each subquery runs the full hybrid pipeline; RRF combines their rankings so a result strong across several subqueries rises to the top.

When you want a single leg — for evaluation, or because you already know which signal matters — call it directly:

await lbb.search.fullText("customer identity", { topK: 10 }); // BM25 only
await lbb.search.vector({ query: "customer identity", topK: 10 }); // ANN only

For offline recall/latency comparison, the console Search & eval view runs accuracy and latency checks so you can compare single-leg vs. hybrid configurations side by side.

To retrieve over a real corpus, ingest it with bulk NDJSON (POST /v1/graph/import) rather than per-fact commits — it’s tens of times faster and supports flat properties and external keys. Then build indexes once and search. See the HTTP API.

  • Configure managed embeddings and pick a model per graph for real semantic recall — see Embeddings & feedback tuning.
  • Grade results with search_feedback (3 = relevant, 1 = partial, 0 = irrelevant) to produce qrels and fine-tune the embedding model to your domain.
  • Use explain: true and check explain.vector_model_id to confirm the vector leg is using the model you configured.
  • Fold in rule-derived and type-closure facts with reason: true when relevant context is entailed rather than written verbatim.