LaserData Cloud
Laser SDK

Views

Ask the stream a question.

A view is a query already kept up to date. Filter, aggregate, paginate, or search without moving events into another database.

Built for

Dashboards, order books, analytics APIs

How it works

A view needs a projection and a source binding. laser-plane applies new source records to the projection. Application code does not run the fold.

Registering a projection: which fields get indexed

A projection declares its shape once, on the projector, not on every published message:

  • field(name) indexes a top-level JSON field. field_at(name, pointer) does the same from an explicit RFC 6901 pointer, for a nested field. fields([...]) declares several top-level fields in one call.
  • field_typed(name, type) / field_at_typed(name, pointer, type) add a storage-type hint (Int, Float, Bool, Text) for backends that create real typed columns. The embedded backend ignores the hint and keeps native JSON types.
  • vector_field(pointer) extracts an embedding vector for semantic search.
  • content_type(..) sets the codec the projector decodes with (JSON, Avro, and so on).
  • inline_payload() keeps the original payload beside the indexed row. Typed fetches can decode it without reading the log. This is the default. index_only() stores only declared fields. Use it when high-volume projections cannot justify the payload copy.

The log always keeps the original bytes, replayable from offset 0, regardless of what a projection indexes or inlines.

Only declared fields are queryable. Everything else rides through in the inlined body (if on) or is reachable only by replaying the log.

Binding: which topic feeds which projection, and where rows land

