데이터베이스
484e334, 최신 배포판은 v0.14.0 기준입니다.omnigraph는 타입이 있는 그래프를 로컬 디스크나 객체 저장소에 보관하고, 그래프 전체의 버전을 브랜치로 관리하는 데이터베이스입니다. 문서를 벡터로 찾고, 단어로 찾고, 관계를 따라 좁히는 작업도 같은 쿼리 실행 환경에서 조합합니다.
예를 들어 Source는 실험 보고서, Claim은 검증할 주장, Supports는 “이 자료가 이 주장을 뒷받침한다”는 연결입니다. “지연 시간이 줄었다는 주장의 근거를 찾아라”는 질문은 문자열 검색을 넘어 관계 탐색이 됩니다.
여러 에이전트가 같은 지식을 읽되, 각자의 변경은 브랜치에서 검토한 뒤 그래프 전체에 반영하도록 만드는 것이 핵심입니다.
.pg 스키마로 타입, 키, 제약을 선언하고 엔진이 쓰기 때 검사합니다.// schema.pg: 공식 빠른 시작의 근거 그래프
node Source {
slug: String @key
title: String
}
node Claim {
slug: String @key
statement: String
}
edge Supports: Source -> Claim
이 선언을 읽는 출발점은 crates/omnigraph-compiler/src/schema/schema.pest와 crates/omnigraph-compiler/src/schema/parser.rs입니다. 스키마를 파일로 선언하는 이유는 문서화뿐 아니라 쿼리 검사와 데이터 검증이 동일한 모델을 사용하게 하기 위해서입니다.
여러 사람이 같은 원고를 고칠 때 바로 정본을 덮어쓰면 누가 무엇을 바꿨는지 판단하기 어렵습니다. 각자 사본에서 제안하고 차이를 확인한 뒤 합치면 검토가 쉬워집니다.
omnigraph의 사본은 텍스트 파일 복제가 아니라 그래프 스냅샷과 테이블 버전의 참조입니다. 파일 저장소인 Git과 동일한 구현이라는 뜻은 아니며, 충돌 판단에는 그래프의 키·간선·제약도 관여합니다.
| 이름 | 역할 | 여기서 배울 질문 |
|---|---|---|
| Lance | 열 기반 파일, 단일 데이터셋 버전·인덱스·트랜잭션 | 이미 있는 저장 엔진 위에 무엇을 더해야 하는가? |
| omnigraph 엔진 | 여러 데이터셋을 하나의 그래프 상태로 조정 | 테이블 둘을 바꿀 때 절반만 보이는 일을 어떻게 막는가? |
| CLI·HTTP·MCP | 사람·앱·에이전트가 사용하는 진입점 | 입구가 달라도 같은 규칙을 어떻게 강제하는가? |
학습 목표는 데이터베이스를 통째로 재구현하는 것이 아닙니다. 먼저 작은 쿼리 하나가 어떤 타입 검사를 거쳐 어떤 스냅샷을 읽는지, 변경 하나가 언제부터 다른 독자에게 보이는지 설명할 수 있으면 충분합니다.
2026-10-23 선정 기준은 GitHub Trending Rust Weekly #8입니다. 스타 1,679와 포크 237은 관심의 크기를 보여주지만 채택 규모나 성능의 증거는 아닙니다. 아래의 주목 요인은 README의 지향점과 구현을 연결한 해석이며, 유입 경로를 측정한 통계가 아닙니다.
README는 장시간 동작하는 여러 에이전트가 타입이 있는 세계 모델을 공유하는 워크로드를 내세웁니다. 단순 대화 기록 저장과 달리, 관계·키·변경 이력을 유지해야 합니다. 브랜치는 서로 다른 작업의 제안을 분리하고, 병합은 승인한 결과를 한 상태로 합치는 수단입니다.
구현에서도 crates/omnigraph/src/exec/merge.rs가 공통 조상·원본·대상 스냅샷을 비교하고, 합칠 후보에 제약 검증을 적용합니다. 브랜치는 이름만 붙인 복사본에 머무르지 않습니다.
벡터 검색은 의미가 비슷한 대상을, 전문 검색은 단어가 맞는 대상을, 그래프 탐색은 관계를 만족하는 대상을 찾습니다. 검색 시스템을 따로 두면 동기화와 결과 결합을 앱이 맡아야 합니다. omnigraph는 nearest, bm25, rrf와 탐색을 실행 계획 안에서 다룹니다.
다만 “검색을 통합했다”가 “모든 검색을 정확 탐색으로 수행한다”는 뜻은 아닙니다. 근사 벡터 검색의 후보 수와 탐색 범위에는 비용·재현율의 절충이 있고, 인덱스 상태에 따른 거부 조건도 있습니다. 이 구분 자체가 좋은 학습 주제입니다.
로컬 파일과 S3 계열 저장소를 같은 설계로 다루면서, 인덱스·캐시가 늦어져도 논리적인 데이터 의미를 바꾸지 않도록 경계를 세웁니다. 이 원칙은 docs/dev/invariants.md에 명시되어 있고, 쓰기와 인덱스 조정 경로가 분리되어 있습니다.
이 저장소의 교육적 가치는 기능 수보다, 정답을 결정하는 상태와 속도를 높이는 상태를 구분하는 방법에 있습니다.
| 선택지 | 중심 모델·활용점 | omnigraph의 차별점·선택 비용 |
|---|---|---|
| PostgreSQL + pgvector | 관계형 데이터와 SQL에 벡터 유사도 검색을 결합 | 그래프 전체 브랜치·병합을 제품의 핵심 계약으로 제공. 기존 SQL·확장 생태계를 그대로 쓰는 접근과는 도입 비용이 다름 |
| Neo4j | 노드·관계·속성과 Cypher를 중심으로 그래프를 질의 | 객체 저장소의 Lance 테이블 조정과 .pg·.gq·그래프 버전 관리의 결합에 초점. 쿼리 언어 호환을 가정하면 안 됨 |
| Lance를 직접 사용 | 버전이 있는 데이터셋과 검색·저장 기능을 직접 조합 | 여러 노드·간선 테이블의 공개, 스키마, 그래프 제약과 정책을 위에서 조정. 그만큼 추가 계층을 이해해야 함 |
| omnigraph | 타입·관계·검색·변경 제안을 한 그래프 계약으로 관리 | 브랜치 기반 지식 관리가 강점. 젊은 0.x 프로젝트의 계약·업그레이드 경계를 함께 감수해야 함 |
이 표는 처리량 순위가 아닙니다. 비교의 기준 모델은 pgvector 공식 저장소, Neo4j 공식 개념 문서, Lance 버전 관리 문서에서 확인할 수 있습니다. 같은 데이터·하드웨어의 직접 성능 비교는 제시하지 않습니다.
“요청이 성공했다”의 정확한 의미, “새 버전”과 “공개된 버전”의 차이, 권한 검사 위치, 최적화의 안전 조건을 한 저장소에서 연결할 수 있습니다. 작은 예제부터 읽고 해당 기능의 테스트로 이동하는 순서가 효과적입니다.
루트 Cargo.toml은 22개 워크스페이스 멤버를 선언합니다. 기본 빌드 대상은 엔진·CLI·서버 세 패키지입니다. 엔진의 디렉터리는 crates/omnigraph, Cargo 패키지 이름은 omnigraph-engine, Rust 라이브러리 이름은 omnigraph이므로 명령과 경로에서 이름이 다르게 보입니다.
| 기술 | 선언 → 잠금 버전 | 담당 기능·확인 위치 |
|---|---|---|
| Rust | edition 2024 · toolchain 1.97.1 | 패키지별 Cargo.toml · rust-toolchain.toml |
| Lance 계열 | =13.0.0 → 13.0.0 | 데이터셋·버전·인덱스. 루트에서 동일 버전으로 고정, AWS·Azure 기능 활성화 |
| Apache Arrow | 58 → 58.3.0 | arrow-array 등. 열 배열·스키마·배치 표현 |
| DataFusion | 54 → 54.0.0 | 관계 연산과 실행 계획. 기본 기능을 끄고 문자열·중첩 표현식 활성화 |
| Tokio | 1 → 1.52.3 | 비동기 런타임, 네트워크·시간·동기화 |
| Pest | 2 → 2.8.7 | .pest 문법에서 스키마·쿼리 파서 생성 |
| Cedar Policy | 4.9 → 4.11.2 | 행위자·행동·범위에 대한 정책 평가 |
| object_store | 0.14.1 → 0.14.2 | 파일·AWS·Azure 공용 저장소 접근 |
| Serde | 1 → 1.0.228 | 설정·HTTP 데이터·실행 계획 직렬화 |
| Reqwest | 0.12 → 0.12.28 | HTTP 클라이언트, JSON·rustls TLS |
표에서 4.9처럼 보이는 선언은 일반적으로 정확한 고정값이 아닙니다. Cargo의 호환 범위 안에서 잠금 파일이 4.11.2를 선택할 수 있습니다. 반대로 Lance의 =13.0.0은 정확히 그 버전을 요구합니다. Cargo.lock에는 전이 의존성용 object_store 0.13.2와 reqwest 0.13.4도 있으므로, 이름 하나당 버전 하나라고 가정하면 안 됩니다.
| 표면 | 실제 버전 | 의미 |
|---|---|---|
| CLI / Clap | 4.6 → 4.6.1 | 명령 해석·도움말. 로컬 엔진과 원격 서버를 선택해 사용 |
| HTTP / Axum | 0.8 → 0.8.9 | 멀티 그래프 라우팅·인증·요청 수용 제어 |
| OpenAPI / Utoipa | 5 → 5.5.0 | 서버 계약과 루트 openapi.json |
| MCP / rmcp | =3.3.0 → 3.3.0 | crates/omnigraph-mcp/Cargo.toml에서 고정한 서버 SDK |
| 프론트엔드 | 이 저장소에는 해당 앱 없음 | package.json·TS/JS/HTML 앱 소스 없음. React나 웹 빌드 도구를 스택에 넣을 근거가 없음 |
| TypeScript SDK | 이 저장소의 의존성으로 고정되지 않음 | 별도 저장소의 클라이언트. HTTP 계약 호환 확인 필요 |
crates/omnigraph-server/Cargo.toml의 aws 기능은 기본값이 꺼져 있으며, 선택적으로 AWS Secrets Manager 기반 토큰 소스를 더합니다. 이것은 루트 Lance 의존성의 AWS 저장소 기능과 별개입니다.
Dockerfile의 기본 이미지는 public.ecr.aws/debian/debian:bookworm-slim. 빌드 완료된 실행 파일을 복사하고 일반 사용자 omnigraph로 시작합니다.OMNIGRAPH_BIND=0.0.0.0:8080, 포트 8080, /healthz 상태 점검, tini 설치와 별도 진입 스크립트가 있습니다.protoc. CI는 protobuf-compiler와 libprotobuf-dev를 설치합니다..github/workflows/ci.yml에 Clippy·포맷·문서·의존성·금지 API 검사와 테스트 소유권 검사가 있습니다.cargo-nextest는 0.9.143으로 고정됩니다.# 공식 소스 빌드 문서의 예시. 테스트 전용 플래그를 대체한다.
RUSTFLAGS= cargo build --release --locked \
-p omnigraph-cli \
-p omnigraph-server \
-p omnigraph-azure-admission
.cargo/config.toml의 --cfg tokio_unstable은 테스트의 시드 기반 스케줄링에 사용됩니다. 공식 릴리스 빌드는 환경의 RUSTFLAGS로 이를 대체합니다. “unstable 설정이 있으니 nightly 제품이다”라고 읽으면 잘못된 결론입니다.
최신 배포판은 v0.14.0(2026-10-22)이지만, 커밋 484e334의 엔진·서버 Cargo.toml은 0.14.1입니다. 특히 최신 커밋의 최적화는 배포판에 포함되었다고 단정할 수 없습니다. docs/user/install.md는 HTTP 계약 0.14를 요구하고, 0.14 릴리스 시점의 npm 패키지 발행은 대기 상태라고 설명합니다.
이 그림은 crate 간 책임을 묶어 그린 논리 구조입니다. 각 상자가 반드시 별도 프로세스라는 뜻은 아닙니다. CLI는 엔진을 내장해 로컬 그래프를 열 수 있고, 원격 대상에서는 서버 API로 요청을 보냅니다. 서버는 적용된 클러스터 구성을 바탕으로 여러 그래프를 제공합니다.
읽기의 출발점인 Session::query_with_head()는 crates/omnigraph/src/exec/query_doors.rs에 있습니다. 먼저 설정을 정하고 capture_read_view()로 스냅샷과 카탈로그를 함께 잡은 뒤, 이름이 있는 쿼리를 컴파일합니다. 반환하는 그래프 커밋 ID도 실제로 읽은 스냅샷에서 가져옵니다.
crates/omnigraph-compiler/src/query/parser.rs::parse_query()가 Pest 문법으로 문자열을 구문 구조로 바꿉니다.crates/omnigraph-compiler/src/query/typecheck.rs가 바인딩·속성·검색 인자의 타입을 확인하고 검사된 쿼리를 만듭니다.crates/omnigraph-compiler/src/ir/lower.rs::lower_query()가 타입을 유지한 중간 표현 QueryIR로 변환합니다.crates/omnigraph-planner/src/gate.rs::accept_query()가 실행 계획을 만들고, 원래 쿼리의 요구를 보존했는지 검사합니다.crates/omnigraph/src/engine/bind.rs::bind()가 승인된 계획에 매개변수·검색 벡터를 묶어 실행에 넘깁니다.// crates/omnigraph-planner/src/validate/mod.rs 발췌
pub struct AcceptedPlan {
plan: PhysicalPlan,
evidence: Evidence,
}
// 필드는 비공개이며 역직렬화로 이 타입을 만들지 않는다.
// 검증 성공 뒤 bind()가 AcceptedBoundPlan을 만든다.
배울 점: “검증했으니 조심해서 실행하자”를 관례로 남기지 않고, 실행에 필요한 타입을 검증기만 만들도록 합니다. API 입력의 검증 전·후 타입을 나누는 작은 서비스에도 적용할 수 있는 방식입니다.
검증 범위도 과장하지 않습니다. ValidationScope::ExactSubset은 제한된 쿼리 형태에서 재작성 과정을 재구성해 비교하고, InvariantsOnly는 검색·조건·점수·정렬·잘라내기 등의 불변식을 확인합니다. 후자가 모든 쿼리의 결과 동등성을 증명한다는 뜻은 아닙니다. explain과 실제 실행도 같은 승인 경로를 공유합니다.
484e334의 주제는 상위 계획이 불필요한 삭제 확인을 생략하도록 하는 것입니다. crates/omnigraph-planner/src/optimizer.rs는 CSV 덮어쓰기 로드의 Validate 노드를 만들 때 다음 조건을 기록합니다. 아래 코드는 핵심 분기의 발췌입니다.
let removal = if overwrite && retained_edge_checks > 0 {
ValidateRemoval::Scan
} else {
ValidateRemoval::None
};
간선이 참조하는 기존 노드를 교체하면 사라질 ID를 알아야 관계 무결성을 확인할 수 있습니다. 반면 참조 간선 검사가 없거나, 대상 노드 테이블이 비어 해당 검사가 제거되면 그 스캔이 필요 없습니다. 간선 테이블의 교체도 노드 삭제 탐지가 필요 없는 경우입니다.
결정은 validate_removal 통계와 PhysicalNode::Validate.removal에 남고, crates/omnigraph/src/engine/operators/validate.rs의 LoadJudge로 전달됩니다. 실행기가 저장소 상태를 다시 추측하지 않도록, 무엇을 검사할지 계획이 먼저 결정하고 실행은 그 결정을 따른다는 사례입니다. 정량적인 속도 향상 수치는 이 분기만으로 산출할 수 없습니다.
노드와 간선은 서로 다른 Lance 데이터셋입니다. 노드 테이블만 바뀐 순간을 독자가 볼 수 있다면 간선이 존재하지 않는 노드를 가리킬 수 있습니다. 단일 테이블의 트랜잭션만으로는 그래프 전체의 일관성을 설명할 수 없는 이유입니다.
엔진은 WriteTxn의 기준 스냅샷에서 검증하고 테이블별 변경을 detached commit으로 준비합니다. 이 버전은 내구성 있게 저장되어도 아직 그래프의 정본에서 참조되지 않습니다. 마지막에 카탈로그의 __manifest를 한 번 바꾸어 모든 버전 참조와 스키마·계보를 함께 공개합니다.
crates/omnigraph-catalog/src/lib.rs의 ManifestCoordinator::commit_changes_with_lineage_and_precondition()는 조건을 포함해 publisher를 호출한 뒤 성공한 상태를 설치합니다. crates/omnigraph-catalog/src/commit.rs::overwrite_probed()는 Lance Operation::Overwrite를 사용하며, 선형 공개의 CommitBuilder에는 with_max_retries(0)을 설정합니다. 상위 publisher가 공개 전제조건과 테이블 버전 조건을 재확인하며 경합을 다룹니다.
파일에 새 값이 쓰였다는 사실과, 그 값이 그래프의 정식 상태로 보인다는 사실은 서로 다릅니다.
등록된 테이블의 일반적인 쓰기는 선형 HEAD를 계속 올리는 방식이 아닙니다. 생성 시의 선형 버전을 유지하고, 그래프가 참조할 detached 버전을 만듭니다. 따라서 raw Lance HEAD를 임의로 읽거나 직접 쓰면 그래프의 공개 규약을 우회합니다. docs/dev/invariants.md와 docs/dev/writes.md가 이 금지 경계를 설명합니다.
공개 전 장애가 나면 독자는 이전 상태를 읽습니다. 미공개 버전은 나이가 오래됐다는 이유만으로 삭제하지 않고, 그것을 공개할 수 있는 권한 상태가 사라졌는지까지 확인해 회수합니다. 응답이 유실되어 공개 여부가 불확실한 경우에는 성공·실패를 임의로 단정하거나 쓰기를 무조건 재전송하지 않습니다.
브랜치는 부모의 정확한 테이블 버전 참조를 상속합니다. 현재 방식에서는 그래프 브랜치마다 노드·간선의 전체 파일을 복제하거나 모든 테이블의 Lance 브랜치를 새로 만들지 않습니다. 이후 각 그래프 브랜치가 만든 detached 버전 체인은 자신의 __manifest 계보가 소유합니다.
crates/omnigraph/src/exec/merge.rs::branch_merge_as_with_options()가 정책과 옵션의 진입점이며, resolve_merge_base()·branch_merge_impl()이 조상 탐색과 병합 흐름을 맡습니다. plan_lineage_merge()는 파일 동일성과 버전 핀 등의 조건을 확인해 변경 후보를 줄이고, 조건을 증명하지 못하면 일반 비교 경로로 돌아갑니다.
| 기준에서 일어난 변화 | 판단의 예 |
|---|---|
| source만 한 엔터티를 바꿈 | 해당 변경을 후보에 반영한 뒤 제약 확인 |
| 양쪽이 서로 다른 엔터티를 바꿈 | 함께 합칠 후보를 만들 수 있으나, 최종 그래프 제약도 확인 |
| 같은 엔터티를 서로 다르게 바꿈 | divergent_update 같은 구조화된 충돌 |
| 한쪽은 삭제, 다른 쪽은 수정 | delete_vs_update 충돌 가능 |
엔터티 단위 충돌이므로 “속성 이름이 다르면 Git처럼 자동으로 합쳐질 것”이라고 추측하면 안 됩니다. 데이터가 충돌하지 않아도 유일 키나 간선 제약을 어기면 병합이 실패할 수 있습니다. branch_merge_dry_run()은 분류·검증까지 수행하고 최초 효과 전에 멈춥니다.
인증은 “누구인가”, 인가는 “이 그래프·브랜치에서 이 행동을 허용하는가”입니다. 서버의 authenticate_bearer_token()은 토큰에서 행위자를 결정합니다. 정적 토큰 경로는 SHA-256 해시를 비교하며, ct_eq()와 전체 항목 순회로 일치 위치에 따른 시간 차이를 줄입니다. 이후 서명 토큰 검증 경로도 존재합니다.
쓰기 권한의 중심은 crates/omnigraph/src/db/omnigraph.rs::enforce()입니다. 정책이 설정되어 있는데 행위자가 없으면 거부하고, 있으면 PolicyChecker::check(action, scope, actor)를 호출합니다. crates/omnigraph/src/exec/mutation.rs::enforce_change()도 이 문을 통과합니다.
건물 로비에서 신분증을 확인해도 금고를 여는 권한까지 생기지는 않습니다. HTTP 입구는 신원을 확인하고, 엔진의 쓰기 입구는 실제로 바꾸려는 행동의 권한을 확인합니다. CLI·내장 SDK라는 다른 문으로 들어와도 같은 금고 규칙을 적용하는 구조입니다.
단, 정책이 설치되지 않은 내장 개발 환경에서 enforce()는 허용 경로입니다. 또한 현재 ResourceScope를 일반적인 행 단위 RLS로 설명할 근거는 없습니다. 타입에는 그래프·브랜치·대상 브랜치·브랜치 전환 범위가 표현되어 있습니다.
omnigraph-gqt*는 설명을 위해 omnigraph-gqt·omnigraph-gqt-core·omnigraph-gqt-served를 묶어 쓴 표기입니다. 실제 폴더 이름에 별표가 들어가지는 않습니다.
| 궁금한 질문 | 읽을 순서 | 독해 결과물 |
|---|---|---|
| 쿼리는 어떻게 실행되는가? | crates/omnigraph/src/exec/query_doors.rs → 컴파일러 crates/omnigraph-compiler/src/query/ → 플래너 crates/omnigraph-planner/src/gate.rs → crates/omnigraph/src/engine/bind.rs | 문자열부터 승인된 계획까지의 흐름도 |
| 변경은 언제 보이는가? | crates/omnigraph/src/exec/staging.rs → 카탈로그 crates/omnigraph-catalog/src/publisher.rs → crates/omnigraph-catalog/src/commit.rs | 준비·공개·응답의 세 경계 |
| 노드를 어떻게 빠르게 잇는가? | crates/omnigraph/src/graph_index/mod.rs → 플래너 crates/omnigraph-planner/src/cost.rs → crates/omnigraph/src/engine/graph.rs | CSR과 인덱스 탐색을 선택하는 조건 |
| 대상 | 파일 수 | 물리적 줄 수 |
|---|---|---|
Rust .rs | 649 | 618,109 |
Python .py | 27 | 9,440 |
Shell .sh | 12 | 1,920 |
YAML .yml + .yaml | 28 | 6,829 |
쿼리·스키마 .gq + .pg | 8 | 360 |
GQT 사례 .gqt | 549 | 159,316 |
커밋 484e334의 확장자별 파일 집계이며 .git은 제외합니다. 테스트·주석·빈 줄을 포함한 물리적 줄 수이므로 제품 구현만의 LOC가 아닙니다. Rust 중 tests 경로는 172,708줄이고 나머지에도 인라인 테스트가 있습니다. Markdown 코드 블록은 이 표에 포함되지 않습니다.
엔진 crate만 203,048줄, 컴파일러 36,951줄, 플래너 28,351줄입니다. 분량의 차이는 중요도의 순위가 아닙니다. 작은 컴파일러 경계부터 읽으면 저장소 호출을 이해하지 않고도 타입·IR 설계를 배울 수 있습니다.
crates/omnigraph/src/session.rs의 Session은 Arc<Omnigraph>와 요청의 설정·설정 출처를 가집니다. Arc는 엔진을 여러 사용자가 공유하게 하고, 세션 설정은 호출별로 보관합니다. 기준 설정도 따로 있어 reset의 의미를 유지합니다.
작은 실습: Session::from_defaults()·rebind()·detach()의 인자와 반환 타입을 적어 보세요. 엔진 핸들을 다시 열어도 어떤 설정이 남는지, Clone이 대규모 데이터 복제를 뜻하는지 구분하는 연습입니다.
crates/omnigraph-compiler/src/query/query.pest는 표현의 형태를, crates/omnigraph-compiler/src/query/typecheck.rs는 타입과 사용 가능성을, crates/omnigraph-compiler/src/ir/lower.rs는 실행 계층에 전달할 구조를 담당합니다. 문법상 맞는 $source.typo도 스키마에 없는 속성이면 의미 검사에서 거부할 수 있습니다.
작은 실습: 유효한 쿼리와 속성 오타가 있는 쿼리를 나란히 적고, 토큰화·파싱·타입 검사 중 어디서 갈리는지 표시하세요. “문자열이 들어왔다”에서 바로 저장소 호출로 뛰어들지 않는 이유를 설명해 보세요.
crates/omnigraph/src/graph_index/mod.rs::TypeIndex는 문자열 ID와 u32 인덱스를 대응시키고, CsrIndex::build()는 간선을 두 배열로 묶습니다. neighbors()의 핵심은 다음처럼 짧습니다.
// CsrIndex::neighbors()의 배열 접근 부분
let start = self.offsets[node as usize] as usize;
let end = self.offsets[node as usize + 1] as usize;
&self.targets[start..end]
작은 실습: 노드 3의 간선 두 개를 추가할 때 배열이 어떻게 바뀌는지 손으로 써 보세요. 이어서 from_parts()가 오프셋의 시작·단조 증가·마지막 위치를 왜 검증하는지 설명하세요. 저장한 가속 자료도 읽을 때 구조 검증이 필요합니다.
“먼저 가장 가까운 10개를 찾고, 그중 특정 팀의 문서만 남기기”와 “특정 팀의 문서에서 가까운 10개 찾기”는 다릅니다. 플래너의 필터 위치와 후보 확장 정책은 이 차이를 다룹니다. crates/omnigraph/src/engine/search.rs, crates/omnigraph/src/engine/plan_source.rs, 플래너 crates/omnigraph-planner/src/physical.rs를 함께 읽어야 합니다.
작은 실습: 후보 5개 중 앞의 3개가 팀 필터에서 탈락하는 표를 만들어 보세요. 후보 부족을 막는 재탐색과 ANN 순위 자체의 정확도는 다른 문제라는 점을 표시하세요. 결과 개수가 충분해도 근사 검색이 정확 순위를 보증하지는 않습니다.
bm25의 관련도와 벡터 거리는 같은 숫자 척도가 아닙니다. RRF는 점수를 직접 더하지 않고 각 목록의 순위를 사용합니다. docs/user/search/index.md의 기본값은 k=60입니다. 아래 식의 r은 1부터 시작하는 순위입니다.
RRF(d) = Σ 1 / (60 + r_i(d))
문서 A: 단어 검색 1위, 벡터 검색 5위
문서 B: 단어 검색 4위, 벡터 검색 2위
A = 1/61 + 1/65 ≈ 0.03178
B = 1/64 + 1/62 ≈ 0.03175
작은 실습: 두 검색이 같은 문서를 서로 다른 위치에 둘 때 k를 바꾸며 순위 민감도를 계산하세요. 그래프 필터를 언제 적용하는지도 따로 적어야 합니다. RRF 안의 벡터 검색은 top-k 창이라는 한계가 남습니다.
omnigraph-policy의 PolicyChecker는 엔진과 정책 구현 사이의 경계입니다. 서버의 /mcp 어댑터도 같은 HTTP 처리 계층과 인증·수용 제어를 사용하며, MCP crate 자체가 그래프를 별도로 열어 쓰지 않습니다.
작은 실습: “토큰 없음”, “인증은 됐지만 권한 없음”, “정책이 있는데 엔진 행위자 없음”의 세 사례를 입력→검사 위치→거부 이유 표로 만드세요. HTTP 상태 코드와 내부 오류 타입을 같은 개념으로 섞지 않는 연습입니다.
GQT 파일은 --- schema·--- seed·--- query·--- expect 계열 구획으로 입력과 기대 결과를 표현합니다. 일부 사례는 결과 타입과 실행 계획까지 검증합니다. crates/omnigraph-gqt/cases/v2/planner/switch_ran_on_the_report.gqt는 hash_join과 indexed_scan 중 무엇이 실행됐는지도 확인합니다.
DST는 하나의 시드에서 스케줄·시간·ID·저장소 장애를 만들어 실패를 재현합니다. 다만 crates/omnigraph-dst/README.md는 실제 시간의 동시 실행과 프로세스 강제 종료 사례까지 모두 정확 재생된다고 주장하지 않습니다. 재현 가능 범위를 명시하는 태도를 배울 수 있습니다.
좋은 테스트는 정상 결과뿐 아니라, 실패한 순간 무엇이 보이고 어떤 작업이 끝나지 않았는지까지 설명합니다.
작은 실습: crates/omnigraph/tests/detached_commit_matrix.rs의 “테이블 준비 후·공개 전”과 crates/omnigraph/tests/failpoints.rs의 “공개 후·응답 유실” 사례를 비교해 기대 상태를 적으세요. 저장소 요청 비용을 기록한 crates/omnigraph-dst/cost_table.txt와 비교하면 결과의 정당성과 비용 계약도 구분할 수 있습니다.
| 항목 | 확인된 내용 | 학습할 때의 의미 |
|---|---|---|
| 배포 아카이브 | Linux x86_64·arm64, macOS arm64, Windows x86_64 | docs/user/install.md와 릴리스 빌드 행렬 기준 |
| Linux 아카이브 | glibc 2.39 이상 | 오래된 배포판에서 단순히 압축을 풀어 실행된다고 가정하지 않기 |
| 다른 플랫폼 | Intel macOS 등을 위한 소스 빌드 안내 | 도구 체인·네이티브 의존성을 별도로 준비 |
| 소스 빌드 | Rust 1.97.1, Cargo, protoc | 실행 파일만 사용하는 독자에게 Rust 설치가 필수인 것은 아님 |
| 최소 CPU·RAM | 고정된 공식 최솟값 미기재 | 데이터 크기·검색·동시성에 맞춰 계측 필요 |
| GPU | 기본 그래프·문자열·벡터 연산에 GPU를 필수로 선언하지 않음 | 외부 임베딩 제공자나 별도 로컬 모델의 요구량은 따로 계산 |
| 첫 실습의 저장소 | 로컬 파일 기반 그래프 지원 | 객체 저장소 계정 없이 작은 예제로 시작 가능 |
컨테이너 기본 이미지가 Debian bookworm이라고 해서 배포 아카이브도 그 환경에 호환된다는 뜻은 아닙니다. Dockerfile은 빌드된 바이너리를 복사하며, 주석은 이미지와 바이너리의 glibc 조합을 맞추도록 설명합니다. 컨테이너 배포와 아카이브 설치의 조건을 구분하세요.
| 설정 | 기본값 | 무엇을 제한하는가 |
|---|---|---|
query_memory_bytes | 157,286,400바이트 = 150 MiB | 쿼리의 추적 대상 메모리와 로드의 일부 재사용 버퍼 |
write_max_bytes | 33,554,432바이트 = 32 MiB | 쓰기의 행 데이터·Blob 물질화 등에 각기 적용되는 예산 |
load_source_max_bytes | 32 MiB | load csv가 읽는 단일 소스 객체 크기 |
stage_write_concurrency | 8, 허용 범위 1~64 | 준비할 테이블·CSV 파싱 스트림·Lance 쓰기의 폭 |
traversal_work_limit | 1,000,000 | 한 쿼리의 탐색 작업량. 메모리를 반환해도 작업량은 환급되지 않음 |
ann_nprobes | 20, 0은 상한 해제 | ANN에서 인덱스 델타별로 탐색할 파티션 수 |
출처는 crates/omnigraph-compiler/src/settings.rs::DEFINITIONS입니다. 같은 파일이 설정 이름·범위·기본값·환경 변수 대응을 함께 선언하므로, 문서 표와 구현을 비교하기 좋은 구조입니다.
쿼리 하나의 메모리 예산은 서버 프로세스 전체의 메모리 상한이 아닙니다. 예산을 끝까지 쓰는 쿼리가 10개 동시에 실행되면 추적 대상만 산술적으로 약 1.46 GiB입니다. 여기에 캐시·그래프 인덱스·네이티브 버퍼·입력·런타임 비용이 더해집니다. 이 계산은 용량을 보장하는 측정 결과가 아닙니다.
.cargo/config.toml은 Cargo가 띄우는 개발 프로세스에 LANCE_MEM_POOL_SIZE=1073741824(1 GiB)를 제공합니다. 이 값은 제품 바이너리에 영구히 박히는 설정이 아니며, 위의 쿼리별 예산과도 다른 풀입니다.
서류를 창고에 싸게 쌓아 둘 수 있어도, 서류를 펼칠 책상과 찾으러 갈 시간은 필요합니다. 객체 저장소는 보관 장소이고, CPU·RAM·네트워크 요청은 질의하는 동안 드는 비용입니다. 브랜치와 과거 버전을 많이 유지하면 회수 정책도 저장 비용에 영향을 줍니다.
CSR 배열만 보면 한 방향에서 대략 4 × (노드 수 + 1) + 4 × 간선 수바이트입니다. 노드 100만·간선 1,000만이면 약 42 MiB이지만, 이는 u32 배열 두 개만의 산술값입니다. 문자열 ID 사전·역방향·타입별 구조·구축 임시 메모리는 포함하지 않습니다.
처음에는 기존 64비트 개발 기기와 수십 개 노드로 흐름을 익히고, 이후 데이터·질의·동시성을 고정해 측정하세요. 기본 쿼리 실습에 임베딩 API나 클라우드 저장소를 먼저 연결할 필요는 없습니다. Azure 쓰기는 admission 래퍼가 필요한 별도 검증 프리뷰 범위입니다.
아래 명령은 docs/user/quickstart.md의 CLI 계약에 맞춘 실습 예시입니다. omnigraph CLI가 설치된 환경에서 별도의 로컬 연습 디렉터리를 사용하세요. 출력 설명은 예상 결과와 확인 기준이며, 소스 커밋의 동작을 이해하기 위한 것입니다.
각 과제의 완료 기준은 명령이 끝나는 것이 아니라, 결과가 왜 그렇게 나왔는지 소스와 연결해 설명하는 것입니다.
배울 것: 타입·키·간선·한 번의 그래프 공개. 1절의 스키마를 schema.pg로 저장하고, 아래 내용을 evidence.jsonl로 준비합니다.
{"type":"Claim","data":{"slug":"lower-latency","statement":"마이그레이션 뒤 지연 시간이 줄었다."}}
{"type":"Source","data":{"slug":"load-test","title":"부하 테스트 보고서"}}
{"type":"Source","data":{"slug":"production-metrics","title":"운영 지표"}}
{"edge":"Supports","from":"load-test","to":"lower-latency"}
{"edge":"Supports","from":"production-metrics","to":"lower-latency"}
omnigraph init --schema schema.pg graph.omni
omnigraph load --data evidence.jsonl --mode overwrite --target ./graph.omni
확인 기준: 노드 3개·간선 2개로 모델을 그릴 수 있고, @key의 slug가 간선의 노드 ID로 쓰인다는 점을 설명합니다. JSONL 한 줄은 노드나 간선 하나를 나타내는 JSON 객체입니다.
소스 연결: crates/omnigraph/src/loader/mod.rs, docs/dev/ingestion.md. 그래프 배치의 엄격한 입력 경계와 공개 단위를 구분해 읽습니다.
배울 것: 이름이 있는 쿼리·매개변수·관계 탐색. 다음을 queries.gq로 저장합니다.
query sources_for_claim($claim: String) {
match {
$source: Source
$claim_node: Claim { slug: $claim }
$source supports $claim_node
}
return { $source.title as source }
order { source asc }
}
omnigraph query sources_for_claim --query queries.gq \
--params '{"claim":"lower-latency"}' --format table --target ./graph.omni
확인 기준: 제목 두 개가 나옵니다. 속성 $source.title을 $source.typo로 바꾼 별도 쿼리는 거부되어야 합니다. 다시 원래 쿼리로 돌아오면 데이터가 변하지 않았음을 확인합니다.
생각할 질문: Supports라는 선언과 supports라는 탐색 표기를 어떻게 연결하는가? 매개변수 이름과 노드 바인딩을 왜 $claim·$claim_node로 나누었는가?
소스 연결: crates/omnigraph-compiler/src/query/parser.rs, crates/omnigraph-compiler/src/query/typecheck.rs의 typecheck_query().
배울 것: 같은 질문이 브랜치에 따라 다른 스냅샷을 읽는 이유. 아래를 benchmark.jsonl로 저장합니다.
{"type":"Source","data":{"slug":"independent-benchmark","title":"독립 벤치마크"}}
{"edge":"Supports","from":"independent-benchmark","to":"lower-latency"}
omnigraph branch create review/add-benchmark --target ./graph.omni
omnigraph load --data benchmark.jsonl --mode append \
--branch review/add-benchmark --target ./graph.omni
omnigraph query sources_for_claim --query queries.gq \
--params '{"claim":"lower-latency"}' --branch main --target ./graph.omni
omnigraph query sources_for_claim --query queries.gq \
--params '{"claim":"lower-latency"}' \
--branch review/add-benchmark --target ./graph.omni
omnigraph branch merge review/add-benchmark --into main \
--dry-run --json --target ./graph.omni
omnigraph branch merge review/add-benchmark --into main --target ./graph.omni
확인 기준: 병합 전 main에는 제목 2개, review에는 3개입니다. 미리보기는 공개하지 않고, 실제 병합 뒤 main에서도 3개를 읽습니다. 같은 입력을 append로 재적재하면 기존 ID 충돌이므로 단순 재실행 성공을 기대하지 않습니다.
소스 연결: crates/omnigraph/src/exec/merge.rs의 branch_merge_dry_run(). dry run 분기가 검증 후, 최초 효과 전에 위치하는지 확인하세요.
배울 것: 단순 파일 덮어쓰기와 세 방향 병합의 차이. main에서 review/title 브랜치를 새로 만들고, 같은 load-test 노드에 서로 다른 제목을 적재합니다.
# title-main.jsonl
{"type":"Source","data":{"slug":"load-test","title":"부하 테스트 요약"}}
# title-review.jsonl
{"type":"Source","data":{"slug":"load-test","title":"수정된 부하 보고서"}}
위 구획의 파일 이름 주석은 설명용입니다. 실제 JSONL 파일에는 해당 JSON 한 줄만 저장합니다.
omnigraph branch create review/title --target ./graph.omni
omnigraph load --data title-main.jsonl --mode merge \
--branch main --target ./graph.omni
omnigraph load --data title-review.jsonl --mode merge \
--branch review/title --target ./graph.omni
omnigraph branch merge review/title --into main \
--dry-run --json --target ./graph.omni
확인 기준: divergent_update 계열의 충돌과 해당 엔터티를 찾고, 미리보기 뒤 main의 제목은 유지됨을 확인합니다. 각 브랜치의 제목을 같은 값으로 조정했을 때의 결과도 비교해 보세요.
소스 연결: crates/omnigraph/src/exec/merge.rs::branch_merge_on_captured_target(), crates/omnigraph/tests/integration/merge_truth_table.rs. 단순한 표와 실제 충돌 분류를 대조합니다.
배울 것: 최적화가 생략하는 작업과 여전히 지켜야 할 계약. crates/omnigraph-planner/src/optimizer.rs의 Validate 분기와 docs/user/queries/explain-load.md를 나란히 읽습니다.
| 덮어쓰기 대상 | 예상 removal | 설명할 근거 |
|---|---|---|
| 간선 타입 | none | 노드 제거를 찾는 검사가 아님 |
| 간선이 참조하지 않는 노드 타입 | none | retained_edges 검사 없음 |
| 간선이 참조하며 기존 행이 있는 노드 타입 | scan | 제거될 ID를 알아야 간선 검증 가능 |
| 간선이 참조하지만 기존 행이 0인 노드 타입 | none | 빈 대상의 해당 검사를 계획에서 제거 |
확인 기준: 네 경우의 조건식을 추적하고, “이 스캔이 생략돼도 그래프 제약이 유지되는 이유”를 각 한 문장으로 씁니다. 이어서 테이블 준비 뒤 장애가 난 경우와 manifest 공개 뒤 응답이 유실된 경우의 관측 상태를 표로 나눕니다.
추가 도전: crates/omnigraph/tests/failpoints.rs에서 공개 후 응답 유실 사례를 찾고, 실패 재현에는 왜 단순한 성공 쿼리 테스트만으로 부족한지 설명합니다. 최신 커밋의 분기를 실습할 때는 v0.14.0과의 소스 차이도 구분하세요.
Rust를 처음 배우는 독자라면 소유권·참조·열거형·Result·기본 비동기 문법을 먼저 익히세요. 아래 주차는 속도 경쟁이 아니라, 매 단계에서 설명할 수 있어야 하는 결과물을 기준으로 잡았습니다.
| 주차 | 공부할 개념 | 읽을 코드·문서 | 완료 결과물 |
|---|---|---|---|
| 1주 | 그래프 모델, 키와 간선, Rust의 struct·enum | docs/user/quickstart.md, crates/omnigraph-compiler/src/schema/ast.rs | 근거 그래프와 잘못된 입력 3개 |
| 2주 | 파서·타입 검사·IR, 오류 위치 | crates/omnigraph-compiler/src/query/, crates/omnigraph-compiler/src/ir/lower.rs | 쿼리 한 개의 표현 변환 지도 |
| 3주 | 논리·물리 계획, Arrow 배치, CSR | crates/omnigraph-planner/src/gate.rs, crates/omnigraph/src/graph_index/mod.rs | 탐색의 배열 예제와 계획 승인 설명 |
| 4주 | 스냅샷·낙관적 동시성·조건부 공개 | docs/dev/writes.md, crates/omnigraph-catalog/src/commit.rs | 공개 전·후 장애의 관측 상태 표 |
| 5주 | 세 방향 병합·제약·인증과 인가 | crates/omnigraph/src/exec/merge.rs, crates/omnigraph-policy/src/lib.rs | 병합 충돌표와 권한 검사 위치 지도 |
| 6주 | 검색 재현율·비용·결정적 테스트·배포 | docs/user/search/index.md, crates/omnigraph-dst/README.md, .github/workflows/ci.yml | 질의·예산·장애 조건이 명시된 검증 계획 |
매주 읽은 코드의 입력·불변식·실패 결과를 한 장으로 남기면, 다른 데이터 시스템에도 옮겨 쓸 수 있는 설계 지식이 됩니다.
CsrIndex의 순수 배열 연산과 컴파일러의 타입 구조를 읽습니다.심화 단계에서는 docs/rfcs/0047-search-plan-truth.md와 docs/rfcs/README.md에서 검색 계획의 의미 보존에 관한 설계 논의를 찾아보세요. 현재 규약은 개발 가이드, 결정의 이유는 RFC, 정확한 예외는 코드와 테스트에서 확인하는 습관을 들이는 편이 좋습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 속성 그래프 | 대상인 노드와 연결인 간선이 각각 값을 가질 수 있는 데이터 모델 |
| 온톨로지 | 업무 세계에 어떤 종류의 대상·관계·속성이 있는지 정한 설계도 |
.pg / .gq | 각각 그래프 스키마와 쿼리·변경을 작성하는 언어 파일 |
| 객체 저장소 | 이름 붙은 파일 같은 객체를 API로 읽고 쓰는 저장 공간. 연산용 CPU·RAM까지 제공한다는 뜻은 아님 |
| Lance 데이터셋 | 열 기반 데이터 파일과 메타데이터·버전·인덱스를 함께 관리하는 저장 단위 |
| Arrow / RecordBatch | 타입이 있는 열 배열과, 같은 행 수를 가진 열들의 묶음 |
| AST | 문자열로 쓴 문법의 구조를 트리로 표현한 것 |
| 타입 검사 | 존재하는 속성인지, 비교·검색에 맞는 값인지 실행 전에 확인하는 과정 |
| IR | 문법과 실제 실행 사이에서 의미를 구조로 전달하는 중간 표현 |
| 논리 계획 / 물리 계획 | 무엇을 계산할지와, 어떤 연산·접근 경로로 계산할지를 나눈 표현 |
AcceptedPlan | 플래너의 검사를 통과했다는 증거와 함께 포장된 실행 계획 |
| 스냅샷 / 버전 핀 | 읽는 동안 고정한 그래프 상태와, 그 상태가 사용할 정확한 테이블 버전 참조 |
__manifest | 어떤 스키마·테이블 버전·계보가 공개된 그래프를 이루는지 정하는 카탈로그 |
| detached commit | 테이블의 선형 HEAD를 움직이지 않고 준비한 내구성 있는 버전. 그래프에서 보이려면 공개 참조가 필요 |
| 원자적 공개 | 여러 테이블의 변경을 독자에게 하나의 완성된 그래프 상태로 보이게 하는 경계 |
| CAS / 낙관적 동시성 | 준비 중 동시 작업을 허용하되, 기준 상태가 그대로일 때만 최종 변경을 받아들이는 방식 |
| 세 방향 병합 | 공통 조상·source·target을 비교해 각자의 변경과 충돌을 판단하는 과정 |
| CSR | 각 노드 이웃의 시작 위치와 목적지 목록을 배열 두 개로 저장하는 인접 관계 표현 |
| BM25 | 단어 빈도·희소성·문서 길이 등을 이용하는 전문 검색의 관련도 점수 |
| ANN / IVF | 전부 비교하는 대신 후보 영역을 줄여 가까운 벡터를 찾는 근사 검색과, 이를 위한 분할 인덱스 방식 |
| RRF | 단위가 다른 원점수 대신 검색 목록의 순위로 결과를 합치는 방식 |
| 수용 제어 / 배압 | 처리 가능한 작업만 받아들이거나, 과부하가 앞단으로 전달되도록 제한하는 방식 |
| Cedar | 행위자·행동·대상에 대한 허용 규칙을 코드로 작성하고 평가하는 정책 언어 |
| MCP | 에이전트가 도구·자원을 발견하고 호출하는 공통 프로토콜. 자체 데이터베이스 엔진은 아님 |
| GQT / DST | 쿼리의 입력·기대 결과 사례와, 시드로 스케줄·장애를 제어하는 시뮬레이션 테스트 |
| 내구성 / 응답 유실 | 변경이 저장되어 살아남는 성질과, 저장은 성공했어도 호출자가 응답을 받지 못할 수 있는 상황 |
헷갈리기 쉬운 세 쌍은 그래프 브랜치와 Lance native ref, 정책 인가와 쓰기 소유권 admission, 결과 개수 충족과 ANN 순위 정확성입니다. 서로 관련은 있지만 같은 계약이 아닙니다.
Lance 문서의 데이터셋 버전·태그 개념은 기반 저장 계층을 이해하는 자료입니다. 여러 데이터셋의 그래프 공개 계약은 omnigraph의 카탈로그가 추가하므로, 두 문서를 동일한 수준의 보장으로 읽지 않아야 합니다. 의존성의 실제 동작을 추적할 때는 13.0.0 버전도 함께 확인하세요.
| 항목 | 상태 |
|---|---|
| 선정 | GitHub Trending Rust Weekly #8 · 2026-10-23 분석 |
| 스타 / 포크 | ⭐1,679 / 237 · 2026-10-23 GitHub 기준 |
| 설명 | 객체 저장소를 기반으로 브랜치와 타입이 있는 온톨로지를 제공하는 그래프 데이터베이스 |
| 생성일 | 2026-04-10 |
| 최근 push | 2026-10-23 |
| 최신 릴리스 | v0.14.0 · 2026-10-22 |
| 최근 커밋 | 484e334 · 2026-10-23 · perf(planner): make an overwrite's removal a plan decision (#1141) — 덮어쓰기의 제거 확인 여부를 계획이 결정하도록 개선 |
| 소스 패키지 버전 | 엔진·서버 등 0.14.1 · 릴리스 버전과 구분 |
| 주 언어·구성 | Rust · Cargo 워크스페이스 멤버 22개 |
| 코드 규모 | Rust 649파일·618,109줄 / Python 9,440줄 / Shell 1,920줄 / YAML 6,829줄 |
| 검증 사례 | .gqt 549파일·159,316줄 · 일반 코드 줄 수와 별도 집계 |
| 규모 집계 기준 | 커밋 484e334의 물리적 줄 수, 테스트·주석·빈 줄 포함, .git 제외 |
| 라이선스 | MIT · LICENSE의 저작권 표기는 EQTR, Inc.와 OmniGraph contributors |
① 버전·성숙도. 생성 약 6개월의 0.x 프로젝트입니다. 릴리스와 main의 소스 버전이 다르고 HTTP 계약도 맞춰야 합니다. docs/user/install.md에 적힌 npm 발행 대기 상태를 현재 설치 가능성의 보증으로 읽으면 안 됩니다. 호환·업그레이드 문서를 먼저 확인하세요.
② 쓰기·스키마의 지원 범위. 서버는 클러스터 전용입니다. 스키마 적용은 main과 단일 활성 브랜치 제약을 유지하며, 일부 제어·유지보수는 단일 변경 프로세스 경계를 갖습니다. 프로세스 내부 락이 있다고 분산 다중 작성자 안전성이 자동으로 생기지는 않습니다. Azure 쓰기는 admission 래퍼가 필요한 검증 프리뷰입니다.
③ 검색·임베딩. ANN에는 비용과 재현율의 절충이 남습니다. 전문 검색은 선언·인덱스·분석기 호환 조건을 만족해야 하며, 준비되지 않은 경우 조용히 다른 검색으로 바뀌지 않고 거부될 수 있습니다. 일반 Load는 @embed를 보고 자동 임베딩하지 않습니다. 벡터를 공급하거나 별도 embed 흐름을 사용해야 하고, 외부 제공자를 사용하면 텍스트 전송·호출 비용도 생깁니다.
④ 운영·성능. 기본 메모리 예산은 전체 RSS 상한이 아닙니다. 객체 저장소의 요청 비용, 인덱스 유지보수, 버전 보존·회수, 백업과 권한 정책을 운영자가 설계해야 합니다. 이 글은 실제 처리량·지연·장애 복구 시간을 측정한 성능 보증을 제공하지 않습니다.