Skip to main content
Your schema says what. Routing decides where.

1. What Routing Does

Routing maps each entity field to one or more backend instances and storage strategies. A single entity can keep scalar fields in one backend, vectors in another, and searchable text in a third while queries still start from ws.query(Entity). At query time, HeavenBase compiles each filter or near clause for the backend that owns the field, executes the supported fragments, and merges result frames by object_id.
object_id placement is fixed. HeavenBase replicates the identity where needed so split fields can still hydrate into one logical row.

2. Start with Automatic Placement

Workspace presets choose practical defaults. In debug, ordinary rows go to main, vector fields can route to vec, and search-oriented fields can route to search.
import heavenbase as hb


class Document(hb.Entity):
    title = hb.field(hb.ShortText)
    body = hb.field(hb.LongText)
    embedding = hb.field(hb.Vector[2])


ws = hb.HeavenBase("core-routing-auto", preset="debug")
ws.register(Document)

ws.upsert_many(
    Document,
    [
        {"object_id": "d1", "name": "Agent routing", "title": "Agent routing", "body": "Agents query one surface.", "embedding": [1.0, 0.0]},
        {"object_id": "d2", "name": "Backend notes", "title": "Backend notes", "body": "Backends store fields.", "embedding": [0.0, 1.0]},
    ],
)

frame = ws.query(Document).near(Document.embedding, [1.0, 0.0], top_k=1).select("title", "score").execute()
print(frame.rows()[0]["title"])

3. Place Fields Explicitly

Use .store(to=..., strategy=...) when a field must land on a specific backend.
class RoutedDocument(hb.Entity):
    title = hb.field(hb.ShortText).store(to="main")
    body = hb.field(hb.LongText).store(to="main")
    embedding = hb.field(hb.Vector[2]).store(to="vec", strategy=hb.VectorIndex)


ws = hb.HeavenBase(
    "core-routing-explicit",
    backends={
        "main": {"type": "inmem"},
        "vec": {"type": "inmem"},
    },
)
ws.register(RoutedDocument)

routes = (
    ws.query(hb.MetaSchema)
    .where(hb.MetaSchema.kind == "storage")
    .where(hb.MetaSchema.subject_id == "routed-document")
    .select("field", "backend", "strategy")
    .execute()
    .rows()
)

print([(row["field"], row["backend"]) for row in routes])
Storage strategies are backend-facing hints such as InlineColumn, SideTable, VectorIndex, InvertedIndex, JsonField, GraphEdge, and ExternalRef. Most users only need VectorIndex explicitly.

4. Inspect Query Decisions

Use explain() when you need to see which backend and handler a query would use.
plan = (
    ws.query(RoutedDocument)
    .near(RoutedDocument.embedding, [1.0, 0.0], top_k=3)
    .explain()
)

print(plan["steps"][0]["backend"])
Each step reports the selected backend, storage strategy, handler kind, handler mode, and unsupported reason when a native path is unavailable.

5. Keep the Mental Model Small

For application code, think in this order:
  1. Define the entity once.
  2. Let a preset route fields automatically.
  3. Add .store(...) only for fields that need explicit placement.
  4. Use explain() when performance or backend choice matters.
Multi-backend writes are best-effort across backend boundaries. Keep cross-backend invariants simple until your deployment has a clear transaction strategy.

Further Exploration

Related resources: