WHSearch
AI-native research search engine built as a lightweight modular monolith.
Current phase
v0.3.0 — all phases + hardening: keyless multi-provider discovery
(DuckDuckGo lite/html + Wikipedia en/th routed + Google News + OpenAlex +
arXiv + Crossref + YouTube + optional self-hosted SearXNG), robots-gated
reader (HTML/PDF/plain/RSS, JS-shell L3 fallback), weighted RRF fusion,
BM25 passage retrieval with section boosts, adaptive multi-round planning
with evidence-gap follow-ups, Thai-aware claims/contradictions with
PSL domains, claim-level verification, MCP agent (search, read_page,
search_and_read, research), bounded TTL SQLite/FTS5 index, and explicit
research budgets.
Design rules
- Evidence first: retrieve sources and passages before generating research conclusions.
- Online first: use external discovery initially; keep persistent storage bounded.
- Respect robots.txt, access policies, and conservative per-domain rate limits.
- Domain models and protocols must not depend on HTTP clients, providers, MCP, or extractors.
- MCP is an adapter layer; research/search logic stays in application modules.
- Optional integrations must not be required for importing the core domain.
- Resource limits are explicit so the system remains usable on low-memory machines.
- Keyless only: no paid API keys required for any default path.
Architecture
domain (models/protocols, no httpx/bs4/mcp)
^ ^ ^
search/reader/retrieval/research/evidence (pure app logic)
^ ^ ^
agent/service (plan → search → read → verify, budgeted)
^
application (composition root: providers, reader, index, agent)
^
mcp/server + tools (thin adapters) / infrastructure/http
Query DSL
site:, -exclude, "exact phrase", filetype:, OR (AND/NOT → spaces).
site: becomes a domain allowlist; -/"phrase" filter locally so flaky
HTML providers still work. recency_days boosts fresh dated results
(undated kept) and the agent drops stale dated pages post-read.
Thai: th/en/mixed detection routes to one Wikipedia edition, sets
kl/hl/gl/ceid + Accept-Language, and enables Thai-aware BM25/claims.
Planned phases
Foundation: contracts, configuration, logging, testing, architecture checks.Done.Web discovery: provider abstraction and DuckDuckGo discovery.Done (+Wikipedia).Web reader: robots policy, fetching, extraction, metadata, passages.Done.Retrieval: passage ranking and deduplication.Done.Research: adaptive query planning and stopping conditions.Done.Evidence: claims, source independence, contradictions, verification.Done.Search agent: orchestration through the MCP tools.Done.Local index: SQLite/FTS5 bounded cache and reusable evidence.Done.Autonomous research budgets and larger-scale discovery.Done (budgets + fan-out).- v0.3 hardening: weighted fusion, follow-up planning, Thai evidence, PDF/RSS, TTL caches, PSL domains, request IDs + JSON logs. Done.
Development
Use the repository virtual environment when available:
.venv/bin/python -m pytest
.venv/bin/python -m compileall -q src tests
.venv/bin/ruff check src tests
The quality gate must pass before moving to the next phase.
Install as an MCP server
Requires Python >= 3.12. After the whsearch package is published to PyPI,
no manual install is needed — uvx fetches and runs it on first use:
{
"mcpServers": {
"whsearch": { "command": "uvx", "args": ["--from", "whsearch[mcp]", "whsearch"] }
}
}
Alternatives:
uv tool install "whsearch[mcp]" && whsearch # persistent install via uv
pipx install "whsearch[mcp]" && whsearch # persistent install via pipx
pip install -e ".[mcp]" && whsearch # from source
WHSEARCH_INDEX_PATH enables the persistent local index.
Configuration (env)
| Var | Default | Meaning |
|---|---|---|
WHSEARCH_USER_AGENT |
WHSearch/0.2 |
Outbound UA |
WHSEARCH_REQUEST_TIMEOUT |
15 |
HTTP timeout (s) |
WHSEARCH_MAX_RESPONSE_BYTES |
5000000 |
Reader cap |
WHSEARCH_MAX_SEARCH_RESULTS |
30 |
Fusion cap |
WHSEARCH_MAX_PAGES |
20 |
Agent page budget default |
WHSEARCH_DOMAIN_DELAY |
1.0 |
Per-domain politeness (s) |
WHSEARCH_CACHE_MAX_BYTES |
500000000 |
Index byte bound |
WHSEARCH_INDEX_PATH |
— | Enable SQLite index |
WHSEARCH_INDEX_MAX_ENTRIES |
5000 |
Count bound |
WHSEARCH_INDEX_TTL_DAYS |
— | TTL pruning (empty = off) |
WHSEARCH_SEARCH_CACHE_TTL |
600 |
Positive search cache (s) |
WHSEARCH_SEARCH_NEGATIVE_TTL |
60 |
Empty-result cache (s) |
WHSEARCH_READ_CONCURRENCY |
5 |
Parallel page reads |
WHSEARCH_TOP_K / WHSEARCH_MIN_SCORE |
3 / 0.5 |
Evidence thresholds |
WHSEARCH_MIN_SUPPORT / WHSEARCH_MIN_DOMAINS |
2 / 2 |
SUPPORTED bar |
WHSEARCH_MIN_NEW_RATIO / WHSEARCH_MIN_SUPPORTED |
0.15 / 2 |
Stopping |
WHSEARCH_MAILTO |
whsearch@example.com |
Polite OpenAlex/Crossref UA |
WHSEARCH_SEARXNG_URL |
— | Opt-in self-hosted SearXNG |
WHSEARCH_PDF |
1 |
0 disables PDF parsing |
WHSEARCH_LOG_LEVEL / WHSEARCH_LOG_FORMAT |
INFO / text |
json for structured logs |
pip install -e ".[pdf]" enables PDF (pypdf).
Optional headless-browser fallback (L3)
Pages that render only via JavaScript (JS-shell SPAs) defeat static
extraction. When the js extra is installed, the reader tries headless
Chromium only for pages where static extraction yields almost nothing:
pip install -e ".[mcp,js]" && .venv/bin/playwright install chromium
Behavior and limits (all free, no keys):
- L1 trafilatura → L2 embedded JSON/meta → L3 headless, first hit wins.
- L3 triggers only below 200 extracted chars; rendered text must also clear it.
- At most 2 concurrent renders, 15s each, images/fonts/media blocked.
- Missing playwright (or any render failure) degrades silently to static text.
WHSEARCH_BROWSER=0disables it;WHSEARCH_BROWSER_TIMEOUTtunes seconds.WHSEARCH_CHROMIUM_PATH=/usr/bin/chromiumreuses a system browser instead of downloading one (playwright install chromium).
Video vertical (YouTube, keyless)
Video-intent queries (youtube/video/คลิป, or site:youtube.com) also fan out
to a first-party YouTube search provider (ytInitialData parsing — no key,
no Invidious/Piped instances). Watch URLs read back as documents built from
the video's own metadata + description, with MM:SS chapter lines as passage
sections, so a multimodal caller knows where in the video to look:
- Direct captions are intentionally not fetched: YouTube's timedtext now requires proof-of-origin tokens and rate-limits keyless access (429).
whsearch://statsreportsreader.browser_installed/enabled/timeout.
License
GPL-3.0-or-later, see LICENSE. Copyright (C) 2026 WHSearch contributors.
Per-file copyright holder names were intentionally left generic; update them
to your name before publishing if you are the sole author.
Release files for whsearch 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| whsearch-0.3.0.tar.gz | 73.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| whsearch-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 162.0 kB
Release files / whsearch-0.3.0.tar.gz
| Download URL | whsearch-0.3.0.tar.gz |
|---|---|
| Size | 73.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
683ab4cd9a7ab3355438702e638f3d6fd8389f360997feb5021011e77300f763
|
|
BLAKE2b-256 checksum How to use checksums |
b9ca3e58bc6345167506a72ff20783a848e1403078f1b8eb4a535771351b234e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.7
|
Release files / whsearch-0.3.0-py3-none-any.whl
| Download URL | whsearch-0.3.0-py3-none-any.whl |
|---|---|
| Size | 88.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1c6e3ffee10386bb23fbe0a43dab617e68ba106d6741fb5d812c515cfff4d850
|
|
BLAKE2b-256 checksum How to use checksums |
15df76fd39f331f49358fad2d12142226a928ab27213fe6d445e63ace0e2eca9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.7
|