package search import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) type SearXNGClient struct { baseURL string client *http.Client } type SearXNGResult struct { Title string `json:"title"` URL string `json:"url"` Content string `json:"content"` Engine string `json:"engine"` } type SearXNGResponse struct { Results []SearXNGResult `json:"results"` } func NewSearXNGClient(baseURL string) *SearXNGClient { return &SearXNGClient{ baseURL: baseURL, client: &http.Client{Timeout: 15 * time.Second}, } } func (c *SearXNGClient) Search(ctx context.Context, query string, limit int) ([]SearXNGResult, error) { u, err := url.Parse(c.baseURL + "/search") if err != nil { return nil, fmt.Errorf("parse url: %w", err) } q := u.Query() q.Set("q", query) q.Set("format", "json") q.Set("language", "ru-RU,en-US") q.Set("categories", "general,news") if limit > 0 { q.Set("pageno", "1") } u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("User-Agent", "DeepRes/1.0") resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("search request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("searxng returned %d: %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } var sr SearXNGResponse if err := json.Unmarshal(body, &sr); err != nil { return nil, fmt.Errorf("parse response: %w", err) } if len(sr.Results) > limit { sr.Results = sr.Results[:limit] } return sr.Results, nil } // SearchWithVariations запускает несколько поисковых запросов с разными формулировками func (c *SearXNGClient) SearchWithVariations(ctx context.Context, baseQuery string, variations []string, limit int) ([]SearXNGResult, error) { seen := make(map[string]bool) var allResults []SearXNGResult queries := append([]string{baseQuery}, variations...) for _, q := range queries { results, err := c.Search(ctx, q, limit) if err != nil { // Логируем но продолжаем — один упавший запрос не ломает весь ресерч continue } for _, r := range results { if !seen[r.URL] { seen[r.URL] = true allResults = append(allResults, r) } } } return allResults, nil }