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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions