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:
+92
-10
@@ -8,15 +8,35 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stringList []string
|
||||
|
||||
func (s *stringList) String() string { return strings.Join(*s, ",") }
|
||||
func (s *stringList) Set(v string) error {
|
||||
*s = append(*s, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func searchMain() {
|
||||
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
k := fs.Int("k", 0, "result cap")
|
||||
asJSON := fs.Bool("json", false, "native envelope")
|
||||
var include, exclude stringList
|
||||
fs.Var(&include, "include-domain", "restrict or prefer this host")
|
||||
fs.Var(&exclude, "exclude-domain", "drop this host")
|
||||
domainsMode := fs.String("domains-mode", "", "restrict|prefer")
|
||||
timeRange := fs.String("time-range", "", "day|week|month|year")
|
||||
startDate := fs.String("start-date", "", "YYYY-MM-DD")
|
||||
endDate := fs.String("end-date", "", "YYYY-MM-DD")
|
||||
topic := fs.String("topic", "", "general|news|finance")
|
||||
exact := fs.Bool("exact-match", false, "require quoted phrases")
|
||||
minScore := fs.Float64("min-score", 0, "Tavily score cut")
|
||||
queriesFile := fs.String("queries-file", "", "JSON array of 1-4 queries")
|
||||
args := os.Args[1:]
|
||||
flagArgs, pos, err := splitAroundPositional(args, map[string]bool{"k": true})
|
||||
flagArgs, pos, err := splitAroundPositional(args, searchValueFlags)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
@@ -25,20 +45,65 @@ func searchMain() {
|
||||
fmt.Fprintln(os.Stderr, searchUsage)
|
||||
os.Exit(2)
|
||||
}
|
||||
if len(pos) != 1 {
|
||||
fmt.Fprintln(os.Stderr, searchUsage)
|
||||
os.Exit(2)
|
||||
body := map[string]any{}
|
||||
if *queriesFile != "" {
|
||||
if len(pos) != 0 {
|
||||
fmt.Fprintln(os.Stderr, searchUsage)
|
||||
os.Exit(2)
|
||||
}
|
||||
raw, err := os.ReadFile(*queriesFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
}
|
||||
var qs []string
|
||||
if err := json.Unmarshal(raw, &qs); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "queries-file: want JSON array of strings")
|
||||
os.Exit(2)
|
||||
}
|
||||
body["queries"] = qs
|
||||
} else {
|
||||
if len(pos) != 1 {
|
||||
fmt.Fprintln(os.Stderr, searchUsage)
|
||||
os.Exit(2)
|
||||
}
|
||||
body["query"] = pos[0]
|
||||
}
|
||||
if *k > 0 {
|
||||
body["k"] = *k
|
||||
}
|
||||
if len(include) > 0 {
|
||||
body["include_domains"] = []string(include)
|
||||
}
|
||||
if len(exclude) > 0 {
|
||||
body["exclude_domains"] = []string(exclude)
|
||||
}
|
||||
if *domainsMode != "" {
|
||||
body["include_domains_mode"] = *domainsMode
|
||||
}
|
||||
if *timeRange != "" {
|
||||
body["time_range"] = *timeRange
|
||||
}
|
||||
if *startDate != "" {
|
||||
body["start_date"] = *startDate
|
||||
}
|
||||
if *endDate != "" {
|
||||
body["end_date"] = *endDate
|
||||
}
|
||||
if *topic != "" {
|
||||
body["topic"] = *topic
|
||||
}
|
||||
if *exact {
|
||||
body["exact_match"] = true
|
||||
}
|
||||
if *minScore > 0 {
|
||||
body["min_score"] = *minScore
|
||||
}
|
||||
query := pos[0]
|
||||
base, key, err := loadClient()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(5)
|
||||
}
|
||||
body := map[string]any{"query": query}
|
||||
if *k > 0 {
|
||||
body["k"] = *k
|
||||
}
|
||||
raw, _ := json.Marshal(body)
|
||||
req, err := http.NewRequest(http.MethodPost, base+"/v1/search", bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
@@ -80,7 +145,24 @@ func searchMain() {
|
||||
if sn, _ := m["snippet"].(string); sn != "" {
|
||||
fmt.Println(sn)
|
||||
}
|
||||
if d, _ := m["published_date"].(string); d != "" {
|
||||
fmt.Println(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const searchUsage = `usage: sift search "<query>" [--k N] [--json]`
|
||||
var searchValueFlags = map[string]bool{
|
||||
"k": true, "include-domain": true, "exclude-domain": true,
|
||||
"domains-mode": true, "time-range": true, "start-date": true, "end-date": true,
|
||||
"topic": true, "min-score": true, "queries-file": true,
|
||||
}
|
||||
|
||||
const searchUsage = `usage: 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]`
|
||||
|
||||
Reference in New Issue
Block a user