mirror of
https://github.com/searxng/searxng.git
synced 2026-07-28 19:01:24 +00:00
169 lines
4.7 KiB
Python
169 lines
4.7 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""Engine to search using the official `Exa Search API`_. Exa is a search engine for AI agents.
|
|
|
|
.. _Exa Search API: https://exa.ai/docs/reference/search
|
|
|
|
Configuration
|
|
=============
|
|
|
|
The engine has the following mandatory setting:
|
|
|
|
- :py:obj:`api_key`
|
|
|
|
You can obtain an API key from the `API Key section <https://dashboard.exa.ai/api-keys>`_ in the Exa dashboard.
|
|
|
|
Optional settings are:
|
|
|
|
- :py:obj:`results_per_page`
|
|
- :py:obj:`search_type`
|
|
- :py:obj:`content_mode`
|
|
- :py:obj:`content_max_characters`
|
|
|
|
.. code:: yaml
|
|
|
|
- name: exaapi
|
|
engine: exaapi
|
|
shortcut: exa
|
|
api_key: "..."
|
|
results_per_page: 10
|
|
search_type: auto
|
|
content_mode: highlights
|
|
inactive: false
|
|
|
|
The API supports SafeSearch and region-aware results.
|
|
"""
|
|
|
|
import typing as t
|
|
|
|
from dateutil import parser
|
|
|
|
from searx.exceptions import SearxEngineAPIException
|
|
from searx.result_types import EngineResults
|
|
from searx.utils import html_to_text
|
|
|
|
if t.TYPE_CHECKING:
|
|
from searx.extended_types import SXNG_Response
|
|
from searx.search.processors import OnlineParams
|
|
|
|
|
|
SearchType = t.Literal["fast", "auto", "instant", "deep", "deep-lite", "deep-reasoning"]
|
|
ContentMode = t.Literal["highlights", "text"]
|
|
|
|
about = {
|
|
"website": "https://exa.ai",
|
|
"wikidata_id": None,
|
|
"official_api_documentation": "https://exa.ai/docs/reference/search",
|
|
"use_official_api": True,
|
|
"require_api_key": True,
|
|
"results": "JSON",
|
|
}
|
|
|
|
api_key: str = ""
|
|
"""API key for Exa Search API (required)."""
|
|
|
|
categories = ["general", "web"]
|
|
safesearch = True
|
|
|
|
base_url = "https://api.exa.ai/search"
|
|
results_per_page: int = 10
|
|
"""Maximum number of results per request. Value must be between 1 and 100, default is 10."""
|
|
|
|
search_type: SearchType = "auto"
|
|
"""Search type. Default is auto, see documentation for more information."""
|
|
|
|
content_mode: ContentMode = "highlights"
|
|
"""Content to request from the API: ``highlights`` (excerpts) or ``text`` (page text)."""
|
|
|
|
content_max_characters: int = 500
|
|
"""Maximum characters for the requested content."""
|
|
|
|
|
|
def init(_):
|
|
if not api_key:
|
|
raise SearxEngineAPIException("No API key provided")
|
|
if not 1 <= results_per_page <= 100:
|
|
raise ValueError("results_per_page must be between 1 and 100")
|
|
if search_type not in t.get_args(SearchType):
|
|
raise ValueError(f"Unsupported search type: {search_type}")
|
|
if content_mode not in t.get_args(ContentMode):
|
|
raise ValueError(f"Unsupported content mode: {content_mode}")
|
|
if content_max_characters < 1:
|
|
raise ValueError("content_max_characters must be at least 1")
|
|
|
|
|
|
def _contents_payload() -> dict[str, t.Any]:
|
|
if content_mode == "text":
|
|
return {"text": {"maxCharacters": content_max_characters, "stripLinks": True}}
|
|
return {"highlights": {"maxCharacters": content_max_characters}}
|
|
|
|
|
|
def _extract_content(result: dict[str, t.Any]) -> str:
|
|
if content_mode == "text":
|
|
return html_to_text(result.get("text") or "")
|
|
return html_to_text(" ".join(result.get("highlights") or []))
|
|
|
|
|
|
def request(query: str, params: "OnlineParams") -> None:
|
|
"""Create the API request."""
|
|
body: dict[str, t.Any] = {
|
|
"query": query,
|
|
"type": search_type,
|
|
"numResults": results_per_page,
|
|
"contents": _contents_payload(),
|
|
}
|
|
|
|
# Apply SafeSearch if enabled
|
|
if params["safesearch"]:
|
|
body["moderation"] = True
|
|
|
|
# Apply region-aware results if specified
|
|
locale_parts = params["searxng_locale"].split("-")
|
|
region = locale_parts[-1]
|
|
if len(locale_parts) > 1:
|
|
body["userLocation"] = region.upper()
|
|
|
|
params["url"] = base_url
|
|
params["method"] = "POST"
|
|
params["headers"]["x-api-key"] = api_key
|
|
params["json"] = body
|
|
|
|
|
|
def _extract_published_date(value: str | None):
|
|
"""Extract and parse the published date from the API response.
|
|
|
|
Args:
|
|
value: Raw date string from the API
|
|
|
|
Returns:
|
|
Parsed datetime object or None if parsing fails
|
|
"""
|
|
if not value:
|
|
return None
|
|
try:
|
|
return parser.parse(value)
|
|
except (parser.ParserError, TypeError, OverflowError):
|
|
return None
|
|
|
|
|
|
def response(resp: "SXNG_Response") -> EngineResults:
|
|
"""Process the API response and return results."""
|
|
res = EngineResults()
|
|
|
|
for result in resp.json().get("results", []):
|
|
url = result.get("url")
|
|
if not url:
|
|
continue
|
|
|
|
res.add(
|
|
res.types.MainResult(
|
|
url=url,
|
|
title=html_to_text(result.get("title") or url),
|
|
content=_extract_content(result),
|
|
thumbnail=result.get("image") or "",
|
|
publishedDate=_extract_published_date(result.get("publishedDate")),
|
|
author=result.get("author") or "",
|
|
)
|
|
)
|
|
|
|
return res
|