178 lines
5.8 KiB
Go
178 lines
5.8 KiB
Go
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
|
||
}
|
||
|