Files
mesh-catalog/modules/searxng/client.ts
T
jschoubben 8f8e0153b1 searxng, icecast, photos: full nox modules (ADR 0044/0046)
searxng: a search tool — tools-only, a stateless search has nothing to observe.
icecast: status tool, emits stream.started/.stopped by diffing live mountpoints.
photos: status/albums/recent tools, emits item.added (immich-shaped API, coded
defensively since the stub ships a placeholder image — flagged in the code).
Typecheck; manifests parse.
2026-09-04 02:46:24 +02:00

77 lines
2.5 KiB
TypeScript

// SearXNG's API client — searxng's own code, living in the module (novox/hq ADR 0044). SearXNG is a
// privacy-respecting metasearch engine: it forwards a query to many upstream engines and returns the
// merged results. Its JSON API (`/search?q=...&format=json`) is what makes a `searxng_search` tool
// useful; the client speaks only that. No credential — the instance is reached inside the mesh.
export interface SearxResult {
title: string;
url: string;
content?: string;
engine?: string;
category?: string;
score?: number;
}
export interface SearxSearch {
query: string;
numberOfResults: number;
results: SearxResult[];
suggestions: string[];
answers: string[];
}
export interface SearxOptions {
categories?: string;
language?: string;
pageno?: number;
}
export class SearxngClient {
readonly baseUrl: string;
constructor(url: string) {
this.baseUrl = url.replace(/\/$/, "");
}
/** Build from the module's environment. No key: SearXNG's search API is open on the mesh, so a URL
* is all it takes — defaulting to the container's own listen port. */
static fromEnv(env: NodeJS.ProcessEnv = process.env): SearxngClient {
const url = env.MESH_SEARXNG_URL ?? `http://127.0.0.1:${env.SEARXNG_PORT ?? "8080"}`;
return new SearxngClient(url);
}
async search(query: string, opts: SearxOptions = {}): Promise<SearxSearch> {
const params = new URLSearchParams({ q: query, format: "json" });
if (opts.categories) params.set("categories", opts.categories);
if (opts.language) params.set("language", opts.language);
if (opts.pageno) params.set("pageno", String(opts.pageno));
const res = await fetch(`${this.baseUrl}/search?${params.toString()}`, {
headers: { Accept: "application/json" },
});
if (!res.ok) throw new Error(`SearXNG search: ${res.status} ${await res.text()}`);
const data = (await res.json()) as {
results?: SearxResult[];
suggestions?: string[];
answers?: string[];
number_of_results?: number;
};
const results = data.results ?? [];
return {
query,
// SearXNG's own count is often 0 even with results; fall back to what we actually got.
numberOfResults: data.number_of_results || results.length,
results: results.map((r) => ({
title: r.title,
url: r.url,
content: r.content,
engine: r.engine,
category: r.category,
score: r.score,
})),
suggestions: data.suggestions ?? [],
answers: data.answers ?? [],
};
}
}