import { useEffect, useState } from "react"; const METHODS = [ { id: "hybrid", label: "Hybrid", hint: "BM25 + BERT fusion (default)", semantic: true }, { id: "bert", label: "BERT", hint: "Dense semantic retrieval", semantic: true }, { id: "bm25", label: "BM25", hint: "Modern lexical ranking" }, { id: "tfidf", label: "TF-IDF", hint: "Classic ranking, for comparison" }, { id: "prf", label: "Relevance Feedback", hint: "Mark results relevant, then refine" }, { id: "wordnet", label: "WordNet", hint: "BM25 + synonym expansion" }, ]; // Only allow http(s) links through to href; guards against javascript:/data: // URLs sneaking in from the dataset (defensive — the dataset is trusted). function safeUrl(url) { return typeof url === "string" && /^https?:\/\//i.test(url) ? url : "#"; } const EXAMPLES = [ "covid vaccine health", "election president", "movie film review", "stock market crash", "climate change", ]; export default function App() { const [query, setQuery] = useState(""); const [method, setMethod] = useState("hybrid"); const [topK, setTopK] = useState(10); const [category, setCategory] = useState(""); const [categories, setCategories] = useState([]); const [health, setHealth] = useState(null); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); // Doc ids the user marked relevant (for true relevance feedback on "prf"). const [relevantIds, setRelevantIds] = useState(() => new Set()); useEffect(() => { fetch("/api/health") .then((r) => r.json()) .then((d) => { setHealth(d); // Fall back to BM25 if BERT embeddings weren't built (e.g. `run.bat lite`). if (!d.bert_available) setMethod("bm25"); }) .catch(() => {}); fetch("/api/categories") .then((r) => r.json()) .then((d) => setCategories(d.categories || [])) .catch(() => {}); }, []); // withFeedback=true reuses the marked-relevant docs (the "Refine" action); // a fresh search clears them. async function runSearch(q = query, withFeedback = false) { if (!q.trim()) return; if (!withFeedback) setRelevantIds(new Set()); setLoading(true); setError(""); try { const params = new URLSearchParams({ q, method, top_k: topK }); if (category) params.set("category", category); if (method === "prf" && withFeedback) { for (const id of relevantIds) params.append("relevant_ids", id); } const res = await fetch(`/api/search?${params}`); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `Request failed (${res.status})`); } setData(await res.json()); } catch (e) { setError(e.message); setData(null); } finally { setLoading(false); } } function toggleRelevant(id) { setRelevantIds((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } const bertDisabled = health && !health.bert_available; const feedbackMode = method === "prf"; return (

News Search Engine

Lexical (BM25/TF-IDF) & semantic (BERT) retrieval with query expansion {health && ( {" "}· {health.documents.toLocaleString()} docs ·{" "} {health.vocabulary.toLocaleString()} terms )}

setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && runSearch()} autoFocus />
{EXAMPLES.map((ex) => ( ))}
{METHODS.map((m) => { const disabled = m.semantic && bertDisabled; return ( ); })}
{feedbackMode && (

Relevance feedback: tick the results that match what you want, then refine the search.

)}
{error &&
⚠ {error}
} {data && (
{data.total_hits} result{data.total_hits !== 1 ? "s" : ""} for{" "} “{data.query}” {data.method} · {data.elapsed_ms} ms
{data.expansion_terms?.length > 0 && (
Expanded with: {data.expansion_terms.map((t) => ( {t} ))}
)} {feedbackMode && data.results.length > 0 && (
{relevantIds.size} marked relevant
)} {data.results.length === 0 && (
No documents matched this query.
)}
    {data.results.map((r) => (
  1. {feedbackMode && ( toggleRelevant(r.id)} /> )} #{r.rank} {r.headline}

    {r.short_description}

    {r.category} {r.date} score {r.score}
  2. ))}
)}
); }