feat(search): P3.1 filters and multi-query merge

Native include/exclude domains, time_range, topic, exact_match, min_score,
and up to 4 queries fused with RRF. Honesty notes stay off the Warn/fail
path. Tavily keeps include_answer false and sets auto_parameters false.
Cache key is canonical JSON. CLI flags and skill recipe included.
This commit is contained in:
2026-09-29 15:30:19 -07:00
parent 5c6a4a40ec
commit 8b26776cdd
21 changed files with 1606 additions and 195 deletions
+31 -7
View File
@@ -25,22 +25,37 @@ var sourceRank = map[string]int{
// Hit is one search result after mapping. No page body, no vendor answers.
type Hit struct {
URL string `json:"url"`
Title string `json:"title"`
Snippet string `json:"snippet"`
Score float64 `json:"score"`
Source string `json:"source"`
URL string `json:"url"`
Title string `json:"title"`
Snippet string `json:"snippet"`
Score float64 `json:"score"`
Source string `json:"source"`
PublishedDate string `json:"published_date,omitempty"`
}
// Filter is native search knobs. The zero value is P3 behaviour.
type Filter struct {
IncludeDomains []string
ExcludeDomains []string
IncludeDomainsMode string // restrict | prefer; empty means restrict when include is set
TimeRange string // day|week|month|year
StartDate string
EndDate string
Topic string // general|news|finance; empty means general
ExactMatch bool
MinScore float64
}
// Source is one upstream. Config-fixed endpoint; never a request URL.
type Source interface {
Name() string
Search(ctx context.Context, query string, k int) SourceOut
Search(ctx context.Context, query string, k int, f Filter) SourceOut
}
type SourceOut struct {
Hits []Hit
Warn string
Warn string // failure only (timeout, HTTP error, pin)
Notes []string // honesty: filters not applied, recall loss
Credits int
}
@@ -80,6 +95,7 @@ func Dedupe(in []Hit) []Hit {
continue
}
if s, ok := seen[canon]; ok {
keepDate := firstDate(s.h.PublishedDate, h.PublishedDate)
if len(h.Snippet) > len(s.h.Snippet) {
src := s.h.Source
keep := s.h.URL
@@ -91,6 +107,7 @@ func Dedupe(in []Hit) []Hit {
s.h.Score = sc
}
}
s.h.PublishedDate = keepDate
continue
}
cp := h
@@ -104,6 +121,13 @@ func Dedupe(in []Hit) []Hit {
return out
}
func firstDate(a, b string) string {
if a != "" {
return a
}
return b
}
func sortBySourceOrder(hits []Hit) {
sort.SliceStable(hits, func(i, j int) bool {
ri, rj := sourceRank[hits[i].Source], sourceRank[hits[j].Source]