物理配置を論理キーとして扱う―世界規模時空間データストアのアーキテクチャ考察
概要
本稿では、Rust で実装された世界規模・時空間・制約指向データストアの内部アーキテクチャを、設計思想・クレート構成・4次元データモデル・空間/時間インデックス・チャンクバイナリ形式・クエリ実行系・グラフ探索系・サーバ運用面という8つの観点から論じる。本アーキテクチャの中心的な設計判断は「物理配置がそのまま論理キーである」という一点に集約される。すなわち、空間セル・時間バケット・レイヤー・解像度(LOD)・エンティティ型・世代という6要素からなる複合キーChunkKeyが、チャンクファイルのパスおよびバイト列内のオフセットへ直接エンコードされており、メタデータストアへの問い合わせを介さずとも、76バイト固定長のヘッダーと自己完結的なフッターインデックスだけを読み取ることで、必要なバイト範囲を一意に特定できる。以下、実装コードと設計文書を突き合わせながら、この設計判断がデータモデル・バイナリフォーマット・クエリ実行エンジン・グラフ探索エンジンとしてどのように具体化されているかを、実装の詳細に即して記述する。
1. はじめに――世界規模の時空間データが抱える構造的な難しさ
地理空間データを扱うシステムを設計する際に必ず直面する困難は、検索次元が単一ではないという事実である。第一に空間次元(どこか)、第二に時間次元(いつか)、第三に解像度次元(どれだけ詳細か)、そして第四にデータの種類(ベクタの道路網なのか、ラスタの地形データなのか、あるいはグラフ構造のネットワークなのか)という、性質の異なる4つの軸が同時に絡み合う。さらに実務上は、同じ空間・時間の組み合わせに対して「確定版(スナップショット)」と「差分更新(デルタ)」が併存するケースが頻発し、これが5番目・6番目の次元として複合キーに加わることになる。
こうした複合的な検索次元を扱う設計の多くは、次元ごとに異なるインデックス構造やストレージ層を後付けで積み重ねる方向に向かいやすい。空間インデックスは強いが時間粒度の異なるバケット管理を想定していない構成、時系列データの扱いには長けているが空間解像度が可変であることを前提としていない構成、あるいはベクタデータの表現は洗練されている一方でラスタやグラフ構造を同一の物理レイアウトの上で扱う仕組みを持たない構成、といった具合である。このような設計のもとでは、ベクタ・ラスタ・グラフというモダリティごとに異なるサブシステム間でデータをETLし続ける必要が生じ、一貫性の担保とオペレーションコストが増大していく。
この問題に対しては、「6つの検索次元をすべて一つの複合キーChunkKeyに落とし込み、そのキーをそのままバイト列上の物理配置に対応させる」という、単純だが徹底した解法が有効である。この設計判断がもたらす具体的な帰結を、以降の章で順を追って見ていく。
2. 設計思想――3つの原則
設計文書は、この設計判断を3つの原則に要約している。第一に「Range-GETファースト」、すなわち76バイトのヘッダーとフッターだけを読み取ってセクション索引を取得し、必要なセクションだけをバイト範囲指定で取得するという方針である。第二に「述語プッシュダウン」、すなわちフッターに埋め込まれたmin_value/max_valueの統計を用いて属性フィルタをセクション単位で適用し、不要なデータの転送を未然に回避するという方針である。第三に「トレイト境界の明示」、すなわちMetaStoreとObjectStoreという2つのトレイトを介してバックエンド実装を注入可能にし、テスト用のインメモリ実装から本番用のクラウド実装へと差し替え可能にするという方針である。
これら3つの原則は独立した工夫ではなく、相互に補強し合う一つの体系として機能している。Range-GETファーストの原則がなければ述語プッシュダウンは「不要なデータを読んだ後で捨てる」だけの最適化に留まってしまうし、トレイト境界が明示されていなければ、ローカルファイルシステム上でしか動かないプロトタイプが、S3のような実運用環境へ移行する際に大規模な書き換えを要することになる。
3. クレート構成とモジュール境界
実装は14個の Rust クレートによるワークスペースとして構成されている。依存関係は一方向であり、循環依存は存在しない。中核となる core は外部依存を一切持たない純粋な型定義クレートであり、ChunkKey・CellId・TimeBucket・Compression・Errorといった全クレート共通の型がここに定義される。その上に空間演算(cell)・時間演算(timebucket)・バイナリ形式(chunk)・圧縮とエンコーディング(codec)・オブジェクトストア抽象(objectstore)・メタストア抽象(metadb)という6つの基盤クレートが並列に積み上がり、さらにその上でクエリ実行系(query)とグラフ探索系(graph)が構築される。最上位には、これらすべてを統合するファサードであるapi、GISファイル取り込みを担うimport、HTTPサーバを提供するserver、そしてライセンス検証を担うlicenseが存在する。
licenseの位置づけは、モジュール境界の設計として特筆に値する。この crate は serde / sha2 / hmac / hex にのみ依存し、coreを含む他のどのデータエンジン系クレートにも依存していない。ライセンス検証というロジックが、データモデルやストレージ実装から型レベルで完全に切り離されているのである。これにより、ライセンス機構の単体テストを他のデータエンジン系クレートのビルドとは独立に行うことができ、またこの検証ロジックを変更してもコア機能側への影響が生じないことがコンパイル時に保証される。
4. 4次元データモデル――ChunkKey
全てのデータ単位はChunkKeyという複合主キーによって一意に識別される。このキーは以下の6フィールドで構成される。
| フィールド | 型 | 意味 |
|---|---|---|
cell_id | CellId(u64) | 空間セル識別子 |
time_bucket | TimeBucket | 時間バケット(開始エポック+粒度) |
layer_id | LayerId(u32) | データレイヤー番号 |
lod | Lod(u8) | Level of Detail(解像度) |
entity_type | EntityType | Raster / Vector / Graph |
generation | Generation | Snapshot(基底) / Delta(u64)(差分) |
generationフィールドの存在は、本アーキテクチャが単なる読み取り専用のデータレイクではなく、LSM(Log-Structured Merge)ツリーに近い更新モデルを内包していることを示している。ある空間セル・時間バケットに対する最初の投入はGeneration::Snapshotとして記録され、以降の増分更新はGeneration::Delta(1), Generation::Delta(2), ... という連番のデルタチャンクとして追記される。クエリ実行時にはQueryExecutorがスナップショットとデルタをインメモリでマージして返す。運用上デルタが蓄積しすぎた場合には、POST /api/v1/compactエンドポイントが対応するセル・時間バケット・レイヤー・LOD・エンティティ型の組を指定してコンパクション(デルタのスナップショットへのマージ)を行い、マージされたデルタ件数をdeltas_mergedとして返す。これは、LSMツリーにおけるメジャーコンパクションと同種の運用パターンを、地理空間データという文脈に適用したものと理解できる。
さらにサーバはlayer_idごとのスキーマレジストリを持つ。POST /api/v1/layer/schemaによって、あるレイヤーがどのエンティティ型・圧縮方式・エンコーディング方式を持つかを事前登録でき、GET /api/v1/layer/schemasで一覧取得できる。これにより、レイヤーごとの物理表現の取り決めをアプリケーションコードではなくメタデータとして管理できる。
5. 空間インデックス――CellId と CellCover アルゴリズム
空間セルは64ビット整数CellIdとして表現される。上位4ビットがLOD(0〜15)、続く30ビットが緯度インデックス、下位30ビットが経度インデックスに割り当てられる。LOD Lにおけるセルサイズは1/2^L度四方であり、LOD 0では国・大陸レベル(1°×1°)、LOD 4では市区町村レベル(約7km四方)、LOD 8では街区レベル(約430m四方)、LOD 12では建物レベル(約27m四方)、LOD 15では詳細測量レベル(約3m四方)に相当する。インデックスは WGS-84 座標系に基づき、lon_index = floor((lon + 180) × 2^L)、lat_index = floor((lat + 90) × 2^L)として計算される。
この空間セルに対して、バウンディングボックスと LOD を受け取り、交差する全てのCellIdを返すのがcell_cover(bbox, lod)関数である。実装上の要点は、セルが半開区間[i/scale, (i+1)/scale)を表すことに起因する境界補正にある。max_lon_idxの計算はceil((bbox.max_lon + 180) × scale) - 1という形を取り、この-1の補正がなければ、bbox.maxがちょうどセル境界に一致するケースにおいて余分な1列(または1行)のセルが誤って含まれてしまう。図2はこの補正の効果を可視化したものである。
CellIdに対してはparent_cell(LOD-1の親セルを返す)、child_cells(LOD+1の子セル4個を返す)、bbox_of_cell(セルのWGS-84バウンディングボックスを返す)という階層操作が定義されている。これらは、あるLODで取り込んだデータを別のLODへ再サンプリングする際や、クライアント側でズームレベルに応じたタイル要求を行う際に利用される。
6. 時間バケット――TimeBucket
時間軸はTimeBucket { epoch_ms: i64, granularity: Granularity }として表現される。粒度は秒(1,000ms)・分(60,000ms)・時(3,600,000ms)・日(86,400,000ms)の4段階が定義されており、TimeBucket::new(epoch_ms, granularity)はエポックミリ秒を粒度のduration_msで切り捨てて整列する。ある時間区間TimeRangeに対して重複する全てのバケットを返すtime_bucket_cover(range, granularity)は、空間側のcell_coverと対をなす関数であり、後述するクエリプランナはこの2つの関数の出力を直積することでチャンクキーの集合を導出する。
空間セルの粒度(LOD)と時間バケットの粒度(Granularity)がそれぞれ独立にレイヤー単位で設定可能である点は、「解像度は空間・時間で常に同期しているべき」という暗黙の前提が置かれていないことを意味する。たとえば道路網レイヤーは低頻度の日次粒度でLOD 3、気象観測レイヤーは高頻度の分次粒度でLOD 8、といった具合に、データの性質に応じて異なる時空間解像度を独立に選択できる。
7. チャンクバイナリ形式と Range-GET
チャンクファイルは物理的に3つの領域から構成される。先頭に固定長76バイトのヘッダー、続いて可変長のデータセクション群、末尾に可変長のフッターインデックスである。ヘッダーにはmagic(8バイトの識別子"ST4DCHNK")・バージョン・エンティティ型・圧縮方式・エンコーディング方式・粒度・cell_id・time_bucket_ms・layer_id・lod・generation・created_at・payload_len・footer_offset、そして先頭72バイトに対する CRC32 が格納される。フッターはsection_count(u32)に続き、40バイト固定長のSectionIndexがsection_count個並び、末尾にsection_countとセクション群を対象とする CRC32、そして4バイトのテールマジック0x53415453(リトルエンディアンで"STAR")が付与される。
各SectionIndexはsection_type(Geometry / RasterTileIndex / RasterTileData / AttributeColumn / GraphNodes / GraphEdges / GraphAdjacency / GraphConstraints の8種別)・column_id・flags・チャンク先頭からの絶対バイトオフセットoffset・長さlength、そして述語プッシュダウン用のmin_value/max_value(いずれも f64)を保持する。クエリ実行時、footer.filter_sections(SectionType::AttributeColumn, min_filter, max_filter)は[min_value, max_value]が[min_filter, max_filter]と重複するセクションのみを返し、該当しないセクションについては Range-GET 自体が発行されない。
この一連の流れが実際のオブジェクトストレージ通信としてどのように現れるかを図3に示す。まずヘッダー(76バイト)とフッター全体だけを対象とする最初の Range-GET が発行され、これによってどのセクションがどこにあるかという索引が手に入る。続いて属性フィルタの条件(たとえば「速度40〜80km/hの区間のみ」)がフッターのmin_value/max_valueと照合され、条件に合致するセクションのみが2回目の Range-GET の対象として選ばれる。この時点で、ジオメトリセクションやラスタタイル、無関係な属性列は一切ネットワーク上を流れない。
この設計が持つアーキテクチャ上の意味は大きい。フッターに埋め込まれた統計値による事前フィルタリング自体は様々なバイナリフォーマットで見られる一般的な技法であるが、本アーキテクチャではこれを地理空間・時間という具体的な検索次元(セル・時間バケット・属性値レンジ)と直接結びつけている点、そして専用のクエリサーバを介さずとも HTTP Range リクエストさえ発行できればオブジェクトストレージに対して直接絞り込みクエリが成立し得る点が、このバイナリ形式設計の核心である。
コーデックと差分フィルタ
codecはエンティティ型ごとに異なる圧縮戦略を採用する。ラスタデータには Zstd(レベル3)、ベクタデータには速度優先の LZ4(lz4_flexによる純Rust実装)が適用され、CSR形式ですでに密なグラフデータには圧縮を適用しない。ラスタデータについては圧縮の前段階として「バイト差分フィルタ」(output[i] = input[i] - input[i-1]、wrapping_sub)が適用される。隣接ピクセル間の差分が小さい滑らかなラスタデータでは残差が微小になるため、Zstdの圧縮率が大幅に向上する。このフィルタが適用されたかどうかはSectionIndex.flagsのビット1 << 8(SECTION_FLAG_DELTA_ENCODED)で管理され、デコード時にのみ逆変換(prefix-sum)が適用される。属性列に対してはランレングスエンコーディング(rle_encode/rle_decode)と辞書エンコーディング(dict_encode/dict_decode_table/dict_resolve)が個別に利用可能である。
8. クエリパイプライン
宣言的クエリはQueryPlannerとQueryExecutorという2段階のパイプラインで処理される。プランナーは、クエリの空間条件(GeometrySpec::Bboxによる直接指定、あるいはGeometrySpec::Corridorによる中心矩形+バッファ距離指定)から外接バウンディングボックスを導出し、これにcell_coverを適用してセル集合を得る。同時に時間条件(TimeSpec::Atによる単一時刻、あるいはTimeSpec::Rangeによる時間区間)からtime_bucket_coverによってバケット集合を得る。最終的にセル集合・バケット集合・要求されたレイヤー集合の直積を取ることで、問い合わせるべきChunkKeyの全体集合が確定する。
エグゼキュータは、確定したChunkKeyの集合それぞれについて、まずMetaStore::get_descriptorでチャンクの存在確認を行い(存在しなければ静かにスキップする)、存在すればObjectStore::get(または部分取得の場合はget_range)でバイト列を取得し、ChunkReader::openでヘッダー・フッターをパースし、述語プッシュダウンを適用したのちread_sectionで実データを読み出す。このChunkKeyごとの処理はqueryクレートが依存するrayonによって並列化されており、多数のセル・バケットにまたがるクエリであっても、チャンク単位の I/O 待ちが逐次的にボトルネックにならない構造になっている。
LayerOp::SlopeやLayerOp::MinMaxのような派生演算をLayerSpec.opsに指定すると、プランナーはEntityType::Rasterのチャンクを優先的に選択する。ラスタ統計の付与(POST /api/v1/query/attach_raster_stats)を利用する際には、opsに最低1つラスタ系の演算を含めておくことが実務上重要であり、これを怠るとプランナーがベクタ系チャンクを選んでしまい統計結果が空になるという、ドキュメント化された既知の落とし穴が存在する。
9. グラフエンジンと制約バイトコードVM
道路網や配管網のようなネットワーク構造は、GraphNodes(24バイト/ノード: id, lon, lat)・GraphEdges(16バイト/エッジ: from, to, base_cost, constraint_id)・GraphAdjacency(CSR行ポインタ)という3種類のセクションとしてチャンクに格納される。LocalGraph::build(nodes, edges)はエッジをfromでソートしたうえで CSR(Compressed Sparse Row)形式を構築し、adj[u]..adj[u+1]がノードuの出辺インデックス範囲を表す。
グラフエンジンが特徴的なのは、単なる最短経路探索に留まらず、エッジごとに動的なコスト修正を適用できる「制約バイトコードVM」を内蔵している点である。GraphEdge.constraint_id(0は制約なし)はGraphConstraintsセクション内のエントリを指し示し、各エントリはconstraint_type(Time/Vehicle/Env)・バイトコードのオフセットと長さを持つ。バイトコードはスタックベースの単純な命令セットで構成される。
| コード | ニーモニック | スタック効果 |
|---|---|---|
| 0x01 | PUSH_CONST | リテラルf32をpush |
| 0x02 | PUSH_BASE | base_costをpush |
| 0x10 | MUL | pop b,a → push a×b |
| 0x11 | ADD | pop b,a → push a+b |
| 0x12 | SUB | pop b,a → push a-b |
| 0x13 | DIV | pop b,a → push a÷b(ゼロ除算はdividendを返す) |
| 0x20 | CLAMP_POS | pop a → push max(a,0) |
| 0xFF | RET | pop → 戻り値として返す |
ConstraintTable::evaluate(constraint_id, base_cost)はこのバイトコードを実行して修正済みコストを返し、GraphExecutor::with_constraintsによってDijkstra探索の各エッジコスト評価に組み込まれる。Dijkstra自体はBinaryHeap<Reverse<OrdF32>>によるmin-heapとして実装されており、OrdF32はf32::total_cmpによってNaNが存在しないことを前提とした全順序を提供する。shortest_path(source, target)はコストと経路を、reachable(source, max_cost)は指定コスト以内で到達可能な全ノードを返す。
この設計がもたらすアーキテクチャ上の利点は、「時間帯によって通行止めになる」「特定車種は通行不可」「環境規制区域では追加コストがかかる」といった制約条件を、グラフ構造やアプリケーションコードを変更することなく、チャンクに埋め込まれたバイトコードの差し替えだけで動的に更新できる点にある。制約条件の評価がグラフ探索エンジン自体の内部にスタックマシンとして組み込まれているため、経路探索とその制約評価は同一の実行系のなかで完結し、外部の設定ファイルやグラフ構造そのものの再構築を要しない。
10. Engine ファサードとバックエンド抽象化
EngineはMetaStore・ObjectStore・QueryExecutor・GraphExecutorを束ねる単一のエントリポイントである。Engine::local(root)はローカルファイルシステムとインメモリメタストアを用いる開発向けの構成を、Engine::with_backends(meta, store)は任意のArc<dyn MetaStore>とArc<dyn ObjectStore>を注入する本番向けの構成を提供する。両トレイトはいずれもSend + Sync境界を持つトレイトオブジェクトとして扱われ、put/get/get_range/exists/delete(ObjectStore)、put_descriptor/get_descriptor/delete_descriptor(MetaStore)という最小限のインターフェースしか要求しない。
この抽象化がもたらすアーキテクチャ上の価値は、テストと本番運用のコードパスを完全に一致させられる点にある。ローカルのtempfileディレクトリとMemoryMetaStoreを用いた統合テストが、S3バックエンドのObjectStore実装とRocksDBバックエンドのMetaStore実装に差し替えた本番環境と、Engineより上のアプリケーションコードを一切変更せずに同一の振る舞いを示すことが、トレイト境界によって型システムレベルで保証されている。
11. 信頼度メタデータ・時間補間・ギャップフィル
クエリ実行系が一次サポートする3つの機能について述べる。
第一に、ChunkDescriptorはconfidence: f64 ∈ [0.0, 1.0]という品質スコアフィールドを持つ。取り込み時にこの値を指定でき(省略時のデフォルトは1.0)、クエリ結果の各SectionResultにそのまま伝播される。呼び出し側は、メタデータを再取得することなく、このスコアに基づいてセクションを重み付けしたり、閾値未満のセクションを破棄したりできる。
第二に、時系列の欠損を補完するPOST /api/v1/query/fillエンドポイントは、指定範囲内の全てのTimeBucketを列挙し、実データが存在するバケットについてはfilled: falseとともにそのまま返し、存在しないバケットについては指定戦略(ZeroFill/ForwardFill/BackwardFill)に従って合成する。合成されたバケットの信頼度は、実データからのホップ数に応じてfilled_confidence = source_confidence × 0.9^hopsという指数関数的な減衰式で計算される(ZeroFillは常に信頼度0.0)。
第三に、任意の2時刻間の補間を行うPOST /api/v1/query/interpolateエンドポイントは、t0_msとt1_msそれぞれのスナップショットを通常のクエリパイプラインで独立に取得し、alpha = clamp((target_ms - t0_ms) / (t1_ms - t0_ms), 0.0, 1.0)を計算して、両時刻の生セクションバイト列とalphaをクライアントへ返す。サーバ自身はバイト単位の補間を行わない。これは、セクション内部のデータスキーマ(浮動小数点の配列なのか、四元数による姿勢表現なのか等)をサーバが一切関知しないという設計判断に基づく。スキーマを知るクライアント側がresult = t0_bytes * (1 - alpha) + t1_bytes * alpha、あるいは姿勢データであれば球面線形補間(slerp)のようなドメイン固有の補間を適用する。
これら3つの機能に共通するのは、時空間データに本質的に伴う「不確実性」と「観測の疎らさ」を、アプリケーション層のロジックとしてではなく、エンジンのデータモデルとAPI契約(confidenceフィールド・filledフラグ・alpha値)として表現している点である。合成された値であるかどうか、どの程度信頼できるかという情報がクエリ結果のデータ構造そのものに埋め込まれているため、呼び出し側はこれを機械的に扱うことができる。
12. GISデータインポートパイプライン
GeoJSON・GeoTIFF・ESRI Shapefile という3つの主要な GIS ファイル形式をチャンクへ変換する取り込みエンドポイントが提供される。いずれも共通してlayer_id・lod・time_ms・granularity・entity_type・generation・confidenceというフィールドを受け付け、レスポンスとしてchunks_ingested・features_processed・errors(致命的でないフィーチャ単位のエラーを蓄積する配列)を返す。
POST /api/v1/import/geojsonはFeatureCollection内の各フィーチャのジオメトリをcell_cover(bbox_of_geometry, lod)でカバーし、該当する各セルへジオメトリセクションと属性列セクションを持つチャンクを生成する。POST /api/v1/import/geotiffはBase64エンコードされた.tifファイルをピクセル単位でデコードし、256×256ピクセル単位でタイル分割した上でRasterTileIndex/RasterTileDataとして格納する。ただし現時点の実装では、空間範囲の決定にリクエストで明示的に指定されたbboxパラメータのみが用いられ、GeoTIFF内部の座標参照系(CRS)情報は解釈されない。この点はアーキテクチャドキュメントにも明記された既知の制約であり、外部でCRS変換を済ませた上でbboxを渡す運用が前提となる。POST /api/v1/import/shapefileはBase64エンコードされた.shpと.dbfのペアを受け取り、各シェイプをジオメトリセクション、.dbfのレコードを属性列セクションとして格納する。
13. サーバ・クライアントアーキテクチャと運用面
実装はサーババイナリ(server)とクライアントライブラリ(client)という2つの成果物として構成される。serverは tokio + axum による非同期HTTPサーバであり、設計上重要なのはこのサーバが完全にステートレスであるという点である。ObjectStoreとMetaStoreのバックエンドを共有ストレージ(S3/GCS、あるいはNFS上のRocksDB)に向けることで、複数のサーバインスタンスをL7ロードバランサの背後に並べるだけで水平スケールが成立する。SIGTERMやCtrl+Cを受け取った際には処理中のリクエストを完了してからプロセスを終了するグレースフルシャットダウンに対応しており、KubernetesのterminationGracePeriodSecondsと自然に組み合わせられる。/health/live(常に200を返すliveness probe)と/health/ready(MAX_CONNECTIONS飽和時に503を返すreadiness probe)が分離されている点も、コンテナオーケストレーション環境での運用を強く意識した設計であることを示している。
サーバは環境変数META_BACKEND(memoryまたはrocksdb)とOBJECT_BACKEND(localまたはs3)によってバックエンドを切り替える。RocksDBバックエンド(RocksMetaStore)はrocksdb-backendフィーチャで有効化され、チャンク記述子用の列ファミリに加えて、WALインデックス・統計集計・永続化されたホットセット・コンパクションジョブ追跡という4つの補助的な列ファミリを持つ。ホットセットはHotsetMetaStoreというMetaStoreのデコレータ実装として提供され、内部のHashMapベースのキャッシュに対する書き込みを永続バックエンドへも同時に書き込む(write-through)方式を採る。キャッシュ容量を超えた場合は任意の1エントリを退避する軽量な実装であり、厳密なLRUではない点は実装上の特筆事項である。HOTSET_CAPACITY環境変数でキャッシュ容量を、WORKERSでtokioワーカースレッド数を、MAX_CONNECTIONSで同時処理リクエスト数の上限をそれぞれ制御できる。
認証面では、API キーとステートレスな HMAC トークンによる2段階の仕組みが採用されている。クライアントはまずPOST /api/v1/auth/tokenにAPIキーを提示してBearerトークンを取得し、以降のリクエストではこのトークンをAuthorizationヘッダーに付与する。サーバ側はトークンをHMAC-SHA256で検証するのみであり、セッション状態をサーバ側に保持しない。トークンの有効期間はTOKEN_TTL(デフォルト3600秒)で制御され、署名鍵TOKEN_SECRETを固定値として設定しておかなければ、サーバ再起動のたびに既存トークンが無効化されてしまう点は運用上の注意点である。
サーバは GIS クライアントとの相互運用性を意識したエンドポイント群も備えている。GET /ogc/v1/collectionsとGET /ogc/v1/collections/{id}/itemsはOGC API - Features仕様に準拠したレイヤー一覧・GeoJSONフィーチャー取得を提供し、GET /tiles/mvt/{id}/{z}/{x}/{y}はMapbox Vector Tile(protobufバイナリ)を、GET /tiles/raster/{id}/{z}/{x}/{y}は単バンドfloat32・EPSG:4326のGeoTIFFラスタタイルを返す。clientはライブラリクレートとしてC FFI境界を公開しており、他言語からの呼び出しはこの境界を経由する。
ライセンス検証と機能ゲートの分離
licenseクレートは、UTF-8のJSONドキュメントとしてHMAC-SHA256で署名されたライセンスファイル(デフォルトパス./license.key、LICENSE_PATHで上書き可能)を起動時に読み込み検証する。ライセンスファイルはcustomer_id・plan(Trial/Standard/Professional/Enterprise)・発行時刻・失効時刻・署名を持ち、署名不一致または有効期限切れの場合はサーバ起動が拒否される(開発時はSKIP_LICENSE=1で検証を迂回できる)。プランに応じて解放される機能(Capabilities)は、RocksDB永続バックエンドの利用可否・S3/GCSオブジェクトストレージバックエンドの利用可否・複数ワーカースレッドおよび複数インスタンス配備の利用可否という3点の真偽値として表現される。
アーキテクチャ上の要点は、このCapabilities判定ロジックがcoreを含む他のデータエンジン系クレートに一切依存していないことである(§3参照)。ライセンス検証は署名済みJSONの読み込みと検証という独立した処理としてlicenseクレート内に閉じており、serverの起動シーケンスがその検証結果(Capabilities構造体)を受け取って、どのバックエンド実装をEngine::with_backendsへ注入するかを決定する、という一方向の依存関係になっている。
14. 既知の制約と今後の課題
アーキテクチャドキュメントに記載された既知のギャップ(RocksDB MetaStore、クエリ時のデルタ適用、256×256ラスタタイル分割、attach_raster_statsエンドポイント、真のコリドー幾何クリッピング、多言語SDKバインディング)は、いずれも本稿執筆時点で実装済みであることをソースコード上で確認した。一点、現時点でも残る制約として、GeoTIFFインポートにおいて内部のCRS(座標参照系)情報が解釈されず、リクエストで明示的に渡されたbboxのみが空間範囲として採用される点が挙げられる。任意の座標系のGeoTIFFをそのまま投入できるわけではなく、事前にEPSG:4326へのCRS変換とバウンディングボックスの算出を外部で行っておく必要がある。この制約は将来的な改善余地として明記しておくべきであろう。
15. おわりに
このアーキテクチャを一言で要約するならば、「メタデータストアが担うべき役割を、可能な限り物理レイアウト自体に埋め込む」という徹底した設計思想である。この思想は、76バイトの固定長ヘッダーと自己完結的なフッターインデックスという単純な形式として結実し、その単純さの上に、空間・時間の階層的な索引(CellId/TimeBucket)、スナップショット/デルタによる更新モデル、述語プッシュダウンとRange-GETによる部分読み出し、rayonによる並列クエリ実行、CSR形式とバイトコードVMによる制約付きグラフ探索、そして信頼度伝播・時間補間・ギャップフィルといった時系列固有の意味論が、一貫した型システムとトレイト境界のもとに積み上げられている。個々の要素技術そのものに新規性があるというよりも、「物理配置=論理キー」という単一の原則から、データモデル・バイナリ形式・実行エンジン・運用面までが筋道立てて導出されている、という設計の一貫性そのものが、このアーキテクチャの中心的な特徴である。