memoryweave/go/internal/storage/searchcache.go

139 lines
2.9 KiB
Go

// 织忆 MemoryWeave — 搜索引擎缓存层(多 Agent 支持)
package storage
import (
"crypto/sha256"
"encoding/json"
"fmt"
"sync"
"time"
)
// SearchCache 基于 LRU + TTL 的搜索缓存
// 多 Agent 场景:本地缓存 + 跨 Agent 通过 API 层调用 InvalidateRemote 失效
type SearchCache struct {
mu sync.RWMutex
entries map[string]*CacheEntry
maxSize int
ttl time.Duration
}
type CacheEntry struct {
Key string `json:"key"`
Results []byte `json:"results"`
CreatedAt time.Time `json:"created_at"`
Hits int `json:"hits"`
}
func NewSearchCache(maxSize int, ttl time.Duration) *SearchCache {
sc := &SearchCache{
entries: make(map[string]*CacheEntry),
maxSize: maxSize,
ttl: ttl,
}
// 后台清理过期条目
go sc.reaper()
return sc
}
// Get 获取缓存结果
func (sc *SearchCache) Get(query, namespace string) ([]byte, bool) {
key := cacheKey(query, namespace)
sc.mu.RLock()
entry, ok := sc.entries[key]
sc.mu.RUnlock()
if !ok || time.Since(entry.CreatedAt) > sc.ttl {
return nil, false
}
sc.mu.Lock()
entry.Hits++
sc.mu.Unlock()
return entry.Results, true
}
// Set 写入缓存
func (sc *SearchCache) Set(query, namespace string, results interface{}) {
key := cacheKey(query, namespace)
data, err := json.Marshal(results)
if err != nil {
return
}
sc.mu.Lock()
defer sc.mu.Unlock()
// LRU 驱逐
if len(sc.entries) >= sc.maxSize {
sc.evictLRU()
}
sc.entries[key] = &CacheEntry{
Key: key,
Results: data,
CreatedAt: time.Now(),
Hits: 0,
}
}
// Stats 返回缓存统计
func (sc *SearchCache) Stats() map[string]interface{} {
sc.mu.RLock()
defer sc.mu.RUnlock()
return map[string]interface{}{
"size": len(sc.entries),
"max_size": sc.maxSize,
"ttl": sc.ttl.String(),
}
}
// Invalidate 使指定 namespace 的缓存失效(本地)
func (sc *SearchCache) Invalidate(namespace string) {
sc.mu.Lock()
defer sc.mu.Unlock()
for key, entry := range sc.entries {
var parsed map[string]interface{}
json.Unmarshal(entry.Results, &parsed)
if ns, ok := parsed["namespace"].(string); ok && ns == namespace {
delete(sc.entries, key)
}
}
}
func (sc *SearchCache) evictLRU() {
var oldestKey string
var oldestTime time.Time
for key, entry := range sc.entries {
if oldestKey == "" || entry.CreatedAt.Before(oldestTime) {
oldestKey = key
oldestTime = entry.CreatedAt
}
}
if oldestKey != "" {
delete(sc.entries, oldestKey)
}
}
func (sc *SearchCache) reaper() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
sc.mu.Lock()
for key, entry := range sc.entries {
if time.Since(entry.CreatedAt) > sc.ttl {
delete(sc.entries, key)
}
}
sc.mu.Unlock()
}
}
func cacheKey(query, namespace string) string {
h := sha256.Sum256([]byte(query + "|" + namespace))
return fmt.Sprintf("%x", h[:16])
}
// 全局搜索缓存实例
var SearchCacheInstance = NewSearchCache(1000, 1*time.Hour)