Skip to content

feat(app): make search N-ary, with multi-source as the default shape - #2869

Draft
teeohhem wants to merge 6 commits into
mainfrom
tom/search-n-ary-default
Draft

feat(app): make search N-ary, with multi-source as the default shape#2869
teeohhem wants to merge 6 commits into
mainfrom
tom/search-n-ary-default

Conversation

@teeohhem

@teeohhem teeohhem commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Supersedes #2862 and #2865 (both draft, both contained here as commits). Review this one instead — it delivers the same capability, but multi-source is the default shape of search rather than a mode bolted onto it.

Searching has meant picking exactly one source, so a question spanning sources (several log sources, or logs plus traces) takes multiple tabs and manual correlation. Search is now N-ary: pick up to 3 log/trace sources and results interleave into one timestamp-ordered timeline with per-row source badges, normalized columns, a histogram stacked by source, summed totals, merged filter facets, and an add-column picker over the union of the sources' columns.

The important structural point is that one source is not a separate code path — it is N=1. The results table takes a list of sources and, with one, carries that source's own SELECT and ORDER BY, so it renders the authored columns (no badge column), sorts, denoises, and reports errors to the page exactly as it always has. With more than one, each source projects canonical aliases (__hdx_timestamp, __hdx_severity, ...) derived from its semantic expressions and a client-side k-way merge orders rows behind a "safe frontier" — the timestamp every source has covered — so the timeline never shows a gap a slower source could still fill, and "fetch more" advances only the lagging source. There is no UNION ALL: sources can live on different ClickHouse connections, each keeps its per-table machinery (Lucene serializer, text-index detection, materialized-column rewrites, query settings), and a failing source degrades to a status chip instead of failing the search.

Filters are per-source: checking a value filters every source that has the field, and a source whose table lacks a filtered column is excluded with a visible reason rather than silently returning unfiltered rows. Multi mode is Lucene-only for now (raw SQL names one table's columns); saved searches and alerts remain single-source. Selections are shareable via ?sources=.

Design notes for reviewers

  • SearchResultsTable (renamed from the multi-source-only component) is the search page's only results table now. DBSqlRowTable stays for dashboards, k8s pages, and side panels — only search's use of it is gone.
  • The denoise pipeline and select-column resolution moved out of DBRowTable into shared exports so both callers use one implementation.
  • Dynamic hook counts are handled by useMultiSourceSlots, which unrolls a fixed MAX_SEARCH_SOURCES hook slots in exactly one documented place.
  • Two parity details worth checking: row queries keep their own dbSqlRowTable query-key prefix (so "is the search fetching?" measures what it always did), and the empty-SELECT guard is preserved.

Verification

Single-source parity is the bar this has to clear, so: the full search E2E folder passes (37 passed, 8 flaky, 0 failed), and single-source was driven manually against the demo stack for columns, sorting, denoise, side panel, and live tail. One test (Should pin filter and verify it persists after reload) fails on this branch's first attempt — it fails identically on origin/main and on the superseded branches, so it is pre-existing, not a regression; it is a null-guard bug in usePersonalPinnedFilters worth a separate fix.

How to test on Vercel preview

Preview routes: /search

Steps:

  1. Open /search and confirm results load with the source's own columns and no "Source" column.
  2. Click a column header to sort, and confirm the results re-order.
  3. Click the "+" button next to the source selector (data-testid "add-search-source").
  4. Click the sources multi-select (data-testid "source-multi-selector"), select "Demo Traces", then press Escape.
  5. Verify the table now shows a "Source" column with both sources' rows interleaved in one timestamp-descending list, and the histogram legend lists both sources.

The search page's source selector can expand into a multi-select (up to
5 log/trace sources). Each selected source runs its own query pipeline
(own connection, Lucene serializer, windowed pagination) with its SELECT
rewritten to canonical aliases; a client-side k-way merge interleaves
the streams by timestamp behind a "safe frontier" so the timeline never
shows a gap a lagging source could still fill. Results render in one
table with per-row source badges, a histogram stacked by source, summed
totals, and an add-column picker over the union of the sources' columns.

Multi mode is Lucene-only and URL-shareable (?sources=). Saved searches,
alerts, filters, and delta/pattern modes stay single-source and are
gated off with explanations; single-source behavior is unchanged.

