SudharshanR
ElideDB query by example: no text, no model at query time
a1dd5ba
Raw
History Blame Contribute Delete
8.86 kB
"""METADATA-FIRST PLANNING. Vectors are the last resort, not the first.
The defect this fixes: asking for "a yellow object" ran a text encoder
and a matmul over every episode vector in the corpus, because the only
thing the store indexed was embeddings. That is a filesystem with a
similarity function bolted on, not a database. A database answers a
value predicate from STATISTICS and touches the payload only for rows
that survive.
Three layers, cheapest first, each strictly narrowing the candidate set:
1 LABELS an inverted index sorted by (kind, value, ts). A lookup
for value='yellow object' is a contiguous RANGE, so the
zone maps in the commit log drop whole files and the
Parquet row-group statistics drop whole groups before
anything decompresses. Exact, not approximate.
2 FLAGS has_open / has_close / ... booleans on the episode row.
min/max statistics on a bool are free set membership:
max=false means the group cannot contain the action, so
it is skipped without being read.
3 VECTORS only over what layers 1-2 left standing.
The order matters more than any one layer. Layer 1 is exact and costs
kilobytes; layer 3 is fuzzy and costs the corpus. Running 3 first, as
the old path did, means paying the most for the least selective filter.
"""
from __future__ import annotations
import numpy as np
import pyarrow.compute as pc
from .store import QueryStats
def flag_kinds(store):
"""The transition kinds THIS corpus produced, read from the store.
Was a literal tuple of eight English verbs - open, close, put_into,
put_on, take_out, adjust, contact, release - which is a table-top
manipulation prior. A driving log has none of them and a warehouse
camera has no drawers, so the system could not recognise a domain it
had not been told about.
Now: whatever `has_*` columns the write path created, which are
whatever transition types the corpus turned out to have. Low
cardinality is still the requirement - a bool column's min/max IS
the set-membership test - and discovery enforces that by clustering
rather than by someone keeping the list short.
"""
if "episodes" not in store.tables():
return ()
return tuple(sorted(c[4:] for c in store.table("episodes").scan().column_names
if c.startswith("has_")))
def label_lookup(store, values, kinds=None, stats: QueryStats | None = None):
"""Episodes carrying ANY of `values`, from the inverted index alone.
Returns {(stream, ts)} plus the bytes it cost. No vectors are touched
and no video is opened; this is a range scan over a sorted string
column with statistics doing the pruning.
"""
if "labels" not in store.tables():
return None, stats
t = store.table("labels")
stats = stats or QueryStats()
vals = [v.lower() for v in values if v]
if not vals:
return set(), stats
# PUSHED DOWN, not filtered afterwards. The table is sorted by
# (kind, value) with one row group per value, so the reader skips
# every group whose min/max cannot contain a requested value.
tb, stats = t.scan_values(
"value", vals, columns=["stream", "value", "kind", "ep_ts"],
stats=stats)
if kinds and len(tb):
tb = tb.filter(pc.field("kind").isin(list(kinds)))
out = set(zip([str(s) for s in tb.column("stream").to_pylist()],
[int(a) for a in tb.column("ep_ts").to_pylist()]))
return out, stats
def flag_lookup(store, kinds, stats: QueryStats | None = None):
"""Episodes whose has_<kind> boolean is true, for any kind asked."""
if "episodes" not in store.tables():
return None, stats
t = store.table("episodes")
stats = stats or QueryStats()
cols = [f"has_{k}" for k in kinds if f"has_{k}"
in t.scan().column_names]
if not cols:
return None, stats
tb = t.scan(columns=["stream", *cols], stats=stats)
m = None
for c in cols:
col = np.asarray(tb.column(c).to_pylist(), bool)
m = col if m is None else (m | col)
ts = np.asarray(tb.column("ts").to_pylist(), np.int64)
ss = tb.column("stream").to_pylist()
return {(str(ss[i]), int(ts[i])) for i in np.where(m)[0]}, stats
def objects_in(store, stream, t0, t1, stats: QueryStats | None = None):
"""Which object ids were visible in [t0, t1] of `stream`.
A time-range scan of `instances`, which is a timeseries table, so
the commit-log zone map on ts drops files and the row-group
statistics drop groups. Returns ids, not names - there are no names.
"""
if "instances" not in store.tables():
return set(), stats
stats = stats or QueryStats()
tb = store.table("instances").scan(
t0=t0, t1=t1, columns=["stream", "object_id", "n_frames"],
stats=stats)
if not len(tb):
return set(), stats
m = pc.equal(tb.column("stream"), stream)
tb = tb.filter(m)
return {int(v) for v in tb.column("object_id").to_pylist()}, stats
def object_lookup(store, object_ids, stats: QueryStats | None = None):
"""Episodes containing ANY of these physical objects.
The point of the whole object store, expressed as a database
operation: an equality predicate on a CLUSTERED column, answered by
statistics. `instances` is sorted and grouped by object_id, so the
lookup is a contiguous range - the commit log drops files, the
footer drops row groups, the page index drops pages, and only then
does anything decompress.
No embedding is touched and no video is opened. This is layer 1 of
the planner for identity, exactly as `labels` is for vocabulary.
"""
if "instances" not in store.tables() or not object_ids:
return None, stats
stats = stats or QueryStats()
tb, stats = store.table("instances").scan_values(
"object_id", [int(o) for o in object_ids],
columns=["stream", "ep_ts", "object_id"], stats=stats)
return set(zip([str(s) for s in tb.column("stream").to_pylist()],
[int(a) for a in tb.column("ep_ts").to_pylist()])), stats
def like_this_clip(store, stream, t0, t1):
"""Query by example, metadata only: same physical objects, elsewhere.
Identify the objects in the given window, then find every other
episode holding one of them. Two index lookups, no vector scan, no
text anywhere in the path - which is what having a stable id per
physical object buys.
"""
stats = QueryStats()
ids, stats = objects_in(store, stream, t0, t1, stats)
before = stats.bytes_touched
eps, stats = object_lookup(store, ids, stats)
return {"objects": sorted(ids), "episodes": eps,
"steps": [{"stage": "objects_in", "kept": len(ids),
"bytes": before},
{"stage": "object_lookup",
"kept": len(eps or ()),
"bytes": stats.bytes_touched - before}],
"bytes_touched": stats.bytes_touched,
"corpus_bytes": stats.corpus_bytes}
def plan(store, text, nouns=None, kinds=None, object_ids=None):
"""Narrow by metadata, report what each layer cost.
The return value is deliberately auditable: a caller can see how many
episodes survived each stage and how many bytes it took, which is the
only way to tell a real prune from a decorative one.
"""
steps = []
stats = QueryStats()
cand = None
if nouns:
got, stats = label_lookup(store, nouns, stats=stats)
if got is not None:
steps.append({"stage": "labels", "asked": sorted(nouns),
"kept": len(got),
"bytes": stats.bytes_touched})
cand = got if cand is None else (cand & got)
if kinds:
before = stats.bytes_touched
got, stats = flag_lookup(store, kinds, stats=stats)
if got is not None:
steps.append({"stage": "flags", "asked": sorted(kinds),
"kept": len(got),
"bytes": stats.bytes_touched - before})
cand = got if cand is None else (cand & got)
if object_ids:
before = stats.bytes_touched
got, stats = object_lookup(store, object_ids, stats=stats)
if got is not None:
steps.append({"stage": "objects",
"asked": sorted(int(o) for o in object_ids),
"kept": len(got),
"bytes": stats.bytes_touched - before})
cand = got if cand is None else (cand & got)
return {"candidates": cand, "steps": steps,
"bytes_touched": stats.bytes_touched,
"corpus_bytes": stats.corpus_bytes}