RORK LABEN
DEADLINE — Google Play は2026年8月31日以降、新規アプリと既存アプリの更新の双方に対象 API レベル36(Android 16)以上を要求します。残り13日ですEXTENSION — 期限に間に合わない場合は Play Console の期限延長フォームから申請することで11月1日まで配信を継続できます。延長は自動ではなく、申請自体を期限内に行う必要がありますTARGET SDK — Rork などのビルダーが出力する Expo / React Native アプリでも targetSdkVersion は自分で確認が必要です。テンプレートが古い SDK に固定されていると生成しただけでは要件を満たしませんPOLICY — スパムと最低限の機能に関するポリシーが改定され、高品質な機能とコンテンツ体験が求められます。薄いアプリを量産する運用は正面から影響を受ける領域ですPRIVACY — 収集するデータの項目・用途・第三者への共有の有無について詳細な説明が求められます。ビルダーが自動で組み込む解析 SDK や広告 ID も申告の対象ですRORK — 従来の Rork が React Native / Expo を出力するのに対し、Rork Max は SwiftUI を生成します。無料枠から始められ、有料プランは月25ドルからですDEADLINE — Google Play は2026年8月31日以降、新規アプリと既存アプリの更新の双方に対象 API レベル36(Android 16)以上を要求します。残り13日ですEXTENSION — 期限に間に合わない場合は Play Console の期限延長フォームから申請することで11月1日まで配信を継続できます。延長は自動ではなく、申請自体を期限内に行う必要がありますTARGET SDK — Rork などのビルダーが出力する Expo / React Native アプリでも targetSdkVersion は自分で確認が必要です。テンプレートが古い SDK に固定されていると生成しただけでは要件を満たしませんPOLICY — スパムと最低限の機能に関するポリシーが改定され、高品質な機能とコンテンツ体験が求められます。薄いアプリを量産する運用は正面から影響を受ける領域ですPRIVACY — 収集するデータの項目・用途・第三者への共有の有無について詳細な説明が求められます。ビルダーが自動で組み込む解析 SDK や広告 ID も申告の対象ですRORK — 従来の Rork が React Native / Expo を出力するのに対し、Rork Max は SwiftUI を生成します。無料枠から始められ、有料プランは月25ドルからです
記事一覧/開発ツール
開発ツール/2026-08-02上級

2文字の検索語が静かに0件になる — Expo アプリの日本語検索インデックスを実測で選び直す

SQLite FTS5 の trigram トークナイザは2文字以下の日本語クエリを例外なしで0件にします。2万件のカタログで線形走査・bigram 転置索引・FTS5 を実測し、索引を足すべき閾値と起動コストの実数を出した記録です。

Rork535Expo171SQLite4FTS5日本語検索パフォーマンス33

プレミアム記事

朝、家族から「猫の壁紙が1枚も出てこない」と言われました。

自分の端末で確かめると、たしかに「猫」と打った瞬間に結果が空になります。カタログには猫のタイトルが600枚以上あります。エラーログには何も残っていません。クラッシュもしていません。ただ、0件。

原因は、その前の週に私自身が入れた検索インデックスでした。件数が増えてきたので「そろそろ線形走査は限界だろう」と考え、SQLite の FTS5 に載せ替えたところだったのです。速くはなりました。ただ、日本語で最も打たれやすい長さのクエリが、まるごと落ちていました。

この記事は、その後で腰を据えて計測をやり直した記録です。線形走査・bigram 転置索引・FTS5 trigram の3つを同じデータで走らせ、どの規模でどれを選ぶべきかを実数で決め直しました。結論から言えば、私は索引を捨てて線形走査に戻しています。

計測の土台 — 再現可能なカタログを作る

実データをそのまま貼ると再現できないので、壁紙カタログを模した擬似データを決定論的に生成しました。タイトルは「修飾+被写体+接尾」、タグは3つ。1件あたり正規化後およそ13〜20文字という、実物に近い分布になっています。

