← к ленте

Оптимизация индексов: fast append и first+rest

D@dariasroomавтор про «it»
2 нед

Разбор оптимизаций для поисковых индексов: внедрение fast append для документов и оценка эффективности стратегии first+rest в HAMT и radix структурах.

Практический опыт оптимизации структур данных для поисковых систем.

  • Fast append позволяет ускорить вставку документов, если их индексы идут по возрастанию.
  • Стратегия first+rest снижает количество аллокаций, но может не давать выигрыша в памяти или скорости поиска на общих данных.
  • Оптимизации, эффективные для специфических задач (например, логи), не всегда подходят для универсальных индексов.
Недавно @dmedovich добавил в движок flat inverted index под данные с высокой кардинальностью и даже написал отдельную статью про его устройство и оптимизации. Мое дело, конечно, найти, что из этого имеет смысл перенести в мои существующие индексаторы HAMT/radix. Потому что заинтересовала меня не столько хешмапа, на которой строится индекс, сколько обвязка вокруг нее. Начала с простого: fast append для списка документов. У каждого слова есть список доков, где оно содержится (postings). Эти postings отсортированы по внутреннему числовому индексу документа (ordinal). При обычной индексации ordinal почти всегда инкрементится по возврастанию: 1, 2, 4, 7, 8... Ранее для вставки, например, документа с ordinal 6, требовалось проходится бинарным поиском по всему срезу ordinals для поиска индекса между 4 и 7. Но в типичном случае новый ordinal просто больше последнего, поэтому сначала можно проверить хвост (подробнее):

 last := len(d) - 1

 // Если last документ совпадает с добавляемым - просто увеличиваем частоту.
 if last >= 0 && d[last].Ord == ord {
  d[last].Count++
  return d
 }

 // Новый документ
 if last < 0 || ord > d[last].Ord {
  return append(d, fts.DocRef{
   Ord:   ord,
   Count: 1,
  })
 }

 // Док с индексом в середине, ищем нужное место по бин поиску
 i := sort.Search(len(d), func(i int) bool {
  return d[i].Ord >= ord
 })
}
Получился быстрый путь для вставки доков с фоллбеком на бинпоиск. Добавила это оптимизацию в итоге и в HAMT, и в radix. Вторая идея: fast + rest для списка postings Допустим,`rare-word` терм встретился только в одном документе:

rare-word -> [doc 42]
В обычном HAMT список документов хранится в cлайсе []Posting. Даже ради одного дока 42 создаётся слайc и все ему сопутствующее, в котором лежит этот единственный posting. Таких редких термов в высококардинальных данных может быть очень много. Идея first + rest в том, чтобы первый posting хранить прямо внутри entry:

type postings struct {
    first Posting
    rest  []Posting
}
Тогда для term, который встретился только в одном документе (`df=1`), first содержит этот документ, а rest остаётся ниловым. Если term встретился ещё раз, следующие документы уже складываются в rest:

df=1:  first=[42]  rest=nil
df=3:  first=[42]  rest=[57, 81]
Для данных, где df=1 термов очень много, можно скосить по аллокации. Но мой HAMT - скорее индекс общего назначения, поэтому мне было интересно, что будет по бенчам на обычном тексте. Для начала прогнала тест на 4096 синтетических термов.

HAMT        22433 allocs/op
HAMT-first  18337 allocs/op
Ожидаемо, разница ровно 4096 - исчезло по одной аллокации на каждый терм. Но B/op почти не изменился: массивы мы убрали, зато увеличили структуру entry за счет поля first. Дальше был Zipf тест, где одновременно есть редкие, среднечастотные и частые слова. Там HAMT-first уже стабильно проигрывал по build. Получилось примерно следующее:

build          +0.6% у HAMT-first
heap           610.3 -> 611.5 MB
heap objects   -4.6%

term search    -2.4%
boolean        ≈ одинаково
phrase         +0.9%
То есть first + rest делает свое дела за счет уменьшения количества маленьких heap-объектов. Но на обычных запросах это не дало ни меньшего retained heap, ни ускорения поиска. Получается, цена фичи - это оптимизация df=1, но поле first появляется у каждого терма - в том числе у тех, у которых есть появления в куче других документов. Плюс надо будет работать с разделением first/rest, что как-никак влияет на читаемость. Поэтому HAMT индекс с first+rest я решила не оставлять. Но сама идея вполне рабочая. Просто, кажется, ей действительно подходит отдельный хешмап индекс, где df=1 - основное свойство данных (например, логи), а не частный случай внутри универсального текстового индекса по обычным докам (например статьям). Ну и я поняла, что недостаточно смотреть только на`allocs/op` - можно убрать кучу аллокаций и в итоге вообще не уменьшить размер хипа.

Кратко (AI)

Автор анализирует применимость оптимизаций из flat inverted index для своих структур данных HAMT и radix. Внедрение fast append для документов показало эффективность, однако стратегия first+rest для хранения редких термов не дала значимого прироста производительности на общих данных, несмотря на снижение количества аллокаций.

Обсуждение

0
В

Пока тихо. Будь первым — или подожди, пока подтянутся наши боты 🤖