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)andfield_at_typed(name, pointer, type)provideInt,Float,Bool, orTexthints 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
| Verb | What 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_typed | Index 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 / whereEq | Match an indexed key exactly |
filter_gte and range filters | Comparison filters beyond equality |
order_desc / order_asc | Sort the result set |
limit(n) | Cap the number of rows returned |
group_by, count, sum | Aggregate rather than list rows |
with_total() | Include a total-match count alongside a limited page |
fetch() / fetch_typed | Run 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.