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:
parent
385a7acab7
commit
94322ceff4
9 changed files with 361 additions and 9 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ashie/gosearch/internal/cache"
|
||||
"github.com/ashie/gosearch/internal/contracts"
|
||||
"github.com/ashie/gosearch/internal/engines"
|
||||
"github.com/ashie/gosearch/internal/upstream"
|
||||
|
|
@ -14,12 +15,14 @@ import (
|
|||
type ServiceConfig struct {
|
||||
UpstreamURL string
|
||||
HTTPTimeout time.Duration
|
||||
Cache *cache.Cache
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
upstreamClient *upstream.Client
|
||||
planner *engines.Planner
|
||||
localEngines map[string]engines.Engine
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewService(cfg ServiceConfig) *Service {
|
||||
|
|
@ -42,6 +45,7 @@ func NewService(cfg ServiceConfig) *Service {
|
|||
upstreamClient: up,
|
||||
planner: engines.NewPlannerFromEnv(),
|
||||
localEngines: engines.NewDefaultPortedEngines(httpClient),
|
||||
cache: cfg.Cache,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +54,34 @@ func NewService(cfg ServiceConfig) *Service {
|
|||
//
|
||||
// Individual engine failures are reported as unresponsive_engines rather
|
||||
// than aborting the entire search.
|
||||
//
|
||||
// If a Valkey cache is configured and contains a cached response for this
|
||||
// request, the cached result is returned without hitting any engines.
|
||||
func (s *Service) Search(ctx context.Context, req SearchRequest) (SearchResponse, error) {
|
||||
// Check cache first.
|
||||
if s.cache != nil {
|
||||
cacheKey := cache.Key(req)
|
||||
if cached, hit := s.cache.Get(ctx, cacheKey); hit {
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
|
||||
merged, err := s.executeSearch(ctx, req)
|
||||
if err != nil {
|
||||
return SearchResponse{}, err
|
||||
}
|
||||
|
||||
// Store in cache.
|
||||
if s.cache != nil {
|
||||
cacheKey := cache.Key(req)
|
||||
s.cache.Set(ctx, cacheKey, merged)
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// executeSearch runs the actual engine queries and merges results.
|
||||
func (s *Service) executeSearch(ctx context.Context, req SearchRequest) (SearchResponse, error) {
|
||||
localEngineNames, upstreamEngineNames, _ := s.planner.Plan(req)
|
||||
|
||||
// Run all local engines concurrently.
|
||||
|
|
@ -176,5 +207,3 @@ func shouldFallbackToUpstream(engineName string, r contracts.SearchResponse) boo
|
|||
}
|
||||
return len(r.Results) == 0 && len(r.Answers) == 0 && len(r.Infoboxes) == 0
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue