Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ webSearch:
searxngApiKey: "${SEARXNG_API_KEY}"
searchProvider: "serper" # Options: "serper", "searxng", "tavily"

# Optional: query options sent to a SearXNG instance
searxngSearchOptions:
engines: "google,bing,startpage" # Default: "google,bing,duckduckgo"
language: "en" # Default: "all"

# Tavily Configuration (search and/or scraper)
tavilyApiKey: "${TAVILY_API_KEY}"
# Optional: custom Tavily-compatible endpoints
Expand Down Expand Up @@ -107,6 +112,89 @@ LibreChat automatically exempts a configured HTTP(S) forward-proxy endpoint so i

**Note:** This is optional and only needed if your SearXNG instance requires authentication.

### searxngSearchOptions

<OptionTable
options={[
['searxngSearchOptions', 'Object', 'Query options sent to your SearXNG instance on every search. Every subkey is optional, and the block is only read when searchProvider is "searxng".', ''],
]}
/>

**Subkeys:**

<OptionTable
options={[
['engines', 'String or Array of Strings', 'Engines your instance should query, sent to SearXNG as the engines parameter. Accepts a comma-separated string or a YAML list; both are normalized to the comma-separated form, with surrounding whitespace and empty entries dropped. A value that is empty once trimmed is treated as unset.', 'Default: "google,bing,duckduckgo"'],
['language', 'String', 'Result language code, sent to SearXNG as the language parameter. Use a code your instance accepts, such as "en", "de", or "fr".', 'Default: "all"'],
['timeRange', 'String', 'Restricts results by publish date, sent to SearXNG as time_range. Left out of the request entirely when unset.', 'Options: "day", "month", "year"'],
['timeout', 'Number', 'HTTP request timeout in milliseconds for calls to your instance. Must be a positive integer no greater than 120000; 0 is rejected because it would disable the timeout.', 'Default: 10000'],
]}
/>

<Callout type="info" title="Availability">
`searxngSearchOptions` reaches SearXNG through the search tool in `@librechat/agents`, and needs `@librechat/agents` v3.6.9 or later. Releases built against an earlier version ignore the block rather than failing to start, so an instance that keeps returning default results may simply be running an older build.
Comment thread
berry-13 marked this conversation as resolved.
</Callout>

**Example:**

```yaml filename="webSearch"
webSearch:
searchProvider: "searxng"
searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
searxngApiKey: "${SEARXNG_API_KEY}" # Optional
searxngSearchOptions:
engines:
- google
- bing
- startpage
- qwant
language: "en"
timeRange: "month"
timeout: 10000
```

`engines` also accepts the comma-separated form SearXNG itself uses, so this is equivalent to the list above:

```yaml filename="webSearch"
webSearch:
searxngSearchOptions:
engines: "google, bing, startpage, qwant"
```

#### Choosing engines

Engine names must match engines that are enabled on your own instance, and two things follow from that:

- SearXNG ignores an engine it does not recognize instead of reporting an error. A typo, or an engine that is disabled on your instance, shows up as fewer results rather than as a failure.
- The valid names are instance-specific. Check the enabled list at `https://your-instance/config`, which returns JSON including every enabled engine, or open the **Engines** tab of your instance preferences page.

The default is `google,bing,duckduckgo`. SearXNG aggregates whatever the selected engines return, so one blocked engine costs you its share of the results rather than the whole response, and a search only comes back empty when every selected engine fails or returns nothing.

DuckDuckGo is the engine in that default set most likely to be blocked, since it serves CAPTCHAs to most self-hosted instances. With only three engines in the set, losing it at the same time as a rate-limited Google or Bing is a common way to end up with no results at all. Setting `engines` explicitly, leaving DuckDuckGo out, and listing more than three engines all reduce that risk.

Engines that answer quickly and tolerate self-hosted traffic make the best starting set, for example:

```yaml filename="webSearch"
webSearch:
searchProvider: "searxng"
searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
searxngSearchOptions:
engines:
- google
- bing
- brave
- startpage
- qwant
```

Adding more engines widens coverage but also slows every search down, since SearXNG waits on the slowest engine in the set before responding. Keep `timeout` in mind when you grow the list.

#### What these options do not control

- **Safe search** is not part of this block. It comes from the top-level [`safeSearch`](#safesearch) key and is forwarded to SearXNG as `safesearch`.
- **Result categories** are chosen per query by LibreChat, which maps the search type to `general`, `images`, `videos`, or `news`. They cannot be overridden here.
- **Page number and result format** are fixed. LibreChat always requests page 1 in JSON format, which is why your instance must have `json` enabled under `formats`.

### tavilyApiKey

<OptionTable
Expand Down Expand Up @@ -476,6 +564,7 @@ webSearch:
- If a specific service type is not specified, the system will try all available services in that category
- Safe search provides three levels of content filtering: OFF (0), MODERATE (1), and STRICT (2)
- Tavily does not inherit the global safe search setting by default; set `tavilySearchOptions.safeSearch` explicitly only when your Tavily account supports `safe_search`
- SearXNG query behavior (engines, language, time range, request timeout) is configured under `searxngSearchOptions`; see [Choosing engines](#choosing-engines) if searches come back empty
- Never put actual API keys in the YAML configuration - only use environment variable names

## Setting Up SearXNG
Expand Down Expand Up @@ -539,3 +628,65 @@ You can configure SearXNG in LibreChat within the UI or through `librechat.yaml`

6. **The Web Search badge should now be enabled, meaning your queries can now utilize the web search functionality**
![Web search badge confirmation](/images/web-search/search_badge_confirm.png)

#### YAML Configuration

To configure SearXNG for everyone on the instance instead of per user, set the same values in `librechat.yaml`:

```yaml filename="librechat.yaml"
webSearch:
searchProvider: "searxng"
searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}"
# searxngApiKey: "${SEARXNG_API_KEY}" # Only if your instance requires authentication
# Required: a self-hosted instance is a private destination, so exempt it from the connection guard
allowedAddresses:
- "localhost:55011"
searxngSearchOptions:
engines:
- google
- bing
- startpage
- qwant
language: "en"
timeout: 10000
```

The URL and the key are still read from the environment, so pair the config above with:

```bash filename=".env"
SEARXNG_INSTANCE_URL=http://localhost:55011
# SEARXNG_API_KEY=your_api_key
Comment thread
berry-13 marked this conversation as resolved.
```

<Callout type="warning" title="Private instances need an allowedAddresses entry">
`localhost:55011` is a loopback address, which LibreChat's [SSRF protection](#ssrf-protection-and-private-providers) blocks at connect time. Without the matching `allowedAddresses` entry the search fails even when SearXNG is healthy. The entry must be the exact `host:port` pair from your instance URL.

When LibreChat itself runs in Docker, `localhost` is the LibreChat container, not the SearXNG one. Use a hostname the container can reach, such as the compose service name, and exempt that instead:

```yaml filename="librechat.yaml"
webSearch:
searxngInstanceUrl: "${SEARXNG_INSTANCE_URL}" # SEARXNG_INSTANCE_URL=http://searxng:8080
allowedAddresses:
- "searxng:8080"
```
</Callout>

See [`searxngSearchOptions`](#searxngsearchoptions) for every subkey this block accepts.

#### Troubleshooting empty results

A SearXNG search that returns nothing is almost always the request never reaching a working instance rather than LibreChat dropping results. Work through these in order:

1. **The instance is a private address with no `allowedAddresses` entry.** LibreChat blocks loopback and private destinations at connect time, so a self-hosted instance needs its exact `host:port` listed under [`allowedAddresses`](#ssrf-protection-and-private-providers). This blocks the request outright rather than returning an empty result set.
2. **`json` is missing from `formats`.** LibreChat requests results as JSON. If the `formats` section of your instance settings file does not list `json`, the instance answers with an error page and every search comes back empty. This is step 3 of the Docker setup above.
3. **Every selected engine failed.** SearXNG merges the engines that did answer, so an empty response means none of them returned anything. DuckDuckGo is in the default set (`google,bing,duckduckgo`) and serves CAPTCHAs to most self-hosted instances, which leaves only two engines to carry the search. Set `searxngSearchOptions.engines` explicitly, leave DuckDuckGo out, and list a few more engines.
4. **An engine name does not exist on your instance.** SearXNG silently skips engines it does not recognize, so a typo just means fewer results. Compare your list against `https://your-instance/config`.
5. **The instance is slower than the timeout.** Raise `searxngSearchOptions.timeout` above the default `10000` if your instance queries many engines or sits behind a slow network path.

You can reproduce what LibreChat sends by calling the instance directly:

```bash
curl -s "http://localhost:55011/search?q=librechat&format=json&engines=google,bing,startpage" | head -c 500
```

This runs from your own machine, so it bypasses LibreChat's connection guard entirely. An empty `results` array here confirms the problem is on the SearXNG side; results here while LibreChat still finds nothing points back at `allowedAddresses`.
6 changes: 6 additions & 0 deletions content/docs/features/web_search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Each enabled external service requires its own API key. Here's how to obtain the
1. Follow the setup instructions in the [Web Search Configuration](/docs/configuration/librechat_yaml/object_structure/web_search#setting-up-searxng) documentation
2. Set `SEARXNG_INSTANCE_URL` to your instance URL
3. Optionally set `SEARXNG_API_KEY` if your instance requires authentication
4. Add your instance's exact `host:port` to [`allowedAddresses`](/docs/configuration/librechat_yaml/object_structure/web_search#ssrf-protection-and-private-providers) if it is on a private or loopback address, otherwise LibreChat blocks the connection
5. Optionally tune which engines your instance queries with [`searxngSearchOptions`](/docs/configuration/librechat_yaml/object_structure/web_search#searxngsearchoptions). The default set is only three engines and includes DuckDuckGo, which serves CAPTCHAs to most self-hosted instances, so widening it helps if searches come back empty

#### Tavily
1. Visit [Tavily](https://app.tavily.com/home)
Expand Down Expand Up @@ -106,6 +108,7 @@ Search providers are responsible for performing the initial web search and retur
- **SearXNG**: Open-source, self-hosted meta search engine
- Self-host your own instance
- Privacy-focused search results
- Configurable engines, result language, time range, and request timeout
- **Tavily**: AI-optimized search API
- Get your API key from [Tavily](https://app.tavily.com/home)
- Supports configurable search depth, topic filtering, domain filtering, and more
Expand Down Expand Up @@ -162,6 +165,9 @@ webSearch:
searxngApiKey: "${CUSTOM_SEARXNG_API_KEY}" # ✅ Correct: Using environment variable name
# searxngInstanceUrl: "http://..." # ❌ Wrong: Never put actual URLs here
# searxngApiKey: "sk-123..." # ❌ Wrong: Never put actual API keys here
searxngSearchOptions: # Query options, not secrets, so real values belong here
engines: "google,bing,startpage"
language: "en"

# Tavily Configuration (search and/or scraper)
tavilyApiKey: "${CUSTOM_TAVILY_API_KEY}"
Expand Down
Loading