6.5 KiB
6.5 KiB
Индексы в LineDB
Обзор
Индексы ускоряют поиск записей по полям при вызове ReadByFilter. Без индексов каждый запрос сканирует весь JSONL‑файл. С индексами для простых фильтров вида { field: value } используется предварительно построенный индекс, и возвращаются только подходящие записи.
Как это работает
- Конфигурация: в
JSONLFileOptionsзадаётсяIndexedFields— список полей, по которым строится индекс. - Хранилище: данные индекса лежат в
IndexStore— реализация интерфейса, которую можно подключать. - При Init: если есть коллекции с
IndexedFields, индекс строится из текущих данных. - При Insert: новые записи добавляются в индекс.
- При Update/Delete: индекс коллекции полностью перестраивается.
- При ReadByFilter: если фильтр — одно поле из
IndexedFieldsи точное совпадение, используется индекс; иначе — полный скан файла.
Интерфейс IndexStore
type IndexStore interface {
// Возвращает индексы строк (0-based) для заданного значения поля.
Lookup(collection, field, value string) ([]int, error)
// Полная пересборка индекса по всем записям коллекции.
Rebuild(collection string, fields []string, records []any) error
// Очистить индекс коллекции.
Clear(collection string) error
}
Lookup— поиск по(collection, field, value), возвращает номера строк в файле.Rebuild— полная пересборка индекса коллекции (использует порядок записей как номера строк).Clear— очистить индекс коллекции.
Реализации
1. InMemoryIndexStore (по умолчанию)
Используется автоматически при IndexedFields и IndexStore == nil:
initOptions := &linedb.LineDbInitOptions{
DBFolder: "./data",
Collections: []linedb.JSONLFileOptions{
{
CollectionName: "users",
AllocSize: 256,
IndexedFields: []string{"id", "email", "name"},
},
},
}
db := linedb.NewLineDb(nil)
db.Init(false, initOptions)
// Индекс создаётся в памяти автоматически
Явная установка:
store := linedb.NewInMemoryIndexStore()
db := linedb.NewLineDb(&linedb.LineDbOptions{IndexStore: store})
db.Init(false, initOptions)
2. MemcachedIndexStore
Хранит индексы в Memcached. Требуется реализовать интерфейс MemcachedClient:
type MemcachedClient interface {
Get(key string) ([]byte, error)
Set(key string, value []byte, expireSeconds int) error
Delete(key string) error
}
Пример с gomemcache:
import "github.com/bradfitz/gomemcache/memcache"
// Адаптер под MemcachedClient
type memcacheAdapter struct {
c *memcache.Client
}
func (m *memcacheAdapter) Get(key string) ([]byte, error) {
it, err := m.c.Get(key)
if err == memcache.ErrCacheMiss {
return nil, nil
}
if err != nil {
return nil, err
}
return it.Value, nil
}
func (m *memcacheAdapter) Set(key string, value []byte, exp int) error {
return m.c.Set(&memcache.Item{Key: key, Value: value, Expiration: int32(exp)})
}
func (m *memcacheAdapter) Delete(key string) error {
return m.c.Delete(key)
}
// Использование
client := memcache.New("127.0.0.1:11211")
store, err := linedb.NewMemcachedIndexStore(linedb.MemcachedIndexStoreOptions{
Client: &memcacheAdapter{c: client},
KeyPrefix: "linedb:idx:",
ExpireSeconds: 3600,
})
if err != nil {
log.Fatal(err)
}
db := linedb.NewLineDb(&linedb.LineDbOptions{IndexStore: store})
db.Init(false, initOptions)
Подключение реализации
Вариант A: In-memory по умолчанию
Ничего не указывать — при наличии IndexedFields создаётся in-memory индекс:
db := linedb.NewLineDb(nil)
initOptions.Collections[0].IndexedFields = []string{"id", "email"}
db.Init(false, initOptions)
Вариант B: Явно in-memory
store := linedb.NewInMemoryIndexStore()
db := linedb.NewLineDb(&linedb.LineDbOptions{IndexStore: store})
db.Init(false, initOptions)
Вариант C: Memcached
store, _ := linedb.NewMemcachedIndexStore(linedb.MemcachedIndexStoreOptions{
Client: myMemcachedClient,
})
db := linedb.NewLineDb(&linedb.LineDbOptions{IndexStore: store})
db.Init(false, initOptions)
Вариант D: Собственная реализация
Реализуйте интерфейс IndexStore и передайте её в LineDbOptions.IndexStore.
Ограничения
- Индекс используется только при одном поле в фильтре и точном совпадении значения.
- Для партиционированных коллекций индекс не используется.
MemcachedIndexStore.Clearне удаляет ключи (Memcached не поддерживает поиск по префиксу); старые ключи исчезнут по TTL.- При Update и Delete индекс перестраивается полностью (чтение всех записей и Rebuild).
Примеры
// Поиск по email (если email в IndexedFields)
users, err := db.ReadByFilter(map[string]any{"email": "alice@test.com"}, "users", opts)
// Поиск по id
user, err := db.ReadByFilter(map[string]any{"id": 1}, "users", opts)
// Фильтр по нескольким полям — индекс не используется, идёт полный скан
users, err := db.ReadByFilter(map[string]any{"name": "alice", "active": true}, "users", opts)