feat: Valkey cache for search results

- Add internal/cache package using go-redis/v9 (Valkey-compatible)
- Cache keys are deterministic SHA-256 hashes of search parameters
- Cache wraps the Search() method: check cache → miss → execute → store
- Gracefully disabled if Valkey is unreachable or unconfigured
- Configurable TTL (default 5m), address, password, and DB index
- Environment variable overrides: VALKEY_ADDRESS, VALKEY_PASSWORD,
  VALKEY_DB, VALKEY_CACHE_TTL
- Structured JSON logging via slog throughout cache layer
- Refactored service.go: extract executeSearch() from Search() for clarity
- Update config.example.toml with [cache] section
- Add cache package tests (key generation, nop behavior)
This commit is contained in:
Franz Kafka 2026-03-21 15:43:47 +00:00
parent 385a7acab7
commit 94322ceff4
9 changed files with 361 additions and 9 deletions

View file

@ -11,9 +11,10 @@ import (
// Config is the top-level configuration for the gosearch service.
type Config struct {
Server ServerConfig `toml:"server"`
Server ServerConfig `toml:"server"`
Upstream UpstreamConfig `toml:"upstream"`
Engines EnginesConfig `toml:"engines"`
Engines EnginesConfig `toml:"engines"`
Cache CacheConfig `toml:"cache"`
}
type ServerConfig struct {
@ -26,11 +27,19 @@ type UpstreamConfig struct {
}
type EnginesConfig struct {
LocalPorted []string `toml:"local_ported"`
LocalPorted []string `toml:"local_ported"`
Brave BraveConfig `toml:"brave"`
Qwant QwantConfig `toml:"qwant"`
}
// CacheConfig holds Valkey/Redis cache settings.
type CacheConfig struct {
Address string `toml:"address"` // Valkey server address (e.g. "localhost:6379")
Password string `toml:"password"` // Auth password (empty = none)
DB int `toml:"db"` // Database index (default 0)
DefaultTTL string `toml:"default_ttl"` // Cache TTL (e.g. "5m", default "5m")
}
type BraveConfig struct {
APIKey string `toml:"api_key"`
AccessToken string `toml:"access_token"`
@ -71,6 +80,10 @@ func defaultConfig() *Config {
ResultsPerPage: 10,
},
},
Cache: CacheConfig{
DB: 0,
DefaultTTL: "5m",
},
}
}
@ -99,6 +112,18 @@ func applyEnvOverrides(cfg *Config) {
if v := os.Getenv("BRAVE_ACCESS_TOKEN"); v != "" {
cfg.Engines.Brave.AccessToken = v
}
if v := os.Getenv("VALKEY_ADDRESS"); v != "" {
cfg.Cache.Address = v
}
if v := os.Getenv("VALKEY_PASSWORD"); v != "" {
cfg.Cache.Password = v
}
if v := os.Getenv("VALKEY_DB"); v != "" {
fmt.Sscanf(v, "%d", &cfg.Cache.DB)
}
if v := os.Getenv("VALKEY_CACHE_TTL"); v != "" {
cfg.Cache.DefaultTTL = v
}
}
// HTTPTimeout parses the configured timeout string into a time.Duration.
@ -114,6 +139,14 @@ func (c *Config) LocalPortedCSV() string {
return strings.Join(c.Engines.LocalPorted, ",")
}
// CacheTTL parses the configured cache TTL string into a time.Duration.
func (c *Config) CacheTTL() time.Duration {
if d, err := time.ParseDuration(c.Cache.DefaultTTL); err == nil && d > 0 {
return d
}
return 5 * time.Minute
}
func splitCSV(s string) []string {
if s == "" {
return nil