Skip to content

GFQL: search-text index for interactive searchAny (render is term-independent; blob index = 43x @1M) #1703

Description

@lmeyerov

Summary

searchAny / search_nodes / search_edges (WYSIWYG search, #1700) render each candidate column to its display string, then substring-match. Modeling interactive search (typing into a search box → a new query per keystroke over a fixed base graph / filtered view), the render dominates and is term-independent — so it should be materialized once as a search-text index, not recomputed per keystroke. This belongs with the manual/explicit CREATE INDEX work (#531, #1658), not as an implicit cache inside searchAny (statefulness + invalidation on data/format-option change is a footgun and would silently diverge).

Findings (dgx-spark, pandas, 1M rows, 8 mixed-typed columns)

Per-keystroke latency for a 4-char type-in, auto-searching all eligible columns:

approach per-keystroke vs today
(A) today — render + match every keystroke 2903 ms
(B) per-column render cache — render once, N Contains (OR) 439 ms 6.6×
(C) concatenated search-blob index — render+join once, ONE Contains 68 ms 43×
  • One-time build (term-independent): blob 567 ms; per-column cache 2480 ms.
  • Base vs view: slicing the base blob to a 50% filtered view = 8 ms slice + 36 ms keystroke — the index lives at the base-graph level and slices to the filtered/rendered view; no per-keystroke re-render.
  • Render-vs-match split @1m: datetime render = 8× a single match, float = 2.3×, int ≈ 1×, string ≈ free. (Contains itself is ~125–155 ms/column @1m — the irreducible per-column floor, which the blob collapses to one match.)

Recommendation

A search-text index (new index type under #531 / the CREATE INDEX stack): materialize one concatenated, lowercased WYSIWYG search-text column per (data, format-options), built once, sliced to the active view, so each keystroke is a single Contains. This is the 43× win — 1M-row interactive search goes ~3 s → ~68 ms/keystroke.

Design notes:

  • Key by (columns, float_precision, tz, temporal_format, case_sensitive); invalidate on data or option change (explicit CREATE INDEX sidesteps implicit-staleness).
  • Blob (single joined column) beats a per-column cache (68 ms vs 439 ms) because it's one Contains regardless of column count; keep a per-column variant if column-scoped columns= targeting must stay index-accelerated.
  • WYSIWYG render is pandas-exact / cuDF-polars honest-NIE for float+datetime (see feat(gfql): searchAny WYSIWYG float + datetime search (#1695) #1700), so the search-text index is pandas-only for those dtypes; string/int index cross-engine.
  • Placement is open: GFQL/Plottable-level explicit index vs the streamgl-viz worker that owns the interactive session lifecycle.

Already landed in #1700 (render speedups, byte-identical)

  • Float render: list-comp (the np.char.mod/masked-scatter was slower).
  • Datetime render: vectorized .dt-component assembly for the default+UTC case (pandas dt.strftime is ~20× slower, ~2.2 s/1M); strftime retained for custom format / non-UTC tz.
  • Per-keystroke @1m: datetime 3111 → 1512 ms (2.05×), float 674 → 557 ms.

Repro

Interactive + index-prototype benchmarks over a 100K/1M mixed-column table (string/int/float/datetime/bool), rendering each column via graphistry.compute.gfql.search_any._canonical_float_str / _canonical_datetime_str; blob = Series.str.cat(sep="\x00") of the lowercased renders; view = blob.loc[filtered_index]. Run on dgx image graphistry/test-rapids-official:26.02-gfql.

Refs: #1700 (WYSIWYG searchAny), #531 (GFQL indexing), #1658 / #1697 / #1676 (index stack).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions