d6e29cbbad
Asked for a domestic-only default, so every general-web index SearXNG ships for China was added to keep_only, the container was restarted, and each was measured over 5 runs per query. None of them is usable. The default stays bing,yandex. engine en hits zh hits outcome 360search 5/5 5/5 reliable, 0% relevant quark 0/5 0/5 rate-limited dead after two good responses baidu - 0 CAPTCHA, 1h IP suspension sogou - 0 upstream AttributeError baidu kaifa - 0/10 code index: "opencode v2" matches OpenCV2 bilibili - - video titles, not a web index chinaso news - - falls through to the default engine set quark was the one that looked like the fix. On its first two requests it returned the exact article being asked about as result #1 on both the Chinese and the English query -- better than anything else measured -- and then answered 0 results on 5 consecutive runs of each. Had it not been run 5 times, it would have looked like the obvious upgrade. baidu kaifa is worth remembering as a category error: it is a code search index, so it matches "opencode v2" against OpenCV2 and returns CSDN blog posts at 0% relevance. It is reachable and does not CAPTCHA, which makes it look healthy in /config while being useless. The foreign engines that were reachable (brave, duckduckgo, google, qwant, wikipedia) were added at the same time and all returned 0, consistent with the network timeouts already recorded in settings.yml. mojeek and startpage are not present in this SearXNG release at all, so the earlier note calling them "unreachable" was imprecise. settings.yml was restored from a backup and diffed byte-identical before this commit; the probe engines never left the instance. cn.bing.com is the only dependable endpoint and is a China-hosted Bing, so bing counts as the domestic option. Its one real limitation stands: it ignores the site: operator, so site-restricted search is unavailable through this instance. Also fixes a duplicated `bing` row left in the README engine table by the previous commit.
211 lines
10 KiB
Markdown
211 lines
10 KiB
Markdown
# searxng-search
|
||
|
||
An OpenCode V2 plugin that adds a `searxng_search` tool backed by a
|
||
self-hosted SearXNG instance.
|
||
|
||
It does **not** replace OpenCode's built-in `websearch` provider. It registers
|
||
one extra, opt-in tool, so nothing existing changes behaviour. Replacing a
|
||
built-in provider means taking over a key the user chose; adding a tool does
|
||
not.
|
||
|
||
- OpenCode V2 plugin docs: <https://opencode.ai/v2/docs/build/plugins/>
|
||
- OpenCode plugin config: <https://opencode.ai/v2/docs/plugins>
|
||
- SearXNG search API: <https://docs.searxng.org/dev/search_api.html>
|
||
- How the paired instance was deployed: [`SEARXNG-SETUP.md`](SEARXNG-SETUP.md)
|
||
|
||
## Requirements
|
||
|
||
1. A reachable SearXNG instance with `json` in `search.formats`
|
||
(disabled upstream by default). See `SEARXNG-SETUP.md`.
|
||
2. `@opencode/plugin` installed at **your exact OpenCode version**:
|
||
|
||
```sh
|
||
opencode --version # e.g. v2.0.24
|
||
cd ~/.config/opencode
|
||
npm i @opencode/plugin@2.0.24 # must match
|
||
```
|
||
|
||
3. A service restart. The running service caches module resolution, so a
|
||
freshly installed package is invisible until then:
|
||
|
||
```sh
|
||
opencode service restart
|
||
```
|
||
|
||
Check the log if the tool does not appear:
|
||
|
||
```sh
|
||
grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log | tail
|
||
```
|
||
|
||
A line like this is good:
|
||
|
||
```
|
||
loading plugin id=/home/you/.config/opencode/plugins/searxng-search
|
||
```
|
||
|
||
A `Cannot find package '@opencode/plugin'` error means the package is
|
||
**missing**, not that the docs are wrong. See the stale-package trap in
|
||
`../../skills/opencode-plugins/SKILL.md`.
|
||
|
||
## How it is loaded
|
||
|
||
By directory discovery, not by config. OpenCode loads every immediate
|
||
subdirectory of `~/.config/opencode/plugins/` as a plugin package and resolves
|
||
an entrypoint inside it. This directory has no `package.json`, so the entrypoint
|
||
falls back to `index.ts`:
|
||
|
||
```
|
||
loading plugin id=/home/you/.config/opencode/plugins/searxng-search
|
||
entrypoint=file:///.../plugins/searxng-search/index.ts
|
||
```
|
||
|
||
Sibling files such as this `README.md` do not affect that resolution — verified
|
||
by restarting the service with the docs in place. There is deliberately nothing
|
||
to add to `opencode.jsonc`.
|
||
|
||
That has a consequence worth knowing before you try to configure it:
|
||
|
||
> **`ctx.options` is always `{}` for a discovered plugin.**
|
||
|
||
Options reach a plugin only through an object entry in the `plugins` array in
|
||
`opencode.json(c)`, which is keyed by package or path. A directory that was
|
||
found by scanning has no such entry, so it receives no options at all. This was
|
||
verified by writing `JSON.stringify(ctx.options)` to a file from inside
|
||
`setup()` — the file contained `{}`.
|
||
|
||
The `baseUrl` / `engines` / `timeoutMs` / `maxSnippetChars` options in
|
||
`PluginConfig` are therefore **inert** as shipped. Two working alternatives:
|
||
|
||
| Approach | How |
|
||
| --- | --- |
|
||
| Environment variable (easiest) | `export SEARXNG_URL=http://host:39217` before starting OpenCode |
|
||
| Register explicitly | Move this directory out of `plugins/`, then add a `{"package": "<path>", "options": {...}}` entry to `opencode.jsonc` |
|
||
|
||
To keep the `engines` default editable without moving anything, change
|
||
`DEFAULT_ENGINES` in `index.ts` and push — see "Push every change" in
|
||
`../../skills/opencode-plugins/SKILL.md`.
|
||
|
||
If you do register it explicitly, remove the directory from `plugins/`
|
||
first. Otherwise the plugin loads twice: once by discovery, once by config,
|
||
and the tool is registered against the same name twice.
|
||
|
||
## The tool
|
||
|
||
Effective name: **`searxng_search`** (tool name + `_` + namespace).
|
||
|
||
| Argument | Type | Notes |
|
||
| --- | --- | --- |
|
||
| `query` | string, required | The search query. |
|
||
| `engines` | string | Comma-separated. Omit for the instance default. |
|
||
| `category` | string | Only sent when you pass it — see the additive trap below. |
|
||
| `language` | string | e.g. `zh-CN`, `en-US`. Omit for the instance default. |
|
||
| `safesearch` | 0 \| 1 \| 2 | Omit for the instance setting. |
|
||
| `pageno` | number | From 1. Engines without paging are dropped above page 1. |
|
||
| `time_range` | string | `day`, `week`, `month`, `year`. Engines without it are dropped entirely. |
|
||
|
||
At `setup()` the plugin reads the instance's `/config` and bakes what the
|
||
instance actually supports into each argument's `description`, so the model is
|
||
told which engines lose paging or time filtering instead of discovering it by
|
||
returning fewer results.
|
||
|
||
### Two SearXNG behaviours that silently mislead
|
||
|
||
Both are handled, both are worth understanding because they fail silently.
|
||
|
||
**1. `categories` is additive, not a filter.** If you pass both `engines` and
|
||
`category`, SearXNG takes the union: the named engines *plus* every engine
|
||
enabled by default in that category. A careful `engines=bing,yandex` becomes
|
||
`bing,yandex` + whatever the category contributes. The plugin therefore
|
||
sends `categories` only when the caller explicitly asked for a category, never
|
||
alongside an engine list it chose itself.
|
||
|
||
**2. An unknown engine name falls back instead of erroring.** If `engines=`
|
||
names something the active engine list does not contain, SearXNG ignores the
|
||
name and searches with the **default engine set**. No error, no
|
||
`unresponsive_engines` entry, and results that look like a success. Verified on
|
||
this instance:
|
||
|
||
```
|
||
engines=bing -> bing x10
|
||
engines=360search -> 360search x7
|
||
engines=baidu -> 0 results, unresponsive_engines empty
|
||
engines=mojeek -> bing x10 <-- fell back, mojeek is not active
|
||
engines=duckduckgo -> bing x10 <-- fell back
|
||
engines=nosuchengine -> bing x10 <-- typo: also fell back, no warning
|
||
```
|
||
|
||
Three signatures, and only the third means the engine was really used:
|
||
|
||
| Observation | Meaning |
|
||
| --- | --- |
|
||
| 0 results **and** empty `unresponsive_engines` | Engine is disabled or not in the active list |
|
||
| Results, but none of them from the requested name | Fell back to the default engine set |
|
||
| Results including the requested name | Genuinely used |
|
||
|
||
`/config` is **not** a reliable way to check this. On this instance it reports
|
||
`disabled: false` for every engine while `baidu` and `sogou` are in fact being
|
||
dropped, because the listing reflects upstream defaults rather than the
|
||
`disabled: true` overrides in `settings.yml`. Diagnose with the table above.
|
||
|
||
## Measured state of the paired instance
|
||
|
||
SearXNG 2026.10.7-6671d89be, `docker.io/searxng/searxng:latest` (built
|
||
2026-10-07), `http://127.0.0.1:39217`.
|
||
|
||
| Engine | Status |
|
||
| --- | --- |
|
||
| `bing` | Active and reliable, 5/5 non-empty runs on both query types. Needs `base_url: https://cn.bing.com`; `www.bing.com` blocks scraping from this network. Default. |
|
||
| `yandex` | Active and reliable, 5/5, 15 results per run. Best measured relevance here; first result for a V1→V2 migration query is the migration doc itself. Default. |
|
||
| `naver` | Reliable, 5/5. Strongest on English (migration doc ranked #1) but 0/9 on a Chinese query, so it is left out of the defaults and worth passing explicitly for English lookups. |
|
||
| `360search` | Reliable but returns 4–5 results at 0% relevance. It splits the query on "V1"/"V2" and answers with Android and CANN version-difference articles. Removed from the defaults. |
|
||
| `quark` | Unusable. Returned 11/11 relevant results twice, then 0 results on 5 consecutive runs of each query. Rate-limited after a couple of requests. |
|
||
| `baidu` | Disabled. Answers with a CAPTCHA and suspends the client IP for an hour. |
|
||
| `sogou` | Disabled. The upstream engine raises `AttributeError` at `searx/engines/sogou.py:56` (`resp.next_request`) in this release. |
|
||
|
||
`bing` supports neither paging nor `time_range`, so on this instance `pageno > 1`
|
||
and `time_range` both narrow the result to nothing useful. The tool says so in
|
||
the argument descriptions rather than returning a silently short list.
|
||
|
||
### There is no working domestic-only engine set
|
||
|
||
Stated plainly because it was measured, not assumed. Every general-web index
|
||
SearXNG ships for China was added to `keep_only`, the container was restarted,
|
||
and each was measured over 5 runs per query:
|
||
|
||
| Engine | en hits | zh hits | Outcome |
|
||
| --- | --- | --- | --- |
|
||
| `360search` | 5/5 | 5/5 | Reliable, 0% relevant |
|
||
| `quark` | 0/5 | 0/5 | Rate-limited dead after two good responses |
|
||
| `baidu` | — | 0 | CAPTCHA, 1-hour IP suspension |
|
||
| `sogou` | — | 0 | Upstream `AttributeError` |
|
||
| `baidu kaifa` | — | 0/10 | A **code** index: matches "opencode v2" as OpenCV2 and returns CSDN blog posts |
|
||
| `bilibili` | — | — | Video titles, not a web index |
|
||
| `chinaso news` | — | — | Falls through to the default engine set |
|
||
| `brave`, `duckduckgo`, `google`, `qwant`, `wikipedia` | 0 | 0 | Unreachable from this network |
|
||
| `mojeek`, `startpage` | — | — | Not present in this SearXNG release |
|
||
|
||
`quark` is the trap. On its first two requests it returned the exact article
|
||
being asked about as result #1 on both queries — better than anything else
|
||
measured here — and then went permanently dark. Do not reinstate it on the
|
||
strength of a first impression.
|
||
|
||
`cn.bing.com` is the only dependable endpoint, and it is a China-hosted Bing,
|
||
so `bing` serves as the domestic option. Its one real limitation is that it
|
||
ignores the `site:` operator completely: `site:opencode.ai v2 migration` returns
|
||
0 results from opencode.ai, while the same query without `site:` returns 3.
|
||
Site-restricted search is not available through this instance — fetch the docs
|
||
index and follow links instead.
|
||
|
||
## Troubleshooting
|
||
|
||
| Symptom | Check |
|
||
| --- | --- |
|
||
| Tool absent from the catalog | `grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log \| tail` |
|
||
| `Cannot find package '@opencode/plugin'` | Package missing or wrong version. Match `opencode --version` exactly. |
|
||
| Installed it but still missing | `opencode service restart` — the service caches module resolution. |
|
||
| `fetch failed` / no results at all | `curl -s "http://127.0.0.1:39217/config" \| head -c 200` — is it up? |
|
||
| JSON format rejected | `json` must be in `search.formats` in `settings.yml`. |
|
||
| Results from an unexpected engine | The fallback in the table above. Check which engines actually appear. |
|
||
| Everything works, then stops after an edit | The running service loaded the old file. Restart it. |
|