// gen.mjs — 線形合同法で決定論的に生成(同じ seed なら同じカタログ)
let seed = 20260802;
const rnd = () => (seed = (seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff;
const pick = (a) => a[Math.floor(rnd() * a.length)];
 
const subj = ["夜景", "桜", "富士山", "海", "星空", "猫", "森", "雪原", "花火", "滝", "紅葉", "銀河"];
const mod  = ["静かな", "淡い", "鮮やかな", "霧の", "真夜中の", "冬の", "黄昏の", "雨上がりの"];
const suf  = ["の風景", "のシルエット", "のグラデーション", "のパノラマ", "", ""];
const tagp = ["ミニマル", "ダーク", "パステル", "モノクロ", "和風", "自然", "高解像度", "縦向き"];
 
export function makeCatalog(n) {
  const out = [];
  for (let i = 0; i < n; i++) {
    out.push({
      id: "w" + i,
      title: pick(mod) + pick(subj) + pick(suf),
      tags: [pick(tagp), pick(tagp), pick(tagp)],
    });
  }
  return out;
}

正規化は3層です。NFKC で全角半角を潰し、小文字化し、カタカナをひらがなへ寄せ、空白を落とします。表記ゆれの吸収そのものは以前まとめたとおりですが、今回はこの関数の実行コストが主役になります。

const norm = (s) =>
  s
    .normalize("NFKC")
    .toLowerCase()
    .replace(/[ァ-ヶ]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0x60))
    .replace(/\s+/g, "");

計測環境は Linux コンテナ上の Node 22.22 と Python 3.10(SQLite 3.37.2)です。手元の iPhone ではありません。以下の数値は絶対値ではなく比率と桁を読むためのものとして扱ってください。実機は一般に数倍遅く、私の手元の検証では3〜5倍を見込むと大きく外れませんでした。

素朴な線形走査は、思っていたよりずっと遠くまで走る

まず「何もしない」実装を測ります。読み込み時に正規化済みのキー配列を作り、クエリごとに indexOf で舐めるだけの実装です。

// 正規化済みキーを平坦な文字列配列で保持する
const keys = new Array(items.length);
for (let i = 0; i < items.length; i++) {
  keys[i] = norm(items[i].title + items[i].tags.join(""));
}
 
function search(q) {
  const nq = norm(q);
  const hits = [];
  for (let i = 0; i < keys.length; i++) {
    if (keys[i].indexOf(nq) >= 0) hits.push(i);
  }
  return hits;
}

8種類のクエリを各20回、中央値を取った結果です。比較のため { id, k } のオブジェクト配列に対する filter も並べました。

件数平坦な文字列配列オブジェクト配列 + filterキー保持量(概算)
20,0000.80 ms1.11 ms1.4 MB
50,0001.99 ms2.79 ms3.6 MB
100,0004.05 ms6.48 ms7.2 MB
200,0008.50 ms12.43 ms14.4 MB
500,00020.21 ms29.65 ms35.9 MB

60fps のフレーム予算は16.7ms です。この環境では20万件まで1フレームに収まりました。実機の3〜5倍という補正を掛けても、数万件規模なら線形走査で十分に戦えます。

同じデータ・同じ判定でありながら、平坦な配列とオブジェクト配列で1.4倍の差が出ている点は覚えておく価値があります。プロパティアクセスが1段挟まるだけの違いですが、走査回数が件数分あるので効いてきます。索引を設計する前に、まずここを平坦にするほうが費用対効果は高いです。

私が最初に索引へ手を伸ばした判断は、この表を作る前のものでした。「2万件は多い」という体感だけで動いていたわけです。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
FTS5 の trigram は2文字以下のクエリで例外を投げず0件を返す。実測では「猫」606件・「夜景」816件・「桜」1,263件が静かに消えた
20,000件の線形走査は中央値0.80ms。索引を足す判断は「件数がフレーム予算を割ったとき」に限ってよい
本当のボトルネックは検索ではなく起動時の正規化パス。20,000件で39.5ms、事前正規化と改行連結の配布で1.1msまで縮んだ
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-07-10
React Compiler を Expo に入れて、手書き memo を 41 箇所消しました
Rork が生成した React Native 画面に React Compiler を導入し、再レンダリング回数を Profiler で実測しました。手書き memo/useCallback の削除判断、効かない箇所の見分け方、CI での回帰検知までをまとめています。
開発ツール2026-04-12
Rorkアプリが重い・遅い・フリーズする原因と解決法:パフォーマンストラブルシューティングガイド
Rorkで作ったアプリが重い、画面切替が遅い、フリーズするときの診断ガイドです。スクロールのカクつき、遷移のもたつき、起動の遅さ、メモリリーク、画像のチラつきという症状別に原因を特定し、Expo DevToolsでの可視化も含めて解決策を示します。
開発ツール2026-08-14
ライセンス表示に載せる依存は、lockfile の dev/prod では決まりません
AI ビルダーが書き出したプロジェクトの依存を実際に分類し、ライセンス表示に載せる範囲をどう決めたかの記録です。copyleft の検出結果と配布物の境界の食い違いを、本番プロジェクトの実データで確かめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →