feat(search): „+ weitere Ergebnisse"-Button für lokale und Web-Suche
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m20s

Die Ergebnislisten waren oft kurz, weil lokale Suche auf LIMIT 30 und
die Web-Suche auf die erste SearXNG-Seite beschränkt war. Jetzt lässt
sich beides nachladen.

- `searchLocal` nimmt jetzt einen `offset` und der `/api/recipes/search`-
  Endpoint einen `?offset=`-Parameter.
- `searchWeb` nimmt jetzt eine `pageno`-Option und reicht sie als
  `pageno`-Parameter an SearXNG weiter. `pageno=1` wird weggelassen,
  damit bestehendes Verhalten unverändert bleibt.
- `/search` und `/search/web` zeigen unterhalb der Liste einen
  „+ weitere Ergebnisse"-Button. Beide deduplizieren nachgeladene
  Hits (ID bzw. URL), weil SearXNG das gleiche Ergebnis auf zwei
  Seiten liefern kann.

Kein Endless-Scroll: expliziter Button ist mobil robuster und spart
die teure Thumbnail-Enrichment-Roundtrip-Zeit, die bei jeder neuen
Web-Seite anfällt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-17 21:58:47 +02:00
parent b4a7355b24
commit a62b32aa1e
8 changed files with 184 additions and 8 deletions

View File

@@ -29,7 +29,8 @@ function buildFtsQuery(q: string): string | null {
export function searchLocal(
db: Database.Database,
query: string,
limit = 30
limit = 30,
offset = 0
): SearchHit[] {
const fts = buildFtsQuery(query);
if (!fts) return [];
@@ -48,9 +49,9 @@ export function searchLocal(
JOIN recipe_fts f ON f.rowid = r.id
WHERE recipe_fts MATCH ?
ORDER BY bm25(recipe_fts, 10.0, 0.5, 2.0, 5.0)
LIMIT ?`
LIMIT ? OFFSET ?`
)
.all(fts, limit) as SearchHit[];
.all(fts, limit, offset) as SearchHit[];
}
export function listRecentRecipes(

View File

@@ -251,7 +251,12 @@ async function enrichAllThumbnails(
export async function searchWeb(
db: Database.Database,
query: string,
opts: { searxngUrl?: string; limit?: number; enrichThumbnails?: boolean } = {}
opts: {
searxngUrl?: string;
limit?: number;
enrichThumbnails?: boolean;
pageno?: number;
} = {}
): Promise<WebHit[]> {
const trimmed = query.trim();
if (!trimmed) return [];
@@ -260,12 +265,14 @@ export async function searchWeb(
const searxngUrl = opts.searxngUrl ?? process.env.SEARXNG_URL ?? 'http://localhost:8888';
const limit = opts.limit ?? 20;
const pageno = Math.max(1, opts.pageno ?? 1);
const siteFilter = domains.map((d) => `site:${d}`).join(' OR ');
const q = `${trimmed} (${siteFilter})`;
const endpoint = new URL('/search', searxngUrl);
endpoint.searchParams.set('q', q);
endpoint.searchParams.set('format', 'json');
endpoint.searchParams.set('language', 'de');
if (pageno > 1) endpoint.searchParams.set('pageno', String(pageno));
const body = await fetchText(endpoint.toString(), {
timeoutMs: 15_000,