Files
oc/plugins/searxng-search/README.md
T
xiaoshuai aa70b381bf searxng-search: default engines bing,yandex instead of bing,360search
The default was chosen by a single Chinese test query and generalized from
it. Measuring both a Chinese and an English technical query shows it was
inverted on both counts. Results mentioning "opencode", out of returned:

                       zh "OpenCode V2 和 V1 区别"   en "opencode v2 migrate from v1 plugin api"
  bing                          10/10                     missed the doc entirely
  360search                      0/5                      4, unrelated (sohu/toutiao)
  yandex                        14/15                     15, migration doc ranked #1
  naver                           0/9                     15, migration doc ranked #1

yandex had been excluded on the claim that it "has almost no Chinese index".
Measured, it has the best Chinese relevance of any engine here, and on the
English query the V1->V2 migration doc is its first result -- the exact
document that the old default could not find at all.

360search had been the default. It is the worst engine on both query types.
It splits the query on "V1"/"V2" and answers with Android and CANN
version-difference articles, which produced the 10-of-15 noise in the first
round of testing.

naver is left out rather than added: strongest on English, 0/9 on Chinese, so
including it would trade one kind of noise for another. It is documented as
worth passing explicitly for English lookups.

The comment above the constant now carries the measurement table, so the
default can be re-chosen from data instead of re-derived from a hunch.

Not changed, and worth knowing: cn.bing.com ignores the site: operator.
engines=bing&q=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.

Unreproduced: one run of the Chinese query returned entirely unrelated
results from bing (Doubao, Zhipin, Zhihu) while four other runs with the same
parameters returned 10/10 relevant. No parameter difference was found between
the good and bad runs. Treated as transient upstream behaviour, not a plugin
bug, and not documented as a rule.

README engine table updated to the measured numbers.
2026-10-08 13:35:57 +08:00

182 lines
8.5 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. Needs `base_url: https://cn.bing.com`; `www.bing.com` blocks scraping from this network. |
| `yandex` | Active. Best measured relevance here on both Chinese and English technical queries; first result for a V1→V2 migration query is the migration doc itself. Default. |
| `bing` | Active, but needs `base_url: https://cn.bing.com`; `www.bing.com` blocks scraping from this network. Default. Supports neither paging nor `time_range`. |
| `naver` | Active and strongest on English, but 0/9 relevant on a Chinese query, so it is left out of the defaults rather than allowed to dilute them. Pass `engines=naver` explicitly for English lookups. |
| `360search` | Active, and the **worst** engine on both query types: 0/5 relevant on a Chinese query, where it splits on "V1"/"V2" and returns Android and CANN version-difference articles. Removed from the defaults. |
| `baidu` | Disabled. Answers with a CAPTCHA and suspends the client IP for an hour. |
| `sogou` | Disabled. The upstream engine raises `AttributeError` in this release. |
| `mojeek`, `ecosia`, `marginalia` | Dropped by `keep_only`; not reachable from this network. |
`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.
## 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. |