LaserData Cloud
Laser SDK

Views

Extract fields from topics, maintain indexes, and query typed results

Views maintain queryable data from messages in the log. A projection defines the extracted fields. A binding selects the topic that feeds it. laser-plane applies new records without a fold in application code.

Built for

Use Views for dashboards, order books, and analytics APIs.

How it works

Register a projection and connect it to a source topic through a binding. The projector then applies arriving records. Queries read the resulting index.

Registering a projection: which fields get indexed

Define extraction once in the projection:

  • field(name) selects a top-level JSON field. field_at(name, pointer) uses an RFC 6901 pointer for nested data. fields([...]) selects several top-level fields.
  • field_typed(name, type) and field_at_typed(name, pointer, type) provide Int, Float, Bool, or Text hints for typed-column backends. Embedded storage keeps native JSON types and ignores these hints.
  • vector_field(pointer) extracts an embedding vector for semantic search.
  • content_type(..) selects the decoder, such as JSON or Avro.
  • inline_payload() stores the original payload with the row and is the default. Typed reads can decode it without reading the log. index_only() stores declared fields without the payload copy.

The log retains original bytes according to its retention policy. Projection retention is independent and can keep rows after source messages expire. Only declared fields are queryable. Other fields remain in the inline payload, when enabled, or in retained log records.

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

A binding selects a (stream, topic) and the projections permitted for that source:

  • source(stream, topic) selects the topic.
  • allow(projection_id) selects permitted projections. Messages tagged for another projection are skipped or dead-lettered.
  • default_projection(id) handles messages without an explicit projection reference.
  • index(name) names the operational index. The default backend is local to the replica. backend(binding) selects an exact backend resource ID and generation. Shared destinations are registered separately.
  • retention(policy) controls row retention independently of topic retention. It can mirror log expiry, retain forever, retain until source deletion, or impose a TTL or maximum row count.
  • notify() enables the change feed. Notifications are off by default.

The index and topic have separate names. For example, orders can feed orders_v1. You can version the view without renaming the topic that consumers use.

Registration and binding, in code

After registration, new source messages update the view 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,
  index: "orders_v1",
  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")
    .index("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,
    "index": "orders_v1",
    "notify": True,
})

Two ways to get a field indexed

With a declared projection, the producer sends a payload and the projector extracts fields through the registered pointers. The producer does not need projection details.

Alternatively, .index(key, value) adds an agdx.idx.<key> header during publication. Use headers for raw payloads or unregistered writer schemas. An explicit header overrides an extracted value with the same field name.

Materialization, the process that builds stored query rows, is asynchronous. A new binding can lag behind publication. In production, wait for projector progress or health information. Examples can poll until data appears. Use Changes for later updates.

Queries use a structured language for these operations:

  • Equality and range filters.
  • Sorting and limits.
  • Grouping and aggregation.
  • Vector similarity search.
  • Total counts.

Results contain an ordered fields schema and matching typed values in each row. They also include pagination and query context. Read values through the result accessors.

Views require laser-plane through Laser Stack or LaserData Cloud. Standalone Iggy reports this 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()

import { queryResultValue, typedValueDiagnosticText } from "@laserdata/laser-sdk"

for (const row of rows.rows) {
  const total = queryResultValue(rows, row, "total")
  console.log(total === undefined ? "missing" : typedValueDiagnosticText(total))
}
let rows = laser
    .query("orders_v1")
    .where_eq("status", "paid")
    .limit(10)
    .fetch()
    .await?;

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

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

Rust and Python use where_eq for indexed-key equality. TypeScript uses whereEq, with byKey as an alias. Use filter_eq and related filters for other conditions.

Complete examples: Rust, Python, and 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
.index(name) / .backend(binding)Operational index and optional exact backend resource generation
.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

The full query builder also includes vector search and windowed aggregation. See the SDK source for all operations.

Operational Views and Destinations

An operational binding maintains an index on the deployment's backend. A materialization destination is a separate resource with its own identity, source scope, generation, checkpoints, and lifecycle. It is not another target within that binding.

A query selects an operational index, destination, or query route. Advertised backend support determines available operations and consistency. The contract defines logical schemas, source incarnations, and Arrow IPC input for supported paths. A defined wire type does not mean that every deployment implements it.

See Managed Data for readiness, destination lifecycle, and typed results.

Running it

Laser Stack and LaserData Cloud provide laser-plane. The example exits normally if query support is unavailable.

On this page