104 lines
2.6 KiB
Go
104 lines
2.6 KiB
Go
package search
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
)
|
|
|
|
// SearchResult represents a single web search result.
|
|
type SearchResult struct {
|
|
Title string `json:"title"`
|
|
URL string `json:"url"`
|
|
Snippet string `json:"snippet"`
|
|
}
|
|
|
|
// Provider is the interface for web search backends.
|
|
type Provider interface {
|
|
// Search performs a web search and returns up to maxResults results.
|
|
Search(ctx context.Context, query string, maxResults int) ([]SearchResult, error)
|
|
|
|
// Name returns the provider identifier (e.g. "duckduckgo", "searxng").
|
|
Name() string
|
|
}
|
|
|
|
// ── Provider Registry ───────────────────────
|
|
|
|
var (
|
|
mu sync.RWMutex
|
|
providers = map[string]Provider{}
|
|
active string
|
|
)
|
|
|
|
// RegisterProvider adds a search provider to the registry.
|
|
func RegisterProvider(p Provider) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
providers[p.Name()] = p
|
|
log.Printf("🔍 Registered search provider: %s", p.Name())
|
|
}
|
|
|
|
// SetActive sets the active search provider by name.
|
|
func SetActive(name string) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if _, ok := providers[name]; !ok {
|
|
return fmt.Errorf("unknown search provider: %s", name)
|
|
}
|
|
active = name
|
|
log.Printf("🔍 Active search provider: %s", name)
|
|
return nil
|
|
}
|
|
|
|
// Active returns the currently active provider, or nil if none configured.
|
|
func Active() Provider {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
if active == "" {
|
|
return nil
|
|
}
|
|
return providers[active]
|
|
}
|
|
|
|
// Available returns the names of all registered providers.
|
|
func Available() []string {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
names := make([]string, 0, len(providers))
|
|
for name := range providers {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// Config holds the persisted search configuration.
|
|
type Config struct {
|
|
Provider string `json:"provider"` // "duckduckgo" or "searxng"
|
|
Endpoint string `json:"endpoint"` // SearXNG endpoint URL
|
|
APIKey string `json:"api_key"` // SearXNG API key (optional)
|
|
MaxResults int `json:"max_results"` // default results per search
|
|
}
|
|
|
|
// DefaultConfig returns the default search config (DuckDuckGo, no key needed).
|
|
func DefaultConfig() Config {
|
|
return Config{Provider: "duckduckgo", MaxResults: 5}
|
|
}
|
|
|
|
// ApplyConfig applies a Config to the active provider registry.
|
|
// Called on startup and when admin saves settings.
|
|
func ApplyConfig(cfg Config) error {
|
|
if cfg.Provider == "" {
|
|
cfg.Provider = "duckduckgo"
|
|
}
|
|
|
|
// Configure SearXNG if selected
|
|
mu.RLock()
|
|
if sxng, ok := providers["searxng"].(*SearXNG); ok {
|
|
sxng.Configure(cfg.Endpoint, cfg.APIKey)
|
|
}
|
|
mu.RUnlock()
|
|
|
|
return SetActive(cfg.Provider)
|
|
}
|