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
151
internal/engines/wikipedia.go
Normal file
151
internal/engines/wikipedia.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package engines
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ashie/gosearch/internal/contracts"
|
||||
)
|
||||
|
||||
type WikipediaEngine struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (e *WikipediaEngine) Name() string { return "wikipedia" }
|
||||
|
||||
func (e *WikipediaEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) {
|
||||
if e == nil || e.client == nil {
|
||||
return contracts.SearchResponse{}, errors.New("wikipedia engine not initialized")
|
||||
}
|
||||
if strings.TrimSpace(req.Query) == "" {
|
||||
return contracts.SearchResponse{Query: req.Query}, nil
|
||||
}
|
||||
|
||||
lang := strings.TrimSpace(req.Language)
|
||||
if lang == "" || lang == "auto" {
|
||||
lang = "en"
|
||||
}
|
||||
// Wikipedia subdomains are based on the language code; keep it simple for MVP.
|
||||
lang = strings.SplitN(lang, "-", 2)[0]
|
||||
lang = strings.ReplaceAll(lang, "_", "-")
|
||||
wikiNetloc := fmt.Sprintf("%s.wikipedia.org", lang)
|
||||
|
||||
endpoint := fmt.Sprintf(
|
||||
"https://%s/api/rest_v1/page/summary/%s",
|
||||
wikiNetloc,
|
||||
url.PathEscape(req.Query),
|
||||
)
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return contracts.SearchResponse{}, err
|
||||
}
|
||||
// Wikimedia APIs require a descriptive User-Agent.
|
||||
httpReq.Header.Set(
|
||||
"User-Agent",
|
||||
"gosearch-go/0.1 (compatible; +https://github.com/ashie/gosearch)",
|
||||
)
|
||||
// Best-effort: hint content language.
|
||||
if req.Language != "" && req.Language != "auto" {
|
||||
httpReq.Header.Set("Accept-Language", req.Language)
|
||||
}
|
||||
|
||||
resp, err := e.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return contracts.SearchResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return contracts.SearchResponse{
|
||||
Query: req.Query,
|
||||
NumberOfResults: 0,
|
||||
Results: []contracts.MainResult{},
|
||||
Answers: []map[string]any{},
|
||||
Corrections: []string{},
|
||||
Infoboxes: []map[string]any{},
|
||||
Suggestions: []string{},
|
||||
UnresponsiveEngines: [][2]string{},
|
||||
}, nil
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024))
|
||||
return contracts.SearchResponse{}, fmt.Errorf("wikipedia upstream error: status=%d body=%q", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var api struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Titles struct {
|
||||
Display string `json:"display"`
|
||||
} `json:"titles"`
|
||||
ContentURLs struct {
|
||||
Desktop struct {
|
||||
Page string `json:"page"`
|
||||
} `json:"desktop"`
|
||||
} `json:"content_urls"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&api); err != nil {
|
||||
return contracts.SearchResponse{}, err
|
||||
}
|
||||
|
||||
pageURL := api.ContentURLs.Desktop.Page
|
||||
if pageURL == "" {
|
||||
// API returned a non-standard payload; treat as no result.
|
||||
return contracts.SearchResponse{
|
||||
Query: req.Query,
|
||||
NumberOfResults: 0,
|
||||
Results: []contracts.MainResult{},
|
||||
Answers: []map[string]any{},
|
||||
Corrections: []string{},
|
||||
Infoboxes: []map[string]any{},
|
||||
Suggestions: []string{},
|
||||
UnresponsiveEngines: [][2]string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
title := api.Titles.Display
|
||||
if title == "" {
|
||||
title = api.Title
|
||||
}
|
||||
|
||||
content := api.Description
|
||||
|
||||
urlPtr := pageURL
|
||||
pub := (*string)(nil)
|
||||
|
||||
results := []contracts.MainResult{
|
||||
{
|
||||
Template: "default.html",
|
||||
Title: title,
|
||||
Content: content,
|
||||
URL: &urlPtr,
|
||||
Pubdate: pub,
|
||||
Engine: "wikipedia",
|
||||
Score: 0,
|
||||
Category: "general",
|
||||
Priority: "",
|
||||
Positions: nil,
|
||||
Engines: []string{"wikipedia"},
|
||||
},
|
||||
}
|
||||
|
||||
return contracts.SearchResponse{
|
||||
Query: req.Query,
|
||||
NumberOfResults: len(results),
|
||||
Results: results,
|
||||
Answers: []map[string]any{},
|
||||
Corrections: []string{},
|
||||
Infoboxes: []map[string]any{},
|
||||
Suggestions: []string{},
|
||||
UnresponsiveEngines: [][2]string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue