Native /v1/search knobs (domains, time, topic, exact match, min_score) plus multi-query URL dedupe. Tavily passthrough; SearXNG approximations with warnings. No crawl/map/research. Claude reviews, titan implements.
9.5 KiB
Sift P3.1 — search filters and multi-query merge
Drafted 2026-09-29 by talos from Kyle's ask to steal useful Tavily-cookbook patterns for sift (thread in Mattermost). Reviewer: Claude. Implementer: titan.
P3 shipped /v1/search + /v2/search with SearXNG / Brave LLM Context / Tavily
fan-in, canonical-URL dedupe, RRF, cache, and a daily Tavily cap. The Tavily
client hardcodes search_depth: "basic" and sends no filters
(internal/search/tavily.go). Native search accepts only query, k,
rerank, digest.
This phase adds the cookbook's high-leverage search knobs without taking crawl,
map, research, or include_answer.
1. Scope
In:
- Native filters on
POST /v1/searchandsift search. - Passing those filters through to Tavily when that source is enabled.
- Honest SearXNG approximations where the engine can do it; warnings where it cannot.
- Multi-query search: several focused queries, one URL-deduped result list.
- Optional
min_scorecut after merge. published_dateon a hit when a source supplies it (Tavily news).- Skill recipe: search → score/domain filter →
sift batch, do not fetch every hit.
Out:
/v2/searchstays the Firecrawl contract (query,limitonly). Unknown fields ignored. No filters on v2.- Crawl, map, Tavily
/extract/crawl/map,include_answer,include_raw_content,include_images— P3 §3.3 still holds. - HTML lexical chunks, same-host map, Research/SSE/JSON-schema — later phases.
- Changing
internal/guard/orinternal/auth/. - Fetching result URLs inside search.
2. Native request (POST /v1/search)
Existing fields unchanged. Additive:
{
"query": "…",
"queries": ["…", "…"],
"k": 8,
"rerank": true,
"digest": false,
"include_domains": ["openai.com"],
"exclude_domains": ["pinterest.com"],
"include_domains_mode": "restrict",
"time_range": "month",
"start_date": "2026-09-01",
"end_date": "2026-09-29",
"topic": "news",
"exact_match": false,
"min_score": 0
}
Validation (400 invalid query or 400 invalid search):
- Exactly one of
query(non-empty) orqueries(1–4 non-empty strings). Both set, orquerieslonger than 4, is 400. Each string still has P3's 1500-rune cap. include_domains/exclude_domains: hostnames only (no scheme, no path). Max 20 include, 20 exclude. Empty list = omitted.include_domains_mode: omitted/restrict/prefer. Set withoutinclude_domains→ 400.time_range: omitted orday/week/month/year(and Tavily aliasesd/w/m/y, normalised to the long form).start_date/end_date:YYYY-MM-DDor omitted.endbeforestart→ 400.topic: omitted/general/news/finance.min_score: omitted or0= off; otherwise0 < x ≤ 1.digest: truestill warnsdigest ignored(P3).
/v2/search does not grow. Hermes never sends these fields.
3. Response
/v1/search results gain optional "published_date" (RFC 1123 or ISO date
string as the vendor sent it; omit the key when unknown). Do not invent dates.
warnings grows with per-source honesty, e.g. searxng: exclude_domains ignored,
brave_llm: domain filters ignored.
sizes unchanged. cached unchanged.
4. Tavily client
Still POST the configured URL only. Still include_answer: false,
include_raw_content: false, include_images: false, search_depth: "basic".
Still drop inbound answer. Still never call /extract, /crawl, /map.
When the request has filters, add only the fields that are set:
include_domains,exclude_domains,include_domains_modetime_rangeorstart_date/end_datetopicwhen notgeneralexact_matchwhen trueinclude_published_date: truewhentopicisnewsor a date filter is set
Map results[].published_date onto Hit. Count usage.credits as today.
A fixture test must still prove include_answer is false and no extract/crawl
path exists.
5. SearXNG and Brave
Do not pretend they support Tavily's full filter set.
SearXNG (GET {url}/search, same client/dialer as P3):
- One
include_domainsentry: prefix the query withsite:<host>. - Several:
site:a OR site:b OR …up to 3, then warnsearxng: extra include_domains dropped. exclude_domains: ignored + warning.include_domains_mode=prefer: ignored (site: is always restrict) + warning.time_range: pass SearXNG'stime_rangequery param (day/week/month/year) if we already talk to a SearXNG that accepts it; if the live instance 400s in tests against the fixture, keep the warning path instead of guessing.topic=news: prefix or!newsonly if the existing SearXNG config actually has that engine; otherwise warningsearxng: topic ignored. Default: warning.exact_match: wrap the query in ASCII double quotes.start_date/end_date: ignored + warning unless SearXNG is proven to accept them.
Brave LLM Context: no documented domain/date filters. Ignore all of the new
fields and add one warning brave_llm: filters ignored when any filter was set.
Do not add a second Brave HTTP API.
6. Multi-query merge
queries: [q1, q2, …] (max 4):
- Run the existing fan-in once per query (same sources, same filters, same per-source timeout). Parallel across queries is allowed; do not exceed the existing per-source client. A Tavily call still counts toward the daily cap; if the cap trips mid-batch, later queries skip Tavily with the same warning.
- Concatenate hits in query order, then canonical-URL dedupe (P3
Dedupe: first source wins attribution; later longer snippet replaces text). - RRF as today, treating each
(source, query)list as a ranked list, or RRF per query then merge by URL — pick one and test it. Simpler and enough: concatenate per-query RRF results, thenDedupe, then truncate tok. - Apply
min_scoreafter merge (hits withScore == 0from sources that do not score are kept unlessmin_score > 0, in which case score-less hits are dropped — document that SearXNG/Brave may have 0). - Cache key must include the normalised query list and the filter canonical
form (see §7). A single-
queryrequest is the list of one.
Do not fetch any result URL.
7. Cache
P3 key is sha256(norm(query) | sources | k | hour_bucket | caller_class).
Extend with | filters where filters is a stable encoding of the §2 fields
that affect retrieval (sorted domains, mode, time_range, dates, topic,
exact_match, min_score, the query list). Same query without filters must not
hit a filtered entry.
Still: do not cache if any attempted source failed. Still no page bodies, no Tavily answers.
8. CLI
sift search "<query>" [--k n] [--json]
[--include-domain host]... [--exclude-domain host]...
[--domains-mode restrict|prefer]
[--time-range day|week|month|year]
[--start-date YYYY-MM-DD] [--end-date YYYY-MM-DD]
[--topic general|news|finance]
[--exact-match]
[--min-score 0.5]
[--queries-file path]
--queries-file is a JSON array of 1–4 strings. It is mutually exclusive with
the positional query. Repeatable --include-domain / --exclude-domain.
Default text output: if published_date is present, print it as a fourth line
under the snippet. --json is the native envelope.
9. Skill
skill/SKILL.md:
- Document the new flags with one example matching the cookbook Product News
Tracker: official domain search +
topic=news+time_range=month. - Recipe: after
sift search --json, do notsift fetchevery URL. Drop hits below a score you care about (or take top N), thensift batch submit. web_searchremains the fallback only when sift is unreachable. Hermesweb_searchstill hits/v2/searchand does not get filters — say so.
10. Tests (minimum)
- Tavily outbound JSON: filters present when set;
include_answeralways false;search_depthbasic; no extract/crawl. - Tavily inbound:
answerdropped;published_datemapped; missing date omitted. - Validation table for §2.
- SearXNG:
site:prefix for one domain; exclude_domains warning; exact_match quotes. - Brave: any filter →
brave_llm: filters ignored, request URL unchanged. - Multi-query: two queries, overlapping URL, one hit after Dedupe; k cap applies.
- Cache: same query different
include_domains→ miss; same filters → hit. /v2/searchgolden unchanged (no new keys required).- Daily cap still skips Tavily on the Nth call across queries in one request.
- Restricted caller: same sources; logs still have no query/host/URL.
make gate (and contract if P3's contract harness is still in the tree).
11. Code layout
Touch, do not explode:
internal/search—Filtertype; extendSource.Search; Tavily/SearXNG/Braveinternal/api/search.go— native decode + validationcmd/sift/searchcmd.go— flagsinternal/dcachesearch keyskill/SKILL.md,README.md
No new dependency. No internal/api/api.go growth if a new file is cleaner
(internal/api/search_filter.go). Guard and auth stay closed.
12. Rollout
- Claude reviews this doc (filters vs P3 guard/contract; SearXNG honesty).
- titan implements on
git.wntrmute.dev:kyle/siftafter that review. Split commits if it helps; one phase, not a P6-style task-file plan unless the reviewer asks for one. - Claude reviews the code.
- straylight bumps the orion pin. titan does not edit
~/src/nixos.
13. Explicit non-goals (do not sneak in)
Map, crawl, HTML BM25 chunks, Research, include_answer, social-platform
crawlers, changing /v2/search. Those were ranked later in the cookbook
write-up on purpose.