139 lines
2.9 KiB
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)
|