A binding pairs a (stream, topic) source with the projections allowed to fire on it:

  • source(stream, topic) - the topic this binding watches.
  • allow(projection_id) - which registered projections may materialize from this source. A message tagged for a projection outside this set is skipped or dead-lettered.
  • default_projection(id) - applied to messages that carry no explicit projection reference.
  • target_table(name) - sugar for materializing into a table on the default embedded backend. target_on(backend, table) targets a specific named backend instead (for example, routing one topic's rows to an external warehouse while another stays on the embedded engine).
  • retention(policy) - how long materialized rows live, independent of the source topic's own Iggy retention. Options: mirror the log's own expiry (default), keep rows forever, keep them until the source topic is deleted, a fixed time-to-live, or a max row count.
  • notify() - opts this binding into the change feed. Off by default.

The index is named apart from its source topic - orders feeds orders_v1 - which is what lets a view be versioned without renaming the topic every consumer already reads.

Registration and binding, in code

Register the projection and binding once. New messages on the source topic then materialize automatically.

const id = parseProjectionId("orders_v1.v1")
const projection: Projection = {
  id,
  name: "orders_v1",
  version: 1,
  kind: { kind: "row" },
  contentType: ContentType.Json,
  extraction: {
    fields: ["id", "status", "total"].map((name) => ({
      name,
      pointer: `/${name}`
    })),
    inlinePayload: false
  },
  inlinePayloadDefault: false
}
await laser.projections().register(projection)

const binding: ProjectionBinding = {
  source: { stream: "shop", topic: "orders" },
  allowedProjections: [id],
  defaultProjection: id,
  targets: [{
    backend: "embedded",
    table: "orders_v1",
    role: "readWrite",
    delivery: "effectivelyOnce",
    required: true
  }],
  notify: true
}
await laser.bindings().apply(binding)
let projection = Projection::builder("orders_v1")
    .name("orders_v1")
    .version(1)
    .content_type(ContentType::Json)
    .index_only()
    .field("id")
    .field("total")
    .field("status")
    .build();
laser.projections().register(projection).await?;

let binding = ProjectionBinding::builder()
    .source("shop", "orders")
    .allow("orders_v1")
    .default_projection("orders_v1")
    .target_table("orders_v1")
    .notify()
    .build();
laser.bindings().apply(binding).await?;
projection_id = "orders_v1.v1"
await laser.register_projection({
    "id": projection_id,
    "name": "orders_v1",
    "version": 1,
    "content_type": "json",
    "extraction": {
        "fields": [
            {"name": f, "pointer": f"/{f}"}
            for f in ("id", "total", "status")
        ],
        "inline_payload": False,
    },
    "inline_payload_default": False,
})

await laser.apply_binding({
    "source": {"stream": "shop", "topic": "orders"},
    "allowed_projections": [projection_id],
    "default_projection": projection_id,
    "targets": [{
        "backend": "embedded",
        "table": "orders_v1",
        "role": "read_write",
        "delivery": "effectively_once",
        "required": True,
    }],
    "notify": True,
})

Two ways to get a field indexed

  • Declared on the projection. The producer publishes the payload. The worker extracts fields through the declared pointers. Producers remain independent of projection details.
  • Stamped on each message. .index(key, value) writes agdx.idx.<key> at publish time without schema registration. Use it for raw payloads or unregistered writer schemas. Explicit headers override schema-extracted values with the same field name.

Materialization is asynchronous. A new binding may lag behind published messages. Production code should wait on projector metadata or a health check. Examples can poll until a query returns data. Use Changes for ongoing notifications.

Querying a live view is a filter, sort, and paginate DSL, not a general-purpose query language:

  • Equality and range filters
  • Sort order and limit
  • Grouping and aggregation
  • Vector similarity search
  • A total-count flag

Rows come back with a headers map keyed by the extracted field names, not a fixed struct, since a projection's shape is declared at registration time.

Views require laser-plane in Laser Stack or LaserData Cloud. Standalone Iggy reports the capability as unavailable.

Quick example

// topic ensured, the orders_v1 projection registered and bound, and
// a couple of sample orders published and materialized above this block
const rows = await laser
  .query("orders_v1")
  .whereEq("status", "paid")
  .limit(10)
  .fetch()

for (const row of rows.rows) {
  console.log(`${row.headers.get("id")}: ${row.headers.get("total")}`)
}
let rows = laser
    .query("orders_v1")
    .where_eq("status", "paid")
    .limit(10)
    .fetch()
    .await?;

for row in rows.rows {
    println!("{}: {}", row.headers["id"], row.headers["total"]);
}
rows = await laser.query("orders_v1").where_eq(
    "status", "paid"
).limit(10).fetch()

for row in rows.rows:
    order_id = row.headers.get("id")
    print(f"order {order_id}: total={row.headers.get('total')}")

Rust and Python use where_eq for an indexed-key match. TypeScript uses whereEq and retains byKey as an alias. Use filter_eq and related filters for conditions beyond an exact key match.

Full runnable example: Rust · Python · TypeScript

Key operations

VerbWhat it does
Projection::builder(id)Start declaring a projection's shape
.field(name) / .field_at(name, pointer) / .fields([...])Index a field, by top-level name or an explicit JSON pointer
.field_typed / .field_at_typedIndex a field with a storage-type hint
.vector_field(pointer)Extract an embedding vector for semantic search
.inline_payload() / .index_only()Keep a copy of the payload alongside the row, or index-only
projections().register(projection)Register the declared projection
ProjectionBinding::builder()Start declaring which topic feeds which projection
.source(stream, topic) / .allow(id) / .default_projection(id)Which topic, which projections, and the fallback for untagged messages
.target_table(name) / .target_on(backend, table)Where materialized rows land
.retention(policy)How long rows live, independent of the source topic's own retention
.notify()Push changes to the change feed
bindings().apply(binding)Apply the declared binding
publish().index(key, value)Stamp an explicit indexed header at publish time, no projection schema needed
query(index)Open a query against a materialized projection
where_eq / whereEqMatch an indexed key exactly
filter_gte and range filtersComparison filters beyond equality
order_desc / order_ascSort the result set
limit(n)Cap the number of rows returned
group_by, count, sumAggregate rather than list rows
with_total()Include a total-match count alongside a limited page
fetch() / fetch_typedRun the query, generic or decoded into your own type

Not an exhaustive list. Vector similarity search and windowed aggregation exist on the fuller query surface. See the SDK source for the complete builder.

Running it

Views run on Laser Stack and LaserData Cloud. Both provide laser-plane. The example exits cleanly when the capability is unavailable.

On this page