feat(gfql): searchAny WYSIWYG float + datetime search (#1695)#1700
feat(gfql): searchAny WYSIWYG float + datetime search (#1695)#1700lmeyerov wants to merge 7 commits into
Conversation
…fied _canonical_float_str reproduces the streamgl-viz inspector search render (defaultFormat number/integer branch): WHOLE -> bare integer string (exponent-free, 1e16 -> '10000000000000000'); FRACTIONAL -> fixed `precision`-decimal. Both branches route through an exponent-free decimal-based render so pandas/cuDF/polars can agree byte-for-byte (the astype(str) exponent + half-boundary divergence is why floats were excluded). + _is_float_dtype. pandas path unit-verified vs the inspector rule; cuDF branch drafted. NOT yet wired into the gate/kernel and float exclusions NOT lifted — gated on the dgx cross-engine parity fuzzer (cuDF/polars-gpu, never local) per the parity-or-honest-NIE / no-silent-wrong rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…as-exact (#1695 P1) The dgx cross-engine float parity fuzzer (oracle = the viz's REAL sprintf-js) settled the render and falsified the "cross-engine decimal cast = parity" hypothesis: - Correct fractional render is `"%.4f" % v` applied DIRECTLY to the raw double (single correctly-rounded conversion) — byte-identical to the inspector's sprintf-js (e.g. 3.28045->"3.2805", -3.74825->"-3.7483"). Whole floats (v%1==0, |v|<1e21) -> str(int(v)). - The prior draft's `np.round(v,4)` prepass DOUBLE-ROUNDS and mis-renders 73/210 half-tie cases (-3.74825->"-3.7482"); removed. - The prior cuDF branch used `astype("decimal128[38,4]")` (string form), which RAISES in cudf 26.02; moot regardless — cuDF float->decimal128 TRUNCATES (wrong on generic values, not just ties) and polars' decimal cast diverges at 5th-decimal ties + drops the sign on -0.0000. No native GPU printf; a per-cell UDF would be a host-bridge. So `_canonical_float_str` is now pandas-only. Result (decision A, symmetric auto-gate): float WYSIWYG search is pandas-EXACT via explicit `columns=[float]`; cuDF/polars honestly decline (NotImplementedError / None); float stays OUT of the auto gate on every engine (unchanged), so auto behavior is cross-engine-identical. `search_any_mask` renders float columns through `_canonical_float_str` instead of the divergent astype(str); adds a forward-ready `float_precision` param (defaults to the inspector's 4). Validated on dgx-spark (image test-rapids-official:26.02-gfql, all engines): new test_search_any_float_wysiwyg + full conformance suites 302 passed / 1 skipped, pandas exact + cuDF/polars honest-NIE, half-tie regression pinned. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
…z-localized (#1695 P1b) Explicit `columns=[<datetime>]` searches the inspector's date render on pandas: `_canonical_datetime_str` reproduces moment `formatDate`'s `'MMM D YYYY, h:mm:ss a z'` (MMM=%b, D=%-d, YYYY=%Y, h=%-I 12h, mm=%M, ss=%S, a=lowercased am/pm, z=%Z tz-abbrev), localized to a caller `tz`. Verified byte-identical to the viz's REAL moment.utc for the UTC path (`moment.utc('2021-03-14T15:09:26Z').format(...)` == `"Mar 14 2021, 3:09:26 pm UTC"`). `tz=` is Leo's localization call-param — pass the viewer's zone for byte parity with what that viewer sees (per-timestamp DST-correct via zoneinfo: PST/PDT, EST/EDT). Default `tz='UTC'` chosen for DETERMINISM (a server-local default would vary by deployment and break the parity oracle) — FLAG for Leo, who suggested server-time; trivially switchable. Like float (P1), datetime is pandas-only: cuDF/polars honestly decline (their native datetime->string + tz handling diverge; a per-cell UDF would be a host-bridge). Datetime stays OUT of the auto gate on every engine (decision A, symmetric). `search_any_mask` renders datetime columns via the new helper and gains forward-ready `temporal_format`/`tz` params (default = inspector). Validated on dgx-spark (all engines): new test_search_any_datetime_wysiwyg (pandas exact incl. 12h/am-pm/tz-abbrev/null, cuDF/polars honest-NIE) + full conformance 303 passed / 1 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
…poralFormat/tz (#1695 P4/P5) Thread the #1695 format options end-to-end so callers control the WYSIWYG render (default = the streamgl-viz inspector: 4 decimals; moment date format; UTC): - cypher option map: `searchAny(a, term, {floatPrecision: N, temporalFormat: '...', tz: '...'})` — `_SEARCH_ANY_OPT_KEYS` + strict typed parsing (floatPrecision->int, temporalFormat/tz->string literal); unknown keys / wrong types raise as before. - ast `search_any` op + `RowPipelineMixin.search_any` + call-validation safelist (float_precision:int, temporal_format/tz:string) + python twins `search_nodes`/`search_edges` all gain the three params and pass them to the kernel. - polars dispatch unchanged: float/datetime decline regardless of these params, so the extra params are simply not read (string/int search is unaffected). The `temporal` key from the original option sketch is left out (unclear semantics; datetime is explicit-columns-only, so an enable-toggle is moot) — can add later. Validated on dgx-spark (all engines): new test_search_any_format_options (floatPrecision=2 -> "-3.75"; tz='America/Los_Angeles' -> "8:09:26 am"; twin float_precision; strict-validation raises) + full conformance & cypher suites 2041 passed / 7 skipped / 15 xfailed, no regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
…, option validation (#1695 P8) Three confirmed findings from the adversarial review (multi-angle finders + verify): 1. inf/-inf floats rendered "inf"/"-inf" (+ a RuntimeWarning from `inf % 1`) but the inspector shows "Infinity"/"-Infinity" (node-verified: sprintf('%.4f',Inf) == "Infinity"). Now handled explicitly before the `% 1` split — correct render, no warning. 2. The am/pm lowercasing was a blind global `str.replace("AM"/"PM")` that corrupted an alpha tz abbreviation (America/Manaus -> "AMT" became "amT") and literal text in a custom temporal_format. Now word-bounded (`\bAM\b`/`\bPM\b`), so only the standalone am/pm token is lowercased. 3. Negative / bool `float_precision` (and empty tz / temporal_format) silently misrendered via the python twins, which bypass the call safelist (`"%.*f" % -1` clamps to 0; `tz_convert('')` raises an opaque IndexError). Added `validate_format_opts` at the kernel (the single choke point all surfaces pass through) for a consistent GFQLValidationError, and tightened the safelist validator to `is_nonneg_int` so the ASTCall/cypher path (all engines) rejects them before dispatch too. Also documents the astronomically-rare whole-float >= 2**53 shortest-round-trip limitation (str(int(v)) vs JS String()). The review's cross-engine-safety and parity-invariant hypotheses were refuted (no cuDF/polars render path, no silent divergence, no host-bridge). Regression pins added (inf render + no-RuntimeWarning; twin option validation). dgx all-engines: 1436 search/validation/lowering passed; conformance 304/1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
Interactive dgx benchmark ("typing into a search window", 100K/1M rows, mixed
columns) surfaced two render hot-spots (the render re-runs per keystroke):
- Float: replaced the np.char.mod/numpy-masked-scatter render with a plain python
list-comp — measured FASTER (1M: ~305ms -> the masked-scatter was ~357ms; the
simple loop wins because it avoids several full-array passes + object boxing).
Byte-identical output (differential vs the prior element-wise semantics, all
precisions + inf/NaN/nullable).
- Datetime: pandas `dt.strftime` is a ~20x-slow per-element path (~2.2s/1M). For
the default format in UTC (the common, deterministic case) assemble the render
vectorized from `.dt` components + a month-name lookup — byte-identical to
strftime (verified incl. midnight->"12:00:00 am", noon, single-digit day, NaT).
Custom temporal_format / non-UTC tz keep the strftime path (correct, slower).
Result (per-keystroke @ 1M): datetime 3111ms -> 1512ms (2.05x), float 674ms ->
557ms. All searchAny conformance tests still pass on dgx (all engines).
Note: the render is TERM-INDEPENDENT, so the real interactive win is a base-graph
search-text index (render once, slice to the filtered view) — quantified
separately (blob index: 2903ms -> 68ms/keystroke @1m). Tracked for follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5vkD2ZCyv3bmecBYoYYQy
Perf: interactive latency + the base-graph index questionBenchmarked the "typing into a search window" scenario on dgx-spark (pandas, 100K/1M rows, 8 mixed-typed columns), since each keystroke dispatches a fresh query but the columns+data are fixed. Render optimizations landed (1ce40e6)The per-keystroke render was the hot spot:
Per-keystroke @ 1M: datetime 3111ms → 1512ms (2.05×), float 674ms → 557ms. The index question (base vs view) — quantifiedThe render is term-independent, so it should be computed once and reused. Measured @ 1M / 8 cols:
Recommendation: a base-graph search-text index — a single concatenated, lowercased WYSIWYG search-text column built once per (data, format-options), sliced to the active view, so each keystroke is one (The correctness feature in this PR is unchanged and CI-green; the render opts are pure speedups with byte-identical output.) |
What
Makes GFQL
searchAny/search_nodes/search_edgesmatch what the streamgl-viz inspector RENDERS for float and datetime columns — you search the displayed string, not the raw value (#1695). Adds WYSIWYG format call-params{floatPrecision, temporalFormat, tz}.Floats were previously excluded from search because
astype(str)diverges across engines (exponent regime + half-tie double-rounding). This makes the render byte-identical to the inspector and, per the parity-or-honest-NIE convention, keeps every engine either exact or an honest decline.The decisive experiment (dgx cross-engine parity fuzzer)
Ran a float-render parity fuzzer on dgx-spark (all engines) with the real viz
sprintf-jsas the oracle. It settled the render and falsified the original "cross-engine decimal cast = parity" plan:"%.4f" % vdirectsprintf-js(3.28045→"3.2805",0.12345→"0.1235")np.roundprepass (old draft)-3.74825→"-3.7482")-0.0000There is no native GPU printf, and a per-cell UDF would be a forbidden host-bridge. Datetime render verified byte-identical to real
moment.utcfor the UTC path.Design
"%.4f" % vdirect for fractional (single correctly-rounded conversion),str(int(v))for whole. pandas-exact; cuDF/polars honest-NIE.inf→"Infinity"(matches JS).'MMM D YYYY, h:mm:ss a z'via strftime, localized to a callertz(per-timestamp DST-correct). pandas-only; cuDF/polars honest-NIE.{floatPrecision, temporalFormat, tz}: threaded through the cypher option-map (strict typed parse), the ast op, call-validation, and the python twins; defaults to the inspector (4 decimals; UTC).searchAny('...')behaves identically cross-engine; float/datetime are reached via explicitcolumns=. (@leochose this over the "GPU raises" alternative.)Datetime default
tz='UTC'(chosen for determinism — a server-local default varies by deployment and would break the parity oracle / tests). You suggested "default to server time" — trivially switchable if you'd prefer that; thetz=call-param already delivers the localization knob either way.Adversarial review (
.agents/skills/review)Multi-angle finders + verify. Three confirmed findings fixed: (1)
inf/-infrender + RuntimeWarning; (2) am/pm lowercasing corrupting alpha tz abbrevs (AMT→amT) — now word-bounded; (3) negative/boolfloatPrecision+ emptytzsilently misrendering via the twins (which bypass the safelist) — added kernel-levelvalidate_format_opts+ tightened the safelist. Cross-engine-safety and parity-invariant hypotheses were refuted (clean).Testing
All on dgx-spark (
test-rapids-official:26.02-gfql, pandas + cuDF + polars + polars-gpu):test_search_any_float_wysiwyg,test_search_any_datetime_wysiwyg,test_search_any_format_options— pandas exact + cuDF/polars honest-NIE, half-tie/inf/tz/validation pinned.🤖 Generated with Claude Code