
로컬 복제 그래프 사용 사례를 위해 설계된 저메모리 그래프 DB로, Bolt+TLS 지원, 저장 데이터 암호화 및 벡터를 제공합니다.
현재 버전: v0.25.2 — 모든 릴리스.
한 줄 요약: Slater는 메모리에 맞지 않는 그래프 — 수억 개의 노드와 수십억 개의 엣지를 수백 MB 남짓의 RAM으로 — 표준 Bolt 프로토콜로 서비스하므로 어떤 neo4j 드라이버든 그대로 동작하며, 그래프 옆에는 디스크 기반 벡터 검색이 자리 잡고, 이 모든 것을 포기하지 않으면서 실시간 내구성 있는 쓰기를 처리합니다. 상주 메모리는 그래프 크기가 아니라 사용자가 선택한 캐시 예산에 의해 결정됩니다.
바로가기
| Slater가 필요한 이유 | 읽기와 쓰기 | 제공 기능 | 기능 |
| Docker로 실행하기 | 작동 방식 | 쓰기 가능 계층 | 스토리지 백엔드 |
| 마운트 | 구성 | ACL | 상태 확인 |
| 작업 예제 | 개발 | 성능 | 라이선스 |
| Graphiti 메모리 저장소 | 📖 전체 매뉴얼 |
그래프 데이터베이스는 데이터를 사물(노드)과 사물 간의 관계(엣지)로 저장하며, 관계를 일급 시민으로 취급합니다. 이는 질문이 행이 아니라 연결에 관한 것일 때 필요한 것입니다. — "이 계정에서 세 홉 이내에 있는 사람은?", "이 빌드 뒤의 전체 의존성 체인은?", "어떤 계정이 장치, 주소, 카드를 공유하는가?" — SQL에서는 재귀 조인의 늪이 되지만 그래프에서는 자연스럽게 풀리는 질문들입니다.
그래프 데이터베이스에 대한 가장 흔한 불만은 RAM에 담을 수 있는 크기 이상으로 확장되지 않는다는 것입니다. 많은 제품(예: neo4j, Memgraph, FalkorDB 등)은 전체 그래프를 메모리에 상주시킵니다. 40 GB 그래프는 40 GB 메모리를 요구합니다 — 인스턴스당. 지역별, 테넌트별, 파드별로 복제본을 원하시나요? 비용이 배가됩니다. 그리고 특정 크기를 넘어서면 아예 로드되지 않습니다. 예를 들어 9천만 노드 / 15억 엣지 Wikidata 그래프는 ~64–128 GiB 상주 메모리가 필요하므로 인메모리 엔진은 전혀 열 수 없습니다.
Slater는 그에 대한 반박입니다. 그래프를 메모리에 로드하는 대신 오프라인에서 한 번 컴파일합니다. slater-build는 데이터를 콘텐츠 주소 지정 방식의 불변 온디스크 이미지로 변환하고, 원하는 수의 Slater 서버가 그 이미지를 Bolt 프로토콜로 서비스합니다(따라서 기존 neo4j 드라이버가 그대로 동작합니다). 블록을 요청 시 페이지인하고 고정된 캐시 예산만 상주시키는 방식입니다. 이것이 동일한 9천만 노드 그래프를 수백 MB의 RAM으로 서비스하는 이유입니다 — 그래프 크기와 메모리 비용이 분리됩니다. 4 GB 그래프와 400 GB 그래프를 서비스하는 데 필요한 RAM은 동일하므로, 저렴하고 상태 없는 읽기 복제본을 팬아웃하고 힙(heap)이 아닌 스토리지가 그래프를 보유하게 합니다.
따라서 RAG 뒤의 지식 그래프, 추천 및 신원 그래프, 의존성 그래프 — 저렴하고 자주 쿼리하고 싶은 크고 연결된 모든 것 — 에 자연스럽게 적합합니다. 디스크 기반 벡터 검색이 그래프 바로 옆에 있으므로, 같은 엔진이 임베딩의 검색 계층도 담당합니다.
하지만 한 번 컴파일한다고 해서 고정된 것은 아닙니다. 그 이미지는 **기본(base)**이지 최종 상태가 아닙니다. 옵트인 쓰기 계층이 그 위에 있으므로, 라이브 그래프를 재빌드 없이 수정하고 확장할 수 있습니다.
코어는 불변이지만 그래프는 불변이 아닙니다. 쓰기 가능 계층(delta.enabled)을 켜면 Bolt로 쓰기를 수행할 수 있습니다. — 속성 하나를 수정하고, 노드를 추가하고, 엣지를 철회하세요 — 이미지를 재빌드하지 않아도 변경 사항이 내구성 있게 저장됩니다. 읽기 측면에서 비용을 낮게 유지하는 비결은 쓰기가 어디에 저장되는가입니다.
쓰기는 불변 코어 위의 로그 구조 병합(LSM) 계층에 누적됩니다. 즉, 쓰기-어헤드 로그와 인메모리 테이블이 있고, 불변 델타 세그먼트로 넘쳐 흐르며, 주기적인 **통합(consolidation)**을 통해 새 코어로 접혀 들어갑니다. 이를 통해 얻는 이점은 다음과 같습니다.
count(*), 레이블 및 관계 유형 주변값(marginals) — 은 쓰기가 쌓여 있어도 메타데이터 읽기로 유지됩니다. 델타는 자체 카운터를 유지하므로, 50만 건의 대기 중인 쓰기가 있는 91.6M 노드 코어에 대한 count(*)는 블록 하나를 건드리지 않고도 수십 밀리초 안에 응답합니다.fsync 후에만 SUCCESS를 반환합니다. 쓰기를 그룹화하면 비용이 저렴합니다 — 쓰기-UNWIND는 행마다가 아니라 배치마다 하나의 fsync를 커밋합니다.MERGE / MATCH … SET / DELETE (및 CREATE / REMOVE, 분리 삭제, 관계 쓰기) — 또는 동일한 경로로 내려가는 ISO GQL 데이터 수정 문(INSERT / SET / / ). 데이터가 이미 정리된 방식으로 노드와 엣지에 대해 수정, 삽입, 업서트, 철회를 수행합니다.계층이 꺼진 상태(기본값)에서는 Slater가 순수한 불변 코어만 서비스하고 쓰기를 거부합니다. 전체 모델은 쓰기 가능 계층을 참조하세요.
이름에 대하여. Slater는 Archer (훌륭한 쇼)에 나오는 CIA 요원의 이름을 따서 지어졌습니다. 그 요원은 단 하나의 이름으로 불리기를 고집합니다 — "그냥… Slater" — 그리고 제가 가장 좋아하는 캐릭터 중 하나이기도 합니다. 자세한 내용은 캐릭터 위키 페이지를 참조하세요.
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) — 반면 쓰기 가능 계층의 델타 오버레이는 해당 표면 아래에 있으며 비어 있을 때 비용이 0이므로 읽기는 쓰기 측 메커니즘을 전혀 수반하지 않습니다. 그래프를 갱신하는 방법은 두 가지입니다: Bolt를 통해 라이브로 쓰기(쓰기 가능 계층 참조), 또는 오프라인에서 새 세대를 빌드하고 current 포인터를 원자적으로 교체하는 것입니다. 실행 중인 서버는 세대 가드를 통해 이를 인식합니다 (세대 가드 참조).
전체 사용자 매뉴얼은 **docs/manual/**에 있습니다 — 모든 기능에 대해 무엇인지, 왜 존재하는지, 어떻게 사용하는지 설명하는 기능별 가이드로, 번들로 제공되는 샘플 그래프로 실행할 수 있는 작업 예제가 포함되어 있습니다. 이 개요 이상의 내용은 여기서 시작하세요.
graphiti-slater는
Graphiti가 시간적 지식 그래프를 Slater에 저장할 수 있게 하는 어댑터입니다. 실행 가능한
docker-example/
— MCP 서버로 Claude Code에 노출하는 것도 포함합니다. 작동 방식과 실행 방법은 해당 저장소를 참조하세요.
Slater는 Docker 배포로 실행되도록 설계되었습니다 — 그것이 예상되는 사용 방식입니다. 사전 빌드된 멀티 아키텍처 이미지(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
빌더 단계는 rustls의 `aws-lc-rs` 백엔드를 위해 `cmake`, `clang`, `libclang-dev`를 설치합니다. `git`(기본 이미지에 이미 포함됨)은 `hs-utils`의 git+tag 의존성에 필요하며, `.cargo/config.toml`이 git CLI를 통해 이를 가져옵니다.
아래 섹션에서는 디스크상의 형식, 구성, 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가 설정되면, 불변 generation은 작은 로그 구조 병합(log-structured-merge) 트리의 **완전히 압축된 하위 레벨("core")**이 되고, 실시간 쓰기는 그 위에서 이뤄집니다:```
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.** 각 변이는 그래프별 단일 writer 뒤에서 직렬화되어,
그래프별 write-ahead 로그에 추가되고 Bolt `SUCCESS`가 반환되기 전에 `fsync`됩니다.
따라서 *승인됨 ⇒ 내구성 있음*이며, 재생 시 불완전하게 기록된 꼬리는 버려집니다.
배치 쓰기-`UNWIND`은 해당 행들을 추가하고 전체 배치에 대해 **한 번의** `fsync`으로
커밋합니다. WAL은 **로컬 디스크 전용**입니다(스토리지 백엔드를 거치지 않음).
따라서 *writer* 노드는 상태를 가지며, `delta.walDir`에 내구성 있는 로컬 볼륨이
필요합니다. 읽기 복제본은 상태 없이 유지됩니다.
* **Memtable → L0 → 통합.** 쓰기는 RAM 내 memtable에 누적됩니다
(`delta.memtableBytes`로 제한). 가득 차면 불변 L0 delta 세그먼트로
플러시됩니다. **통합(consolidation)**은 병합된 뷰를 `slater-build`를 통해
다시 직렬화하고 `current`를 원자적으로 교체하여 `{core + delta}`를 새
코어로 접습니다. 이는 게시된 모든 세대와 동일한 콘텐츠 해시 보호 장치입니다.
수동으로는 `CALL slater.consolidate()`로, 자동으로는 코어 크기의
`delta.deltaCorePercent`에서(선택적으로 비수기 `delta.consolidateWindow`로
제한) 트리거하거나, `delta.deltaHardBytes` 스로틀이 폭주하는 증가를
막도록 둘 수 있습니다.
* **오버레이는 읽기 표면 아래에 위치합니다.** 실행기는 `ReadView`를 통해 읽습니다.
이 뷰는 순수 코어(delta가 항상 비어 있음)이거나 병합된 `(core, delta)` 뷰입니다.
엔진은 이 뷰에 대해 모노모피즈되므로, 빈 delta는 하나의 예측 가능한 분기로
컴파일되고 읽기 전용 경로는 바이트 단위로 동일합니다. 전체 그래프 카운터
(`count(*)`, 라벨/관계 유형 주변값)는 delta 자체의 라이브 카운터에서 제공되므로,
쓰기가 보류 중이어도 메타데이터 읽기로 유지됩니다.
* **쿼리는 안정적인 스냅샷을 봅니다.** 쿼리는 수명 전체에 걸쳐 하나의
`(core, delta)` 튜플을 고정합니다. 다중 문 트랜잭션도 롤백도 없습니다. 쓰기는
OLTP 트랜잭션이 아니라 내구성 있는 비즈니스 키 주소 지정 수정입니다.
정확한 쓰기 문법과 조정 항목은 아래의 [구성](#environment--configuration)
표(`delta.*`)와 [작동 예제](#worked-example)에 있습니다.
### 범위 인덱스 (ISAM)
범위 인덱스(`range/<name>.isam`, 인덱스된 `(label, property)`마다 하나)를 사용하면
`MATCH (n:Label {prop: v})` 또는 `WHERE n.prop <op> v`가 **라벨을 스캔하지 않고**
일치하는 노드 id를 찾을 수 있습니다. 이는
**[ISAM](https://en.wikipedia.org/wiki/ISAM)**(Indexed Sequential Access Method)
구조입니다. 즉, 고전적인 *정적, 정렬, 블록 구조* 인덱스로, 불변 세대에 정확히
맞는 형태입니다. 재균형을 위한 삽입이 없으므로 ISAM의 단순함은 B-트리의 변이
메커니즘이 오히려 복잡하게 만들 뿐인 부분을 얻습니다.
* 항목 `(value, entity_id)`은 값별로 정렬되어 다른 모든 것과 동일한
zstd 압축 256 KiB 블록에 담깁니다.
* 작은 **상주 최상위 레벨(resident top-level)**은 각 블록의 첫 번째 키를 보유합니다
(스파스 인덱스). 조회는 이 메모리 내 최상위 레벨을 이진 탐색하여 키가 있을 수 있는
*하나의* 블록을 찾은 뒤, 해당 블록을 읽고 압축을 풀어 스캔합니다. 따라서 동등
조회는 **블록 읽기 한 번**이며, 범위 스캔은 해당 범위에 걸친 연속적인 블록들을
차례로 탐색합니다. (이것이 `meshUi` 인덱스 조회가 수 밀리초인 반면, 인덱스되지
않은 속성에 대한 동일한 매치는 전체 라벨을 스캔하는 이유입니다.)
* 플래너는 `NodeScan::RangeEq` / `RangeRange`를 통해 이를 선택합니다. 인덱스되지
않은 조건은 라벨 스윕 또는 전체 스캔으로 폴백하며, 실행기는 어느 쪽이든 모든
조건을 다시 확인합니다.
### 벡터 검색 (Vamana + PQ) — cosine, L2 및 dot, 읽기 *및* 쓰기
벡터 KNN(`db.idx.vector.queryNodes`)은 **cosine, L2 또는 dot-product (MIPS)**
인덱스에 대해 실행됩니다. 기본 인덱스는 오프라인으로 두 가지 실행 경로로 구축되며,
인덱스별로 `--ann-threshold`(기본값 50 000 벡터)에 따라 선택됩니다.
* **임계값 미만 — brute force.** 전체 `f32` 벡터는 `vectors.f32.blk`에 있습니다.
쿼리는 인덱스의 그룹을 스캔하고 인덱스의 메트릭으로 정확한 거리를 계산합니다.
단순하고 정확하며, 벡터 집합이 작을 때 적합합니다.
* **임계값 이상 — Vamana + PQ**. 벡터 수와 관계없이 상주 메모리를 제한된
범위로 유지하는 디스크 네이티브 ANN 경로입니다:
* [**Vamana**](https://arxiv.org/pdf/2401.11324)는 DiskANN 계열 작업의 그래프
인덱스입니다. 단일 근접 그래프이며 가장자리가 정리되어(`--vamana-r` out-degree와
`--vamana-alpha` long-edge factor) *탐욕적 빔 탐색* — 메도이드에서 시작하여
쿼리 쪽으로 반복적으로 점프하며 `vectorQuery.beamWidth` 너비의 후보 목록을 유지 —
이 몇 번의 홉으로 노드의 실제 이웃에 도달합니다. 즉, **쿼리당 몇 번의 랜덤
블록 읽기**가 발생합니다. 그래프 블록(`vector/<label>.<prop>.vamana`)은 벡터
캐시를 통해 페이징되며, 전체를 보유하지 않습니다.
* [**Product quantisation (PQ)**](https://medium.com/aiguys/product-quantization-k-nn-for-big-datasets-12431d764c4e)은
각 벡터를 짧은 코드(`--pq-subspaces` × `--pq-bits`)로 압축합니다. 차원은 부분
공간으로 나뉘고 각각 독립적으로 k-means 클러스터링되며, 벡터는 가장 가까운
중심점 id들의 튜플로 저장됩니다. 이 코드들(`vector/<label>.<prop>.pq`)은
**상주**시킬 수 있을 만큼 작아서, 빔 탐색은 RAM에서 후보를 점수화하고 선택된
소수의 전체 벡터만 디스크에서 읽습니다. 이 상주 PQ 집합이
`cache.vectorCacheBytes` 풀이 고정하는 대상입니다.
**기록 가능한 임베딩 — 벡터 쓰기 사다리 ([FreshDiskANN](https://arxiv.org/abs/2105.09613) 방식).**
인덱스된 임베딩은 일급 쓰기 가능 값입니다. `SET n.embedding = vecf32([…])`(및 `REMOVE`)는
쓰기 delta에 반영되고 **정확한 순위로 즉시 KNN에 표시**되며, 이후 세그먼트 플러시,
병합, 통합을 거쳐도 유지됩니다. 쿼리는 봉인된 기본 인덱스, 봉인된 세그먼트별
인덱스, 메모리 내 **RW-index**(쓰기 delta 위의 라이브 가변 Vamana)의 최대 세
레벨을 병합합니다. 따라서 쓰기가 누적되어도 지연 시간은 보류 중인 쓰기 수에 따라
증가하지 않고 일정하게 유지됩니다. 삭제는 *구멍(hole)*을 남깁니다. 노드는 반환되지
않지만 배경 **delete-consolidation**이 그래프에서 잘라낼 때까지 탐색 경유지로
남습니다. 따라서 삭제는 더 이상 쿼리 IO 비용을 발생시키지 않습니다. 그리고
디스크상의 그래프는 노드 id가 아니라 레이아웃 위치로 이웃을 주소 지정하므로,
`CALL slater.consolidate()`는 Vamana를 **참조로** 전달합니다(하드 링크, 바이트 단위
동일). 즉, 작은 id 열만 다시 쓰면서 O(N·R·L) 그래프 재구축 **없이** 벡터 쓰기를
기본 인덱스로 접어 넣습니다. 측정 수치와 주의사항은 [성능 보고서](https://github.com/hikari-systems/slater/blob/main/docs/PERF-REPORT.md)에 있습니다.
## 스토리지 백엔드 (파일시스템 / S3 / GCS)
모든 세대 파일은 `std::fs`를 직접 사용하지 않고 **`ObjectStore`** 추상화를 통해
열립니다. 따라서 *동일한* 디스크 바이트 형식(블록, 인덱스, 매니페스트, `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 | `HEAD`를 통한 서버 **SHA-256**(없으면 BLAKE3 본문 재해시) | 구성 키, AWS 체인 또는 IAM 역할 |
| `gcs` | HTTP 범위 읽기 | `get_object`를 통한 서버 **CRC32C**(없으면 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`, 임시 STS 자격 증명에는
`awsSessionToken` 추가), 비어 있으면 표준 AWS 체인(`AWS_ACCESS_KEY_ID` /
`AWS_SECRET_ACCESS_KEY` 환경 변수, 공유 프로필 또는 인스턴스/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)JSON API를 통해 접근하는 GCS 버킷입니다. 인증은 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를 사용하세요. 일반적으로: 한 번 게시하고 동일한 버킷을 읽는 많은 무상태(stateless), 디스크 없는 서버 복제본으로 팬아웃하거나, 빌드 호스트와 서브 호스트를 분리하거나, 볼륨을 관리하는 대신 스토어의 내구성/버전 관리/수명 주기 기능을 활용하는 경우입니다. 대가는 지연 시간입니다: 콜드 블록은 로컬 읽기(~0.1ms) 대신 네트워크 왕복(~10–50ms)이 발생합니다. Slater는 인메모리 블록 캐시, 동시 읽기-어헤드(read-ahead), 그리고 아래의 선택적 디스크 캐시로 이를 대부분 숨깁니다. 생성물이 이미 빠른 로컬 스토리지에 있고 중앙 버킷 모델이 필요 없다면 fs가 더 간단하고 빠릅니다.
인메모리 BlockCache는 의도적으로 작게 유지됩니다(제한된 RSS가 핵심 보장입니다). 따라서 RAM보다 큰 작업 세트에서는 동일한 블록이 스필(spill)될 때마다 객체 스토어에서 다시 가져와야 합니다. 선택적 로컬 SSD 2차 캐시 계층이 이를 해결합니다: RAM에서 축출된 블록은 새 객체 GET 대신 로컬 디스크(~0.1ms)에서 제공되며, 인메모리 축출에서도 살아남아 객체 스토어 요청 수/비용을 줄입니다 — 워밍업 후에는 객체 스토어 백엔드 노드를 로컬 파일시스템 성능에 가깝게 만듭니다. s3 및 gcs 모두 옵트인(opt-in) 방식이며, dataBackend.<s3|gcs>.diskCacheBytes > 0과 쓰기 가능한 diskCacheDir을 설정하여 활성화합니다.
--encrypt 생성물의 경우) 여전히 AEAD로 봉인된 상태로 — 복호화/압축 해제 아래에서 처리합니다. 캐시 계층은 암호화 키를 보유하지 않으며 다시 암호화하지 않으므로 저장 시(at-rest) 상태가 그대로 유지됩니다: 암호화된 생성물은 여전히 봉인된 채 디스크에 저장됩니다.diskCacheDir 은 반드시 실제 쓰기 가능한 볼륨을 가리켜야 합니다 — 절대 tmpfs가 아님(tmpfs는 RAM이므로 제한된 RSS 보장을 무너뜨립니다). 이를 추적하는 인메모리 인덱스는 약간의 RAM(캐시된 블록당 수십 바이트)을 소비하며, 이는 RSS 상한에 포함됩니다 — 디렉터리를 인메모리 블록 캐시보다 훨씬 크게 잡으세요.blockCacheBytes / 8(기본값 8MiB, diskCacheBytes에 의해 하한 조정)로 제한되며, 증가 대신 버려지므로 콜드 스캔이 이를 부풀릴 수 없습니다. 버려진 블록은 다음 미스 시 단순히 다시 가져옵니다. 구성이 필요 없습니다: blockCacheBytes에 따라 확장되므로 디스크 계층은 인덱스 외에 RSS 예산에 새 숫자를 추가하지 않습니다.읽기 복제본(read replica) 은 읽기 전용 루트 파일시스템과 비루트 사용자(appuser:1000)로 실행됩니다 — 필요한 모든 것은 읽기 전용으로 마운트됩니다. 쓰기 노드(delta.enabled)는 WAL을 위해 내구성 있는 쓰기 가능 볼륨이 하나 더 필요합니다.
구성은 하우스 표준 계층형 로더로 로드됩니다: 내장된 config.json, 그 위에 /sandbox/config.json을 딥-머지하고, 그 다음 KEY__sub 환경 변수 오버라이드(중첩은 이중 밑줄; 키는 camelCase 구성과 일치)가 적용됩니다.
모든 구성 노브 — camelCase 키, KEY__sub 환경 변수 오버라이드, 기본값, 그리고 기능 — 은 구성 참조 에 표로 정리되어 있습니다. 가장 많이 조정되는 노브는 캐시 예산(cache.*), 쿼리 가드(query.*), 연결 상한(server.*), 스토리지 백엔드(dataBackend.*), 쓰기 가능 계층(delta.*)입니다.
상주 메모리는
blockCacheBytes + vectorCacheBytes + resultCacheBytes를 항목별 및 할당자 오버헤드의 제한된 범위 내에서 추적합니다 — 각 풀은 자체 내용물(문자열과 컨테이너는 할당된 용량 기준)의 무게를 계산하고 예산 내에 유지하도록 축출하지만, 항목별 부기와 할당자의 크기 클래스 반올림은 설정한 숫자 위에 추가됩니다 — 여기에 약간의 고정 오버헤드(그리고 합계 count(endpoint) 빠른 경로가 실행된 후 lazy 차수 열에 대해 최대 degreeColumnBytes)가 더해집니다. 그래프 크기와 무관합니다 — 이것이 핵심 보장이며, rss_stays_bounded_under_sustained_knn_load 통합 테스트로 검증됩니다. 이 테스트는 합산 예산 안에서 피크 대비 워밍 RSS 증가를 유지합니다. 연결별 버퍼는 캐시 예산 밖에 있으므로, 공격적인 부하에서도 server.maxConnections이 동시에 존재할 수 있는 연결 수를 제한하기 때문에 보장이 유지됩니다.
Slater는 읽기 복제본 핸들입니다. 1차 연결 보안 제어는 바이너리가 아니라 네트워크입니다. 사설 인터페이스에 바인딩하고, 네트워크 계층(보안 그룹 / 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(기본값): 서버는 치명적(fatal) 로그를 남기고 0이 아닌 종료 코드로 종료되어 오케스트레이터가 새 생성물로 깨끗하게 재시작합니다.reloadStrategy=swap: 서버는 새 생성물을 열고 검증한 다음(부팅과 동일한 콘텐츠 해시 가드), 원자적으로 교체하고 진행 중인 쿼리는 이전 생성물에서 완료되도록 합니다. 손상되었거나 불완전한 새 이미지는 거부되며 이전 생성물이 계속 서빙됩니다.acl.json은 사용자를 argon2id 비밀번호 해시 및 그래프별 read / write 권한에 매핑합니다. 해시는 다음과 같이 생성합니다(평문을 저장하지 마세요):```sh
slater hash-password 's3cret' # prints a $argon2id$… string for acl.json
저장소 루트에는 시작용 `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 — slater hash-password에서 생성된 $argon2id$… 문자열
(절대 평문이 아님; 파일 자체는 일반 JSON이며 공유 스토리지에 저장됨).
grants — 그래프별 권한 목록. 두 가지 권한이 의미 있음:
read — 그래프를 질의. 사용자의 grants에 없는 그래프는 사용자에게 보이지 않음.write — 쓰기 가능 계층(delta.enabled)을 통해 그래프를 변경: MERGE / SET / DELETE 문과 CALL slater.consolidate().이 둘은 독립적입니다: read 권한은 쓰기 액세스를 부여하지 않습니다. 따라서 쓰기 가능
계층을 켜도 기존 읽기 사용자가 쓰기 사용자로 승격되지 않습니다. 쓰기 사용자는
둘 다 — — 필요합니다. 비즈니스 키를 확인해 쓰는 것 자체가 읽기이기 때문입니다.
인식되지 않은 권한 문자열은 무시됩니다(아무것도 부여하지 않음).
aclPath가 지정하는 경로(기본값 /config/acl.json)에 읽기 전용으로 마운트하세요.
서버는 세대(generation) 핫스왑마다 이를 다시 로드하며, 저장된(at-rest) ACL 스탬프는
모든 리로드 시 다시 확인됩니다(requireAclStamp 참조).
slater 바이너리는 자체 liveness 프로브 역할도 합니다: 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"]]}
Nodes and relationships expand to their labels/type and properties. Use -q
when you want machine-parseable output (the result JSON is the only thing on
stdout); omit it for an operator-facing run with logs. Without -q a
metrics-only summary is logged after each run — e.g.```text
INFO query executed cost=2389 resultCount=10 execMs=441 limitRowCount=10
쿼리 `cost`(청구된 요소 수), `resultCount`, `execMs`, 그리고
`limitRowCount`(쿼리가 `LIMIT`를 지정한 경우에만)를 전달합니다 — 쿼리 텍스트나
결과 값은 절대 포함하지 않습니다. 종료 상태는 성공 시 `0`, 구문 분석/열기/실행
오류 시 `1`입니다(stderr에 메시지 출력).
## 그래프 내보내기 (`slater dump`)
`slater dump`는 **실행 중인** 서버에서 그래프를 비즈니스 키 `MERGE`
Cypher로 내보냅니다 — `slater-build`가 읽어들이는 것과 동일한 방언 — 따라서
마이그레이션이나 텍스트 백업을 위해 그래프가 왕복할 수 있습니다
(dump → `slater-build` → 새 세대 생성). `slater query`와 달리 **Bolt**
프로토콜로 연결하고, 인증하며, 그래프별 ACL을 준수하므로 서버에 대한 디스크
접근이 필요 없습니다. 비밀번호는 `SLATER_DUMP_PASSWORD` 또는 stdin에서
읽습니다(절대 플래그로 받지 않으므로 `ps`/히스토리에 노출되지 않습니다).```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>로 재정의하세요. CREATE INDEX DDL이 먼저 생성되어 재구축 시 인덱스가 다시 만들어집니다. 다중 라벨 노드는 모든 라벨을 유지하며, MERGE (n:Ident:Other {key: v})로 생성됩니다. 이때 아이덴티티 라벨(비즈니스 키를 공급하는 라벨)이 먼저 오고 나머지는 정렬됩니다. MERGE는 아이덴티티 라벨만을 기준으로 하므로, 나머지 라벨들은 새 노드를 만들지 않고 기존 노드에 기록됩니다. 특수 문자를 포함하는 라벨, 관계 유형, 속성 키는 생성 시 백틱으로 인용되므로, 특이한 이름이 충실하게 왕복되며 재구축에 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
각 크레이트는 graph-format/{s3,gcs}로 전달되는 일치하는 s3 / gcs 기능을 노출합니다. 런타임에 백엔드를 요청할 때(dataBackend.kind=s3|gcs 또는 slater-build --publish-{s3,gcs}-*) 해당 기능이 컴파일되어 있지 않으면 명확한 "built without the … feature" 오류와 함께 즉시 실패합니다. 게시된 Docker 이미지는 둘 다 활성화합니다(Dockerfile CARGO_FEATURES). 따라서 미리 빌드된 이미지에는 추가 플래그가 필요 없습니다. 이는 소스에서 빌드할 때만 관련됩니다. 통합 테스트도 마찬가지로 게이트됩니다: --features s3 --test s3_minio, --features gcs --test gcs_emulator(fake-gcs-server), --features gcs --test gcs_real(ADC를 통한 실제 GCS). 각 테스트는 SLATER_* 환경 변수가 설정되지 않으면 건너뜁니다.
디자인, 마일스톤 대장 및 결정 로그는 docs/PLAN.md, docs/PROGRESS.md, docs/DECISIONS.md를 참조하세요.
최대 6개 엔진, 단일 클라이언트 스위트, 62k 노드 토이 그래프부터 Wikidata 91.6M 노드 / 1.5B 엣지까지. 각 엔진은 격리된 상태로 측정됩니다(다른 모든 컨테이너는 중지 — RSS와 지연 시간은 해당 엔진만의 자취). 아래 지연 시간 표는 Slater 0.21.0(쓰기 가능 빌드)에서 다시 측정되었습니다: 소형/중형 그래프(MeSH, EU-AI-Act)는 새로, 91.6M 그래프는 새로운 동일 박스, 공유 앵커 slater-vs-Neo4j 패스로(해당 표 참조). 상주 메모리 수치는 이전 패스에서 이어받습니다(컨테이너 cgroup으로 측정, 쓰기 가능 레이어가 유휴 상태일 때 읽기 경로는 바이트 단위로 동일). 다른 엔진의 수치는 기존의 교차 엔진 실행입니다(버전/성능은 변경되지 않음). 모든 수치는 중앙값(ms) 또는 최대 상주 메모리(MiB)입니다. 모든 곳에서 낮을수록 좋음; 굵게 = 해당 행 최고. slater는 로컬 파일시스템(fs) 백엔드에서 실행되었습니다. S3 및 GCS 백엔드는 로컬 읽기 지연 시간을 객체 스토어 왕복과 맞바꿉니다(인메모리 캐시와 선택적 로컬 디스크 캐시 계층으로 완화). 따라서 이 수치는 네트워크 스토리지 배포가 아닌 엔진 자체를 특성화합니다.
디스크에서 페이징하는 세 엔진 — slater, Neo4j 5, LadybugDB — 은 다섯 그래프를 모두 로드합니다. 인메모리 3총사(Memgraph · FalkorDB · ArcadeDB)는 1.5B 엣지 그래프를 전혀 담을 수 없으며(~64–128 GiB 상주 필요), ArcadeDB의 임포터도 끝낼 수 없습니다.
각 수치는 커밋된 작업 메모리입니다 — OS가 회수할 수 없는 메모리. slater를 제외한 모든 엔진은 커밋된 익명 메모리(자체 힙, Neo4j의 off-heap 페이지 캐시 또는 버퍼 풀)에 그래프를 보유하므로, 피크 RSS가 곧 커밋된 풋프린트입니다. 오직 slater만 디스크 기반 저장소의 회수 가능한 OS 페이지 캐시에서 서빙하므로, 그 수치는 익명 작업 세트입니다. 저장소의 페이지 캐시(압박 시 퇴거 가능 — slater는 계속 서빙)는 제외되며, 91.6M 그래프의 경우 괄호 안에 전체로 표시됩니다. 굵게 = 최저.
slater는 모든 규모에서 최저이며 그래프가 ~1,500× 커지는 동안 ~50× 성장합니다 — 그 풋프린트는 그래프가 아닌 쿼리 작업 세트를 따릅니다(전체적으로 유휴 ~16–71 MiB). 인메모리 3총사는 ~선형으로 성장하며 1.5B 그래프를 로드할 수 없습니다. Neo4j는 쿼리와 무관하게 ~2 GiB 힙을 커밋합니다. († LadybugDB는 제한된 형태에서만 — 1.5B 엣지에서의 허브/가변 길이/shortestPath 순회는 읽기 풀을 ≥2 GiB로 올려야 하며, slater의 자동 maxIntermediate 상한과 대조됩니다.) 빌드 시 값→카운트 히스토그램은 무시할 수준의 상주 메모리를 추가합니다 — 낮은 카디널리티 인덱스 컬럼의 경우 몇 KB, Wikidata 같은 고유 키 그래프의 경우 0(wikidata_id가 히스토그램 카디널리티 상한을 초과하므로 아무것도 저장되지 않음) — 따라서 이 수치는 해당 기능으로 변경되지 않습니다.
slater는 메타데이터 / 인덱스 / 스캔 형태(count, label, idx-eq, scan — ~0.4 ms, 서비스 엔진 대비 10–200×), 인덱스 포인트 조회(0.43 ms, 이제 인메모리 듀오의 0.48 ms를 살짝 앞섬), 앵커 없는 멀티홉(relationship-type 스캔을 통한 2-hop 1.40 ms, 이 분야에서 가장 빠름), 그리고 — 인덱스된 그룹핑 키에 대한 빌드 시 값→카운트 히스토그램을 통해 — 전체 레이블 group-by / count(DISTINCT)(0.45 ms, LadybugDB의 컬럼 기반 5.3 ms보다 앞섬)를 차지합니다. 인메모리 서버는 raw 1-hop만 유지합니다(Memgraph 1.21 ms 대 slater의 1.28 ms). (pole 62k/106k도 동일해 보입니다: slater는 count/scan에서 유일하게 가장 빠른 ~0.4 ms, 홉에서는 ~1.3–2.6 ms.)
slater는 정확한 brute-force 스캔으로 kNN에 응답합니다(이 세트는 50k 벡터 ANN 임계값 미만). 다른 엔진은 근사 상주 HNSW를 사용합니다 — 따라서 slater의 결과는 정확합니다(recall 1.0). SIMD 거리 커널 + 상주하는 사전 정규화 벡터 행렬로 Concept은 ~23 → ~2.9 ms, Chunk는 ~10 → ~2.4 ms로 줄어, slater는 이제 Neo4j와 LadybugDB를 앞서고 Memgraph의 ~1.4배 이내이며, FalkorDB에만 뒤처집니다 — 그것도 정확하면서 말입니다.
위 표들은 교차 엔진 읽기 비교입니다. 벡터 쓰기 경로(정적 Vamana 베이스 위의 FreshDiskANN 스타일 쓰기 래더)에는 교차 엔진 대응물이 없습니다 — 여기 다른 엔진 중 디스크 네이티브 쓰기 가능 ANN을 제공하는 엔진은 없습니다 — 따라서 아래 수치는 합성된 embedding류 픽스처(저랭크 매니폴드, dim 768, 부등 노름)에 대한 단일 엔진 컴포넌트 벤치마크이며, crates/slater/benches/ 아래에 커밋되어 있고 방법론과 모든 주의사항이 docs/PERF-REPORT.md에 완전히 기술되어 있습니다. Recall은 항상 라이브 세트에 대한 정확한 brute force를 기준으로 측정되며, 절대 한 인덱스를 다른 인덱스와 비교하지 않습니다. 여기서 스케일은 대표적이며, 지표가 크기 선형인 경우에만 외삽됩니다.
전용 성능 박스가 필요한 유일한 수치는 슬로우 패스 통합 재작성 처리량입니다 — 통합이 순수 순열 대신 삭제나 새 벡터를 수반할 때, 단일 스레드 zstd와 로컬 디스크에 의해 제한되는 순차적 재압축이므로 절대 MiB/s는 환경별입니다(보고서는 그 형태를 보여주고 환경 범위를 설명합니다).
인메모리 엔진(Memgraph / FalkorDB / ArcadeDB)은 이 그래프를 전혀 로드할 수 없습니다(~64–128 GiB 상주 필요). 오직 slater와 Neo4j 5만 가능합니다. 이는 공유된 고정 앵커 세트에 대한 새로운 동일 박스, 동일 날짜 패스입니다 — 모든 쿼리가 두 엔진 모두에서 동일한 노드를 대상으로 하므로, 정면 비교는 사과 대 사과입니다(중간 차수 앵커의 공통 wikidata_id 풀; 이것이 왜 중요한지는 아래 주석 참조). slater는 두 팬아웃 모두로 표시됩니다(query.maxFanout 1 = 처리량 기본값, 8 = 콜드 블록 읽기를 중첩시키는 지연 시간 다이얼). 굵게 = 해당 행 최고.
솔직한 그림: slater는 메타데이터 / 인덱스 형태를 지배합니다 — count(*)는 메타데이터 서비스로 처리되며(0.41 ms 대 Neo4j의 3.6 s 디스크 스캔, ~8800×), 포인트 조회 / 차수 / 3-hop은 ~2–10× 더 빠릅니다 — 1–2-hop에서는 Neo4j와 대등하고(팬아웃 8이 콜드 읽기에서 앞섬), 그러나 var-length *1..2 distinct에서는 결정적으로 집니다(≈1 s 대 Neo4j의 47 ms): slater의 가변 길이 distinct 확장은 여기서 실질적으로 느리며, 별도 조사가 필요한 진짜 약점입니다. 이 모든 것이 수백 MB RSS로, Neo4j의 커밋된 ~2 GiB 힙과 대조됩니다.
앵커에 관하여. 이러한 순회 수치는 어느 노드에서 시작하는지에 크게 의존합니다 — Wikidata 메가 허브("human", "country")에서 한 링크 떨어진 노드는 수백만 규모의 2-hop 이웃을 가지므로, 가변 길이/홉 비용은 앵커 선택에 따라 수 자릿수로 변동합니다. 이 표의 이전 버전은 각 엔진 자신의 "스캔 기준 처음 N개"를 샘플링했는데, 이는 안정적이지도 비교 가능하지도 않습니다. 이번 패스는 두 엔진 모두에 대해 단일 공유, 차수 제한 앵커 세트를 고정합니다. (이번 패스에서는 shortestPath가 제외됩니다 — 임의의 두 앵커 사이에서는 경로 존재 여부에 의존하고 분산이 너무 커 의미 있는 중앙값을 낼 수 없습니다.)
count(*) — memory decoupled from result size상한 없는 멀티홉 RETURN count(*)는 일치하는 행을 구체화하는 대신 확장 중에 카운트합니다. 91.6M 그래프에서 동일한 허브 앵커, maxIntermediate=20M:
| 3-hop count(*) @ 91.6M | fanout=1 | fanout=8 |
|---|---|---|
| latency / peak working set | 554 ms / 0.66 GiB | 298 ms / 1.9 GiB |
카운트는 O(1) 행을 유지합니다. 비용 부과는 변경되지 않으므로, 메가 허브 카운트는 여전히 컴퓨트(인접성 읽기)에서 maxIntermediate를 트리거하며, 이전과 같이 제한됩니다.
maxFanout)query.maxFanout을 올리면 쿼리의 콜드, I/O 바운드 블록 읽기가 코어들에 걸쳐 중첩됩니다 — 큰 콜드 작업 세트의 디스크 바운드 형태에 도움이 되고, 웜 형태에서는 변화가 없습니다. 1.5B 그래프에서: shortestPath ≤6 918 → 608 ms (1.5×, 최대 검색 6,269 → 2,350 ms, 2.7×); 3-hop count 547 → 298 ms. maxFanout=1이 기본값(처리량 지향)입니다. 8은 더 많은 일시적 워커 메모리를 사용하는 지연 시간 다이얼입니다.
전체 엔진별 표(pole, MeSH, EU-AI-Act + blockCacheBytes RAM↔지연 시간 다이얼, Wikidata 1M & 91.6M)는 perf/cross-engine-hs/README.md에 있습니다. 새로운 slater 전용 패스(두 팬아웃, 모든 데이터셋)는 perf/PERF_CURRENT_STATUS.md에 있습니다.
위 벤치마크는 단일 클라이언트입니다. 보완 축 — 많은 동시 클라이언트 하에서의 동작 — 은 자체 하네스 perf/loadtest/를 가집니다: Bolt 위의 Locust 드라이버와 부하를 증가시키고 CALL slater.diagnostics()를 읽어 용량 한계점(knee)을 찾고 제한 요소를 지목하는 코디네이터입니다(전체 방법은 docs/LOAD-TESTING.md에 있음). Wikidata-1M 그래프에서 256 MiB 캐시 실행(16코어 박스 1대)의 헤드라인:
부하 테스트가 드러낸 두 메모리 문제는 모두 해결되었습니다. 모든 내용은 부하 테스트 문서에 추적됩니다.
Apache License, Version 2.0에 따라 라이선스가 부여됩니다. 전문은 LICENSE, 귀속 정보는 NOTICE를 참조하세요. 명시적으로 달리 명시하지 않는 한, Apache 2.0 라이선스에 정의된 대로 이 저작물에 포함하기 위해 의도적으로 제출된 모든 기여는 추가 약관이나 조건 없이 위와 같이 라이선스가 부여됩니다.
SPDX-License-Identifier: Apache-2.0
REMOVEDELETE| 기능 | 의미 |
|---|
| 제한적이고 예측 가능한 메모리 | 상주 메모리는 사용자가 설정한 세 가지 캐시 예산을 추적하며, 항목당 및 할당자 오버헤드가 제한적입니다. 그래프 크기에 따라 증가하지 않습니다. 전체 그래프를 프로비저닝하는 대신 성능/RAM 트레이드오프를 조정합니다. 백그라운드 정리를 제공하는 jemalloc 할당자는 대량 쿼리 버스트 후 해제된 메모리를 OS에 반환하므로, 상주 크기는 버스트 후 최고 수위에 고정되지 않고 유휴 수준으로 되돌아갑니다. |
| 즉시 사용 가능한 멀티 테넌트 | 하나의 서버가 사용자별 읽기 권한으로 여러 그래프를 호스팅합니다 — 대부분의 그래프 DB가 유료/엔터프라이즈 등급에서만 제공하는 멀티 데이터베이스 격리입니다. |
| 저장 및 전송 중 암호화 | 블록별 XChaCha20-Poly1305 봉인(키는 디스크에 절대 기록되지 않음) 및 선택적 TLS(bolt+s://). 설계상 GDPR 친화적입니다. 암호화는 인증된 무결성도 제공합니다. 빌더는 키가 있는 MAC으로 매니페스트를 봉인하고, 키를 보유한 서버는 이를 검증하며 매니페스트가 위조, 변경, 또는 MAC이 제거된 세대의 서비스를 거부합니다. 키가 없는(평문) 이미지는 키가 없는 콘텐츠 해시로만 보호됩니다 — 완전성과 손상 탐지는 되지만 변조 탐지는 되지 않습니다. 각 구성에서 무결성이 의미하는 바를 참조하세요. |
| 초소형 설치 | distroless glibc 기반(셸/apt 없음)의 작은 스트립 바이너리 — 멀티 아키텍처(amd64/arm64) 이미지는 ~22 MB, 서버 전용 slater:latest-lite 태그는 ~12 MB입니다. 순수 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, 배치 쓰기-UNWIND(배치당 fsync 1회), CALL slater.consolidate() — 그룹 커밋, fsync 내구성, 통합을 통해 새 코어로 접힘. 델타가 비어 있으면 읽기 경로는 바이트 단위로 동일합니다. |
| ISO GQL, 읽기 및 쓰기 | 동일한 Bolt 연결을 통해 ISO GQL(ISO/IEC 39075)의 하위 집합을 사용합니다 — 정량 경로, 경로 제한자, 최단 경로 선택자, 레이블/유형 부울 표현식, FOR, CAST, 선택적 GQL/CYPHER 방언 접두사 — 그리고 쓰기 가능 계층이 켜져 있으면 GQL의 데이터 수정 문(INSERT / SET / REMOVE / [DETACH] DELETE)이 동일한 내구성 쓰기 경로로 내려갑니다. Cypher와 GQL, 읽기와 쓰기를 하나의 엔진에서. |
| 벡터 + 그래프를 하나의 엔진에서 | 임베딩/RAG를 위한 디스크 기반 ANN 벡터 검색(Vamana + PQ; cosine / L2 / dot) 및 그래프 알고리즘(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 위에 딥-머지(deep-merge)됩니다; 또한 acl.json, TLS PEM 자료, 저장 시(at-rest) 키 파일을 보관합니다. |
/tmp, /run | 스크래치(tmpfs). | 읽기 복제본은 기본적으로 디스크에 쓰지 않습니다. |
(쓰기 노드) delta.walDir | delta.enabled일 때 WAL(미리 쓰기 로그) + L0 델타 세그먼트. | 쓰기 가능해야 하며, 내구성 있는 실제 볼륨 — 절대 tmpfs 아님(내구성의 바닥입니다). 상대 경로는 데이터 디렉터리 아래에서 해석됩니다; 여기에 쓰기 노드 전용 영구 볼륨을 할당하세요. |
| (선택) 디스크 캐시 | dataBackend.s3.diskCacheBytes / dataBackend.gcs.diskCacheBytes > 0일 때 로컬 디스크 블록 캐시. | 쓰기 가능해야 하며, 실제 볼륨 — tmpfs 아님. s3 및 gcs 백엔드에서 사용됩니다; 스토리지 백엔드 참조. |
["read", "write"]| engine | class | memory bound |
|---|
| slater | disk-backed, paged | query.maxIntermediate caps the working set automatically |
| Neo4j 5 | disk-backed, JVM | ~2 GiB heap + off-heap, committed regardless of query |
| Memgraph · FalkorDB | in-memory | whole graph resident in RAM |
| ArcadeDB | in-memory, JVM | whole graph resident; heaviest |
| LadybugDB | embedded, columnar | manual buffer pool that must exceed the query |
| graph (nodes / edges) | 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 MiB vec) | 99 | 729 | 229 | 312 | 1,948 | 286 |
| Wikidata — 91.6M / 1.5B | 584 (4,595 total) | ~2,900 | 로드 불가 | 로드 불가 | 로드 불가 | ~652 † |
| shape | slater | Neo4j 5 | Memgraph | FalkorDB | ArcadeDB | LadybugDB |
|---|
| count(*) all nodes | 0.41 | 15.0 | 23.8 | 16.4 | 82.0 | 2.2 |
| label count | 0.42 | 4.2 | 20.7 | 1.1 | 4.4 | 4.3 |
| indexed point lookup | 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-hop (indexed anchor) | 1.28 | 5.8 | 1.21 | 4.1 | 390 | 4.9 |
| 2-hop (unanchored) | 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 |
full-scan CONTAINS | 0.43 | 5.4 | 24.1 | 1.7 | 16.3 | 4.1 |
| shape | 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 지연 시간 vs 대기 중인 쓰기 | RW-인덱스 ~1.5–2 ms, 50k 대기까지 평평; 사전 인덱스 brute-force 오버레이 1.9 → 115 ms(델타에 선형) — 50k에서 61× | 통합 사이에 쓰기가 쌓여도 쿼리 지연 시간은 저하되지 않습니다 |
| Embedding 삽입 | ~1.5–2 ms 벡터당 라이브 인덱스에 삽입 | 쓰기는 즉시 KNN에 보입니다. 델타 재빌드 예산은 ≈ 2 ms × 델타 상한입니다. |
| 동일 recall에서의 삭제 IO | 2.9× 적은 노드 페치/쿼리 (67% 삭제 시), 5.2× (80%에서, recall ≥ 0.90) | 통합된 그래프는 삭제된 벡터에 대해 읽기 비용을 지불하지 않습니다. |
| 통합, 순수 순열 | O(1) — .vamana는 하드 링크로 바이트 단위 동일하고, id 컬럼만 다시 쓰여집니다 | 벡터 쓰기를 베이스에 접으면 O(N·R·L) 재빌드를 건너뜁니다. |
| 래더 전반의 Recall | cosine, L2, dot에 대해 통합 ≥ 베이스 | 쓰기 래더는 모든 단계에서 recall을 보존합니다. |
| shape | slater (fan 1) | slater (fan 8) | Neo4j 5 |
|---|
| count(*) all nodes | 0.41 | 0.41 | 3606 |
| point lookup (indexed) | 0.72 | 0.49 | 6.3 |
| degree (1-hop count) | 0.43 | 0.44 | 6.0 |
| 1-hop neighbours | 9.8 | 4.5 | 10.1 |
| 2-hop | 37 | 23 | 34.5 |
| 3-hop | 32 | 25 | 74 |
var-length *1..2 distinct | 985 | 1056 | 47 |
| dimension | slater | best of the field | verdict |
|---|
| resident memory, any scale | 11–584 MiB (62k → 91.6M) | in-memory 1.5–2.7 GiB; can't load 1.5B | slater |
| count / metadata / scan | ~0.4 ms | service engines 5–80 ms | slater (10–200×) |
| indexed point lookup | 0.43 ms (MeSH) | Memgraph · FalkorDB 0.48 ms | slater (인메모리 듀오를 살짝 앞섬) |
| unanchored multi-hop (rows) | 1.40 ms (MeSH 2-hop) | Neo4j 5.6 ms | slater (relationship-type 스캔) |
| aggregation (group-by / DISTINCT) | 0.45 ms | LadybugDB 5 ms (컬럼 기반) | slater (빌드 시 히스토그램) |
| kNN | 2.4–2.9 ms (정확) | FalkorDB 1.2 ms (HNSW) | Neo4j/Ladybug를 앞섬; Memgraph 대비 ~1.4× 뒤; 정확 |
| 91.6M metadata / point / degree / 3-hop | 0.4–32 ms | Neo4j 6–3,600 ms | slater (2–8800×) |
| 91.6M 1–2-hop | 4.5–23 ms (fan 8) | Neo4j 10–35 ms | ~대등 |
91.6M var-length *1..2 distinct | ~1 s | Neo4j 47 ms | Neo4j (slater의 진짜 약점) |
multi-hop count(*) at scale | 0.3–0.6 GiB | in-memory engines materialise the row set | slater, 제한적 |
| 결과 | 측정값 |
|---|
| 동시 클라이언트 1000개까지 견디며, 실패 0건 | 처리량은 ~2.5k rps에서 피크; 지연 시간 한계점은 약 750 클라이언트에서 시작(p99 51 → 750 ms) — 하드 상한이 아니라 코어 경합 하의 큐잉(단일 실행, WSL2) |
| 블록 캐시 제한적이고 효과적 | 100% 히트율, 퇴거 0건, 캐시에 맞는 작업 세트에 대해 50 MB 상주 |
| 지속 부하에서 RSS 유지 | jemalloc 할당자는 100→500 클라이언트 wiki_cache_churn 램프 전반에 걸쳐 RSS를 ~0.6 GB로 유지 — 캐시 바운드 및 안정적, 아무런 MALLOC_* 튜닝 없음(기존 MALLOC_ARENA_MAX=2 + 트림 임계값은 폐기됨); 백그라운드 퍼지도 버스트 후 고수위를 고정된 채로 두지 않고 반환합니다 |
| 전체 메모리 제한 | 서버 전체 query.maxIntermediateGlobal + 인접성 비용 부과 확장이 1000 클라이언트에서 wiki_budget 2-hop 플러드를 OOM 없이 유지(RSS ~0.6 GB; 가드는 허브 쿼리의 ~60%를 재시도 가능한 예산 오류로 거부합니다) |