Spaces:
Running
Running
File size: 8,861 Bytes
a1dd5ba | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """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}
|