From ddd72155a3ff768e023254004a339ebc8f400bfd Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Sat, 11 Jul 2026 13:31:25 -0700 Subject: [PATCH 1/2] #171: defer the lite join out of the substrate filter-build critical path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark 2026-07-11 caught it (Codex-verified mechanism): the substrate builder joined EVERY matched pid to the remote lite parquet for display fields only the 50 panel rows need — making big-result queries (bare 'pottery', 42,541 matches: 57.4s cold) SLOWER than the baseline they replace. The interim path never pays this because it reads display fields from the same file it already scanned. search_pids from the substrate now carries pid + relevance_score with NULL display columns (schema unchanged); every consumer joins lite and reads COALESCE(s.*, l.*), so interim rows keep their own values: - area panel: already join→filter→LIMIT; display fields COALESCEd - world panel: restructured to join lite BEFORE filter/LIMIT (the source filter must see l.source when s.source is NULL); Codex's filters-before-LIMIT condition holds in both shapes - world follow-up COUNT: gains the lite LEFT JOIN for the same reason - sourceSQL snapshot: sourceFilterSQL("COALESCE(s.source, l.source)") facetFilterSQL is pid-IN-based (verified) — unaffected by NULL columns. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AjnkWb4HpuLeDbYfzmY3X9 --- explorer.qmd | 59 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/explorer.qmd b/explorer.qmd index 21acedc..92bffb3 100644 --- a/explorer.qmd +++ b/explorer.qmd @@ -5836,11 +5836,11 @@ zoomWatcher = { WHERE token IN (${tokList}) GROUP BY pid ) - SELECT a.pid, l.label, l.source, - CAST(l.place_name AS VARCHAR) AS place_name, + SELECT a.pid, + NULL::VARCHAR AS label, NULL::VARCHAR AS source, + NULL::VARCHAR AS place_name, a.relevance_score FROM agg a - LEFT JOIN read_parquet('${lite_url}') l USING (pid) WHERE a.m = ${plan.tokens.length} `); } else { @@ -5873,11 +5873,11 @@ zoomWatcher = { count(DISTINCT token) AS m FROM contrib GROUP BY pid ) - SELECT a.pid, l.label, l.source, - CAST(l.place_name AS VARCHAR) AS place_name, + SELECT a.pid, + NULL::VARCHAR AS label, NULL::VARCHAR AS source, + NULL::VARCHAR AS place_name, a.relevance_score FROM agg a - LEFT JOIN read_parquet('${lite_url}') l USING (pid) WHERE a.m = ${plan.tokens.length} `); } @@ -6112,7 +6112,15 @@ zoomWatcher = { // of PR #236 round 1). The string is captured here, before any // `db.query` await, and re-used by both the SELECT and the // COUNT below. - const sourceSQL = sourceFilterSQL('s.source'); + // #171 perf fix: the substrate builder no longer joins lite at + // filter-build time (the 42k-row remote join made big-result + // queries SLOWER than baseline — benchmark 2026-07-11, Codex- + // verified). search_pids now carries NULL display columns for + // substrate searches, so every consumer below joins lite and + // filters on the COALESCEd value (interim rows still carry + // their own values; NULL-vs-value never changes interim + // semantics because COALESCE prefers s.*). + const sourceSQL = sourceFilterSQL("COALESCE(s.source, l.source)"); const facetSQL = facetFilterSQL(); // Telemetry-equivalent of the SQL snapshots above (`hadSourceFilter` // / `hadFacetFilter`) is declared OUTSIDE this try block so it @@ -6157,15 +6165,19 @@ zoomWatcher = { results = await runWorldQuery(); } else { results = await db.query(` - SELECT s.pid, s.label, s.source, l.latitude, l.longitude, - s.place_name, s.relevance_score + SELECT s.pid, + COALESCE(s.label, l.label) AS label, + COALESCE(s.source, l.source) AS source, + l.latitude, l.longitude, + COALESCE(s.place_name, CAST(l.place_name AS VARCHAR)) AS place_name, + s.relevance_score FROM search_pids s INNER JOIN read_parquet('${lite_url}') l USING (pid) WHERE 1=1 ${bboxSQL} ${sourceSQL} ${facetSQL} - ORDER BY s.relevance_score DESC, s.label + ORDER BY s.relevance_score DESC, COALESCE(s.label, l.label) LIMIT 50 `); effectiveQueryShape = 'area'; @@ -6177,21 +6189,19 @@ zoomWatcher = { async function runWorldQuery() { return db.query(` - WITH matches AS ( - SELECT s.pid, s.label, s.source, s.place_name, - s.relevance_score - FROM search_pids s - WHERE 1=1 - ${sourceSQL} - ${facetSQL} - ORDER BY s.relevance_score DESC - LIMIT 50 - ) - SELECT m.pid, m.label, m.source, l.latitude, l.longitude, - m.place_name, m.relevance_score - FROM matches m + SELECT s.pid, + COALESCE(s.label, l.label) AS label, + COALESCE(s.source, l.source) AS source, + l.latitude, l.longitude, + COALESCE(s.place_name, CAST(l.place_name AS VARCHAR)) AS place_name, + s.relevance_score + FROM search_pids s LEFT JOIN read_parquet('${lite_url}') l USING (pid) - ORDER BY m.relevance_score DESC, m.label + WHERE 1=1 + ${sourceSQL} + ${facetSQL} + ORDER BY s.relevance_score DESC, COALESCE(s.label, l.label) + LIMIT 50 `); } @@ -6379,6 +6389,7 @@ zoomWatcher = { : ` SELECT COUNT(*) AS n FROM search_pids s + LEFT JOIN read_parquet('${lite_url}') l USING (pid) WHERE 1=1 ${sourceSQL} ${facetSQL} From 5f147387fec2727ad4fa21e8141339e9770d55c2 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Sat, 11 Jul 2026 13:43:30 -0700 Subject: [PATCH 2/2] #171: keep the cheap 50-row world-panel shape except substrate+source-filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round-2 blocker on the deferral: the join-everything restructure taxed the INTERIM path with a full-match-set lite join it never had (bare 'bone' interim = 543k-row join) and changed tie selection. The expensive join-before-limit shape is now used ONLY when it's actually required — a substrate search (NULL s.source) under an ACTIVE source filter. Interim searches and unfiltered substrate searches keep the original CTE: filter + LIMIT 50 on search_pids, then join lite for just those 50 (COALESCE handles substrate NULL display fields). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AjnkWb4HpuLeDbYfzmY3X9 --- explorer.qmd | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/explorer.qmd b/explorer.qmd index 92bffb3..8f233f3 100644 --- a/explorer.qmd +++ b/explorer.qmd @@ -6189,6 +6189,16 @@ zoomWatcher = { async function runWorldQuery() { return db.query(` + ${ + // Codex review (lite-join deferral, round 2): the + // join-everything-then-filter shape is ONLY needed + // when substrate rows (NULL s.source) must satisfy an + // ACTIVE source filter that reads l.source. In every + // other case — the interim producer, or no source + // filter — keep the original cheap shape: filter + + // LIMIT 50 first, join lite for just those 50. + (window.__searchFilter?.substrate && sourceSQL !== '') + ? ` SELECT s.pid, COALESCE(s.label, l.label) AS label, COALESCE(s.source, l.source) AS source, @@ -6201,7 +6211,28 @@ zoomWatcher = { ${sourceSQL} ${facetSQL} ORDER BY s.relevance_score DESC, COALESCE(s.label, l.label) - LIMIT 50 + LIMIT 50` + : ` + WITH matches AS ( + SELECT s.pid, s.label, s.source, s.place_name, + s.relevance_score + FROM search_pids s + WHERE 1=1 + ${facetSQL} + ${window.__searchFilter?.substrate ? '' : sourceSQL.replaceAll('COALESCE(s.source, l.source)', 's.source')} + ORDER BY s.relevance_score DESC + LIMIT 50 + ) + SELECT m.pid, + COALESCE(m.label, l.label) AS label, + COALESCE(m.source, l.source) AS source, + l.latitude, l.longitude, + COALESCE(m.place_name, CAST(l.place_name AS VARCHAR)) AS place_name, + m.relevance_score + FROM matches m + LEFT JOIN read_parquet('${lite_url}') l USING (pid) + ORDER BY m.relevance_score DESC, COALESCE(m.label, l.label)` + } `); }