StaticQL という大袈裟な名前で、TypeScript ライブラリを作成したので紹介させてください。
レポジトリはこちらです migiwa-ya/staticql。
何ができるか・誰に向いているか
- StaticQL は Markdown/YAML/JSON などの静的ファイルをまとめて扱える TypeScript ライブラリ
-
こんな人におすすめ
- 静的サイトでもページ内で 絞り込み・関連データ参照 をサクッと実装したい
- クエリ処理を ブラウザだけで完結させ、費用をストレージ+転送量だけに抑えたい
- 異なる形式のデータを 1 つのレイヤー で検索・結合したい
- 複数のストレージ/CDN に散在するデータソースを横断的に使いたい
-
主な特徴
- リレーション結合・全文検索・ページネーションを TypeScript API で提供
- プレフィックス分割した転置インデックスで、数万レコードでも実用的なレスポンス
類似ツール比較表
以下は StaticQL と主要な類似ツール 5 種 を、用途が近い順に並べて比較した早見表です。ChatGPT に出してもらったのでやや贔屓目です。
(◎=得意、○=対応、△=限定的、✕=非対応)
※スクロールしないみたいなので画像です
ざっくり使い分け
- StaticQL : 複数フォーマット・リレーション・ページングが必要な静的データサイト
- Astro Collections : 記事100 件程度+簡易タグ検索なら十分
- SQLite/DuckDB WASM : 高速 SQL が欲しい/データ変換を許容できる場合
- Pagefind / FlexSearch : フリーワード全文検索だけ欲しい場合
- Contentlayer : Next.js / Astro 内で型安全に Markdown を読むだけなら手軽
迷ったら「リレーションや複合検索が要るか」で分岐、
Yes → StaticQL / SQL、No → Collections or Pagefind が目安です。
使い方例
// こんな感じでインデックスを生成し
import { defineStaticQL, StaticQLConfig } from "staticql";
import { FsRepository } from "staticql/repo/fs";
import config from "./staticql.config.json";
import type { HerbsRecord, RecipesRecord } from "./staticql-types";
const staticql = defineStaticQL(config as StaticQLConfig)({
repository: new FsRepository("./"),
});
// Generate indexes (required before queries)
await staticql.saveIndexes();
// こんな感じで検索を行います
// Simple indexed filter
const { data: herbs } = await staticql
.from<HerbsRecord>("herbs")
.where("slug", "eq", "arctium-lappa")
.exec();
// Join and filter on related source
const { data: recipes } = await staticql
.from<RecipesRecord>("recipes")
.join("herbs")
.where("herbs.slug", "in", ["centella-asiatica"])
.orderBy("name", "asc")
.pageSize(10)
.exec();
興味ありましたら、詳しくは migiwa-ya/staticql と tests をご参照ください。
拙い利用例として、神社アーカイブサイト(以下、神名帳)のソースコード
や、GitHubで管理している神社データ
などを神名帳に反映させる GitHub Actions のレポジトリ
などの公開しているので、そちらもよろしければ。
さて、ライブラリ作るとこまでやって燃え尽き症候群気味なので、機能面の紹介はここまでにして、ここからは作るに至った経緯について僭越ながら語らせていただきます。
作るに至った経緯と開発過程
もともとは、ブログや管理画面を作るまでもないコンテンツサイトで、エディタ上で管理するために、コンテンツをマークダウンで書き、簡易的なリレーションや検索ができたらいいな、と思ったのが始まりでした。
リレーションや検索は、frontmatter などで読み込んだコンテンツを、js の filter, map などで都度処理する必要があり、データ量が多くなればそれだけ処理に時間もかかり、ページネーションなども実装する必要がある。
これを進めていたところで、学習もかねてパッケージ化してみようと思い立ったのが始まりでした。
Astro Content Collections などの選択肢もありましたが、リレーション管理や、複数のファイル形式に対応していなかったり、ページネーションのために別途実装が必要であること、Astro専用であることなどから、パッケージ化の判断に至りました。
さて、この StaticQL の実装を進めていくうちに、ブログよりも規模の大きいコンテンツにも適用できるのではと考え、以前からやりたいと思っていた 神名帳 に適用させることを要件として、インデックスの仕組みを転置インデックスのみから、転置インデックスとインデックス対象文字列のプレフィックスごとにディレクトリ分けすることで、ひとつのインデックスファイルが巨大化するのを防ぐ形になりました。その分走査するファイルが増えて速度とのトレードオフになりましたが。
インデックスの仕組みは _prefixes_jsonl, _index.jsonl の2つのファイルで構成されています。
_prefixes_jsonl 対象文字列のプレフィックスのリストで、_index.jsonl は転置インデックスのリストです。このファイル群によって、list できない fetch であってもどんなコンテンツがあるかを知ることができる、ということです。
この「list できない fetch であってもどんなコンテンツがあるかを知ることができる」というのが、神名帳 の要件の一つで、例えば、「神社」と「神」と「地域名」のコンテンツソースが別のストレージやCDNにあっても、定義ファイルと、どこにソースがあるか、を設定すれば、手元にそれぞのソースがなくてもリレーション用のインデックスを作成して、検索することができる、ということです。
これにより、ネットワーク上の別の異なるプロジェクト間で、相互的にデータを参照・利用できるようになります。
勿論、先ほど書いたように fetch は速度面で劣るので、ソースが大量である場合は GitHub などで公開されたものをダウンロードし、ローカルで FsRepository を使ってインデックスを生成するほうが速いです。
強いて理念というものを考えてみると、おこがましくも、Linked Open Data(LOD)に代表される Resource Description Framework (RDF) とそのクエリシステム、に近いかもしれません。
StaticQL の利点としては、パーサー次第でどんなファイルでも検索対象にできる点と、検索機能をフロントエンドだけで提供できる分、実質的な費用コストがストレージと通信量のみになるので、低コストである点などがあげられると思います。
同様の利点を持つものとして SQLite や DuckDB の WASM があげられると思います。検索機能や速度についてはおそらく、これらのほうが圧倒的に優れていますが、今回の要件として、複数のファイル形式をフラットな状態で管理したかったこと、異なる環境にあるデータソースを結合させたかったこと、などの点で作る価値があったかな、と思います。
