本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。
Oxigraph は Rust で書かれた RDF(Resource Description Framework)ストアです。単一バイナリで動き、SPARQL(SPARQL Protocol and RDF Query Language)1.1 の Query と Update に対応しています。手元に Turtle 形式の RDF ファイルが 13 本、合計 4GB ほどあったので、これをコンテナに入れて実際にどう動くのかを測りました。日本語の人文系データセット群で、由来の異なる資料が別々のファイルに分かれています。以下ではデータの中身には立ち入らず、規模と挙動だけを扱います。
途中で「1〜2GB 程度のデータでも速いのか」「Virtuoso と比べてメモリ効率はどうなのか」という疑問が出たので、そちらも実測しています。結果として、集約クエリでは Virtuoso が 50〜130 倍速く、一方でメモリ効率についての当初の見立ては崩れました。誤った見立てをどこで作ってしまったかも含めて書きます。
コンテナで起動する
公式イメージは Docker Hub ではなく GitHub Container Registry で配布されています。
docker run -d --name oxigraph --restart unless-stopped -p 127.0.0.1:7878:7878 -v oxigraph-data:/data ghcr.io/oxigraph/oxigraph:latest serve --location /data --bind 0.0.0.0:7878 --cors
引数で注意が要るのは 2 か所です。
コンテナ内で渡す --bind は 0.0.0.0 にします。既定値が localhost:7878 なので、そのままではコンテナの外から届きません。一方でホスト側の公開は 127.0.0.1:7878:7878 と書いて、ループバックに絞っています。Oxigraph には認証機構がなく、-p 7878:7878 にすると同一ネットワーク上の誰でも SPARQL Update でデータを書き換えられる状態になるためです。
起動すると、ブラウザから使える SPARQL エディタが同じポートで開きます。

エンドポイントは次のとおりです。
| パス | 用途 |
|---|---|
GET / | Web の SPARQL エディタ |
POST /query | SPARQL Query |
POST /update | SPARQL Update |
POST /sparql | Query と Update の両方を受ける(上の 2 つの和集合) |
GET / PUT / POST / DELETE /store | Graph Store Protocol(RDF の取得と投入) |
データを入れる経路は 2 つある
最初は HTTP の SPARQL Update で入れようとしたのですが、この経路は 1 つのリクエストの本文として更新文を送る形なので、GB 単位の Turtle をそのまま流すには無理があります。Oxigraph には load サブコマンドがあり、サーバを介さずストアのファイルを直接書きます。
書き込みロックは 1 プロセスしか持てないので、load を使うときはサーバを止める必要があります。
docker stop oxigraph
docker run --rm -v oxigraph-data:/data -v ~/lod:/src:ro ghcr.io/oxigraph/oxigraph:latest load --location /data --file /src/ds1.ttl --graph https://example.org/lod/ds1/
docker start oxigraph
--graph で名前付きグラフを指定できます。データセットごとに別のグラフに入れておくと、後から出所別に絞り込めます。
大きいファイルには 2 つのオプションが要ります。
--non-atomic は、ロードの途中でディスクに書き出していく指定です。既定の atomic は「ファイル全体がストアに入るか、まったく入らないかのどちらか」というトランザクション上の性質で、公式ドキュメントが --non-atomic の利点として挙げているのは、RocksDB が途中で compact・compress できることによるディスク使用量の低減です。代償として CPU 使用量と write amplification が増えるとされています。
ところが手元では所要時間が逆でした。996MB のファイルで atomic が 47 秒、--non-atomic が 29 秒です。必要メモリの閾値は両者とも同じで、差が出ませんでした(後述)。
--lenient は RDF の検証を緩める指定です。これが必要になった経緯は次の節に書きます。
投入した結果は次のようになりました。13 ファイル、合計 30,408,603 トリプルです。所要時間はいずれもメモリ上限なし・--non-atomic での値です。
ファイルサイズ トリプル数 投入時間
996MB 20,109,540 34秒
2.9GB 8,882,723 26秒
133MB 442,846 3.7秒
22MB 343,720
18MB 197,137
11MB 152,501
5.7MB 88,881
35MB 64,597
1.4MB 33,181
2.3MB 25,700
1.3MB 25,325
912KB 21,292
892KB 21,160
──────────────
30,408,603
速度は最大で毎秒 58.5 万トリプルでした。「34 秒」も「58.5 万トリプル/秒」もローダ自身が出力した値で、秒は切り捨てられています(実際は 34.4 秒ほど)。
ストア全体は 10.2GB になりました。元の Turtle が 4GB ほどなので 2.5 倍です。これは optimize をかける前の値で、後述するように optimize を実行するとさらに倍近くまで増えます。
なお 2.9GB のファイルは行数が 1,668 万行あるのに対してトリプル数が 888 万で、半分ほどしかありません。これは記事本文が複数行にわたる長いリテラルとして入っているためで、欠落ではありませんでした。ファイル末尾のトリプルがストアに存在することを ASK で確認しています。行数とトリプル数を突き合わせて「入りきっていないのでは」と疑ったときは、末尾の 1 件を直接引くのが確実でした。
厳格なパーサに 2 本が弾かれた
13 本のうち 2 本が、既定の設定ではロードに失敗しました。どちらも IRI(Internationalized Resource Identifier)の構文エラーです。
Error while loading file /src/A.ttl: Parser error at line 44 between columns 25 and 109: Invalid IRI code point '#'
Error while loading file /src/B.ttl: Parser error between line 669 column 24 and line 670 column 2: Invalid IRI code point '
'
1 本目は、1 つの IRI の中にフラグメント記号 # が 2 回現れているものでした。画像ビューアの URL がそのまま値として入っており、…#?#&… のようにフラグメントが二重になっています。同じ形が 1 万件強ありました。
2 本目は、IRI の途中で改行が入って > が次の行に来ているものが 1 件だけありました。生成時に折り返しが混入したように見えます。
いずれも --lenient を付ければ投入できます。ただし --lenient は検証を飛ばすだけなので、構文として不正な値がそのままストアに入ります。厳格なパーサを持つ他の実装では、同じファイルがまるごとロード失敗になる可能性があります。
この 2 件は、それまで別のツールでは問題なく扱えていたものでした。Oxigraph に入れて初めて表面化した形です。RDF の検証を通したいだけであれば、ストアに入れる前にパーサだけ通すという使い方もできそうです。
名前付きグラフに分けると横断できる
データセットごとに別の名前付きグラフへ入れておくと、共通の語彙をキーに横断できます。今回のデータでは、日付が http://datetime.hutime.org/date/YYYY-MM-DD という形の IRI で表現されていました。HuTime(時間情報学の研究基盤として時間軸上の位置に恒久的な識別子を与えているプロジェクト)が公開しているもので、「1915 年 4 月 3 日」のような日付そのものが 1 つの IRI になります。同じ日付 IRI を持つデータセットが何個あるかを数えてみます。
PREFIX dcterms: <http://purl.org/dc/terms/>
SELECT ?date (COUNT(DISTINCT ?g) AS ?datasets)
WHERE {
GRAPH ?g { ?s dcterms:date ?date }
FILTER(STRSTARTS(STR(?date), "http://datetime.hutime.org/date/"))
}
GROUP BY ?date HAVING(COUNT(DISTINCT ?g) >= 4)
ORDER BY DESC(?datasets) ?date
結果は、多いもので 13 グラフ中 9 つが同じ日付 IRI を共有している、というものでした(該当した日付そのものは伏せます)。8 つ以上が重なる日は 16 日、7 つ以上なら 94 日あります。それぞれ別々に作られたファイルを 1 つのストアに置いただけで、こうした照合ができます。日付の表現が IRI で揃っていたことが効いています。
逆に言えば、揃っていない語彙は横断できません。今回のデータでも、日付以外の項目(地名や人物)は表記が統一されておらず、同じ手では突き合わせられませんでした。
どれだけメモリが要るか
コンテナのメモリ上限を変えて、同じクエリを流しました。データは 3,040 万トリプル、ストアは 10.2GB です。
| クエリ | 上限なし | 1GB 制限 | 2GB 制限 |
|---|---|---|---|
| 主語を 1 つ指定して述語と目的語を引く | 0.002s | 0.002s | 0.003s |
| ラベルの完全一致検索(全 13 グラフが対象) | 0.003s | 0.003s | 0.002s |
| 型指定スキャン LIMIT 1000 | 0.003s | 0.002s | 0.003s |
| 2 ホップ結合 LIMIT 100 | 0.003s | 0.003s | 0.003s |
| 全件 COUNT(3,040 万件を数える) | 8.74s | 7.86s | 7.83s |
| 述語別 GROUP BY(1 グラフ 2,010 万件を集約) | 5.82s | 4.95s | 4.93s |
1GB 上限でも劣化しませんでした。実際のメモリ使用量は 94MB で、上限の 9% しか使っていません。Oxigraph は RocksDB(LSM ツリー、Log-Structured Merge-tree 方式のキーバリューストア)の上に載っていて、データをメモリに載せることを前提にしていないためのようです。
集約クエリで上限なしのほうがかえって遅く出ていますが、この差は測定のばらつきの範囲と見ています。上限による違いを示すものではありません。
ただしこの 94MB という数字は、実際の消費より小さく出ている可能性があります。コンテナのメモリ使用量は cgroup(Linux がプロセス群の資源を制限・計上する仕組み)を通じて測られますが、回収可能なページキャッシュの多くはこの計上から除かれるためです。1GB でも速度が落ちなかったのは、測ったクエリが触るページ数が少なかったからで、全走査を連発する用途なら事情は変わる可能性があります。
投入時は事情が違いました。996MB の Turtle ファイルを空のストアに入れる実験です。
| メモリ上限 | --non-atomic | 既定(atomic) |
|---|---|---|
| 1GB | OOM Kill(2 秒で終了。以下 OOM は Out Of Memory) | OOM Kill(2 秒で終了) |
| 2GB | 成功 29 秒 | 成功 47 秒 |
| 3GB | 成功 30 秒 | — |
| 4GB | 成功 32 秒 | 成功 52 秒 |
| 6GB | 成功 39 秒 | — |
1GB では入りません。2GB あれば通り、それ以上増やしても速くはなりませんでした。むしろ 2GB の 29 秒に対して 6GB では 39 秒と 3 割ほど伸びています。メモリに余裕があるぶん RocksDB が書き出しを遅らせているのではないかと考えていますが、確認は取れていません。
ただし 2GB は境界に近いようです。この表を書いた後、同じマシンで測り直したところ、2GB は 3 回とも成功した一方、別の機会には同じ条件で OOM Kill されました。他のコンテナが動いているかどうかで結果が変わったように見えます。余裕を見るなら 2.5GB 以上を確保したほうがよさそうです。
つまり「検索は 1GB でも足りるが、投入には 2GB 要る」という形で、必要な容量が非対称でした。運用に載せるときは投入時のほうを基準に見積もることになります。
同じデータを Virtuoso にも入れる
ここまでの結果を見て、Virtuoso と比べたらどうなのかが気になりました。Virtuoso はバッファプールを設定で確保する設計で、Oxigraph とは考え方が逆です。当初は「Oxigraph のほうがメモリ効率が桁違いに高いはず」と考えていたのですが、実測すると違いました。
比較は 996MB のファイル(20,109,540 トリプル)を両者に入れて行いました。トリプル数と集約結果は完全に一致しています。
ここから先の Oxigraph 側の数値は、13 本すべてが入った 10.2GB のストアではなく、この 1 本だけを空のストアに入れ直したものに対する値です。加えて optimize(RocksDB のファイルを再編成して読み取りを速くするサブコマンド)を実行しています。前節までの数値と直接は比べられません。
この optimize には副作用がありました。実行するとストアが 1,734,056,163 バイト(1.6GB)から 3,154,916,869 バイト(2.9GB)へ、1.8 倍に増えます。30 秒待っても縮まないので、圧縮の完了待ちではなく確定値のようです。読み取りを速くする代わりにディスクを使う、という挙動だと思われます。
docker run -d --name virtuoso --memory=4g -p 127.0.0.1:8890:8890 -p 127.0.0.1:1111:1111 -v virt-data:/database -v ~/lod:/staging:ro -e DBA_PASSWORD=... -e VIRT_Parameters_NumberOfBuffers=340000 -e VIRT_Parameters_MaxDirtyBuffers=250000 -e VIRT_Parameters_DirsAllowed=". , /database, /staging" openlink/virtuoso-opensource-7:latest
投入はバルクローダを使います。ld_dir で対象を登録し、rdf_loader_run で実行します。
docker exec virtuoso isql 1111 dba dba exec="ld_dir('/staging','big.ttl','https://example.org/lod/big/');"
docker exec virtuoso isql 1111 dba dba exec="rdf_loader_run();"
docker exec virtuoso isql 1111 dba dba exec="checkpoint;"
44 秒で完了し、checkpoint に 2 秒かかりました。投入中の CPU 使用率は 274% で、複数コアが動いていました。
公式の手順では、この後に checkpoint_interval(N); と scheduler_interval(M); を実行して元に戻すよう指示されています。バルクローダが checkpoint と scheduler を一時的に無効化するためです。今回は使い捨ての検証環境なので省きましたが、運用に載せるなら必要な手順です。
Virtuoso にも Web の SPARQL エディタが付いています。

検索性能は、クエリの形で差の出方が変わる
3 回実行した最良値です。
| クエリ | Oxigraph | Virtuoso |
|---|---|---|
| 型指定スキャン LIMIT 1000 | 0.003s | 0.004s |
| 2 ホップ結合 LIMIT 100 | 0.003s | 0.002s |
| 全件 COUNT(20M) | 4.835s | 0.085s |
| 述語別 GROUP BY(20M 集約) | 5.374s | 0.042s |
絞り込みのあるクエリは互角でした。差が出たのは集約で、COUNT が 57 倍、GROUP BY が 128 倍の開きになりました。
Oxigraph 側で集約クエリを流している間の CPU 使用率を測ると 87〜92% で、16 コアのうち 1 コア分でした。これは仕様どおりで、Oxigraph のアーキテクチャ文書は「各クエリの評価は、単純さのため現時点ではシングルスレッドである」と明記しています。CLI の README にも「SPARQL のクエリ評価はまだ最適化されていない」とあります。
対する Virtuoso は、イメージに同梱されている virtuoso.ini.sample の既定が ThreadsPerQuery = 4 で、1 つのクエリを複数スレッドで実行する設定になっています。Virtuoso 側は 42 ミリ秒で終わってしまうため CPU 使用率は同じ方法では測れませんでしたが、少なくとも投入時には 274% を観測しました。カラムストアで述語の列が高圧縮になることも効いていそうです。
集約結果は両者で完全に一致していたので、少なくとも今回のクエリについては、Virtuoso が統計値で近似を返しているわけではないことを確認できました。
メモリの見立ては実測で覆った
当初は「Virtuoso はバッファプールを固定確保するので、この規模でも数 GB は要る」と考えていました。実際には違いました。
| コンテナ上限 | NumberOfBuffers | 実メモリ使用量(RSS, Resident Set Size) | 述語別 GROUP BY |
|---|---|---|---|
| 1GB | 85,000(プール 664MB) | 389MB | 0.043s |
| 2GB | 170,000(1.3GB) | 389MB | 0.042s |
| 4GB | 340,000(2.6GB) | 437MB | 0.045s |
1GB 上限でも動き、集約性能も落ちませんでした。
これは公式ドキュメントの説明とは食い違います。Virtuoso のドキュメントは「各バッファは 8K ページをキャッシュし、およそ 8,700 バイトのメモリを占める」「物理メモリを超える設定は大きな悪影響がある」と書いており、遅延確保のような挙動には触れていません。それでも実測の RSS は設定したプール容量よりはるかに小さく、触れていないぶんは実メモリを消費していないように見えました。仮想メモリ上は確保しつつ、触っていないページが RSS に計上されていないだけ、という可能性もあり、今回の観測だけでは区別できません。
絶対値では Oxigraph のほうが小さく済んでいます。この比較用ストアでの Oxigraph は 55MB、13 本すべてを入れたストアでも 94MB でした。Virtuoso の 389MB に対して 4〜7 倍の差です。
ただし、どちらも 1GB に収まります。当初は「Virtuoso はバッファプールを固定確保するので数 GB は要る、だからメモリ効率で Oxigraph を選ぶ理由がある」と考えていたのですが、崩れたのはこの後半です。Oxigraph が省メモリであること自体は覆っていません。覆ったのは、その差がこの規模で選定の判断材料になる、という部分でした。
設定が効いていないことに気づくのが遅れた
この結論に至る前に、いったん「Virtuoso は 1GB でも 2GB でも OOM Kill される」という誤った測定結果を出しています。原因は環境変数の扱いでした。
最初にこのコンテナを作ったときは、8GB のメモリを想定して NumberOfBuffers=680000 を渡していました。その後メモリ上限を 1GB や 2GB に下げて測り直したのですが、このとき渡した小さい値が反映されていませんでした。
openlink/virtuoso-opensource-7 イメージは virtuoso.ini をデータボリュームに書き出します。そして VIRT_Parameters_* 環境変数が反映されるのは、この ini が生成される初回起動のときだけでした。2 回目以降にメモリ上限だけを変えて再起動しても、ini の中身は初回の値のままです。
イメージの entrypoint スクリプトを読むと、環境変数を ini に流し込む処理が if [ ! -f virtuoso.ini ] のブロックの内側にしかなく、既存の ini に再適用する分岐が存在しないことが確認できます。同じ構造は DBA_PASSWORD にもあるので、2 回目以降の起動ではパスワードも変わりません。
そのため、--memory=1g と VIRT_Parameters_NumberOfBuffers=85000 を同時に渡したつもりが、実際には 680,000 バッファ(約 5.3GB)を要求し続けていて、起動直後に OOM Kill されていました。
紛らわしいのは、この状態でも「それらしい結果」が出てしまうことです。1GB でも 2GB でも落ちるので、「Virtuoso は小さいメモリでは動かない」という一貫した結論に見えました。設定が反映されているかを別途確認しなければ、そのまま信じていたところです。
確認方法は単純で、ボリューム内の ini を直接読みます。
docker exec virtuoso grep -E '^(NumberOfBuffers|MaxDirtyBuffers)' /database/virtuoso.ini
docker exec virtuoso isql 1111 dba dba exec="status();" | grep -E 'RSS|buffers'
設定値を変えて測り直すときは、ini を直接書き換えてから起動しました。
docker run --rm -v virt-data:/database alpine sed -i 's/^NumberOfBuffers.*/NumberOfBuffers = 85000/' /database/virtuoso.ini
コンテナの設定を環境変数で渡す形式は手軽ですが、状態を持つボリュームと組み合わさると、渡したつもりの値が使われないことがあります。パラメータを変えて比較する種類の計測では、渡した値ではなく実際に使われている値を毎回確認したほうがよさそうです。
投入とディスク
Virtuoso 側も、同じ 996MB のファイルを空のデータベースに入れる実験をメモリ上限別に行いました。NumberOfBuffers は上限に合わせて設定しています。
| メモリ上限 | NumberOfBuffers(プール) | 結果 |
|---|---|---|
| 2GB | 170,000(1.3GB) | OOM Kill |
| 2GB | 340,000(2.6GB) | OOM Kill |
| 3GB | 170,000(1.3GB) | 成功 29〜45 秒 |
| 3GB | 255,000(2.0GB) | OOM Kill |
| 4GB | 340,000(2.6GB) | 成功 31〜42 秒 |
ここで分かるのは、必要なメモリが上限そのものではなく、バッファプールの設定に従って動くということです。同じ 3GB でも、プールを 1.3GB に絞れば入り、2.0GB にすると落ちます。投入にはプールに加えて 1.5GB ほどの余裕が要るように見えました。
なお最初にこの実験をしたとき、私は上限だけを変えてプールを 2.6GB に固定したまま測り、「Virtuoso は投入に 3GB 要る」と結論しかけました。前の節で自分が書いたのと同じ失敗です。設定が効いているかを毎回確認する、というのは思っているより難しいことのようです。
クエリを投げるだけなら 1GB で足りるのに、投入にはプール+1.5GB が要るという形で、Oxigraph と同じく非対称でした。両者を並べると次のようになります。
| Oxigraph | Virtuoso | |
|---|---|---|
| 投入時間(996MB) | 34 秒 | 44 秒 |
| 投入に必要なメモリ | 2〜2.5GB(1GB は OOM) | バッファプール+1.5GB ほど |
| クエリに必要なメモリ | 1GB で十分 | 1GB で十分 |
| ディスク使用量 | 1.6GB(optimize 後は 2.9GB) | 1.3GB |
| 起動からクエリを受け付けるまで | 即時 | 数秒 |
投入速度は Oxigraph がやや速く、ディスクは Virtuoso が半分以下でした。投入時に必要なメモリはどちらも元ファイルの 2〜3 倍で、Virtuoso のほうが 1GB 多く要ります。
どちらを選ぶか
今回測った範囲では、判断の軸はメモリではありませんでした。
集約や分析のクエリを投げるなら Virtuoso が向いていそうです。50〜130 倍の差があるので、たとえば「年ごとの件数」のような集計画面を作る場合、Oxigraph では毎回数秒待つことになります。
一方、絞り込み検索と API 提供が中心なら、性能の差はほとんどありません。表の上 2 行は 1 ミリ秒前後の違いで、これは curl での測り方を考えれば誤差の範囲です。つまりデータが示しているのは「集約は Virtuoso が圧倒的、それ以外は引き分け」であって、二者択一のトレードオフではありません。
そうなると Oxigraph を選ぶ理由は性能ではなく、運用の手数になります。単一バイナリで起動が即時、既定値のまま docker run 一発で立ち上がりました。Virtuoso は搭載メモリに合わせて NumberOfBuffers を決める手順が要り、今回の構成では ini の永続化と環境変数の関係も把握しておく必要がありました。
なお今回測っていない軸が多く残っています。同時アクセス、大きな結果集合の返却、OPTIONAL や UNION を含む複雑なクエリ、更新の負荷はいずれも未測定です。Virtuoso にある全文検索(bif:contains)、RDFS(RDF Schema)や OWL(Web Ontology Language)の推論、クラスタ構成、SQL 統合といった機能も比較していません。選定時にはこちらのほうが効いてくる場面もあるはずです。
サーバレス環境で動かせるか
Cloudflare Workers のようなサーバレス環境に SPARQL エンドポイントを置けるかも調べました。調査した限りでは、次のような状況のようです。
Oxigraph の JavaScript 版(npm の oxigraph)は WebAssembly ビルドで、インメモリストア専用です。RocksDB が使えないので、今回のような永続ストアは載りません。加えて Workers にはisolate あたりメモリ 128MB、バンドルは gzip 後 3MB(Free)/ 10MB(Paid)という制約があります。数万トリプル規模の語彙やシソーラスなら成立しそうですが、2,000 万トリプルは難しそうです。
現実的な選択肢は次の 4 つに見えます。
| 方式 | どういう仕組みか |
|---|---|
| Cloudflare Containers | 今回のイメージを動かす。10ms 単位の課金で scale to zero。ただし linux/amd64 要件とインスタンスタイプの上限あり |
| AWS Lambda + Oxigraph | uedayou/oxigraph-sparql-api-serverless。ビルド時に Turtle から RocksDB ストアを作り、それを ZIP パッケージ(provided.al2 カスタムランタイム)に同梱する読み取り専用の構成 |
| Deno Deploy | nishad/serverless-sparql-endpoint。Turtle ファイルに対するインメモリ Oxigraph |
| Linked Data Fragments | サーバは 1 パターンぶんの断片を返すだけにして、Comunica がクライアント側で SPARQL を解決する |
Cloudflare に載せるなら Containers ということになりそうです。Workers ではなく Containers であれば Docker イメージを動かせて、リクエストがない間は課金されません。
ただし制約が 2 つあります。1 つはイメージが linux/amd64 で動く必要があること(Oxigraph の公式イメージは amd64 と arm64 の両方が配布されているので、この点は問題ありません)。もう 1 つは、メモリとディスクがインスタンスタイプで決まることです。最小の lite は 256MiB・ディスク 2GB、basic が 1GiB・4GB で、上は standard-4 の 12GiB・20GB まで。今回の実測(投入に 2GB のメモリ、ストア 10.2GB)を当てはめると lite や basic では投入できず、standard-3(8GiB・16GB)相当が要ることになります。
コールドスタート時のコンテナ起動と、ディスクの永続性の扱いは未検証です。
Linked Data Fragments は発想が逆で、サーバは 1 つのトリプルパターンぶんの断片を返すだけにして、結合はクライアントが行います。サーバ側の負荷は小さくなるかわり、今回測ったような集約クエリは相当遅くなるはずです。なお、標準的な Triple Pattern Fragments サーバは任意のパターンに動的にマッチングして断片を作るので、静的ファイルを置くだけで済むわけではありません。ファイルを置くだけで成立するのは、Comunica が RDF ダンプを直接読む経路を使う場合です。いずれにせよ公開用のカタログには向いても、分析用途には向かないと思われます。
なお Cloudflare の Durable Objects は SQLite が1 オブジェクトあたり最大 10GB(Workers Paid の場合。Free は 1GB でアカウント合計 5GB)なので容量としては足りますが、これをバックエンドにする既製の SPARQL エンジンは見当たりませんでした。
計測環境
機材 Apple M4 Max / メモリ 128GB / macOS 26.5.2
Docker 29.6.2(Docker Desktop の VM に 16 コア・23.4GiB 割り当て)
Oxigraph 0.5.9 ghcr.io/oxigraph/oxigraph:latest
Virtuoso 07.20.3243 (7.2.18-dev, Column Store, aarch64)
openlink/virtuoso-opensource-7:latest
応答時間はいずれも curl の time_total を 3 回測った最良値です。ローカルのループバック接続なので、ネットワーク遅延はほぼ含まれていません。同一マシン上で 1 つずつ順に測っており、両者を同時に動かした状態での比較ではありません。
再現性について 2 点、断っておきます。1 つは、このマシンでは他にも多数のコンテナが動いており、投入の所要時間は測るたびに揺れました。同じ条件の Virtuoso への投入が 29 秒だったり 45 秒だったりします。桁は変わりませんが、秒単位の値をそのまま受け取らないでください。もう 1 つは、メモリ上限が境界に近いときに、同じ条件でも成功したり OOM Kill されたりしたことです。上の表の閾値は、余裕を見て一段上を取るための目安として読んでください。
公開前に、記事の主張は独立した複数のエージェントで検証しました。その過程で、11 グラフの時点で測った値を 13 グラフの結果として書いていた誤り、optimize の効果を逆に書いていた誤り、--non-atomic の説明が公式ドキュメントと食い違っていた誤りが見つかり、いずれも修正しています。



コメント
…