From 175439c195583dfd164acf2f645912c67a2c5c40 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 14 Aug 2026 16:52:44 +0700 Subject: [PATCH] feat(web): search on submit, and show what a search cost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing no longer searches. Every keystroke used to fire a debounced query, and a half-typed word is the expensive case: a name search seeks on the rarest word in the query, and "bu" covers every Bùi, Bửu and Bưu in the dataset. The query now runs when the user presses Tra cứu or Enter, and the clear button is what empties the results. While it runs there is a spinner and "Đang tra cứu…" with the elapsed time. Elapsed time is the only live signal available: the worker reads pages with synchronous XHR, so it cannot answer a getStats call until the query it is running has finished. Afterwards a line under the results reports what the search cost — requests, bytes and seconds, plus the session totals — the same figures the [httpvfs] console lines carry. A search is counted whole rather than per query, since a name search runs two. --- web/src/lib/components/search-form.svelte | 27 ++-------- web/src/routes/[dataset]/+page.svelte | 63 ++++++++++++++++++++++- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/web/src/lib/components/search-form.svelte b/web/src/lib/components/search-form.svelte index 9e97e74..6e09a77 100644 --- a/web/src/lib/components/search-form.svelte +++ b/web/src/lib/components/search-form.svelte @@ -1,8 +1,6 @@ diff --git a/web/src/routes/[dataset]/+page.svelte b/web/src/routes/[dataset]/+page.svelte index 62d8959..6da4b12 100644 --- a/web/src/routes/[dataset]/+page.svelte +++ b/web/src/routes/[dataset]/+page.svelte @@ -9,7 +9,12 @@ import { dbSourceOf } from "$lib/datasets"; import { isExamId } from "$lib/query-mode"; import { MAX_RESULTS, lookupExamId, searchByName } from "$lib/search"; - import { PLAYGROUND_BUDGET_BYTES, RemoteDatabase, SEARCH_BUDGET_BYTES } from "$lib/sqlite.svelte"; + import { + PLAYGROUND_BUDGET_BYTES, + RemoteDatabase, + SEARCH_BUDGET_BYTES, + formatBytes, + } from "$lib/sqlite.svelte"; let { data } = $props(); const dataset = $derived(data.dataset); @@ -17,6 +22,10 @@ let db = $state(null); let results = $state(null); let searchError = $state(null); + let searching = $state(false); + let elapsedMs = $state(0); + /** What the last search cost over the network, for the line under the results. */ + let cost = $state(null); let activeTab = $state("search"); let sqlWarningOpen = $state(false); // Raised once the user has accepted that a hand-written query may fetch a lot. @@ -69,19 +78,48 @@ query = q; writeUrlQuery(q); + // Counted across the whole search rather than per query: a name search runs + // two, one for the word frequencies and one for the rows. + const before = { requests: source.requests, bytes: source.bytesRead }; + const startedAt = performance.now(); + searching = true; + cost = null; + // The worker reads pages with synchronous XHR, so it cannot answer while a + // query runs and there is no request count to show until it finishes. + // Elapsed time is the one honest live signal. + elapsedMs = 0; + const ticking = setInterval(() => (elapsedMs = performance.now() - startedAt), 100); + try { results = isExamId(q) ? await lookupExamId(source, q) : await searchByName(source, q); } catch (err) { searchError = err instanceof Error ? err.message : String(err); + } finally { + clearInterval(ticking); + searching = false; + cost = { + requests: source.requests - before.requests, + bytes: source.bytesRead - before.bytes, + ms: performance.now() - startedAt, + sessionRequests: source.requests, + sessionBytes: source.bytesRead, + }; } } function clearSearch() { results = null; query = ""; + cost = null; + searchError = null; writeUrlQuery(""); } + /** "16,9 giây", or "0,4 giây" — one decimal is enough to compare searches. */ + function seconds(ms) { + return `${(ms / 1000).toLocaleString("vi-VN", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} giây`; + } + function openSqlTab() { if (budget >= PLAYGROUND_BUDGET_BYTES) { activeTab = "sql"; @@ -185,10 +223,21 @@ value={query} onSearch={search} onClear={clearSearch} - disabled={busy} + disabled={busy || searching} examples={dataset.examples} /> + {#if searching} +

+ + Đang tra cứu… {seconds(elapsedMs)} +

+ {/if} + {#if searchError}

Lỗi truy vấn: {searchError}

{/if} @@ -204,6 +253,16 @@ Hiển thị tối đa {MAX_RESULTS} kết quả. Vui lòng tìm kiếm cụ thể hơn.

{/if} + + {#if cost && !searching} +

+ Mạng: {cost.requests.toLocaleString("vi-VN")} yêu cầu · {formatBytes(cost.bytes)} · {seconds( + cost.ms, + )} · Cả phiên: {cost.sessionRequests.toLocaleString("vi-VN")} yêu cầu · {formatBytes( + cost.sessionBytes, + )} +

+ {/if} {:else} {/if}