Also fixes chSqlToAliasMap dropping NULL-literal projections, which
would have made row-WHERE clauses reference nonexistent columns.
Three components were each hand-unrolling slot0..slot4 hook calls to keep
a constant hook count across a variable source selection. Rules of hooks
allow calling a use*-named function parameter from a custom hook, so one
generic useMultiSourceSlots(items, useSlot, opts) now owns the unrolling
and the MAX_SEARCH_SOURCES pin; consumers are a single call. Slot hooks
return memoized values so the returned array is dependency-list safe.
Query cost scales linearly with the selection (~3 ClickHouse queries per
source per refresh, re-fired every live-tail tick, plus one-time
metadata), and past 3 sources the interleaved timeline stops being
legible. 3 covers the common "app logs + infra logs + traces" case.
The filters sidebar now works across a multi-source selection. Each
selected source runs its own facet pipeline (fields + values); the
sidebar merges them by field path with values unioned, reusing the
single-source FilterGroup rendering. Checking a value filters every
source that has the field.

A source whose table lacks a filtered column is excluded from the
search with a visible reason on its status chip, rather than silently
returning rows that ignore the filter. Filter pills and add-to-filter
from the row side panel are re-enabled in multi mode, with filter-key
escaping and date-column detection driven by the union of the selected
sources' schemas.
The search page had two parallel result paths: DBSqlRowTable for a single
source and a separate table for several. This collapses them. The N-ary
pipeline now handles a single source as the degenerate case — its spec
carries the user's own SELECT and ORDER BY, so the table renders the
authored columns (no source badge), sorts, denoises, and reports errors
to the page exactly as before. Multiple sources still project canonical
aliases and merge on the client.

Renames the component to SearchResultsTable, since it is now the search
page's only results table, and extracts the denoise pipeline and the
select-column resolution out of DBRowTable so both callers share one
implementation. DBSqlRowTable stays for dashboards, k8s pages, and side
panels; only the search page's use of it is gone.
@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7e4c1d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/app Minor
@hyperdx/common-utils Minor
@hyperdx/api Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 12, 2026 1:44pm
hyperdx-storybook Ready Ready Preview Aug 12, 2026 1:44pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Search is generalized to query up to three log or trace sources independently and merge their results into a shared timeline.

  • Adds URL-backed multi-source selection and per-source query planning.
  • Adds merged rows, source badges, stacked histograms, aggregate totals, filters, and union-based column selection.
  • Refactors single-source table helpers so the search page uses the same N-ary results component for one or multiple sources.

Confidence Score: 3/5

The PR is not yet safe to merge because a failed schema request can suppress every selected source and generated row-side-panel links discard the multi-source selection.

A selected source whose schema request fails remains absent from columnsBySourceId, causing the active-filter or extra-column gates to return no query specifications for healthy sources; separately, generateSearchUrl writes only the primary source even though multi-source restoration depends on the sources parameter.

Files Needing Attention: packages/app/src/DBSearchPage.tsx, packages/app/src/hooks/useMultiSourceSearch.ts

Important Files Changed

Filename Overview
packages/app/src/DBSearchPage.tsx Introduces multi-source selection and query orchestration, but schema failures can still suppress all streams and generated side-panel links still omit the selected source list.
packages/app/src/components/SearchResultsTable.tsx Provides the unified single- and multi-source results table with per-source pagination, status reporting, and merged rendering.
packages/app/src/hooks/useMultiSourceSearch.ts Adds fixed hook slots and schema-union utilities, but schema query errors are discarded and therefore cannot be distinguished from loading.
packages/app/src/utils/multiSourceMerge.ts Implements timestamp ordering, safe-frontier advancement, source tagging, and multi-stream merge behavior.
packages/common-utils/src/core/searchChartConfig.ts Adds canonical per-source search projections while retaining each source’s ClickHouse query configuration.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant P as Search page
  participant S as Selected sources
  participant Q as Per-source queries
  participant M as Safe-frontier merge
  participant V as Timeline and charts
  U->>P: Select 1–3 sources and search
  P->>S: Resolve source configurations
  loop Each selected source
    S->>Q: Build source-specific query
    Q-->>M: Return rows and progress frontier
  end
  M->>M: Interleave rows by timestamp
  M-->>V: Emit rows safe to display
  Q-->>V: Emit per-source histogram and count data
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into tom/search-n-ar..." | Re-trigger Greptile

Comment thread packages/app/src/DBSearchPage.tsx
Comment thread packages/app/src/components/DBRowTable.tsx
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 280 passed • 1 skipped • 1231s

Status Count
✅ Passed 280
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant