Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
slater — Графовая БД с низким потреблением памяти, поддержкой Bolt+TLS, шифрованием в состоянии покоя и векторами, предназначенная для сценариев использования локальных реплик графов. | Kitploit
Инструменты/GitHubGitHub/hikari-systems/slater
Аутентификация и авторизацияИнструменты шифрования/дешифрованияСетевая безопасностьБезопасность облачных средУтилиты и фреймворкиБезопасность Баз Данных
GitHubhikari-systems/slater

slater

Графовая БД с низким потреблением памяти, поддержкой Bolt+TLS, шифрованием в состоянии покоя и векторами, предназначенная для сценариев использования локальных реплик графов.

Репозиторий
1002324 дней назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

Slater

CI Release

Текущая версия: v0.25.2 — все релизы.

В одной строке: Slater обслуживает графы, которые не помещаются в память — сотни миллионов узлов и миллиарды рёбер в пределах нескольких сотен МБ оперативной памяти — через стандартный Bolt, поэтому любой драйвер neo4j работает без изменений, с дисковым векторным поиском рядом с графом, и он принимает живые, долговечные записи, не отказываясь от этого. Резидентная память определяется заданным вами бюджетом кэша, а не размером графа.


Быстрые ссылки

Зачем существует SlaterЧтение и записьЧто вы получаетеВозможности
Запуск с DockerКак это работаетСлой записиБэкенды хранилища
МонтированияКонфигурацияACLПроверка работоспособности
Практический примерРазработкаПроизводительностьЛицензия
Хранилище памяти Graphiti📖 Полное руководство

Зачем существует Slater

Графовая база данных хранит данные как сущности (узлы) и связи между ними (рёбра), где связи являются полноценными элементами. Это то, что нужно, когда ваши вопросы касаются связей, а не строк — «кто находится в пределах трёх переходов от этого счёта?», «какова полная цепочка зависимостей за этой сборкой?», «какие счета используют одно устройство, один адрес и одну карту?» — запросы, которые в SQL превращаются в болото рекурсивных JOIN, но в графе решаются естественно.

Самая распространённая жалоба на графовые базы данных — они не масштабируются за пределы того, что можно удержать в оперативной памяти. Многие из них (например, neo4j, Memgraph, FalkorDB и др.) держат весь граф в памяти: граф на 40 ГБ требует 40 ГБ памяти — на каждый экземпляр. Нужна реплика на каждый регион, на каждого арендатора или на каждый pod? Умножайте счёт. А сверх определённого размера они просто не загружаются: например, граф Wikidata с 90 миллионами узлов и 1,5 млрд рёбер требует ~64–128 ГиБ резидентной памяти, поэтому движки в памяти вообще не могут его открыть.

Slater — это ответ на эту проблему. Вместо загрузки графа в память он компилирует его один раз, офлайн: slater-build превращает ваши данные в контентно-адресуемый, неизменяемый образ на диске, и любое количество серверов Slater затем обслуживает этот образ через Bolt (поэтому ваши существующие драйверы neo4j работают без изменений), подгружая блоки по требованию и удерживая в памяти только фиксированный бюджет кэша. Именно так тот же граф на 90 млн узлов обслуживается из нескольких сотен МБ оперативной памяти — размер графа и затраты памяти развязаны. Граф на 4 ГБ и граф на 400 ГБ стоят одинаково по памяти для обслуживания, поэтому вы можете легко масштабировать дешёвые реплики чтения без состояния, а хранить граф будет хранилище, а не куча.

Это делает его естественным выбором для графов знаний за RAG-системами, графов рекомендаций и идентичности, графов зависимостей — всего большого и связного, что вы хотите запрашивать дёшево и часто. Дисковый векторный поиск находится прямо рядом с графом, поэтому тот же движок служит и уровнем извлечения для эмбеддингов.

Однако «скомпилирован один раз» не означает «заморожен». Этот образ — основа, а не конечное состояние: поверх него находится опциональный слой записи, поэтому живой граф можно исправлять и расширять без пересборки.

Чтение и запись

Ядро неизменяемо; граф — нет. Включите слой записи (delta.enabled) — и вы пишете через Bolt: исправьте одно свойство, добавьте узел, отзовите ребро — и изменение сохраняется надёжно, без пересборки образа. Что делает это дешёвым на стороне чтения — где живут записи.

Записи накапливаются в слое log-structured-merge (LSM) поверх неизменяемого ядра: журнал упреждающей записи и таблица в памяти, с вытеснением в неизменяемые дельта-сегменты, которые периодически сворачиваются обратно в свежее ядро при консолидации. Что это даёт:

  • Чтение графа без записей стоит ровно столько же, сколько раньше. Пустая дельта — это одна предсказуемая ветвь, а не слияние — путь чтения байт-в-байт идентичен независимо от того, включён ли слой записи.
  • Стоимость чтения записи масштабируется с размером дельты, а не с размером графа. Ответы по всему графу — count(*), маргинальные распределения по меткам и типам связей — остаются чтением метаданных даже при незакрытых записях: дельта ведёт собственные счётчики, поэтому count(*) по ядру на 91,6 млн узлов с полумиллионом ожидающих записей всё равно отвечает за десятки миллисекунд, не касаясь ни одного блока.
  • Подтверждение означает долговечность. Один писатель обрабатывает очередь и возвращает SUCCESS только после fsync, покрывающего запись. Группируйте записи — и они дёшевы: write-UNWIND фиксирует один fsync на пакет, а не на строку.
  • Записи по бизнес-ключам, в обоих диалектах. MERGE / MATCH … SET / DELETE (а также CREATE / REMOVE, detach delete, записи связей), ключуемые по свойству идентичности узла — или эквивалентные операторы модификации данных ISO GQL ( / / / ), которые опускаются на тот же путь. Исправление, вставка, upsert и отзыв — по узлам и рёбрам, адресуемые так, как уже устроены ваши данные.

При выключенном слое — по умолчанию — Slater обслуживает чистое неизменяемое ядро и отказывает в записи. Полную модель см. в разделе Слой записи.

О названии. Slater назван в честь агента ЦРУ из Archer (отличный сериал), который настаивает на одном имени — «Просто… Слейтер» — и одного из моих любимых персонажей в нём. См. страницу персонажа в вики.

Что вы получаете

  • ОЗУ определяется вашим бюджетом кэша, а не размером графа — разворачивайте сколько угодно реплик чтения; граф никогда не обязан помещаться в память.
  • Готовая замена для графа — говорит на Bolt, поэтому любой стандартный драйвер neo4j (JS, Python, Go…) работает без изменений. Это Cypher (плюс часть ISO GQL, чтение и запись); учить ничего нового не нужно.
  • Живые, долговечные записи — опциональный слой LSM поверх неизменяемого ядра: MERGE / SET / DELETE по бизнес-ключам для узлов и рёбер, с групповой фиксацией и долговечностью через fsync, сворачиваемые обратно в свежее ядро консолидацией. Чтение за это не платит.
  • Развёртывание заменой файла — соберите новое поколение с контентным хешем офлайн, атомарно переключите указатель current, и серверы подхватят его. Каждый блок проверяется контрольной суммой, поэтому наполовину скопированный образ будет отклонён, а не обслужен.
  • Встроенный векторный поиск — дисковый приближённый поиск ближайших соседей (косинус, L2 или dot KNN) находится прямо рядом с вашим графом — на случай, когда это уровень извлечения за RAG-конвейером, а эмбеддинги можно записывать на месте — без офлайн-пересборки для добавления или изменения вектора.
  • Защищён по конструкции — права чтения и записи независимы, плюс опциональное шифрование при хранении, TLS Bolt, ACL с хешированием argon2id и корневая файловая система только для чтения для реплик чтения. Настройте мастер-ключ — и образ на диске будет аутентифицирован, а не только зашифрован: его манифест несёт ключевой MAC, поэтому злоумышленник с доступом на запись к каталогу данных, но без ключа, не сможет подделать манифест, который сервер примет. Без ключа вы всё равно получаете контентный хеш, который ловит наполовину скопированный или повреждённый образ — но не намеренную подделку. Какая конфигурация что даёт.

Возможности

Рабочее пространство состоит из двух бинарников:

БинарникРоль
slaterОнлайн-сервер Bolt (ENTRYPOINT контейнера): обслуживает чтение и, при delta.enabled, путь долговечной записи с одним писателем.
slater-buildОфлайн-компилятор: превращает дамп примитивного Cypher в неизменяемый каталог поколения с контентным хешем.

Slater разделяет массовую сборку и обслуживание: slater-build выполняет тяжёлую работу офлайн — приём ваших данных и компиляцию их в неизменяемое поколение — поэтому холодный граф никогда не собирается на горячем пути обслуживания. Внутри сервера поверхность чтения отвечает на широкий срез Cypher — сопоставление с образцом, подзапросы WITH/UNION/CALL {…}, 70+ скалярных и агрегатных функций, временные и геопространственные значения, графовые алгоритмы (algo.*) и дисковый векторный KNN (db.idx.vector.queryNodes) — а дельта-оверлей слоя записи находится ниже этой поверхности и ничего не стоит, когда пуст, поэтому чтение никогда не несёт механику стороны записи. Обновить граф можно двумя способами: писать в него вживую через Bolt (см. Слой записи), или собрать новое поколение офлайн и атомарно переключить указатель current, который работающий сервер подхватывает через свой сторож поколения (см. Сторож поколения).

Документация

Полное руководство пользователя находится в docs/manual/ — пошаговое руководство по функциям, которое объясняет для каждой возможности, что это, зачем она существует и как её использовать, с практическими примерами, которые можно запустить на прилагаемом образцовом графе. Начните там для всего, что выходит за рамки этого обзора.

  • Впервые здесь? Быстрый старт собирает и обслуживает граф за пять шагов.
  • Пишете запросы? Запросы, Функции и выражения, Процедуры и алгоритмы, Векторный поиск, Запись данных.
  • Собираете графы? Сборка графов и Справочник CLI сборки.
  • Эксплуатируете Slater? Развёртывание, Хранилище, Справочник конфигурации, Безопасность, Настройка производительности.

Использование Slater как хранилища памяти Graphiti

graphiti-slater — это адаптер, который позволяет Graphiti хранить свой временной граф знаний в Slater, с готовым к запуску docker-example/ — включая предоставление его Claude Code как MCP-сервера. См. этот репозиторий, чтобы узнать, как это работает и как запустить.

Запуск с Docker

Slater спроектирован для запуска как Docker-развёртывание — это ожидаемый способ его использования. Готовые multi-arch образы (linux/amd64 + linux/arm64) публикуются на Docker Hub по адресу hikarisystems/slater, с тегами :latest и :vX.Y.Z при каждом релизе:```sh docker pull hikarisystems/slater:latest

root@kitploit:~
Руководство по использованию, настройке и эксплуатации только через Docker-команды находится в
[`DOCKERHUB.md`](https://github.com/hikari-systems/slater/blob/main/DOCKERHUB.md) (и продублировано на странице обзора Docker Hub) —
**начните с него, если разворачиваете.** Кратко:```sh
# Build a graph generation with the offline writer:
docker run --rm -v slater-data:/data -v "$PWD/dumps:/dumps:ro" \
  --entrypoint /app/slater-build hikarisystems/slater:latest \
  --input /dumps/people.cypher --graph people --data-dir /data

# Serve it over Bolt on 7687 (read-only unless `delta.enabled`):
docker run -d --name slater -p 7687:7687 \
  -v slater-data:/data:ro -v "$PWD/acl.json:/config/acl.json:ro" \
  hikarisystems/slater:latest

Чтобы собрать образ локально (например, для разработки):```sh

Build the image (both binaries).

docker compose build

Serve (expects generations under the slater-data volume / your /data mount).

docker compose up slater

Build a generation with the offline writer (profile build):

docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data

root@kitploit:~
Стадия сборки устанавливает `cmake`, `clang` и `libclang-dev` для бэкенда `aws-lc-rs` в rustls; `git` (уже присутствует в базовом образе) требуется для зависимости `hs-utils` через git+tag, которую `.cargo/config.toml` получает через CLI git.

Разделы ниже охватывают формат на диске, конфигурацию, ACL и локальный (не-Docker) рабочий пример.

## Как это работает```
            slater-build                         slater (Bolt server)
   dump.cypher ──────────▶ /data/<graph>/<uuid>/ ──────────▶ neo4j driver
   (offline, atomic)        MANIFEST.json, *.blk,            (bolt / bolt+s)
                            range/*.isam, vector/*.{vamana,pq},
                            current → <uuid>
  • Поколение — это один неизменяемый каталог: MANIFEST.json (таблицы символов, дескрипторы индексов, необязательный заголовок шифрования), столбцовые блочные файлы (node_props.blk, node_labels.blk, edge_props.blk, topology.csr.blk, vectors.f32.blk), диапазонные индексы (range/<name>.isam), ANN-индексы выше порога (vector/<label>.<prop>.{vamana,pq}) и текстовый указатель current.
  • Каждый блок сжат zstd и снабжён контрольной суммой BLAKE3; при использовании --encrypt каждый блок дополнительно запечатывается XChaCha20-Poly1305 (AEAD в состоянии покоя).
  • Сервер открывает поколение, повторно хэшируя каждый файл по манифесту, поэтому наполовину скопированный / обрезанный образ — разорванная копия в каталоге данных, который может быть удалённым/сетевым хранилищем — отклоняется, а не обслуживается.
  • Чтение проходит через три ограниченных пула кэша — LRU для распакованных блоков, пул векторных индексов (резидентные PQ-коды + LRU для Vamana-блоков) и LRU для результатов — каждый со своим бюджетом в байтах. Каждый пул взвешивает содержимое и вытесняет записи, чтобы оставаться в рамках бюджета, поэтому RSS отслеживает бюджеты с ограниченными накладными расходами на запись и аллокатор, а не растёт вместе с графом.

Слой для записи

При delta.enabled неизменяемое поколение становится полностью уплотнённым нижним уровнем («ядром») небольшого LSM-дерева, а живые записи размещаются поверх него:``` write (Bolt) read (Bolt) │ │ ▼ ▼ ┌──────────────┐ flush ┌──────────────┐ ┌──────────────────────┐ │ WAL + active │ ───────▶ │ L0 delta │ │ a query pins one │ │ memtable │ │ segments │ │ (core, delta) view │ └──────────────┘ └──────┬───────┘ │ and reads the merge │ (fsync = ack) │ └──────────────────────┘ consolidation │ (folds core + delta → fresh core) ▼ ┌─────────────┐ │ new core │ (atomic current swap) └─────────────┘

root@kitploit:~
* **Нижняя граница долговечности — WAL.** Каждая мутация сериализуется за одним
  писателем на граф, добавляется в журнал упреждающей записи (write-ahead log) для
  каждого графа и синхронизируется через `fsync` до того, как будет возвращён
  `SUCCESS` Bolt — то есть *подтверждено ⇒ долговечно*, а оборванный хвост
  отбрасывается при воспроизведении. Пакетный `UNWIND` для записи добавляет свои
  строки и фиксирует **один** `fsync` для всего пакета. WAL находится **только на
  локальном диске** (он не маршрутизируется через бэкенд хранилища), что делает
  *узел-писатель* сохраняющим состояние: ему нужен долговечный локальный том по
  адресу `delta.walDir`. Реплики чтения остаются без состояния.
* **Memtable → L0 → консолидация.** Записи накапливаются в memtable в оперативной
  памяти (ограниченной `delta.memtableBytes`); когда она заполняется, она
  сбрасывается в неизменяемый сегмент дельты L0. **Консолидация** сворачивает
  `{core + delta}` в новый core, сериализуя объединённое представление обратно
  через `slater-build` и атомарно заменяя `current` — та же защита по хэшу
  содержимого, что и для любого опубликованного поколения. Запустите её вручную с
  помощью `CALL slater.consolidate()`, автоматически при `delta.deltaCorePercent`
  от размера core (опционально ограниченной окном вне пиковой нагрузки
  `delta.consolidateWindow`), или позвольте ограничителю `delta.deltaHardBytes`
  подстраховать неконтролируемый рост.
* **Оверлей находится ниже поверхности чтения.** Исполнитель читает через
  `ReadView`, который представляет собой либо чистый core (дельта всегда пуста),
  либо объединённое представление `(core, delta)`; движок мономорфизирован поверх
  него, поэтому пустая дельта компилируется в одну предсказуемую ветвь, а путь
  только для чтения байт-в-байт идентичен. Счётчики всего графа (`count(*)`,
  маргиналы по меткам/типам отношений) обслуживаются из собственных живых
  счётчиков дельты, поэтому они остаются чтением метаданных даже при ожидающих
  записях.
* **Запрос видит стабильный снимок.** Он фиксирует один кортеж `(core, delta)` на
  всё время своей жизни. Здесь нет многооператорных транзакций и нет отката —
  запись — это долговечная коррекция, адресуемая по бизнес-ключу, а не OLTP-
  транзакция.

Точная грамматика записи и параметры приведены в таблице
[Конфигурация](#environment--configuration) (`delta.*`) и в
[Практическом примере](#worked-example) ниже.

### Диапазонные индексы (ISAM)

Диапазонный индекс (`range/<name>.isam`, один на индексируемую пару
`(label, property)`) позволяет `MATCH (n:Label {prop: v})` или
`WHERE n.prop <op> v` разрешаться в соответствующие идентификаторы узлов
**без сканирования метки**. Это структура
**[ISAM](https://en.wikipedia.org/wiki/ISAM)** (Indexed Sequential Access Method —
индексно-последовательный метод доступа) — классический *статический,
отсортированный, блочно-структурированный* индекс, который идеально подходит для
неизменяемого поколения: здесь нет вставок, требующих ребалансировки, поэтому
простота ISAM даёт то, что механизм мутаций B-дерева только усложнил бы.

* Записи `(value, entity_id)` отсортированы по значению и упакованы в те же
  сжатые zstd блоки по 256 КиБ, что и всё остальное.
* Небольшой **резидентный верхний уровень** хранит первый ключ каждого блока
  (разрежённый индекс). Поиск выполняет бинарный поиск по этому верхнему уровню в
  памяти, чтобы найти *тот единственный* блок, в котором может находиться ключ,
  читает + распаковывает этот блок и сканирует его — поэтому поиск по равенству —
  это **чтение одного блока**, а диапазонное сканирование проходит по непрерывной
  последовательности блоков, которые оно охватывает. (Именно поэтому поиск по
  индексу `meshUi` занимает единицы миллисекунд, тогда как то же совпадение по
  неиндексированному свойству сканирует всю метку.)
* Планировщик выбирает его через `NodeScan::RangeEq` / `RangeRange`;
  неиндексированный предикат откатывается к обходу метки или полному сканированию,
  при этом исполнитель в любом случае повторно проверяет каждый предикат.

### Векторный поиск (Vamana + PQ) — косинус, L2 и скалярное произведение, чтение *и* запись

Векторный KNN (`db.idx.vector.queryNodes`) работает поверх индексов
**косинусной, L2 или скалярной (MIPS)** метрики. Базовый индекс строится офлайн с
двумя путями выполнения, выбираемыми для каждого индекса через
`--ann-threshold` (по умолчанию 50 000 векторов):

* **Ниже порога — полный перебор.** Полные векторы `f32` хранятся в
  `vectors.f32.blk`; запрос сканирует группу индекса и вычисляет точное расстояние
  в метрике индекса. Просто и точно; этого достаточно, когда набор векторов мал.
* **На пороге или выше — Vamana + PQ**, нативный для диска путь ANN, который
  удерживает резидентную память ограниченной независимо от количества векторов:
  * **[Vamana](https://arxiv.org/pdf/2401.11324)** — это графовый индекс из
    линейки работ DiskANN: единый граф близости, рёбра которого обрезаются
    (исходящая степень `--vamana-r` и фактор длинных рёбер `--vamana-alpha`), так
    что *жадный лучевой поиск* — начните с медоида, многократно переходите к
    запросу, сохраняя список кандидатов шириной `vectorQuery.beamWidth`, —
    достигает истинных соседей узла за несколько переходов, то есть **несколько
    случайных чтений блоков на запрос**. Блоки графа
    (`vector/<label>.<prop>.vamana`) подгружаются через кэш векторов, а не
    удерживаются целиком.
  * **[Продуктовое квантование (PQ)](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)**
    сжимает каждый вектор в короткий код (`--pq-subspaces` × `--pq-bits`):
    измерения разбиваются на подпространства, каждое независимо кластеризуется
    методом k-средних, а вектор хранится как кортеж идентификаторов ближайших
    центроидов. Эти коды (`vector/<label>.<prop>.pq`) достаточно малы, чтобы
    хранить их **резидентно**, поэтому лучевой поиск оценивает кандидатов из
    оперативной памяти, и только выбранные немногие полные векторы читаются с
    диска. Именно этот резидентный набор PQ закрепляет пул `cache.vectorCacheBytes`.

**Записываемые эмбеддинги — лестница записи векторов (в стиле
[FreshDiskANN](https://arxiv.org/abs/2105.09613)).** Индексируемый эмбеддинг —
это полноценное записываемое значение. `SET n.embedding = vecf32([…])` (и
`REMOVE`) попадает в дельту записи и **немедленно видим для KNN с точным рангом**,
затем переживает сброс сегмента, слияние и консолидацию. Запрос объединяет до трёх
уровней — запечатанный базовый индекс, запечатанный посегментный индекс и
**RW-индекс** в памяти (живой изменяемый Vamana поверх дельты записи) — поэтому
задержка остаётся стабильной по мере накопления записей, а не растёт с их
количеством. Удаление оставляет *дыру*: узел перестаёт возвращаться, но остаётся
навигационной точкой, пока фоновая **консолидация удалений** не вырежет его из
графа, поэтому удаления перестают стоить операций ввода-вывода при запросах. И
поскольку граф на диске адресует своих соседей по позиции в раскладке, а не по
идентификатору узла, `CALL slater.consolidate()` переносит Vamana **по ссылке** —
жёстко связанным, байт-в-байт идентичным — и переписывает только небольшую
колонку идентификаторов, сворачивая векторные записи в базу **без** перестроения
графа O(N·R·L). Измеренные числа с оговорками приведены в
[отчёте о производительности](https://github.com/hikari-systems/slater/blob/main/docs/PERF-REPORT.md).

## Бэкенды хранилища (файловая система / S3 / GCS)

Каждый файл поколения открывается через абстракцию **`ObjectStore`**, а не
напрямую через `std::fs`, поэтому *тот же* формат байтов на диске — блоки,
индексы, манифест, указатель `current` — обслуживается без изменений из любого
бэкенда; отличается только *откуда берутся байты*, но никогда — читатели, движок
запросов или проверки целостности. Горячий путь — это позиционные чтения
(`read_exact_at`), которые отображаются на `pread` для локального файла и HTTP-
запрос диапазона байтов для объектного хранилища — Slater никогда не использует
mmap, поэтому явная модель чтения с ограничением одинакова везде.

**Три полноценных бэкенда**, выбираемых через `dataBackend.kind`. Файловая
система — простой вариант по умолчанию; **Amazon S3 и Google Cloud Storage —
равноправные, полностью поддерживаемые объектные бэкенды** — опубликованный образ
поставляется с обоими скомпилированными, поэтому каждый из них настраивается
только конфигурацией, а поколение, построенное один раз, может обслуживаться из
любого из них (даже мигрировать `fs` → S3 → GCS) без пересборки.

| `dataBackend.kind` | Позиционное чтение | Целостность при открытии | Учётные данные |
| --- | --- | --- | --- |
| `fs` *(по умолчанию)* | `pread` | полное повторное хэширование BLAKE3 каждого файла | — |
| `s3` | HTTP-запрос `Range` GET | серверный **SHA-256** через `HEAD` (→ повторное хэширование тела BLAKE3, если отсутствует) | ключи конфигурации, цепочка AWS или роль IAM |
| `gcs` | HTTP-чтение диапазона | серверный **CRC32C** через `get_object` (→ повторное хэширование тела BLAKE3, если отсутствует) | ADC / Workload Identity или JSON сервисного аккаунта |

Оба объектных хранилища проверяют целостность по **контрольной сумме, которую
хранилище уже вычисляет и хранит**, получаемой как метаданные объекта:
`slater-build` отправляет контрольную сумму при загрузке (хранилище проверяет байты
по ней и сохраняет её), а сервер считывает её при открытии и сравнивает с
манифестом — один запрос метаданных на файл, без загрузки тела. Это проверка
уровня содержимого и идентична по духу для S3 (SHA-256) и GCS (CRC32C). Когда
объект **не** несёт контрольную сумму, хранящуюся на сервере (скопирован
внеполосно или загружен с другим значением по умолчанию), сервер **повторно
хэширует тело объекта по BLAKE3 из манифеста**, а не доверяет его длине в байтах —
запрошенная проверка целостности никогда молча не понижается до сравнения
размеров. Поколения, опубликованные Slater, всегда несут контрольную сумму,
поэтому остаются на дешёвом пути метаданных.

Что проверяет этот столбец на каждом бэкенде — что файлы **соответствуют
манифесту**. Можно ли доверять самому манифесту — отдельный вопрос, и именно
мастер-ключ отвечает на него: при настроенном ключе манифест несёт ключевой MAC,
который сервер проверяет, прежде чем доверять любому полю (включая эти хэши),
поэтому манифест, переписанный для описания подменённых файлов, отклоняется; без
ключа сравнение не ключевое на всём протяжении, и тот, кто может писать в каталог
данных, может переписать файл и манифест вместе. См.
[Что означает целостность в каждой конфигурации](https://github.com/hikari-systems/slater/blob/main/THREAT_MODEL.md#what-integrity-means-in-each-configuration).
Саму проверку можно отключить с помощью `dataBackend.verifyIntegrity: false`, что
обменивает её на более быстрое открытие.

### Файловая система (`fs`)

Вариант по умолчанию, корнем которого является `dataBackend.fs.dir`. Правильный
выбор для большинства развёртываний: поколение на локальном SSD (или
подключении NFS/EBS), обслуживаемое только для чтения. Целостность — это полное
повторное хэширование BLAKE3 каждого файла при открытии.

### Amazon S3 (`s3`)

Сегмент S3 или совместимый с S3 (AWS, MinIO, localstack). Учётные данные берутся
**в первую очередь** из конфигурации (`dataBackend.s3.awsAccessKey` /
`awsSecretKey`, плюс `awsSessionToken` для временных учётных данных STS) и
откатываются к стандартной цепочке AWS (`AWS_ACCESS_KEY_ID` /
`AWS_SECRET_ACCESS_KEY` env, общий профиль или роль instance/IRSA), когда они
оставлены пустыми.```sh
# serve from S3 (env-var form; see the config table for every key)
dataBackend__kind=s3
dataBackend__s3__bucket=slater
dataBackend__s3__region=eu-west-2
dataBackend__s3__awsAccessKey=…        # omit to use the AWS chain / instance role
dataBackend__s3__awsSecretKey=…
# S3-compatible (e.g. MinIO): also set
dataBackend__s3__endpoint=http://minio:9000
dataBackend__s3__pathStyle=true        # required by most S3-compatible servers
root@kitploit:~
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
  --publish-s3-bucket slater --publish-s3-region eu-west-2 --publish-s3-prefix prod
#   MinIO: add  --publish-s3-endpoint http://localhost:9000 --publish-s3-path-style

Google Cloud Storage (gcs)

Сегмент GCS, доступ к которому осуществляется через JSON API. Авторизация является нативной для GCP: по умолчанию используются Application Default Credentials — GKE Workload Identity, метаданные-сервер GCE или ключ gcloud / GOOGLE_APPLICATION_CREDENTIALS. Задайте dataBackend.gcs.credentialsPath (JSON-файл ключа сервисного аккаунта) или встроенный credentialsJson для явного ключа. dataBackend.gcs.endpoint указывает на эмулятор fake-gcs-server, а dataBackend.gcs.anonymous=true включает неаутентифицированный доступ только для этого эмулятора — никогда не используйте его против реального GCS.```sh

serve from GCS (env-var form; see the config table for every key)

dataBackend__kind=gcs dataBackend__gcs__bucket=slater dataBackend__gcs__prefix=prod dataBackend__gcs__credentialsPath=/secrets/sa.json # omit for ADC / Workload Identity

root@kitploit:~
```sh
# publish a generation into the bucket (remote `current` pointer written last)
slater-build --input people.cypher --graph people --data-dir /data \
  --publish-gcs-bucket slater --publish-gcs-prefix prod
#   explicit key: add  --publish-gcs-credentials /secrets/sa.json

Во всех случаях slater-build сначала записывает готовое поколение в --data-dir (свою локальную промежуточную область) и дополнительно загружает его в бакет; удалённый указатель current записывается последним, поэтому обслуживающий узел никогда не видит наполовину опубликованное поколение.

Когда использовать объектное хранилище (S3 или GCS)

Обращайтесь к s3 или gcs, когда вам нужны поколения в долговечном центральном объектном хранилище, а не на диске узла — как правило: публикуйте один раз и распространяйте на множество не имеющих состояния реплик сервера без дисков, которые все читают один и тот же бакет; разделите хост сборки и хост обслуживания; или полагайтесь на долговечность/версионирование/жизненный цикл хранилища вместо управления томами. Обратная сторона — задержка: холодный блок — это сетевой круговой обход (~10–50 мс) вместо локального чтения (~0,1 мс). Slater скрывает большую часть этого с помощью кэша блоков в памяти, упреждающего чтения и опционального дискового кэша ниже. Если ваши поколения уже находятся на быстром локальном хранилище и вам не нужна модель центрального бакета, fs проще и быстрее.

Локальный дисковый кэш блоков (второй уровень объектного хранилища)

Кэш блоков BlockCache в памяти намеренно мал (ограниченная RSS — главная гарантия), поэтому при рабочем наборе, превышающем RAM, одни и те же блоки повторно извлекались бы из объектного хранилища при каждом вытеснении. Опциональный второй уровень кэша на локальном SSD решает эту проблему: блок, вытесненный из RAM, обслуживается с локального диска (~0,1 мс) вместо нового GET-запроса к объектному хранилищу, переживая вытеснение из памяти и сокращая количество/стоимость запросов к объектному хранилищу — приближая узел на объектном хранилище к производительности локальной файловой системы после прогрева. Он включается вручную как для s3, так и для gcs, путём установки dataBackend.<s3|gcs>.diskCacheBytes > 0 и доступного для записи diskCacheDir.

  • Он кэширует запечатанные байты точно так, как они были получены — уже сжатые и (для поколений с --encrypt) всё ещё запечатанные AEAD — ниже уровня расшифровки/распаковки. Уровень кэша никогда не хранит ключ шифрования и никогда не перешифровывает, поэтому статус хранения сохраняется бесплатно: зашифрованное поколение попадает на диск всё ещё запечатанным.
  • Записи выполняются с отложенной записью: при промахе полученные байты немедленно возвращаются запросу, затем фоновый поток выполняет запись на диск и усечение LRU, поэтому путь запроса никогда не блокируется на дисковом вводе-выводе. Вытеснение удерживает кэш в пределах его байтового бюджета; контрольная сумма каждого файла, проверяемая при каждом чтении, самовосстанавливает повреждённый файл кэша до промаха (→ повторное извлечение из объектного хранилища).
  • diskCacheDir должен указывать на реальный доступный для записи том — никогда не tmpfs (tmpfs — это RAM и нарушил бы гарантию ограниченной RSS). Индекс в памяти, отслеживающий его, стоит немного RAM (~десятки байт на кэшированный блок), что учитывается в вашем пределе RSS — задавайте размер каталога ≫ кэша блоков в памяти.
  • Другая стоимость уровня в RAM — очередь отложенной записи, которая размещает блоки на пути к диску. Она ограничена blockCacheBytes / 8 (с нижней границей diskCacheBytes) — 8 МиБ по умолчанию — и сокращается, а не растёт, поэтому холодное сканирование не может её раздуть; отброшенный блок просто повторно извлекается при следующем промахе. Она не требует настройки: она масштабируется с blockCacheBytes, поэтому дисковый уровень не добавляет новых чисел в бюджет RSS, кроме своего индекса.

Монтирования

Реплика чтения работает с корневой файловой системой только для чтения и непривилегированным пользователем (appuser:1000) — всё, что ей нужно, смонтировано только для чтения. Писатель (delta.enabled) дополнительно нуждается в одном долговечном томе для записи для своего WAL.

Окружение / конфигурация

Конфигурация загружается стандартным многоуровневым загрузчиком: встроенный config.json, затем /sandbox/config.json, глубоко слитый поверх него, затем переопределения окружения KEY__sub (двойное подчёркивание для вложенности; ключи соответствуют конфигурации в camelCase).

Каждый параметр конфигурации — его ключ в camelCase, переопределение окружения KEY__sub, значение по умолчанию и назначение — приведён в таблице в Справочнике по конфигурации. Наиболее настраиваемые параметры — бюджеты кэша (cache.*), защитные ограничения запросов (query.*), лимиты соединений (server.*), бэкенд хранилища (dataBackend.*) и уровень записи (delta.*).

Резидентная память отслеживает blockCacheBytes + vectorCacheBytes + resultCacheBytes с точностью до ограниченных накладных расходов на запись и распределитель — каждый пул взвешивает собственное содержимое (строки и контейнеры по выделенной ёмкости) и вытесняет, чтобы оставаться в рамках бюджета, но учёт на запись и округление классов размеров распределителя лежат поверх заданного вами числа — плюс небольшие фиксированные накладные расходы (и до degreeColumnBytes для ленивого столбца степеней, после задействования быстрого пути суммы степеней count(endpoint)). Это не зависит от размера графа — главная гарантия, проверяемая интеграционным тестом rss_stays_bounded_under_sustained_knn_load, который удерживает рост RSS пик-против-прогретого в пределах суммированных бюджетов. Буферы на соединение живут вне бюджетов кэша, поэтому гарантия выполняется при неблагоприятной нагрузке только потому, что server.maxConnections ограничивает, сколько их может существовать одновременно.

Сетевая позиция

Slater — это дескриптор реплики чтения; основной контроль безопасности соединений — это сеть, а не бинарник. Привяжите его к частному интерфейсу, ограничьте исходные диапазоны на сетевом уровне (группы безопасности / NetworkPolicy) и — если он обращён к чему-то, кроме доверенных клиентов, — поставьте перед ним L4-прокси с ограничением соединений (HAProxy maxconn + stick-table по источнику, или nftables connlimit + hashlimit). Это происходит до того, как файловый дескриптор вообще передаётся процессу, поэтому это самый надёжный лимит.

Встроенные в бинарник лимиты выше (maxConnections, maxPreAuthConnections, maxConnectionsPerIp, дифференциальные байтовые лимиты и loginTimeoutMs) — это защита в глубину: они включены по умолчанию и щедры, поэтому невидимы для легитимной популяции клиентов, но обеспечивают выполнение гарантии ограниченной RSS, даже если прокси забыт. См. docs/HARDENING.md для полной оборонительной позиции, а THREAT_MODEL.md / SECURITY_WORKLIST.md — для канонических деталей.

Защита поколений

Slater опрашивает указатель current каждого графа каждые generationPollMs (опрос, а не inotify — каталог данных может быть удалённым/сетевым хранилищем, например NFS, где события изменений файловой системы ненадёжны). Когда он меняется:

  • reloadStrategy=exit (по умолчанию): сервер записывает фатальную ошибку и завершается с ненулевым кодом, чтобы оркестратор чисто перезапустил его против нового поколения.
  • reloadStrategy=swap: сервер открывает и проверяет новое поколение (та же защита по хэшу содержимого, что и при загрузке), атомарно подменяет его и позволяет выполняющимся запросам завершиться на старом. Повреждённое/неполное новое изображение отклоняется, и старое поколение продолжает обслуживать.

ACL

acl.json сопоставляет пользователей с хэшами паролей argon2id и разрешениями read / write для каждого графа. Создайте хэш (никогда не храните открытый текст) с помощью:```sh slater hash-password 's3cret' # prints a $argon2id$… string for acl.json

root@kitploit:~
A стартовый `acl.json` поставляется в корне репозитория; его структура такова:```json
{
  "users": {
    "reporting": {
      "passwordArgon2id": "$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>",
      "grants": {
        "people": ["read"],
        "products": ["read", "write"]
      }
    }
  }
}
  • users — одна запись на каждый логин, с ключом по имени пользователя.

  • passwordArgon2id — строка $argon2id$… из slater hash-password (никогда не в открытом виде; сам файл — это обычный JSON, хранящийся в общем хранилище).

  • grants — списки возможностей для каждого графа. Значимы два разрешения:

    • read — выполнять запросы к графу. Граф, отсутствующий в grants пользователя, для него невидим.
    • write — изменять граф через слой записи (delta.enabled): операторы MERGE / SET / DELETE и CALL slater.consolidate().

Монтируйте его только для чтения по пути, указанному в aclPath (по умолчанию /config/acl.json). Сервер перезагружает его при каждой горячей смене поколения, а штамп ACL в состоянии покоя проверяется заново при каждой перезагрузке (см. requireAclStamp).

Проверка работоспособности

Бинарник slater также служит собственным зондом живости: slater healthcheck [host] [port] выполняет рукопожатие Bolt (не HTTP-запрос) с сервером и завершается с кодом 0, если согласовывает версию протокола, и 1 в противном случае — по умолчанию используя localhost и настроенный порт Bolt. Именно это выполняет контейнерный HEALTHCHECK, поэтому оркестраторы видят по-настоящему готовый к Bolt сервер, а не просто открытый сокет:```sh slater healthcheck localhost 7687 # exit 0 = healthy docker exec slater /app/slater healthcheck # inside the container

root@kitploit:~
## Одноразовый запрос

Для написания скриптов, проверок в CI и быстрых поисков `slater query` монтирует
текущее поколение графа, выполняет один запрос только для чтения на Cypher
в рамках процесса, выводит результат в виде JSON-объекта и завершает работу — без
сервера и без подключения по Bolt. Он учитывает ту же конфигурацию, что и сервер
(хранилище, ключ шифрования, бюджеты запросов):```sh
# GRAPH defaults to `defaultGraph`. Without -q, normal datestamped logging
# (config, "opened generation", …) is written to stdout alongside the result.
slater query mygraph 'MATCH (n) RETURN count(n) AS c'

# -q/--quiet ⇒ logging suppressed, so stdout is *only* the compact result JSON
slater query mygraph -q 'MATCH (c:Company) RETURN c.ticker AS t LIMIT 3' | jq
# {"columns":["t"],"rows":[["AUPH"],["KYMR"],["MREO"]]}

Узлы и связи раскрываются до их меток/типов и свойств. Используйте -q, когда нужен машиночитаемый вывод (результат JSON — единственное, что попадает в stdout); опустите его для запуска, ориентированного на оператора, с журналами. Без -q после каждого запуска в журнал выводится сводка только по метрикам — например,```text INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10

root@kitploit:~
carrying the query `cost` (elements charged), `resultCount`, `execMs`, and
`limitRowCount` (only when the query specifies a `LIMIT`) — never the query text
or any result value. Exit status is `0` on success, `1` on a parse/open/execute
error (message on stderr).

## Export a graph (`slater dump`)

`slater dump` exports a graph from a **running** server as business-key `MERGE`
Cypher — the same dialect `slater-build` ingests — so a graph round-trips
(dump → `slater-build` → new generation) for migration or text backup. Unlike
`slater query`, it connects over **Bolt**, authenticates, and honours per-graph
ACLs, so it needs no disk access to the server. The password is read from
`SLATER_DUMP_PASSWORD` or stdin (never a flag, keeping it out of `ps`/history).```sh
# List the graphs the authenticated user may read.
SLATER_DUMP_PASSWORD=pw slater dump --list -u reporting

# Dump a graph to a file (identity keys inferred from range indexes).
SLATER_DUMP_PASSWORD=pw slater dump people -u reporting -o people.cypher

# Rebuild it into a fresh generation.
slater-build --input people.cypher --graph people --data-dir ./data

Каждый ключ идентичности метки — это свойство, переносимое её диапазонным индексом; переопределите его с помощью --key Label=prop (повторяемо) или глобального --pk <field>. Сначала выдаётся DDL CREATE INDEX, чтобы пересборка воссоздала индексы. Узел с несколькими метками сохраняет каждую метку — он выводится как MERGE (n:Ident:Other {key: v}), при этом метка идентичности (та, что предоставляет бизнес-ключ) идёт первой, а остальные сортируются; слияние выполняется только по метке идентичности, поэтому завершающие метки записываются на узел без создания нового. Метки, типы связей и ключи свойств, содержащие специальные символы, заключаются в обратные кавычки при выводе, поэтому необычные имена проходят цикл без потерь и не могут внедрить Cypher в пересборку. Векторы (и другие значения без литерального написания в Cypher) не могут участвовать в дампе MERGE и отбрасываются с предупреждением в stderr. Код выхода — 0 при успехе, 1 при ошибке.

Рабочий пример

Полное, запускаемое пошаговое руководство — построение графа, его обслуживание, подключение с помощью драйверов neo4j JavaScript и Python и запись в него — находится на страницах Quickstart и Writing data руководства, с использованием встроенного примера графа из docs/manual/examples/.

Разработка```sh

export PATH="$HOME/.cargo/bin:$PATH" cargo build cargo test # unit + the bounded-RSS headline integration test cargo clippy --all-targets -- -D warnings cargo fmt --all -- --check

root@kitploit:~
### Бэкенды объектного хранилища — опциональные возможности cargo

Обычная команда `cargo build` создаёт бинарный файл **только с файловой системой** — бэкенды `s3` и `gcs`
скрыты за возможностями cargo, чтобы сборка по умолчанию оставалась компактной (без AWS
или Google SDK, без асинхронной среды выполнения). Включите тот, который вам нужен, **и** в `slater`
(serve), **и** в `slater-build` (publish):```sh
# S3 only / GCS only / both
cargo build -p slater -p slater-build --features s3
cargo build -p slater -p slater-build --features gcs
cargo build -p slater -p slater-build --features s3,gcs

Каждый крейт предоставляет парные функции s3 / gcs, которые перенаправляют на graph-format/{s3,gcs}. Запрос бэкенда во время выполнения (dataBackend.kind=s3|gcs или slater-build --publish-{s3,gcs}-*) без скомпилированной соответствующей функции завершается с ошибкой и понятным сообщением «собран без функции …». Опубликованный Docker-образ включает обе (Dockerfile CARGO_FEATURES), поэтому предварительно собранным образам не нужны дополнительные флаги — это важно только при сборке из исходников. Интеграционные тесты также ограничены: --features s3 --test s3_minio, --features gcs --test gcs_emulator (a fake-gcs-server) и --features gcs --test gcs_real (настоящий GCS через ADC); каждый пропускается, если не заданы соответствующие переменные окружения SLATER_*.

См. docs/PLAN.md, docs/PROGRESS.md и docs/DECISIONS.md для описания дизайна, журнала этапов и журнала решений.

Производительность

До шести движков, один набор тестов с одним клиентом, графы от игрушечного с 62k узлами до Wikidata с 91,6M узлов / 1,5B рёбер. Каждый движок измеряется изолированно (все остальные контейнеры остановлены — RSS и задержка относятся только к нему). Таблицы задержек ниже были повторно измерены на Slater 0.21.0 (сборка с возможностью записи): графы малого/среднего размера (MeSH, EU-AI-Act) заново, а граф с 91,6M — как новый проход на одной машине, с общим якорем slater-против-Neo4j (см. эту таблицу). Показатели резидентной памяти перенесены из более раннего прохода (измерено через cgroup контейнера; путь чтения побайтово идентичен, уровень записи простаивает). Показатели других движков взяты из установленного кросс-движкового прогона (их версии/производительность не изменились). Все показатели — медианы (мс) или пиковая резидентная память (МиБ). Везде меньше — лучше; жирным = лучший в строке. slater запускался на своём локальном файловом (fs) бэкенде; бэкенды S3 и GCS обменивают локальную задержку чтения на сетевые запросы к объектному хранилищу (смягчается кэшами в памяти и опциональным уровнем локального дискового кэша), поэтому эти показатели характеризуют движок, а не развёртывание с сетевым хранилищем.

Три движка, которые подкачивают с диска — slater, Neo4j 5 и LadybugDB — загружают все пять графов. Трио в памяти (Memgraph · FalkorDB · ArcadeDB) вообще не может удержать граф с 1,5B рёбер (требуется ~64–128 ГиБ резидентной памяти), а импортёр ArcadeDB также не может его завершить.

Резидентная память (МиБ) — ограничена по мере роста графа ~в 1 500×

Каждый показатель — это выделенная рабочая память — то, что ОС не может вернуть. Каждый движок, кроме slater, хранит свой граф в выделенной анонимной памяти (собственная куча, внекучевой страничный кэш Neo4j или пул буферов), поэтому его пиковый RSS и есть его выделенный объём. Только slater обслуживает запросы из возвращаемого страничного кэша ОС своего хранилища на диске, поэтому его показатель — это анонимный рабочий набор; страничный кэш хранилища (вытесняемый при нехватке памяти — slater продолжает работать) исключён и показан как итого в скобках для графа с 91,6M. Жирным = наименьший.

slater — самый низкий на каждом масштабе и растёт ~в 50 раз, пока граф растёт ~в 1 500 раз — его объём отслеживает рабочий набор запроса, а не граф (в простое ~16–71 МиБ на протяжении всего теста). Трио в памяти растёт ~линейно и не может загрузить граф с 1,5B; Neo4j выделяет кучу ~2 ГиБ независимо от запроса. († LadybugDB только на ограниченных формах — его обходы концентраторов / переменной длины / кратчайшего пути при 1,5B рёбрах требуют увеличения его пула чтения до ≥2 ГиБ, против автоматического ограничения maxIntermediate у slater.) Гистограммы «значение→количество» времени сборки добавляют незначительную резидентную память — несколько КБ для индексированного столбца с низкой кардинальностью и ноль для графов с уникальными ключами, таких как Wikidata (wikidata_id превышает предел кардинальности гистограммы, поэтому ничего не сохраняется) — поэтому эти показатели не изменяются этой функцией.

Задержка (медиана, мс) — граф помещается в ОЗУ (MeSH, 341k / 469k)

slater владеет формами метаданных / индекса / сканирования (count, метки, idx-eq, сканирование — ~0,4 мс, в 10–200 раз быстрее сервисных движков), индексированным точечным поиском (0,43 мс, теперь опережая пару в памяти с 0,48 мс), многопереходным обходом без якоря (2 перехода за 1,40 мс через сканирование по типу отношения, самый быстрый в поле), и — через гистограмму «значение→количество» времени сборки по индексированному ключу группировки — group-by / count(DISTINCT) по всей метке (0,45 мс, опережая колоночные 5,3 мс LadybugDB). Серверы в памяти сохраняют только сырой 1 переход (Memgraph 1,21 мс против 1,28 мс у slater). (pole 62k/106k выглядит так же: slater единственный самый быстрый на count/сканировании ~0,4 мс, ~1,3–2,6 мс на переходах.)

Задержка (медиана, мс) — векторы (kNN EU-AI-Act, 15k × 1024-мерн.)

slater отвечает на kNN точным полным перебором (эти наборы ниже его порога ANN в 50k векторов), где остальные используют приблизительный резидентный HNSW — поэтому результаты slater точны (полнота 1,0). Ядро расстояний SIMD + резидентная, предварительно нормализованная матрица векторов сократили Concept с ~23 до ~2,9 мс и Chunk с ~10 до ~2,4 мс, поэтому slater теперь опережает Neo4j и LadybugDB и находится в пределах ~1,4× от Memgraph, уступая только FalkorDB — оставаясь точным.

Лестница записи векторов — вставка / обновление / удаление без перестроения

Таблицы выше — это кросс-движковые сравнения чтения. Путь векторной записи (лестница записи в стиле FreshDiskANN поверх статической базы Vamana) не имеет кросс-движкового аналога — ни один другой движок здесь не выполняет нативный для диска, записываемый ANN — поэтому цифры ниже представляют собой однодвижковые компонентные бенчмарки на синтетическом наборе, похожем на эмбеддинги (многообразие низкого ранга, размерность 768, неравные нормы), зафиксированные в crates/slater/benches/ и подробно описанные — с методологией и всеми оговорками — в docs/PERF-REPORT.md. Полнота всегда измеряется относительно точного полного перебора по живому набору, никогда не одного индекса против другого. Масштаб здесь репрезентативен и экстраполируется только там, где метрика линейна по размеру.

Единственный показатель, который требует выделенного бокса производительности, — это пропускная способность перезаписи при консолидации на медленном пути — когда консолидация несёт удаления или новые векторы, а не чистую перестановку, это последовательное пересжатие, ограниченное однопоточным zstd и локальным диском, поэтому абсолютные МиБ/с зависят от окружения (в отчёте показана форма и объяснён диапазон влияния окружения).

Задержка (медиана, мс) — граф ≫ ОЗУ (Wikidata 91,6M / 1,5B)

Движки в памяти (Memgraph / FalkorDB / ArcadeDB) вообще не могут загрузить этот граф (~64–128 ГиБ резидентной памяти). Только slater и Neo4j 5 могут. Это новый проход на одной машине, в один день против общего фиксированного набора якорей — каждый запрос попадает на идентичные узлы на обоих движках, поэтому сравнение один в один корректно (общий пул wikidata_id якорей умеренной степени; см. примечание ниже о том, почему это важно). slater показан при обоих значениях fanout (query.maxFanout 1 = пропускная способность по умолчанию, 8 = регулятор задержки, который перекрывает чтения холодных блоков). Жирным = лучший в строке.

Честная картина: slater доминирует в формах метаданных / индекса — count(*) обслуживается из метаданных (0,41 мс против дискового сканирования Neo4j за 3,6 с, ~в 8800 раз), а точечный поиск / степень / 3 перехода выполняются ~в 2–10 раз быстрее — наравне с Neo4j на 1–2 переходах (fanout 8 вырывается вперёд на холодных чтениях), но решительно проигрывает var-length *1..2 distinct (≈1 с против 47 мс у Neo4j): развёртывание переменной длины с distinct у slater здесь существенно медленнее — реальная слабость, заслуживающая отдельного исследования. Всё это при нескольких сотнях МБ RSS против выделенной кучи Neo4j ~2 ГиБ.

О якорях. Эти показатели обходов сильно зависят от того, с каких узлов вы начинаете — узел на расстоянии одного перехода от мега-хаба Wikidata («человек», «страна») имеет окрестность в 2 перехода силой в миллионы, поэтому стоимость переходов/переменной длины колеблется на порядки в зависимости от выбора якоря. Более ранняя редакция этой таблицы выбирала собственные «первые N по сканированию» каждого движка, что не является ни стабильным, ни сопоставимым; этот проход фиксирует единый общий набор якорей с ограниченной степенью для обоих движков. (shortestPath опущен в этом проходе — между двумя произвольными якорями он зависит от существования пути и слишком вариативен для осмысленной медианы.)

Многопереходный count(*) — память не зависит от размера результата

Неограниченный многопереходный RETURN count(*) считает во время развёртывания вместо материализации совпавших строк. Те же якоря-хабы на графе с 91,6M, maxIntermediate=20M:

3-переходный count(*) @ 91,6Mfanout=1fanout=8
задержка / пиковый рабочий набор554 мс / 0,66 ГиБ298 мс / 1,9 ГиБ

Счётчик хранит O(1) строк. Тарификация не изменилась, поэтому счёт по мега-хабу по-прежнему превышает maxIntermediate по вычислениям (чтения смежности), ограниченный как раньше.

Параллелизм на запрос (maxFanout)

Повышение query.maxFanout перекрывает холодные, связанные с вводом-выводом чтения блоков запроса между ядрами — это помогает формам, связанным с диском и большим холодным рабочим набором, и не влияет на тёплые формы. На графе с 1,5B: shortestPath ≤6 918 → 608 мс (в 1,5 раза, самый большой поиск 6 269 → 2 350 мс, в 2,7 раза); 3-переходный count 547 → 298 мс. maxFanout=1 — значение по умолчанию (ориентировано на пропускную способность); 8 — это регулятор задержки, ценой большего объёма временной рабочей памяти.

Где slater выигрывает / проигрывает

Полные таблицы по каждому движку (pole, MeSH, EU-AI-Act + регулятор RAM↔задержка blockCacheBytes, Wikidata 1M и 91,6M) находятся в perf/cross-engine-hs/README.md; свежий проход только slater (оба fanout, каждый набор данных) — в perf/PERF_CURRENT_STATUS.md.

Параллелизм и деградация (нагрузочное тестирование)

Бенчмарки выше — одноклиентские. Дополнительная ось — поведение при множестве одновременных клиентов — имеет собственный стенд, perf/loadtest/: драйвер Locust поверх Bolt плюс координатор, который увеличивает нагрузку, читает CALL slater.diagnostics(), находит колено пропускной способности и называет ограничитель (полный метод в docs/LOAD-TESTING.md). Основные результаты прогона с кэшем 256 МиБ на графе Wikidata-1M (одна машина с 16 ядрами):

Обе проблемы с памятью, выявленные нагрузочным тестом, теперь закрыты; все отслеживаются в документе нагрузочного тестирования.

Лицензия

Лицензировано под Apache License, Version 2.0. См. LICENSE для полного текста и NOTICE для указания авторства. Если вы явно не заявите иное, любой вклад, намеренно представленный для включения в эту работу, как определено в лицензии Apache 2.0, лицензируется как указано выше, без каких-либо дополнительных условий.

SPDX-License-Identifier: Apache-2.0

Скачать инструмент
INSERT
SET
REMOVE
DELETE
ВозможностьЧто это значит для вас
Ограниченная, предсказуемая памятьРезидентная память отслеживает три бюджета кэша, которые вы задаёте, с ограниченными накладными расходами на запись и аллокатор — она не растёт с размером графа; вы настраиваете компромисс производительность/ОЗУ вместо того, чтобы выделять ресурсы под весь граф. Аллокатор jemalloc с фоновой очисткой возвращает освобождённую память ОС после интенсивных всплесков запросов, поэтому резидентный размер возвращается к своему базовому минимуму, а не остаётся зафиксированным на пике после всплеска.
Мультитенантность из коробкиОдин сервер размещает множество графов с правами чтения для каждого пользователя — изоляция нескольких баз данных, которую большинство графовых БД приберегают для платного/корпоративного уровня.
Шифрование при хранении и передачеПоблочное запечатывание XChaCha20-Poly1305 (ключ никогда не записывается на диск) плюс опциональный TLS (bolt+s://). GDPR-дружелюбно по конструкции. Шифрование также даёт аутентифицированную целостность: сборщик запечатывает манифест ключевым MAC, а сервер, владеющий ключом, проверяет его и отказывается обслуживать поколение, чей манифест подделан, изменён или лишён MAC. Образ без ключа (открытый текст) защищён только контентным хешем без ключа — полнота и повреждение, но не подделка. См. Что означает целостность в каждой конфигурации.
МикроустановкаНебольшой stripped-бинарник на базе distroless glibc (без shell/apt) — multi-arch (amd64/arm64) образ весит ~22 МБ, или ~12 МБ для серверного тега slater:latest-lite; чистый Rust TLS, без OpenSSL. Скачал и запустил.
Создан для периодической публикацииСоберите граф офлайн, обслуживайте его неизменяемым, затем атомарно подмените новую версию без простоя — идеально для рабочих нагрузок типа хранилище данных / плановое обновление.
Устойчив под нагрузкойСервер и офлайн-сборщик компилируются с #![forbid(unsafe_code)] — единственный unsafe в движке живёт в проверенном крейте аллокатора jemalloc. Ядро неизменяемо, поэтому чтение не берёт блокировок и никогда не ждёт писателя; один писатель сериализует мутации только за путём записи. Нет пауз GC, нет гонок данных. Один плохой запрос не может уронить сервер.
Работает с вашими инструментами neo4jГоворит на Bolt 5.4 / 4.4 / 4.1 — используйте стандартные драйверы neo4j (JS, Python, Go, Java…), cypher-shell или графовые браузеры без изменений.
Богатая поверхность запросов CypherШирокая поверхность чтения: MATCH/WHERE/WITH/UNION, подзапросы CALL {…}, 70+ функций и агрегаций, временные и геопространственные значения, регулярные выражения.
Живые, долговечные записиОпциональный слой LSM с одним писателем поверх неизменяемого ядра (delta.enabled): MERGE / SET / DELETE / CREATE / REMOVE по бизнес-ключам для узлов и связей, пакетный write-UNWIND (один fsync на пакет) и CALL slater.consolidate() — групповая фиксация, долговечность через fsync, сворачивание обратно в свежее ядро консолидацией. Путь чтения байт-в-байт идентичен, когда дельта пуста.
ISO GQL, чтение и записьГоворит на подмножестве ISO GQL (ISO/IEC 39075) через то же соединение Bolt — квантифицированные пути, ограничители путей, селекторы кратчайших путей, булевы выражения меток/типов, FOR, CAST, опциональный префикс диалекта GQL/CYPHER — а при включённом слое записи операторы модификации данных GQL (INSERT / SET / REMOVE / [DETACH] DELETE) опускаются на тот же долговечный путь записи. Cypher и GQL, чтение и запись, в одном движке.
Векторы + граф в одном движкеДисковый ANN-векторный поиск (Vamana + PQ; косинус / L2 / dot) для эмбеддингов/RAG, плюс графовые алгоритмы (PageRank, BFS, betweenness, WCC…) — ограниченная память даже с миллионами векторов. Эмбеддинги записываемы (лестница записи в стиле FreshDiskANN): вставка / обновление / удаление вектора, сразу видимого для KNN, сворачиваемого в базу без пересборки.
Безопасен на сетевых хранилищахКаждый файл хешируется BLAKE3 по содержимому и проверяется при открытии; разорванные или наполовину скопированные образы отклоняются, а не обслуживаются. Спроектирован для NFS/удалённых томов (без сюрпризов mmap).
Подключаемые бэкенды хранилищаОбслуживайте тот же формат поколения из локальной файловой системы, корзины S3 (S3-совместимой) или корзины Google Cloud Storage — публикуйте один раз, масштабируйте на реплики без состояния — с опциональным кэш-уровнем на локальном SSD перед объектным хранилищем. См. Бэкенды хранилища.
ПутьНазначениеПримечания
/dataПоколения графа (<graph>/<uuid>/… + current).Только для чтения для реплик; создаётся slater-build. Может находиться на удалённом/сетевом хранилище (например, NFS), поэтому чтения не предполагаются быстрыми на локальном SSD.
/sandboxНаложение конфигурации для каждого окружения + секреты./sandbox/config.json глубоко сливается поверх встроенного config.json; также содержит acl.json, PEM-материалы TLS, файл ключа хранения.
/tmp, /runВременные (tmpfs).Реплика чтения по умолчанию никогда не пишет на диск.
(писатель) delta.walDirЖурнал упреждающей записи + сегменты дельты L0, когда delta.enabled.Доступен для записи и является долговечным реальным томом — никогда не tmpfs (это нижняя граница долговечности). Относительный путь разрешается в каталоге данных; дайте писателю собственный постоянный том здесь.
(опционально) дисковый кэшЛокальный дисковый кэш блоков, когда dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0.Доступен для записи и является реальным томом — не tmpfs. Используется бэкендами s3 и gcs; см. Бэкенды хранилища.

Они независимы: разрешение read не даёт доступа на запись. Поэтому включение слоя записи не может превратить ваших существующих читателей в писателей. Писателю нужны оба — ["read", "write"] — потому что разрешение бизнес-ключа для записи — это чтение. Нераспознанные строки разрешений игнорируются (они ничего не дают).

движокклассограничение памяти
slaterна диске, страничныйquery.maxIntermediate автоматически ограничивает рабочий набор
Neo4j 5на диске, JVM~2 ГиБ кучи + вне кучи, выделяется независимо от запроса
Memgraph · FalkorDBв памятивесь граф в ОЗУ
ArcadeDBв памяти, JVMвесь граф в памяти; самый тяжёлый
LadybugDBвстроенный, колоночныйручной пул буферов, который должен превышать запрос
граф (узлы / рёбра)slaterNeo4j 5MemgraphFalkorDBArcadeDBLadybugDB
pole — 62k / 106k117461141401 556198
MeSH — 341k / 469k631 0833584551 631121
EU-AI-Act — 21k / 45k (+55 МиБ векторов)997292293121 948286
Wikidata — 91,6M / 1,5B584 (4 595 итого)~2 900невозможно загрузитьневозможно загрузитьневозможно загрузить~652 †
формаslaterNeo4j 5MemgraphFalkorDBArcadeDBLadybugDB
count(*) всех узлов0,4115,023,816,482,02,2
подсчёт меток0,424,220,71,14,44,3
индексированный точечный поиск0,433,90,480,480,658,8
idx-eq count0,424,95,02,03812,5
1 переход (индексированный якорь)1,285,81,214,13904,9
2 перехода (без якоря)1,405,68,516,74446,4
group-by / count(DISTINCT)0,4547–5163–6431–394115,3
полное сканирование CONTAINS0,435,424,11,716,34,1
формаslaterNeo4j 5MemgraphFalkorDBLadybugDB
kNN top-10 Concept2,98,61,91,22,8
kNN top-10 Chunk2,45,71,91,53,2
свойствоизмеренопочему это важно
Задержка KNN против ожидающих записейRW-индекс ~1,5–2 мс, стабильно до 50k ожидающих; предварительно индексированный слой с полным перебором 1,9 → 115 мс (линейно от дельты) — 61× при 50kзадержка запросов не деградирует по мере накопления записей между консолидациями
Вставка эмбеддинга~1,5–2 мс на вектор в живой индексзапись видна KNN немедленно; бюджет перестроения дельты ≈ 2 мс × предел дельты
Ввод-вывод удаления при изо-полнотев 2,9 раза меньше выборок узлов на запрос при 67 % удалённых, в 5,2 раза при 80 % (полнота ≥ 0,90)консолидированный граф не платит налог на чтение за удалённые векторы
Консолидация, чистая перестановкаO(1) — .vamana жёстко связан побайтово идентично, переписывается только столбец idсворачивание векторных записей в базу пропускает перестроение O(N·R·L)
Полнота по всей лестницеконсолидированный ≥ базового для косинуса, L2 и скалярного произведениялестница записи сохраняет полноту на каждой ступени
формаslater (fan 1)slater (fan 8)Neo4j 5
count(*) всех узлов0,410,413606
точечный поиск (индексированный)0,720,496,3
степень (количество 1 перехода)0,430,446,0
соседи за 1 переход9,84,510,1
2 перехода372334,5
3 перехода322574
переменная длина *1..2 distinct985105647
измерениеslaterлучший в полевердикт
резидентная память, любой масштаб11–584 МиБ (62k → 91,6M)в памяти 1,5–2,7 ГиБ; не может загрузить 1,5Bslater
count / метаданные / сканирование~0,4 мссервисные движки 5–80 мсslater (в 10–200 раз)
индексированный точечный поиск0,43 мс (MeSH)Memgraph · FalkorDB 0,48 мсslater (опережает пару в памяти)
многопереходный обход без якоря (строки)1,40 мс (MeSH, 2 перехода)Neo4j 5,6 мсslater (сканирование по типу отношения)
агрегация (group-by / DISTINCT)0,45 мсLadybugDB 5 мс (колоночный)slater (гистограмма времени сборки)
kNN2,4–2,9 мс (точный)FalkorDB 1,2 мс (HNSW)опережает Neo4j/Ladybug; ~в 1,4 раза от Memgraph; точный
91,6M метаданные / точка / степень / 3 перехода0,4–32 мсNeo4j 6–3 600 мсslater (в 2–8800 раз)
91,6M 1–2 перехода4,5–23 мс (fan 8)Neo4j 10–35 мс~наравне
91,6M переменная длина *1..2 distinct~1 сNeo4j 47 мсNeo4j (реальное слабое место slater)
многопереходный count(*) в масштабе0,3–0,6 ГиБдвижки в памяти материализуют набор строкslater, ограниченный
результатизмерение
Держит 1000 одновременных клиентов, ноль сбоевпропускная способность достигает пика ~2,5k rps; колено задержки наступает около 750 клиентов (p99 51 → 750 мс) — ожидание при конкуренции за ядра, а не жёсткий предел (одиночный прогон, WSL2)
Кэш блоков ограничен и эффективен100 % попаданий, 0 вытеснений, 50 МБ резидентной памяти для рабочего набора, помещающегося в кэш
RSS удерживается под постоянной нагрузкойаллокатор jemalloc удерживает RSS на уровне ~0,6 ГБ при разгоне wiki_cache_churn от 100 до 500 клиентов — ограничен кэшем и стабилен, без настройки MALLOC_* (прежние MALLOC_ARENA_MAX=2 и порог усечения отменены); его фоновая очистка также возвращает пиковое значение после всплеска, а не оставляет его закреплённым
Совокупная память ограниченаобщесерверный query.maxIntermediateGlobal + развёртывание с учётом смежности удерживают 2-переходный поток wiki_budget при 1000 клиентах без OOM (RSS ~0,6 ГБ; защита отклоняет ~60 % запросов к хабам как повторяемые ошибки бюджета)