TGViewer
Daria’s room Daria’s room @dariasroom · 1.2K subscribers
Post #120 1.82K
Недавно @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 реализация стабильно проигрывала.

В конце сравнила оба варианта на 50k документов Simple Wikipedia:


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` - можно убрать кучу аллокаций и в итоге вообще не уменьшить размер хипа.

#fts #projects #perf
Daniil Medovich Плоский инвертированный индекс для данных высокой кардинальности Как общая arena для байтов, inline-postings, ленивое хранение позиций и компактный порядок термов помогают изменяемому индексу работать с высокой кардинальностью.
  • 🔥 9
  • ❤ 7
  • 👍 4
  • ⚡ 1
  • 🤯 1
More from @dariasroom
  1. Sep 15, 2026Автоматическое сжатие на клиенте vs ручное на сервере Стандартный клиент http.Transport са…
  2. Sep 5, 2026Причина перекосов в уровне балансировки L4-балансировщик выбирает серверную ноду при созда…
  3. Aug 28, 2026Итераторы… TL;DR: в iter.Seq итератор сам передаёт следующие элементы в код внутри range,…
  4. Aug 17, 2026Что на самом деле нужно сохранять при сериализации сложной структуры? TL;DR: Важно отделит…
  5. Aug 13, 2026Привет! Вас стало больше, так что пора наконец представиться 🙂 Я Даша, давно пишу на Go,…
  6. Aug 12, 2026HNSW: как устроен графовый индекс для векторного поиска One million years later, я наконец…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →