Skill: gangtao/searxng-web-search (version 1.0.0)

--- name: searxng-web-search description: > Search the web using a self-hosted SearXNG metasearch engine. Use when the user asks to search the web, find information online, look up recent news, research a topic, or needs current data from the internet. Also use when the agent needs to gather external context to answer a question. Requires a running SearXNG instance with JSON API enabled. license: Apache-2.0 compatibility: > Requires Python 3.9+, requests library, and a running SearXNG instance with format: [html, json] enabled in settings.yml. Network access to the SearXNG endpoint. metadata: author: timeplus-io version: "1.0.0" tags: search web searxng metasearch privacy category: web --- # SearXNG Web Search Privacy-respecting web search skill powered by SearXNG, a free metasearch engine that aggregates results from 243+ search services without tracking users. Rewritten from PulseBot's built-in web search skill to use SearXNG as the backend, packaged as a standalone agentskills.io skill. ## Prerequisites 1. A running SearXNG instance (self-hosted or accessible endpoint) 2. JSON format must be enabled in your SearXNG `settings.yml`: ```yaml search: formats: - html - json ``` 3. Python `requests` library installed ## Configuration The skill uses environment variables for configuration: | Variable | Default | Description | |---|---|---| | `SEARXNG_BASE_URL` | `http://localhost:8080` | SearXNG instance URL | | `SEARXNG_MAX_RESULTS` | `10` | Maximum results to return | | `SEARXNG_LANGUAGE` | `all` | Default search language (e.g. `en`, `zh`, `all`) | | `SEARXNG_SAFESEARCH` | `0` | Safe search level: 0=off, 1=moderate, 2=strict | | `SEARXNG_TIMEOUT` | `15` | Request timeout in seconds | | `SEARXNG_CATEGORIES` | `general` | Default categories (comma-separated) | ## Usage Run the search script: ```bash python scripts/searxng_search.py "your search query" ``` With options: ```bash python scripts/searxng_search.py "latest AI news" \ --categories news \ --language en \ --time-range day \ --max-results 5 ``` ### Output Format The script outputs JSON to stdout with the following structure: ```json { "query": "search query", "results": [ { "title": "Result Title", "url": "https://example.com", "snippet": "Text snippet from the page...", "engines": ["google", "bing"], "score": 9.0, "category": "general", "published_date": "2025-01-01T00:00:00" } ], "suggestions": ["related query 1", "related query 2"], "answers": ["direct answer if available"], "total_results": 10, "error": null } ``` If an error occurs, `results` will be empty and `error` will contain the message. ### As a Python Module You can also import and use the search function directly: ```python from scripts.searxng_search import SearXNGSearchTool tool = SearXNGSearchTool(base_url="http://localhost:8080") results = tool.search("quantum computing", categories="science,it", max_results=5) for r in results["results"]: print(f"[{r['title']}]({r['url']})") print(f" {r['snippet']}") ``` ## Integration with PulseBot To register this skill in PulseBot, place it under `skills/` and PulseBot will discover it via SKILL.md frontmatter. The Python script can also be called as a tool function by wrapping it: ```python from skills.searxng_web_search.scripts.searxng_search import SearXNGSearchTool tool = SearXNGSearchTool() def web_search(query: str, categories: str = "general", max_results: int = 10) -> str: """Search the web using SearXNG. Returns JSON results.""" result = tool.search(query, categories=categories, max_results=max_results) return json.dumps(result, indent=2) ``` ## Edge Cases - If SearXNG is unreachable, the script returns a structured error with `error` field set - If no results are found, `results` is an empty list (not an error) - Some engines may be unresponsive; check `unresponsive_engines` in verbose mode - Rate-limited public instances may return 429; prefer self-hosted instances - Queries with special characters are URL-encoded automatically ## SearXNG Setup (Quick Start) For details on deploying SearXNG, see [references/REFERENCE.md](references/REFERENCE.md). ```bash docker run -d --name searxng -p 8080:8080 \ -v "$(pwd)/searxng:/etc/searxng" \ searxng/searxng:latest ``` Then edit `/etc/searxng/settings.yml` to add `json` to `search.formats`.

