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:
@@ -0,0 +1,166 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func resultHost(raw string) string {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
h := strings.ToLower(u.Hostname())
|
||||
return strings.TrimSuffix(h, ".")
|
||||
}
|
||||
|
||||
func hostMatch(host, d string) bool {
|
||||
return host == d || strings.HasSuffix(host, "."+d)
|
||||
}
|
||||
|
||||
func filterDomains(hits []Hit, f Filter) []Hit {
|
||||
if len(f.IncludeDomains) == 0 && len(f.ExcludeDomains) == 0 {
|
||||
return hits
|
||||
}
|
||||
mode := f.IncludeDomainsMode
|
||||
if mode == "" {
|
||||
mode = "restrict"
|
||||
}
|
||||
restrict := mode != "prefer" && len(f.IncludeDomains) > 0
|
||||
out := make([]Hit, 0, len(hits))
|
||||
for _, h := range hits {
|
||||
host := resultHost(h.URL)
|
||||
if host == "" {
|
||||
if restrict {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
drop := false
|
||||
for _, d := range f.ExcludeDomains {
|
||||
if hostMatch(host, d) {
|
||||
drop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if drop {
|
||||
continue
|
||||
}
|
||||
if restrict {
|
||||
ok := false
|
||||
for _, d := range f.IncludeDomains {
|
||||
if hostMatch(host, d) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func filterMinScore(hits []Hit, min float64) []Hit {
|
||||
if min <= 0 {
|
||||
return hits
|
||||
}
|
||||
out := make([]Hit, 0, len(hits))
|
||||
for _, h := range hits {
|
||||
if h.Score >= min {
|
||||
out = append(out, h)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func dedupeStrings(in []string) []string {
|
||||
seen := map[string]bool{}
|
||||
out := make([]string, 0, len(in))
|
||||
for _, s := range in {
|
||||
if s == "" || seen[s] {
|
||||
continue
|
||||
}
|
||||
seen[s] = true
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func searxQueryAndNotes(q string, f Filter) (string, []string) {
|
||||
var notes []string
|
||||
if f.IncludeDomainsMode == "prefer" && len(f.IncludeDomains) > 0 {
|
||||
notes = append(notes, "searxng: include_domains_mode=prefer not applied")
|
||||
} else if n := len(f.IncludeDomains); n == 1 {
|
||||
q = "site:" + f.IncludeDomains[0] + " " + q
|
||||
} else if n > 1 {
|
||||
max := n
|
||||
if max > searxSiteCap {
|
||||
max = searxSiteCap
|
||||
notes = append(notes, "searxng: extra include_domains dropped")
|
||||
}
|
||||
var b strings.Builder
|
||||
for i := 0; i < max; i++ {
|
||||
if i > 0 {
|
||||
b.WriteString(" OR ")
|
||||
}
|
||||
b.WriteString("site:")
|
||||
b.WriteString(f.IncludeDomains[i])
|
||||
}
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(q)
|
||||
q = b.String()
|
||||
}
|
||||
if f.Topic == "finance" {
|
||||
notes = append(notes, "searxng: topic ignored")
|
||||
}
|
||||
if f.StartDate != "" || f.EndDate != "" {
|
||||
notes = append(notes, "searxng: start_date/end_date not applied")
|
||||
}
|
||||
if f.MinScore > 0 {
|
||||
notes = append(notes, "searxng: min_score not applied")
|
||||
}
|
||||
return q, notes
|
||||
}
|
||||
|
||||
func braveNotes(f Filter) []string {
|
||||
var notes []string
|
||||
if f.TimeRange != "" {
|
||||
notes = append(notes, "brave_llm: time_range not applied")
|
||||
}
|
||||
if f.Topic != "" && f.Topic != "general" {
|
||||
notes = append(notes, "brave_llm: topic not applied")
|
||||
}
|
||||
if f.ExactMatch {
|
||||
notes = append(notes, "brave_llm: exact_match not applied")
|
||||
}
|
||||
if f.IncludeDomainsMode == "prefer" && len(f.IncludeDomains) > 0 {
|
||||
notes = append(notes, "brave_llm: include_domains_mode=prefer not applied")
|
||||
}
|
||||
if f.MinScore > 0 {
|
||||
notes = append(notes, "brave_llm: min_score not applied")
|
||||
}
|
||||
return notes
|
||||
}
|
||||
|
||||
func FilterKinds(f Filter) string {
|
||||
var k []string
|
||||
if len(f.IncludeDomains) > 0 || len(f.ExcludeDomains) > 0 {
|
||||
k = append(k, "domains")
|
||||
}
|
||||
if f.TimeRange != "" || f.StartDate != "" || f.EndDate != "" {
|
||||
k = append(k, "time")
|
||||
}
|
||||
if f.Topic != "" && f.Topic != "general" {
|
||||
k = append(k, "topic")
|
||||
}
|
||||
if f.ExactMatch {
|
||||
k = append(k, "exact")
|
||||
}
|
||||
if f.MinScore > 0 {
|
||||
k = append(k, "score")
|
||||
}
|
||||
return strings.Join(k, ",")
|
||||
}
|
||||
Reference in New Issue
Block a user