<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>D1 on デジタルアーカイブシステムの技術ブログ</title><link>https://tech.ldas.jp/ja/tags/d1/</link><description>Recent content in D1 on デジタルアーカイブシステムの技術ブログ</description><generator>Hugo</generator><language>ja</language><lastBuildDate>Mon, 06 Apr 2026 22:00:00 +0900</lastBuildDate><atom:link href="https://tech.ldas.jp/ja/tags/d1/index.xml" rel="self" type="application/rss+xml"/><item><title>Elasticsearch → Static JSON / D1 移行検証 — 小規模データなら全文検索エンジンは不要だった</title><link>https://tech.ldas.jp/ja/posts/elasticsearch-to-static-json-and-d1/</link><pubDate>Mon, 06 Apr 2026 22:00:00 +0900</pubDate><guid>https://tech.ldas.jp/ja/posts/elasticsearch-to-static-json-and-d1/</guid><description>&lt;p>Cloudflare Pages上で動くNext.js製の日本語テキスト検索APIで、Elasticsearchの代替としてCloudflare D1（SQLite）とStatic JSON（インメモリ検索）を実装し、3方式の検索性能を比較しました。&lt;/p>
&lt;h2 id="背景">背景&lt;/h2>
&lt;p>古典日本語テキストの全文検索APIを運用しています。Elasticsearchを外部クラスタとして利用していましたが、以下の理由で代替を検討しました。&lt;/p>
&lt;ul>
&lt;li>外部サービスへの依存を減らしたい&lt;/li>
&lt;li>Cloudflare Pages内で完結させたい&lt;/li>
&lt;li>データ量が小さい（約1,800件）ので、全文検索エンジンは過剰かもしれない&lt;/li>
&lt;/ul>
&lt;h2 id="データ規模">データ規模&lt;/h2>
&lt;div class="table-wrapper">
 &lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>値&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>レコード数&lt;/td>
 &lt;td>1,812件&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>テキスト総量（UTF-8）&lt;/td>
 &lt;td>約2.5 MB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1レコード平均&lt;/td>
 &lt;td>約1.4 KB&lt;/td>
 &lt;/tr>
 &lt;/tbody>
 &lt;/table>
&lt;/div>
&lt;p>各レコードは古典日本語テキスト（数行〜十数行）、ページ番号、巻名、IIIFキャンバスURLで構成されています。&lt;/p>
&lt;h2 id="3方式の概要">3方式の概要&lt;/h2>
&lt;h3 id="1-elasticsearch既存">1. Elasticsearch（既存）&lt;/h3>
&lt;p>外部Elasticsearchクラスタに対して、fetch APIでwildcardクエリを実行する方式です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">wildcard&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s1">&amp;#39;original_text_lines.keyword&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`*&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">*`&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ngramアナライザーでインデクシングしていますが、実際の検索ではwildcardクエリ（&lt;code>*query*&lt;/code>）を使っており、ngramインデックスの恩恵を受けていませんでした。&lt;/p>
&lt;h3 id="2-cloudflare-d1sqlite">2. Cloudflare D1（SQLite）&lt;/h3>
&lt;p>Cloudflare D1にデータを格納し、&lt;code>LIKE&lt;/code>で部分一致検索する方式です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">page&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">original_text&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vol_str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">canvas&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">original_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;%検索語%&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">page&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ASC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">LIMIT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">20&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">OFFSET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>集計（ファセット）はSQLの&lt;code>GROUP BY&lt;/code>で実現します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="cl">&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vol_str&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">COUNT&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">as&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">doc_count&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">texts&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">original_text&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">LIKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;%検索語%&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">GROUP&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vol_str&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">ORDER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">BY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">vol_str&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ASC&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>D1の&lt;code>batch()&lt;/code>APIで検索・カウント・集計の3クエリを同時実行できます。&lt;/p></description></item></channel></rss>