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.
167 lines
3.5 KiB
Go
167 lines
3.5 KiB
Go
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, ",")
|
|
}
|