個人開発で運用している壁紙アプリに検索ボックスを付けたときのことです。App Store に出す前に家族に触ってもらったところ、「さくらで検索しても何も出てこない」と言われました。データ側のタイトルは「サクラ」。人間にとっては同じ言葉でも、プログラムにとっては別の文字列です。
Rork に「検索機能を付けてください」と頼むと、検索そのものは数分で動き始めます。ただ、この表記ゆれの問題は、こちらから仕様として伝えない限り、まず解決されません。実際にどこでつまずき、どう直したのかを、動くコードと Rork への指示文例つきで残しておきます。
Rork が最初に生成する検索は、どこまで動くのか
「検索機能を追加して」とだけ伝えたとき、Rork が生成するのはおおむね次のような実装です。
const [query, setQuery] = useState("");
const results = items.filter((item) =>
item.title.toLowerCase().includes(query.toLowerCase())
);部分一致と、英字の大文字・小文字の吸収までは最初から入っています。英語圏のアプリなら、これで十分実用になります。AI は「検索」という言葉から英語圏の標準的な実装を導くので、ここまでは速いのです。
一方で、日本語のデータに対しては次の取りこぼしが残ります。
- 「さくら」と「サクラ」(ひらがなとカタカナ)
- 「ABC」と「ABC」(半角と全角の英数字)
- 「サクラ」と「サクラ」(半角カナと全角カナ)
- 「夜 景」と「夜景」(間に挟まった空白)
私のアプリの場合、壁紙のタイトルはカタカナ、タグはひらがな、というように人間が入力した時点で表記が揺れていました。蓄積済みのデータを統一し直すより、検索時に吸収するほうが現実的です。
正規化関数を1つ用意して「同じ言葉」に潰す
方針はシンプルで、比較の直前にクエリとデータの両方を同じルールで変換するだけです。この変換を正規化と呼びます。次の関数をそのまま使えます。
// utils/searchNormalize.ts
export function normalizeForSearch(input: string): string {
return input
.normalize("NFKC") // 全角英数→半角、半角カナ→全角カナに統一
.toLowerCase() // 英字を小文字に統一
.replace(/[ぁ-ゖ]/g, (ch) =>
// ひらがな→カタカナ(コード位置が 0x60 ずれているだけ)
String.fromCharCode(ch.charCodeAt(0) + 0x60)
)
.replace(/[\s ]+/g, ""); // 半角・全角の空白を除去
}ポイントは1行目の normalize("NFKC") です。Unicode には「見た目が似た別の文字」を統一形へ変換する仕組みが標準で用意されており、全角の「ABC」は「ABC」に、半角の「サクラ」は「サクラ」になります。自前で変換表を持つ必要はありません。
ひらがなとカタカナの変換だけは NFKC では行われないため、文字コードのオフセットを使って自分で寄せます。ひらがなとカタカナは Unicode 上でちょうど 0x60 ずれて並んでいるので、置換は1行で済みます。
この関数を通すと、次の3つはすべて同じ文字列「サクラ」になります。
normalizeForSearch("さくら"); // "サクラ"
normalizeForSearch("サクラ"); // "サクラ"
normalizeForSearch("サ クラ"); // "サクラ"正規化は「入力のたび」ではなく「読み込み時に1回」
正規化関数ができたら、filter の中で両方を変換すれば動きます。ただし、そのまま書くと「1文字入力するたびに、全アイテムを正規化し直す」コードになります。
私のアプリは約 1,200 件のデータでしたが、古めの Android 実機では入力がワンテンポ遅れる感触がありました。原因は明らかで、キー入力のたびに 1,200 回の正規化が走っていたためです。データ側の正規化は読み込み時に1回だけ済ませ、検索キーとして持たせておきます。
// hooks/useWallpaperSearch.ts
import { useMemo, useState, useDeferredValue } from "react";
import { normalizeForSearch } from "../utils/searchNormalize";
type Wallpaper = { id: string; title: string; tags: string[] };
export function useWallpaperSearch(items: Wallpaper[]) {
const [query, setQuery] = useState("");
// 入力欄の反応を最優先し、結果リストの描画は少し後回しにする
const deferredQuery = useDeferredValue(query);
// データ側の正規化は読み込み時に1回だけ
const indexed = useMemo(
() =>
items.map((item) => ({
item,
key: normalizeForSearch(item.title + " " + item.tags.join(" ")),
})),
[items]
);
const results = useMemo(() => {
const q = normalizeForSearch(deferredQuery);
if (!q) return items;
return indexed
.filter((entry) => entry.key.includes(q))
.map((entry) => entry.item);
}, [indexed, deferredQuery, items]);
return { query, setQuery, results };
}useDeferredValue は React 18 の標準フックで、追加ライブラリなしに描画の優先度付けをしてくれます。数千件程度のクライアント内検索なら、事前インデックス化とこのフックの組み合わせで十分滑らかに動く、というのが私の実感です。逆に、この規模のために検索サーバーや外部サービスを導入するのは過剰だと考えています。
結果ゼロの画面を「行き止まり」にしない
正規化を入れても、結果が0件になることはあります。そのとき空白の画面だけが残ると、ユーザーには故障と区別がつきません。FlatList の ListEmptyComponent で、見つからなかった事実と、次にできる操作の2つだけは返すようにしています。
<FlatList
data={results}
keyExtractor={(w) => w.id}
renderItem={({ item }) => <WallpaperCard wallpaper={item} />}
ListEmptyComponent={
isLoading ? null : (
<View style={{ padding: 32, alignItems: "center" }}>
<Text>「{query}」に合う壁紙が見つかりませんでした</Text>
<Pressable onPress={() => setQuery("")}>
<Text style={{ color: "#6366f1", marginTop: 12 }}>
検索条件をクリアして全件に戻る
</Text>
</Pressable>
</View>
)
}
/>1つ注意があります。ListEmptyComponent はデータがまだ届いていない読み込み中にも表示されるため、isLoading 中は出さないように分岐しないと、起動直後に「見つかりませんでした」が一瞬チラつきます。リストが空に見える問題の切り分けは FlatList が空白で何も映らない—Rork アプリのリスト崩れを切り分ける に、読み込み中の見せ方は Rork アプリでスケルトンスクリーンを実装する — 読み込み中の体感速度を改善する実践パターン にまとめています。
Rork への指示文 — 仕様を日本語で言語化して渡す
ここまでの内容は、最初から Rork に伝えておけば生成段階で組み込んでもらえます。私が実際に使っている指示文をそのまま載せます。
検索機能を追加してください。要件は次の通りです。
1. タイトルとタグを対象に部分一致で絞り込む
2. 比較の前に、クエリとデータの両方を同じルールで正規化する
- Unicode NFKC 正規化(全角英数→半角、半角カナ→全角)
- 英字は小文字に統一
- ひらがなはカタカナに変換してから比較
- 半角・全角の空白は無視
3. データ側の正規化は読み込み時に1回だけ行い、
入力のたびに全件を正規化しない
4. 結果が0件のときは、検索語をクリアするボタンを表示する
5. 読み込み中は0件表示を出さない「検索機能を付けて」と一言で頼んだ場合と比べると、生成されるコードの差は歴然です。ノーコードツールを使っていても、表記ゆれのような自国語特有の要件を仕様として言語化する部分は、依然として人間の仕事だと私は考えています。むしろここが、個人開発者がアプリの使い心地で差をつけられる場所ではないでしょうか。
もう1つ、Rork が生成した検索欄を仕上げる際に私自身よく手を入れるのが、TextInput の属性です。日本語入力では OS の自動大文字化や自動修正が誤作動の元になりやすいため、autoCapitalize="none" と autoCorrect={false} を指定し、確定操作はキーボードの検索ボタンに任せる returnKeyType="search" を明示しています。生成直後のコードにはこの3つが入っていないことが多く、地味ですが入力の気持ちよさが変わる部分です。
なお、検索欄はキーボードが常に出入りする UI です。入力欄がキーボードに隠れる問題に当たったら、Rorkアプリでキーボードが入力フィールドを隠す問題の完全対処ガイド が参考になるはずです。
次の一歩 — 自分のアプリで「取りこぼし」を1つ探す
手元のアプリに検索欄があるなら、「ひらがなでカタカナのデータを引けるか」「全角英数で英字のデータを引けるか」の2つを試してみてください。1つでも取りこぼしがあれば、normalizeForSearch を入れる価値があります。データ側が綺麗に統一されているアプリほど後回しになりがちですが、ユーザーの入力までは統一できない——というのが、検索を付けてみて私が学んだことでした。
この10行ほどの正規化関数が、どなたかのアプリの検索ボックスをすこし賢くするきっかけになれば嬉しいです。