feat: build Go-based SearXNG-compatible search service
Implement an API-first Go rewrite with local engine adapters, upstream fallback, and Nix-based tooling so searches can run without matching the original UI while preserving response compatibility. Made-with: Cursor
This commit is contained in:
parent
7783367c71
commit
dc44837219
32 changed files with 3330 additions and 0 deletions
121
internal/search/merge.go
Normal file
121
internal/search/merge.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ashie/gosearch/internal/contracts"
|
||||
)
|
||||
|
||||
// MergeResponses merges multiple SearXNG-compatible JSON responses.
|
||||
//
|
||||
// MVP merge semantics:
|
||||
// - results are concatenated with a simple de-dup key (engine|title|url)
|
||||
// - suggestions/corrections are de-duplicated as sets
|
||||
// - answers/infoboxes/unresponsive_engines are concatenated (best-effort)
|
||||
func MergeResponses(responses []contracts.SearchResponse) contracts.SearchResponse {
|
||||
var merged contracts.SearchResponse
|
||||
|
||||
mergedResultSeen := map[string]struct{}{}
|
||||
mergedAnswerSeen := map[string]struct{}{}
|
||||
mergedCorrectionsSeen := map[string]struct{}{}
|
||||
mergedSuggestionsSeen := map[string]struct{}{}
|
||||
|
||||
for _, r := range responses {
|
||||
if merged.Query == "" {
|
||||
merged.Query = r.Query
|
||||
}
|
||||
|
||||
merged.NumberOfResults = maxInt(merged.NumberOfResults, r.NumberOfResults)
|
||||
|
||||
for _, mr := range r.Results {
|
||||
key := resultDedupKey(mr)
|
||||
if _, ok := mergedResultSeen[key]; ok {
|
||||
continue
|
||||
}
|
||||
mergedResultSeen[key] = struct{}{}
|
||||
merged.Results = append(merged.Results, mr)
|
||||
}
|
||||
|
||||
for _, ans := range r.Answers {
|
||||
// De-dup by normalized JSON when possible.
|
||||
b, err := json.Marshal(ans)
|
||||
if err != nil {
|
||||
merged.Answers = append(merged.Answers, ans)
|
||||
continue
|
||||
}
|
||||
key := string(b)
|
||||
if _, ok := mergedAnswerSeen[key]; ok {
|
||||
continue
|
||||
}
|
||||
mergedAnswerSeen[key] = struct{}{}
|
||||
merged.Answers = append(merged.Answers, ans)
|
||||
}
|
||||
|
||||
merged.Corrections = unionStrings(merged.Corrections, r.Corrections, &mergedCorrectionsSeen)
|
||||
merged.Suggestions = unionStrings(merged.Suggestions, r.Suggestions, &mergedSuggestionsSeen)
|
||||
|
||||
merged.Infoboxes = append(merged.Infoboxes, r.Infoboxes...)
|
||||
merged.UnresponsiveEngines = append(merged.UnresponsiveEngines, r.UnresponsiveEngines...)
|
||||
}
|
||||
|
||||
// Ensure non-nil slices to keep JSON shape stable.
|
||||
if merged.Results == nil {
|
||||
merged.Results = []contracts.MainResult{}
|
||||
}
|
||||
if merged.Answers == nil {
|
||||
merged.Answers = []map[string]any{}
|
||||
}
|
||||
if merged.Corrections == nil {
|
||||
merged.Corrections = []string{}
|
||||
}
|
||||
if merged.Infoboxes == nil {
|
||||
merged.Infoboxes = []map[string]any{}
|
||||
}
|
||||
if merged.Suggestions == nil {
|
||||
merged.Suggestions = []string{}
|
||||
}
|
||||
if merged.UnresponsiveEngines == nil {
|
||||
merged.UnresponsiveEngines = [][2]string{}
|
||||
}
|
||||
|
||||
return merged
|
||||
}
|
||||
|
||||
func resultDedupKey(r contracts.MainResult) string {
|
||||
urlStr := ""
|
||||
if r.URL != nil {
|
||||
urlStr = *r.URL
|
||||
}
|
||||
// Normalize host to reduce duplicates.
|
||||
if u, err := url.Parse(urlStr); err == nil {
|
||||
if u.Host != "" {
|
||||
urlStr = u.Host + u.Path
|
||||
}
|
||||
}
|
||||
return strings.ToLower(r.Engine) + "|" + strings.ToLower(r.Title) + "|" + urlStr
|
||||
}
|
||||
|
||||
func unionStrings(dst []string, src []string, seen *map[string]struct{}) []string {
|
||||
if *seen == nil {
|
||||
*seen = map[string]struct{}{}
|
||||
}
|
||||
out := dst
|
||||
for _, s := range src {
|
||||
if _, ok := (*seen)[s]; ok {
|
||||
continue
|
||||
}
|
||||
(*seen)[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue