
Графовая БД с низким потреблением памяти, поддержкой Bolt+TLS, шифрованием в состоянии покоя и векторами, предназначенная для сценариев использования локальных реплик графов.
Текущая версия: v0.25.2 — все релизы.
В одной строке: Slater обслуживает графы, которые не помещаются в память — сотни миллионов узлов и миллиарды рёбер в пределах нескольких сотен МБ оперативной памяти — через стандартный Bolt, поэтому любой драйвер neo4j работает без изменений, с дисковым векторным поиском рядом с графом, и он принимает живые, долговечные записи, не отказываясь от этого. Резидентная память определяется заданным вами бюджетом кэша, а не размером графа.
Быстрые ссылки
Графовая база данных хранит данные как сущности (узлы) и связи между ними (рёбра), где связи являются полноценными элементами. Это то, что нужно, когда ваши вопросы касаются связей, а не строк — «кто находится в пределах трёх переходов от этого счёта?», «какова полная цепочка зависимостей за этой сборкой?», «какие счета используют одно устройство, один адрес и одну карту?» — запросы, которые в 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 (отличный сериал), который настаивает на одном имени — «Просто… Слейтер» — и одного из моих любимых персонажей в нём. См. страницу персонажа в вики.
MERGE / SET / DELETE по бизнес-ключам для узлов и рёбер, с групповой фиксацией и долговечностью через fsync, сворачиваемые обратно в свежее ядро консолидацией. Чтение за это не платит.current, и серверы подхватят его. Каждый блок проверяется контрольной суммой, поэтому наполовину скопированный образ будет отклонён, а не обслужен.Рабочее пространство состоит из двух бинарников:
| Бинарник | Роль |
|---|---|
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/ —
пошаговое руководство по функциям, которое объясняет для каждой возможности, что это,
зачем она существует и как её использовать, с практическими примерами, которые можно
запустить на прилагаемом образцовом графе. Начните там для всего, что выходит за рамки
этого обзора.
graphiti-slater — это адаптер,
который позволяет Graphiti хранить свой временной
граф знаний в Slater, с готовым к запуску
docker-example/
— включая предоставление его Claude Code как MCP-сервера. См. этот репозиторий, чтобы узнать,
как это работает и как запустить.
Slater спроектирован для запуска как Docker-развёртывание — это ожидаемый способ
его использования. Готовые multi-arch образы (linux/amd64 + linux/arm64)
публикуются на Docker Hub по адресу
hikarisystems/slater,
с тегами :latest и :vX.Y.Z при каждом релизе:```sh
docker pull hikarisystems/slater:latest
Руководство по использованию, настройке и эксплуатации только через 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
docker compose build
docker compose up slater
build):docker compose run --rm builder
--input /dumps/people.cypher --graph people --data-dir /data
Стадия сборки устанавливает `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.--encrypt
каждый блок дополнительно запечатывается XChaCha20-Poly1305 (AEAD в состоянии покоя).При 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)
└─────────────┘
* **Нижняя граница долговечности — 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
# 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
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
dataBackend__kind=gcs dataBackend__gcs__bucket=slater dataBackend__gcs__prefix=prod dataBackend__gcs__credentialsPath=/secrets/sa.json # omit for ADC / Workload Identity
```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, когда вам нужны поколения в долговечном центральном
объектном хранилище, а не на диске узла — как правило: публикуйте один раз и
распространяйте на множество не имеющих состояния реплик сервера без дисков,
которые все читают один и тот же бакет; разделите хост сборки и хост обслуживания;
или полагайтесь на долговечность/версионирование/жизненный цикл хранилища вместо
управления томами. Обратная сторона — задержка: холодный блок — это сетевой
круговой обход (~10–50 мс) вместо локального чтения (~0,1 мс). Slater скрывает
большую часть этого с помощью кэша блоков в памяти, упреждающего чтения
и опционального дискового кэша ниже. Если ваши поколения уже находятся на
быстром локальном хранилище и вам не нужна модель центрального бакета, fs
проще и быстрее.
Кэш блоков BlockCache в памяти намеренно мал (ограниченная RSS — главная
гарантия), поэтому при рабочем наборе, превышающем RAM, одни и те же блоки
повторно извлекались бы из объектного хранилища при каждом вытеснении.
Опциональный второй уровень кэша на локальном SSD решает эту проблему: блок,
вытесненный из RAM, обслуживается с локального диска (~0,1 мс) вместо нового
GET-запроса к объектному хранилищу, переживая вытеснение из памяти и сокращая
количество/стоимость запросов к объектному хранилищу — приближая узел на
объектном хранилище к производительности локальной файловой системы после
прогрева. Он включается вручную как для s3, так и для gcs, путём
установки dataBackend.<s3|gcs>.diskCacheBytes > 0 и доступного для записи
diskCacheDir.
--encrypt) всё ещё запечатанные AEAD — ниже
уровня расшифровки/распаковки. Уровень кэша никогда не хранит ключ шифрования
и никогда не перешифровывает, поэтому статус хранения сохраняется бесплатно:
зашифрованное поколение попадает на диск всё ещё запечатанным.diskCacheDir должен указывать на реальный доступный для записи том — никогда
не tmpfs (tmpfs — это RAM и нарушил бы гарантию ограниченной RSS).
Индекс в памяти, отслеживающий его, стоит немного RAM (~десятки байт на
кэшированный блок), что учитывается в вашем пределе RSS — задавайте размер
каталога ≫ кэша блоков в памяти.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.json сопоставляет пользователей с хэшами паролей argon2id и разрешениями
read / write для каждого графа. Создайте хэш (никогда не храните
открытый текст) с помощью:```sh
slater hash-password 's3cret' # prints a $argon2id$… string for acl.json
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
## Одноразовый запрос
Для написания скриптов, проверок в 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
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/.
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
### Бэкенды объектного хранилища — опциональные возможности 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 также не может его завершить.
Каждый показатель — это выделенная рабочая память — то, что ОС не может вернуть. Каждый движок, кроме 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 превышает предел кардинальности гистограммы, поэтому ничего не сохраняется) — поэтому эти показатели
не изменяются этой функцией.
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 мс на переходах.)
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 и локальным диском, поэтому абсолютные МиБ/с зависят от окружения (в отчёте показана форма и объяснён диапазон влияния окружения).
Движки в памяти (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,6M | fanout=1 | fanout=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 — это
регулятор задержки, ценой большего объёма временной рабочей памяти.
Полные таблицы по каждому движку (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
INSERTSETREMOVEDELETE| Возможность | Что это значит для вас |
|---|
| Ограниченная, предсказуемая память | Резидентная память отслеживает три бюджета кэша, которые вы задаёте, с ограниченными накладными расходами на запись и аллокатор — она не растёт с размером графа; вы настраиваете компромисс производительность/ОЗУ вместо того, чтобы выделять ресурсы под весь граф. Аллокатор 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 | встроенный, колоночный | ручной пул буферов, который должен превышать запрос |
| граф (узлы / рёбра) | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| pole — 62k / 106k | 11 | 746 | 114 | 140 | 1 556 | 198 |
| MeSH — 341k / 469k | 63 | 1 083 | 358 | 455 | 1 631 | 121 |
| EU-AI-Act — 21k / 45k (+55 МиБ векторов) | 99 | 729 | 229 | 312 | 1 948 | 286 |
| Wikidata — 91,6M / 1,5B | 584 (4 595 итого) | ~2 900 | невозможно загрузить | невозможно загрузить | невозможно загрузить | ~652 † |
| форма | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| count(*) всех узлов | 0,41 | 15,0 | 23,8 | 16,4 | 82,0 | 2,2 |
| подсчёт меток | 0,42 | 4,2 | 20,7 | 1,1 | 4,4 | 4,3 |
| индексированный точечный поиск | 0,43 | 3,9 | 0,48 | 0,48 | 0,65 | 8,8 |
| idx-eq count | 0,42 | 4,9 | 5,0 | 2,0 | 381 | 2,5 |
| 1 переход (индексированный якорь) | 1,28 | 5,8 | 1,21 | 4,1 | 390 | 4,9 |
| 2 перехода (без якоря) | 1,40 | 5,6 | 8,5 | 16,7 | 444 | 6,4 |
| group-by / count(DISTINCT) | 0,45 | 47–51 | 63–64 | 31–39 | 411 | 5,3 |
полное сканирование CONTAINS | 0,43 | 5,4 | 24,1 | 1,7 | 16,3 | 4,1 |
| форма | slater | Neo4j 5 | Memgraph | FalkorDB | LadybugDB |
|---|
| kNN top-10 Concept | 2,9 | 8,6 | 1,9 | 1,2 | 2,8 |
| kNN top-10 Chunk | 2,4 | 5,7 | 1,9 | 1,5 | 3,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,41 | 0,41 | 3606 |
| точечный поиск (индексированный) | 0,72 | 0,49 | 6,3 |
| степень (количество 1 перехода) | 0,43 | 0,44 | 6,0 |
| соседи за 1 переход | 9,8 | 4,5 | 10,1 |
| 2 перехода | 37 | 23 | 34,5 |
| 3 перехода | 32 | 25 | 74 |
переменная длина *1..2 distinct | 985 | 1056 | 47 |
| измерение | slater | лучший в поле | вердикт |
|---|
| резидентная память, любой масштаб | 11–584 МиБ (62k → 91,6M) | в памяти 1,5–2,7 ГиБ; не может загрузить 1,5B | slater |
| 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 (гистограмма времени сборки) |
| kNN | 2,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 % запросов к хабам как повторяемые ошибки бюджета) |