Range Queries with triple_slice and quad_slice

Open inAnthropic

Range queries let you efficiently find documents whose property values fall within a specified range. triple_slice and quad_slice push range constraints directly into the storage engine for O(log n) lookups instead of scanning every triple.

Why Range Queries Matter

When you need to find documents within a time window, a numeric band, or a lexicographic string range, the traditional WOQL approach requires fetching every value and filtering in the query layer:

Example: JavaScript
// The slow way: fetch all timestamps, then filter
let v = Vars("doc", "time");
and(
  triple(v.doc, "timestamp", v.time),
  greater(v.time, literal("2025-01-01T00:00:00Z", "xsd:dateTime")),
  less(v.time, literal("2026-01-01T00:00:00Z", "xsd:dateTime"))
)

This works, but it materializes every triple for the timestamp predicate before discarding the ones outside the range. For large datasets, this is prohibitively slow.

triple_slice solves this by binary-searching the sorted value dictionary and iterating only the matching range. The storage engine never touches values outside your bounds.

When to Use triple_slice

  • Time-series data: Find events, logs, or measurements within a time window
  • Numeric filtering: Select records in a price band, age range, or score bracket
  • String ranges: Lexicographic slicing for alphabetical partitions
  • Membership checks: Verify that a known value falls within expected bounds

What triple_slice Does

Signature

Example: Text
triple_slice(Subject, Predicate, Object, Low, High)
quad_slice(Subject, Predicate, Object, Low, High, Graph)

triple_slice is a superset of triple. It adds two optional bound parameters, Low and High, that constrain the Object to a half-open range [Low, High). When both bounds are unbound, it behaves identically to triple.

quad_slice is the same predicate with an explicit graph selector (like quad extends triple).

Half-Open Range [Low, High)

  • Low is inclusive: the first matching value is >= Low
  • High is exclusive: all matching values are strictly < High
  • Adjacent slices [A, B) and [B, C) partition the space cleanly with no overlap and no gaps

Binding Modes

triple_slice works as both a generator (when Object is unbound, it produces matching triples) and a pattern matcher (when Object is ground, it checks membership).

ObjectLowHighBehavior
unboundunboundunboundSame as triple — generates all triples. Low and High unify with Object.
unboundboundunboundGenerates triples where Object >= Low.
unboundunboundboundGenerates triples where Object < High.
unboundboundboundGenerates triples where Low <= Object < High.
groundunboundunboundSame as triple — membership check. Low and High unify with Object.
groundboundunboundChecks triple exists AND Object >= Low. Fails if Object < Low.
groundunboundboundChecks triple exists AND Object < High. Fails if Object >= High.
groundboundboundChecks triple exists AND Low <= Object < High. Fails if out of range, even if the triple exists.

The key subtlety: when Object is ground and the triple exists but the value falls outside the range, the predicate fails. The range constraint always takes precedence.

Automatic Type Inference

When the predicate is ground (which is the common case), triple_slice infers the correct XSD type from the schema and automatically casts untyped bounds. You do not need explicit typecast calls.

For example, if the schema declares timestamp as xsd:dateTime, then the string "2025-01-01" is automatically cast to xsd:dateTime. If the cast fails (e.g., "not-a-date" to xsd:dateTime), an error is raised — there are no silent failures.

When the predicate is unbound, bounds must either be already typed or be unbound variables.

Supported Types

All data is stored in ordered form with succinct data structures, including the XSD types. Examples of the ordered types that can be used with triple_slice:

  • Date/Time: xsd:dateTime, xsd:dateTimeStamp, xsd:date, xsd:time
  • Numeric: xsd:integer, xsd:decimal, xsd:float, xsd:double, xsd:nonNegativeInteger, xsd:positiveInteger, xsd:long, xsd:int, xsd:short, xsd:byte
  • String: xsd:string

How to Use triple_slice — Worked Examples

The following examples use a schema with a SensorReading class:

Example: JSON
{
  "@type": "Class",
  "@id": "SensorReading",
  "@key": { "@type": "Random" },
  "sensor_id": "xsd:string",
  "timestamp": "xsd:dateTime",
  "temperature": "xsd:decimal",
  "label": { "@type": "Optional", "@class": "xsd:string" }
}

With sample data: five readings across January 2025 with temperatures from 18.5 to 23.1 and labels "A" through "E".


Example 1 — Classic DateTime Range

Find all sensor readings from the first half of January 2025.

Example: JavaScript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time, "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")

What happens: The engine looks up timestamp in the schema, sees it is xsd:dateTime, casts the string bounds to xsd:dateTime, and binary-searches the value dictionary. Only readings with timestamps in [Jan 1, Jan 15) are returned.


Example 2 — Open-Ended High: From a Date Onward

Find all readings from January 20 onward (no upper bound).

Example: JavaScript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time, "2025-01-20T00:00:00Z")

With Low bound and High unbound, this generates all triples where timestamp >= Jan 20.


Example 3 — Open-Ended Low: Before a Date

Find all readings before January 10.

Example: JavaScript
let v = Vars("doc", "time", "low");
and(
  eq(v.low, literal("2025-01-10T00:00:00Z", "xsd:dateTime")),
  triple_slice(v.doc, "timestamp", v.time, null, v.low)
)

With Low unbound and High bound, this generates all triples where timestamp < Jan 10.


Example 4 — No Bounds: Degenerates to triple

When neither Low nor High are set, triple_slice behaves identically to triple.

Example: JavaScript
let v = Vars("doc", "time", "low", "high");
triple_slice(v.doc, "timestamp", v.time, v.low, v.high)

Returns all timestamp triples. For each result, low and high both unify with the timestamp value, so low == high == time.


Example 5 — Membership Check: Object In Range

Check that a specific reading's timestamp falls within the expected window.

Example: JavaScript
let v = Vars("doc");
and(
  isa(v.doc, "SensorReading"),
  triple_slice(v.doc, "timestamp",
    literal("2025-01-10T08:30:00Z", "xsd:dateTime"),
    "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
)

The Object is ground. The predicate checks: does this triple exist AND does 2025-01-10T08:30:00Z fall in [Jan 1, Jan 15)? If yes, succeeds. If the timestamp exists but is outside the range, fails.


Example 6 — Membership Check: Object Out of Range

This demonstrates the key subtlety: a triple can exist but still fail the range check.

Example: JavaScript
let v = Vars("doc");
and(
  isa(v.doc, "SensorReading"),
  triple_slice(v.doc, "timestamp",
    literal("2025-01-25T14:00:00Z", "xsd:dateTime"),
    "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")
)

The timestamp Jan 25 exists in the database, but it is outside [Jan 1, Jan 15), so the predicate fails. This is not an error — it is correct behavior. The range constraint takes precedence over triple existence.


Example 7 — Numeric Range: Integer Values

Find readings with temperature between 19.0 and 22.0.

Example: JavaScript
let v = Vars("doc", "temp");
triple_slice(v.doc, "temperature", v.temp, "19.0", "22.0")

The engine infers xsd:decimal from the schema for temperature and casts the string bounds accordingly.


Example 8 — String Range: Lexicographic Slice

Find readings with labels in the lexicographic range [B, D).

Example: JavaScript
let v = Vars("doc", "lbl");
triple_slice(v.doc, "label", v.lbl, "B", "D")

Returns labels "B" and "C" (but not "D", since the high bound is exclusive).


Example 9 — Adjacent Non-Overlapping Slices

Two adjacent slices partition the data cleanly — no double-counting, no gaps.

Example: JavaScript
let v = Vars("doc", "time");

// First half of January
let slice1 = triple_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z");

// Second half of January
let slice2 = triple_slice(v.doc, "timestamp", v.time,
  "2025-01-15T00:00:00Z", "2025-02-01T00:00:00Z");

The union of slice1 and slice2 equals the full month of January. A reading at exactly 2025-01-15T00:00:00Z appears only in slice2 (since Low is inclusive and High is exclusive).


Example 10 — Unbound Predicate with Typed Bounds

When the predicate is unbound, auto-type-inference cannot determine the target type. Bounds must be explicitly typed.

Example: JavaScript
let v = Vars("doc", "pred", "val");
triple_slice(v.doc, v.pred, v.val,
  literal("2025-01-01T00:00:00Z", "xsd:dateTime"),
  literal("2025-01-15T00:00:00Z", "xsd:dateTime"))

This iterates over all predicates on each document, returning any triple whose object is a xsd:dateTime value in the given range.


Example 11 — Before and After: The Old Way vs triple_slice

Before (full scan + filter):

Example: JavaScript
let v = Vars("doc", "time");
and(
  triple(v.doc, "timestamp", v.time),
  greater(v.time, literal("2025-01-01T00:00:00Z", "xsd:dateTime")),
  less(v.time, literal("2025-01-15T00:00:00Z", "xsd:dateTime"))
)

After (range pushed to storage):

Example: JavaScript
let v = Vars("doc", "time");
triple_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z")

Both return the same results. The triple_slice version binary-searches the value dictionary and never touches values outside the range.


Example 12 — quad_slice on Instance Graph

quad_slice adds an explicit graph parameter, just like quad extends triple.

Example: JavaScript
let v = Vars("doc", "time");
quad_slice(v.doc, "timestamp", v.time,
  "2025-01-01T00:00:00Z", "2025-01-15T00:00:00Z",
  "instance")

Equivalent to triple_slice but explicitly targets the instance graph.


Example 13 — quad_slice on Schema Graph

Query the schema graph for range information.

Example: JavaScript
let v = Vars("cls", "pred", "val");
quad_slice(v.cls, v.pred, v.val,
  literal("A", "xsd:string"),
  literal("M", "xsd:string"),
  "schema")

Searches the schema graph for any triple whose object is a string in [A, M).

Reference

Error Conditions

  • Invalid cast: Bounds that cannot be cast to the predicate's type produce a clear error (e.g., "not-a-date" for an xsd:dateTime predicate)
  • Unbound predicate + untyped bounds: If the predicate is unbound and bounds are plain strings (not typed), an error is raised since the target type cannot be inferred
  • Non-existent predicate: Behaves like triple — no results, no error

Performance Characteristics

  • Binary search: O(log n) to locate range bounds in the sorted value dictionary
  • Streaming: Results are yielded one at a time — memory usage is O(1) regardless of range size
  • No full scan: Values outside the range are never touched
  • Layer stack: Each layer in the stack requires a separate binary search; rollups improve performance for deep stacks

Was this helpful?