path: README.md
content: # searxng-web-search

[![Agent Skills](https://img.shields.io/badge/Agent%20Skills-compatible-blue)](https://agentskills.io)
[![License](https://img.shields.io/badge/License-Apache%202.0-green)](LICENSE)
[![ClawHub](https://img.shields.io/badge/ClawHub-published-orange)](https://clawhub.ai)

A privacy-respecting web search skill for AI agents, powered by [SearXNG](https://docs.searxng.org/) metasearch engine. Follows the [agentskills.io](https://agentskills.io) standard.

Rewritten from [PulseBot](https://github.com/timeplus-io/PulseBot)'s built-in `web_search` skill to use SearXNG as a self-hosted, privacy-first search backend.

## Features

- **Privacy-first**: SearXNG aggregates 243+ search engines without tracking users
- **Self-hosted**: Full control over your search infrastructure
- **Multi-category**: Search general web, news, images, IT, science, and more
- **Structured output**: JSON results with title, URL, snippet, score, and engine attribution
- **Agent-ready**: Works with PulseBot, Claude Code, OpenClaw, GitHub Copilot, and any agentskills.io-compatible agent
- **Robust error handling**: Graceful failures with structured error responses

## Quick Start

### 1. Install the Skill

**Via ClawHub CLI:**

```bash
npm i -g clawhub
clawhub install searxng-web-search
```

**Via Git:**

```bash
git clone https://github.com/timeplus-io/searxng-web-search.git
cp -r searxng-web-search ~/.openclaw/workspace/skills/
# or for Claude Code:
cp -r searxng-web-search .claude/skills/
```

### 2. Start SearXNG

```bash
docker run -d --name searxng -p 8080:8080 \
  -v "$(pwd)/searxng:/etc/searxng" searxng/searxng:latest
```

> **Important**: Enable JSON API in your SearXNG `settings.yml` — see [references/REFERENCE.md](references/REFERENCE.md).

### 3. Install Python Dependency

```bash
pip install requests
```

### 4. Search

```bash
# CLI
python scripts/searxng_search.py "latest streaming SQL developments"

# Human-readable output
python scripts/searxng_search.py "SearXNG setup guide" --text

# News search, last 24 hours
python scripts/searxng_search.py "AI agents" --categories news --time-range day

# Health check
python scripts/searxng_search.py --health
```

## Directory Structure

```
searxng-web-search/
├── SKILL.md                    # Skill metadata + instructions (agentskills.io)
├── README.md                   # This file
├── LICENSE                     # Apache 2.0
├── scripts/
│   └── searxng_search.py       # Main search tool (CLI + importable module)
├── references/
│   └── REFERENCE.md            # SearXNG API reference & setup guide
└── assets/
    └── settings.example.yml    # Example SearXNG configuration
```

## Configuration

| Environment Variable | Default | Description |
|---|---|---|
| `SEARXNG_BASE_URL` | `http://localhost:8080` | SearXNG instance URL |
| `SEARXNG_MAX_RESULTS` | `10` | Max results per query |
| `SEARXNG_LANGUAGE` | `all` | Default language |
| `SEARXNG_SAFESEARCH` | `0` | Safe search (0/1/2) |
| `SEARXNG_TIMEOUT` | `15` | Request timeout (seconds) |
| `SEARXNG_CATEGORIES` | `general` | Default categories |

## Compatibility

This skill follows the [agentskills.io specification](https://agentskills.io/specification) and works with:

- [PulseBot](https://github.com/timeplus-io/PulseBot) — Stream-native AI agent framework
- [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code) — Anthropic's coding agent
- [OpenClaw / ClawHub](https://clawhub.ai) — Open-source AI agent platform
- [GitHub Copilot](https://code.visualstudio.com/docs/copilot/customization/agent-skills) — VS Code agent skills
- [OpenAI Codex](https://developers.openai.com/codex/skills/) — OpenAI's coding agent
- Any agent that supports the agentskills.io SKILL.md standard

## Publishing to ClawHub

```bash
npm i -g clawhub
clawhub login
clawhub publish ./searxng-web-search \
  --slug searxng-web-search \
  --name "SearXNG Web Search" \
  --version 1.0.0 \
  --changelog "Initial release: SearXNG-powered web search skill" \
  --tags "search,web,searxng,privacy,metasearch"
```

## License

Apache 2.0 — see [LICENSE](LICENSE).

## Credits

- Originally based on [PulseBot](https://github.com/timeplus-io/PulseBot) by [Timeplus](https://www.timeplus.com)
- Powered by [SearXNG](https://github.com/searxng/searxng)
- Follows the [Agent Skills](https://agentskills.io) open standard


path: assets/settings.example.yml
content: # SearXNG Settings for PulseBot / Agent Skill Usage
# Copy this file to your SearXNG config directory:
#   cp settings.example.yml /etc/searxng/settings.yml
#
# IMPORTANT: The "json" format MUST be listed under search.formats
# for the API to work. Without it, requests with format=json return 403.

use_default_settings: true

server:
  # Generate a unique secret key for your instance:
  #   openssl rand -hex 32
  secret_key: "CHANGE_ME_TO_A_RANDOM_STRING"

  # For private/agent use, bind to localhost and reverse-proxy
  bind_address: "0.0.0.0"
  port: 8080

  # Disable rate limiter for private instances used by agents.
  # For public instances, set to true and configure limiter.toml.
  limiter: false

  # Proxy images through SearXNG (recommended for privacy)
  image_proxy: true

search:
  # Safe search: 0=off, 1=moderate, 2=strict
  safe_search: 0

  # Default language for searches
  default_lang: "all"

  # CRITICAL: Enable JSON format for API access
  formats:
    - html
    - json

  # Default search engines (optional, uncomment to customize)
  # default_engines:
  #   - google
  #   - bing
  #   - duckduckgo
  #   - brave

  # Autocomplete provider (optional)
  # autocomplete: "google"

# Optional: Configure specific engines
# engines:
#   - name: google
#     engine: google
#     shortcut: g
#     disabled: false
#
#   - name: bing
#     engine: bing
#     shortcut: b
#     disabled: false
#
#   - name: duckduckgo
#     engine: duckduckgo
#     shortcut: ddg
#     disabled: false

# Optional: UI settings (only relevant for browser access)
# ui:
#   default_theme: simple
#   default_locale: ""
#   results_on_new_tab: false


path: scripts/searxng_search.py
content: #!/usr/bin/env python3
"""
SearXNG Web Search Tool

A privacy-respecting web search tool powered by SearXNG metasearch engine.
Rewritten from PulseBot's built-in web_search skill for the agentskills.io standard.

Usage:
    python searxng_search.py "search query" [options]

Environment Variables:
    SEARXNG_BASE_URL    - SearXNG instance URL (default: http://localhost:8080)
    SEARXNG_MAX_RESULTS - Maximum results to return (default: 10)
    SEARXNG_LANGUAGE    - Default language (default: all)
    SEARXNG_SAFESEARCH  - Safe search level 0/1/2 (default: 0)
    SEARXNG_TIMEOUT     - Request timeout in seconds (default: 15)
    SEARXNG_CATEGORIES  - Default categories (default: general)

Copyright 2024-2026 Timeplus, Inc. Apache-2.0 License.
"""

import argparse
import json
import logging
import os
import sys
from datetime import datetime
from typing import Any, Optional
from urllib.parse import urljoin

try:
    import requests
except ImportError:
    print(
        json.dumps(
            {
                "query": "",
                "results": [],
                "suggestions": [],
                "answers": [],
                "total_results": 0,
                "error": "Missing dependency: pip install requests",
            }
        )
    )
    sys.exit(1)

logger = logging.getLogger(__name__)


class SearXNGSearchTool:
    """
    SearXNG-based web search tool.

    Provides structured web search results from a self-hosted SearXNG
    metasearch engine instance. SearXNG aggregates results from 243+
    search services (Google, Bing, DuckDuckGo, Brave, etc.) while
    preserving user privacy — no tracking, no profiling.

    This class is designed to be used both as a standalone CLI tool
    and as an importable module for agent frameworks like PulseBot.
    """

    def __init__(
        self,
        base_url: Optional[str] = None,
        max_results: int = 0,
        language: Optional[str] = None,
        safesearch: Optional[int] = None,
        timeout: Optional[int] = None,
        categories: Optional[str] = None,
    ):
        self.base_url = (
            base_url
            or os.environ.get("SEARXNG_BASE_URL", "http://localhost:8080")
        ).rstrip("/")
        self.max_results = max_results or int(
            os.environ.get("SEARXNG_MAX_RESULTS", "10")
        )
        self.language = language or os.environ.get("SEARXNG_LANGUAGE", "all")
        self.safesearch = (
            safesearch
            if safesearch is not None
            else int(os.environ.get("SEARXNG_SAFESEARCH", "0"))
        )
        self.timeout = timeout or int(os.environ.get("SEARXNG_TIMEOUT", "15"))
        self.categories = categories or os.environ.get(
            "SEARXNG_CATEGORIES", "general"
        )
        self._session = requests.Session()
        self._session.headers.update(
            {
                "Accept": "application/json",
                "User-Agent": "PulseBot-SearXNG-Skill/1.0",
            }
        )

    def search(
        self,
        query: str,
        categories: Optional[str] = None,
        engines: Optional[str] = None,
        language: Optional[str] = None,
        pageno: int = 1,
        time_range: Optional[str] = None,
        safesearch: Optional[int] = None,
        max_results: Optional[int] = None,
    ) -> dict[str, Any]:
        """
        Perform a web search via SearXNG.

        Args:
            query: Search query string.
            categories: Comma-separated categories
                        (general, images, videos, news, map, music, it, science, files, social media).
            engines: Comma-separated engine names (e.g. "google,bing,duckduckgo").
            language: Language code (e.g. "en", "zh", "de", "all").
            pageno: Page number for pagination (default: 1).
            time_range: Time filter — "day", "month", or "year".
            safesearch: Override safe search level (0, 1, 2).
            max_results: Override maximum number of results to return.

        Returns:
            dict with keys: query, results, suggestions, answers, total_results, error
        """
        if not query or not query.strip():
            return self._make_response(query="", error="Empty search query")

        effective_max = max_results or self.max_results
        params = {
            "q": query.strip(),
            "format": "json",
            "categories": categories or self.categories,
            "language": language or self.language,
            "safesearch": safesearch if safesearch is not None else self.safesearch,
            "pageno": pageno,
        }

        if engines:
            params["engines"] = engines
        if time_range and time_range in ("day", "month", "year"):
            params["time_range"] = time_range

        search_url = urljoin(self.base_url + "/", "search")

        try:
            response = self._session.get(
                search_url, params=params, timeout=self.timeout
            )
            response.raise_for_status()
        except requests.exceptions.ConnectionError:
            return self._make_response(
                query=query,
                error=f"Cannot connect to SearXNG at {self.base_url}. "
                f"Ensure the instance is running and accessible.",
            )
        except requests.exceptions.Timeout:
            return self._make_response(
                query=query,
                error=f"Request to SearXNG timed out after {self.timeout}s.",
            )
        except requests.exceptions.HTTPError as e:
            status = e.response.status_code if e.response is not None else "unknown"
            if status == 403:
                return self._make_response(
                    query=query,
                    error="SearXNG returned 403 Forbidden. "
                    "Ensure JSON format is enabled in settings.yml: "
                    "search.formats: [html, json]",
                )
            if status == 429:
                return self._make_response(
                    query=query,
                    error="Rate limited by SearXNG. Try again later or "
                    "use a self-hosted instance with limiter disabled.",
                )
            return self._make_response(
                query=query,
                error=f"SearXNG HTTP error {status}: {e}",
            )
        except requests.exceptions.RequestException as e:
            return self._make_response(
                query=query,
                error=f"Request failed: {e}",
            )

        try:
            data = response.json()
        except (json.JSONDecodeError, ValueError) as e:
            return self._make_response(
                query=query,
                error=f"Invalid JSON response from SearXNG: {e}",
            )

        raw_results = data.get("results", [])
        results = []
        for r in raw_results[:effective_max]:
            result = {
                "title": r.get("title", ""),
                "url": r.get("url", ""),
                "snippet": r.get("content", ""),
                "engines": r.get("engines", []),
                "score": r.get("score", 0),
                "category": r.get("category", ""),
            }
            if r.get("publishedDate"):
                result["published_date"] = r["publishedDate"]
            if r.get("thumbnail"):
                result["thumbnail"] = r["thumbnail"]
            if r.get("img_src"):
                result["image_url"] = r["img_src"]
            results.append(result)

        # Log unresponsive engines for debugging
        unresponsive = data.get("unresponsive_engines", [])
        if unresponsive:
            engine_issues = [f"{eng[0]}: {eng[1]}" for eng in unresponsive if len(eng) >= 2]
            if engine_issues:
                logger.warning("Unresponsive engines: %s", "; ".join(engine_issues))

        return self._make_response(
            query=query,
            results=results,
            suggestions=data.get("suggestions", []),
            answers=data.get("answers", []),
            total_results=len(results),
        )

    def search_news(
        self,
        query: str,
        language: Optional[str] = None,
        time_range: str = "day",
        max_results: Optional[int] = None,
    ) -> dict[str, Any]:
        """Convenience method for news-specific searches."""
        return self.search(
            query=query,
            categories="news",
            language=language,
            time_range=time_range,
            max_results=max_results,
        )

    def search_images(
        self,
        query: str,
        language: Optional[str] = None,
        max_results: Optional[int] = None,
    ) -> dict[str, Any]:
        """Convenience method for image searches."""
        return self.search(
            query=query,
            categories="images",
            language=language,
            max_results=max_results,
        )

    def search_it(
        self,
        query: str,
        language: Optional[str] = None,
        max_results: Optional[int] = None,
    ) -> dict[str, Any]:
        """Convenience method for IT/tech-specific searches."""
        return self.search(
            query=query,
            categories="it",
            language=language,
            max_results=max_results,
        )

    def check_health(self) -> dict[str, Any]:
        """
        Check if the SearXNG instance is reachable and JSON API is enabled.

        Returns:
            dict with keys: healthy (bool), base_url, version, error
        """
        config_url = urljoin(self.base_url + "/", "config")
        try:
            response = self._session.get(config_url, timeout=5)
            response.raise_for_status()
            config = response.json()
            return {
                "healthy": True,
                "base_url": self.base_url,
                "version": config.get("version", "unknown"),
                "engines_count": len(config.get("engines", [])),
                "categories": config.get("categories", []),
                "error": None,
            }
        except requests.exceptions.ConnectionError:
            return {
                "healthy": False,
                "base_url": self.base_url,
                "error": f"Cannot connect to {self.base_url}",
            }
        except Exception as e:
            return {
                "healthy": False,
                "base_url": self.base_url,
                "error": str(e),
            }

    def format_results_as_text(self, response: dict[str, Any]) -> str:
        """
        Format search results as readable text for LLM consumption.

        Args:
            response: Search response dict from self.search()

        Returns:
            Formatted string with numbered results.
        """
        if response.get("error"):
            return f"Search error: {response['error']}"

        results = response.get("results", [])
        if not results:
            return f"No results found for: {response.get('query', '')}"

        lines = [f"Search results for: {response['query']}\n"]

        for i, r in enumerate(results, 1):
            lines.append(f"{i}. {r['title']}")
            lines.append(f"   URL: {r['url']}")
            if r.get("snippet"):
                lines.append(f"   {r['snippet']}")
            if r.get("published_date"):
                lines.append(f"   Published: {r['published_date']}")
            engines_str = ", ".join(r.get("engines", []))
            if engines_str:
                lines.append(f"   Sources: {engines_str}")
            lines.append("")

        suggestions = response.get("suggestions", [])
        if suggestions:
            lines.append(f"Related searches: {', '.join(suggestions)}")

        answers = response.get("answers", [])
        if answers:
            lines.insert(1, f"Direct answer: {answers[0]}\n")

        return "\n".join(lines)

    @staticmethod
    def _make_response(
        query: str = "",
        results: Optional[list] = None,
        suggestions: Optional[list] = None,
        answers: Optional[list] = None,
        total_results: int = 0,
        error: Optional[str] = None,
    ) -> dict[str, Any]:
        """Create a standardized response dict."""
        return {
            "query": query,
            "results": results or [],
            "suggestions": suggestions or [],
            "answers": answers or [],
            "total_results": total_results,
            "error": error,
        }


def main():
    """CLI entry point."""
    parser = argparse.ArgumentParser(
        description="Search the web using SearXNG metasearch engine.",
        epilog="Environment variables: SEARXNG_BASE_URL, SEARXNG_MAX_RESULTS, "
        "SEARXNG_LANGUAGE, SEARXNG_SAFESEARCH, SEARXNG_TIMEOUT, SEARXNG_CATEGORIES",
    )
    parser.add_argument("query", nargs="?", help="Search query")
    parser.add_argument(
        "--base-url",
        default=None,
        help="SearXNG instance URL (default: $SEARXNG_BASE_URL or http://localhost:8080)",
    )
    parser.add_argument(
        "--categories",
        default=None,
        help="Search categories: general, images, videos, news, map, music, it, science, files, social media",
    )
    parser.add_argument(
        "--engines",
        default=None,
        help="Comma-separated engine names (e.g. google,bing,duckduckgo)",
    )
    parser.add_argument(
        "--language",
        default=None,
        help="Search language (e.g. en, zh, de, all)",
    )
    parser.add_argument(
        "--time-range",
        choices=["day", "month", "year"],
        default=None,
        help="Time range filter",
    )
    parser.add_argument(
        "--safesearch",
        type=int,
        choices=[0, 1, 2],
        default=None,
        help="Safe search level: 0=off, 1=moderate, 2=strict",
    )
    parser.add_argument(
        "--max-results",
        type=int,
        default=None,
        help="Maximum number of results to return",
    )
    parser.add_argument(
        "--page",
        type=int,
        default=1,
        help="Page number for pagination (default: 1)",
    )
    parser.add_argument(
        "--text",
        action="store_true",
        help="Output as human-readable text instead of JSON",
    )
    parser.add_argument(
        "--health",
        action="store_true",
        help="Check SearXNG instance health and exit",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Enable verbose logging",
    )

    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
    else:
        logging.basicConfig(level=logging.WARNING)

    tool = SearXNGSearchTool(
        base_url=args.base_url,
        categories=args.categories,
        language=args.language,
        safesearch=args.safesearch,
    )

    # Health check mode
    if args.health:
        health = tool.check_health()
        print(json.dumps(health, indent=2))
        sys.exit(0 if health["healthy"] else 1)

    # Search mode requires a query
    if not args.query:
        parser.error("Search query is required (or use --health)")

    result = tool.search(
        query=args.query,
        categories=args.categories,
        engines=args.engines,
        language=args.language,
        pageno=args.page,
        time_range=args.time_range,
        safesearch=args.safesearch,
        max_results=args.max_results,
    )

    if args.text:
        print(tool.format_results_as_text(result))
    else:
        print(json.dumps(result, indent=2, ensure_ascii=False))

    # Exit with non-zero if there was an error
    if result.get("error"):
        sys.exit(1)


if __name__ == "__main__":
    main()


path: references/REFERENCE.md
content: # SearXNG Reference Guide

## Quick Deploy with Docker

```bash
# Create config directory
mkdir -p searxng

# Create settings.yml (REQUIRED: enable JSON format)
cat > searxng/settings.yml << 'EOF'
use_default_settings: true
server:
  secret_key: "$(openssl rand -hex 32)"
  bind_address: "0.0.0.0"
  port: 8080
  limiter: false
  image_proxy: true
search:
  safe_search: 0
  default_lang: "all"
  formats:
    - html
    - json
EOF

# Run SearXNG
docker run -d --name searxng \
  -p 8080:8080 \
  -v "$(pwd)/searxng:/etc/searxng" \
  --restart unless-stopped \
  searxng/searxng:latest
```

## Docker Compose

```yaml
version: "3.8"
services:
  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    ports:
      - "8080:8080"
    volumes:
      - ./searxng:/etc/searxng:rw
    environment:
      - SEARXNG_BASE_URL=http://localhost:8080/
    restart: unless-stopped
```

## Verify JSON API

```bash
# Should return JSON results (not HTML or 403)
curl -s 'http://localhost:8080/search?q=test&format=json' | python3 -m json.tool | head -20
```

If you get 403 Forbidden, JSON format is not enabled in `settings.yml`.

## API Endpoint Reference

### `GET /search`

| Parameter | Required | Values | Description |
|---|---|---|---|
| `q` | Yes | string | Search query |
| `format` | No | `json`, `csv`, `rss` | Response format (default: `html`) |
| `categories` | No | comma-separated | `general`, `images`, `videos`, `news`, `map`, `music`, `it`, `science`, `files`, `social media` |
| `engines` | No | comma-separated | Specific engines (e.g. `google,bing`) |
| `language` | No | lang code | `en`, `zh`, `de`, `fr`, `all`, etc. |
| `pageno` | No | integer | Page number (default: 1) |
| `time_range` | No | `day`, `month`, `year` | Time filter |
| `safesearch` | No | `0`, `1`, `2` | 0=off, 1=moderate, 2=strict |

### `GET /config`

Returns instance configuration (engines, categories, supported languages).

## Response Fields

Each result in `results[]` contains:

| Field | Type | Description |
|---|---|---|
| `title` | string | Result title |
| `url` | string | Result URL |
| `content` | string | Text snippet |
| `engine` | string | Primary source engine |
| `engines` | string[] | All engines returning this result |
| `score` | float | Aggregated relevance score |
| `category` | string | Result category |
| `positions` | int[] | Rankings in source engines |
| `publishedDate` | string | ISO date (if available) |
| `thumbnail` | string | Thumbnail URL (if available) |
| `img_src` | string | Image source URL (image results) |

Top-level response also includes:

| Field | Type | Description |
|---|---|---|
| `query` | string | Original query |
| `suggestions` | string[] | Related search suggestions |
| `answers` | string[] | Direct answers |
| `corrections` | string[] | Spelling corrections |
| `infoboxes` | object[] | Knowledge panel data |
| `unresponsive_engines` | array[] | Engines that failed `[[name, reason]]` |

## Common Engine Names

General: `google`, `bing`, `duckduckgo`, `brave`, `startpage`, `qwant`, `mojeek`
News: `google news`, `bing news`, `yahoo news`, `wikinews`
IT: `github`, `stackoverflow`, `npm`, `pypi`, `crates.io`, `dockerhub`
Science: `arxiv`, `google scholar`, `semantic scholar`, `pubmed`
Images: `google images`, `bing images`, `flickr`, `unsplash`

## Troubleshooting

**403 Forbidden**: Add `json` to `search.formats` in `settings.yml`.

**429 Rate Limited**: Disable the limiter for private instances (`limiter: false`),
or whitelist your IP in `limiter.toml` under `pass_ip`.

**Empty results**: Some engines may be rate-limited. Check `unresponsive_engines`
in the response. Try specifying different engines with `--engines`.

**Slow responses**: SearXNG queries multiple engines in parallel. Reduce latency
by limiting categories or specifying fewer engines.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)