feat(search+docs): trigram fuzzy search, brand/country filters, developer docs
CI / Python (ingestion) (pull_request) Successful in 12s
CI / Migrations (postgres) (pull_request) Successful in 22s
CI / Go (api) (pull_request) Successful in 47s

Search:
- migration 0009: trigram GIN index on brand.name + btree on country_of_origin
- SearchProducts: typo-tolerant word_similarity matching (>=0.42) on top of
  ILIKE substring + barcode; new brand/country filters; rank by
  similarity * (0.5 + quality_score). Response gains country_of_origin,
  quality_score and per-result relevance score.
- public search UI: brand/country filter inputs; show country in results

Docs:
- serve embedded OpenAPI 3 spec at GET /api/v1/openapi.json (not rate limited)
- ApiDocs page: auth + rate-limit section, updated search params/response
- docs/api.md developer guide

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
novaalphastrikeomegaz663
2026-06-20 09:38:27 +00:00
parent cf255e2380
commit 69a0149bbe
11 changed files with 613 additions and 48 deletions
+26 -2
View File
@@ -13,6 +13,8 @@ export default function Home({
onApi: () => void;
}) {
const [q, setQ] = useState("");
const [brand, setBrand] = useState("");
const [country, setCountry] = useState("");
const [items, setItems] = useState<ProductSummary[]>([]);
const [total, setTotal] = useState(0);
const [searched, setSearched] = useState(false);
@@ -24,7 +26,10 @@ export default function Home({
setLoading(true);
setError("");
try {
const res = await api.search(q.trim(), 1, 30);
const res = await api.search(q.trim(), 1, 30, {
brand: brand.trim() || undefined,
country: country.trim() || undefined,
});
setItems(res.items);
setTotal(res.total);
setSearched(true);
@@ -61,6 +66,20 @@ export default function Home({
{loading ? "检索中…" : "检索"}
</button>
</form>
<div className="mt-3 max-w-2xl mx-auto flex flex-wrap items-center justify-center gap-2 text-sm">
<input
value={brand}
onChange={(e) => setBrand(e.target.value)}
placeholder="按品牌筛选(如 Ferrero"
className="flex-1 min-w-[14rem] bg-white border rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-300"
/>
<input
value={country}
onChange={(e) => setCountry(e.target.value)}
placeholder="按产地筛选(如 China"
className="flex-1 min-w-[14rem] bg-white border rounded-lg px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-300"
/>
</div>
<button
onClick={onApi}
className="mt-4 inline-flex items-center gap-1.5 text-sm text-emerald-700 hover:underline"
@@ -105,7 +124,12 @@ export default function Home({
{p.gtin ? ` · ${p.gtin}` : ""}
</div>
</div>
<span className="text-xs text-gray-400">{p.category_path || ""}</span>
<span className="text-xs text-gray-400 text-right">
<span className="block">{p.category_path || ""}</span>
{p.country_of_origin ? (
<span className="block text-gray-400">{p.country_of_origin}</span>
) : null}
</span>
</button>
</li>
))}