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}