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:
Franz Kafka 2026-03-20 20:34:08 +01:00
parent 7783367c71
commit dc44837219
32 changed files with 3330 additions and 0 deletions

View file

@ -0,0 +1,80 @@
package search
import (
"strings"
"testing"
"github.com/ashie/gosearch/internal/contracts"
)
func TestMergeResponses_DedupResultsAndSets(t *testing.T) {
url1 := "https://example.com/a?x=1"
uPtr := &url1
r1 := contracts.SearchResponse{
Query: "q",
NumberOfResults: 1,
Results: []contracts.MainResult{
{
Template: "default.html",
Title: "Title1",
Content: "C1",
URL: uPtr,
Engine: "wikipedia",
Score: 1.0,
},
},
Answers: []map[string]any{{"title": "A1", "url": url1}},
Corrections: []string{"corr1", "corr2"},
Suggestions: []string{"s1", "s2"},
Infoboxes: []map[string]any{},
UnresponsiveEngines: [][2]string{},
}
r2 := contracts.SearchResponse{
Query: "q",
NumberOfResults: 1,
Results: []contracts.MainResult{
{
Template: "default.html",
Title: "Title1",
Content: "C2",
URL: uPtr,
Engine: "wikipedia",
Score: 2.0,
},
},
Answers: []map[string]any{{"title": "A1", "url": url1}},
Corrections: []string{"corr2", "corr3"},
Suggestions: []string{"s2", "s3"},
Infoboxes: []map[string]any{},
UnresponsiveEngines: [][2]string{},
}
merged := MergeResponses([]contracts.SearchResponse{r1, r2})
if merged.Query != "q" {
t.Fatalf("expected query q, got %q", merged.Query)
}
if merged.NumberOfResults != 1 {
t.Fatalf("expected number_of_results max=1, got %d", merged.NumberOfResults)
}
if len(merged.Results) != 1 {
t.Fatalf("expected 1 merged result, got %d", len(merged.Results))
}
// Corrections/suggestions should be unioned.
joinedCorr := strings.Join(merged.Corrections, ",")
if !strings.Contains(joinedCorr, "corr1") || !strings.Contains(joinedCorr, "corr2") || !strings.Contains(joinedCorr, "corr3") {
t.Fatalf("expected unioned corrections, got %v", merged.Corrections)
}
joinedSug := strings.Join(merged.Suggestions, ",")
if !strings.Contains(joinedSug, "s1") || !strings.Contains(joinedSug, "s2") || !strings.Contains(joinedSug, "s3") {
t.Fatalf("expected unioned suggestions, got %v", merged.Suggestions)
}
if len(merged.Answers) != 1 {
t.Fatalf("expected 1 merged answer, got %d", len(merged.Answers))
}
}