package linedb import ( "encoding/json" "fmt" "sync" ) // MemcachedClient — минимальный интерфейс для работы с Memcached. // Реализуйте его поверх вашего клиента (например github.com/bradfitz/gomemcache/memcache). type MemcachedClient interface { Get(key string) ([]byte, error) Set(key string, value []byte, expireSeconds int) error Delete(key string) error } // MemcachedIndexStore — реализация IndexStore с хранением индексов в Memcached. type MemcachedIndexStore struct { client MemcachedClient prefix string expireSec int mu sync.Mutex } // MemcachedIndexStoreOptions — опции для MemcachedIndexStore. type MemcachedIndexStoreOptions struct { Client MemcachedClient KeyPrefix string // префикс ключей (по умолчанию "linedb:idx:") ExpireSeconds int // TTL записей (0 = без истечения) } // NewMemcachedIndexStore создаёт IndexStore с бэкендом Memcached. func NewMemcachedIndexStore(opts MemcachedIndexStoreOptions) (*MemcachedIndexStore, error) { if opts.Client == nil { return nil, fmt.Errorf("MemcachedClient is required") } prefix := opts.KeyPrefix if prefix == "" { prefix = "linedb:idx:" } return &MemcachedIndexStore{ client: opts.Client, prefix: prefix, expireSec: opts.ExpireSeconds, }, nil } func (s *MemcachedIndexStore) memKey(collection, field, value string) string { // Memcached ограничивает длину ключа (обычно 250 байт) k := s.prefix + collection + ":" + field + ":" + value if len(k) > 250 { // Хэшируем длинные значения — упрощённо берём последние 200 символов if len(value) > 200 { k = s.prefix + collection + ":" + field + ":" + value[len(value)-200:] } } return k } // IndexRecord добавляет запись в индекс. func (s *MemcachedIndexStore) IndexRecord(collection string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } for _, field := range fields { val := getFieldValue(record, field) key := s.memKey(collection, field, val) var list []int if data, err := s.client.Get(key); err == nil && len(data) > 0 { _ = json.Unmarshal(data, &list) } list = append(list, lineIndex) if data, err := json.Marshal(list); err == nil { _ = s.client.Set(key, data, s.expireSec) } } } // UnindexRecord удаляет запись из индекса. func (s *MemcachedIndexStore) UnindexRecord(collection string, fields []string, record map[string]any, lineIndex int) { if len(fields) == 0 || record == nil { return } for _, field := range fields { val := getFieldValue(record, field) key := s.memKey(collection, field, val) data, err := s.client.Get(key) if err != nil || len(data) == 0 { continue } var list []int if json.Unmarshal(data, &list) != nil { continue } var newList []int for _, i := range list { if i != lineIndex { newList = append(newList, i) } } if len(newList) == 0 { _ = s.client.Delete(key) } else if data2, err := json.Marshal(newList); err == nil { _ = s.client.Set(key, data2, s.expireSec) } } } // Lookup ищет индексы строк по полю и значению. func (s *MemcachedIndexStore) Lookup(collection, field, value string) ([]int, error) { key := s.memKey(collection, field, value) data, err := s.client.Get(key) if err != nil { return nil, nil } if len(data) == 0 { return nil, nil } var indexes []int if json.Unmarshal(data, &indexes) != nil { return nil, nil } // Возвращаем копию, чтобы вызывающий код не модифицировал внутренний срез out := make([]int, len(indexes)) copy(out, indexes) return out, nil } // Rebuild перестраивает индекс коллекции (удаляет старые ключи по префиксу и записывает новые). func (s *MemcachedIndexStore) Rebuild(collection string, fields []string, records []any) error { s.mu.Lock() defer s.mu.Unlock() // Memcached не поддерживает перечисление ключей по шаблону. // Стратегия: перезаписываем все ключи для текущих записей. // Старые ключи с другими значениями останутся до TTL. // Строим value -> []lineIndex для каждого поля byFieldValue := make(map[string]map[string][]int) for idx, rec := range records { recMap, ok := rec.(map[string]any) if !ok { continue } for _, field := range fields { val := getFieldValue(recMap, field) if byFieldValue[field] == nil { byFieldValue[field] = make(map[string][]int) } byFieldValue[field][val] = append(byFieldValue[field][val], idx) } } for field, valMap := range byFieldValue { for val, list := range valMap { key := s.memKey(collection, field, val) data, err := json.Marshal(list) if err != nil { continue } if err := s.client.Set(key, data, s.expireSec); err != nil { return fmt.Errorf("memcached set %s: %w", key, err) } } } return nil } // Clear очищает индекс коллекции. Memcached не поддерживает delete by pattern, // поэтому метод является заглушкой — старые ключи истекут по TTL. func (s *MemcachedIndexStore) Clear(collection string) error { _ = collection // Опционально: сохранить в отдельном ключе список всех ключей индекса // и удалять их по одному. Упрощённо — ничего не делаем. return nil }