meilisearchをwebアプリケーションに組みこむ
前回tinkerでsearchに成功したと思う
今回はこれをwebアプリケーションとしていこう。
検索フォームの設置
これはとりあえずlaravel breeze提供のコンポーネントを使って置いてみる。
<div className="mb-4">
<TextInput
placeholder={t('Search users...')}
/>
<PrimaryButton>
{("Search")}
</PrimaryButton>
</div>
まあ、なんか検索パーツがあるなって感じでほとんどデザインされていないが。。
でこれを押したときにgetで取りにいけばいいっちゃいいんだけどformにしちゃってもいいと思う。その場合enterの処理がformに委ねられるので少し楽かも。
import { Head, useForm } from '@inertiajs/react';
でimportして
const {
data, setData, get, processing, errors, reset,
} = useForm({
q: '',
});
としておく。そしたら
<div className="mb-4">
<form onSubmit={submit}>
<TextInput
placeholder={t('Search users...')}
/>
<PrimaryButton>
{("Search")}
</PrimaryButton>
</form>
</div>
とし、onSubmitで定義したsubmit関数を書く。まあsubmitって名前じゃなくてもっとちゃんとした名前にしてもいい。
ここではadmin.users.index というrouteを想定しているが、いずれにせよusers.indexでもいいけどUserコントローラのindexへの動線を書く必要がある。
const submit = (e) => {
e.preventDefault();
get(route('admin.users.index'));
};
ここで、TextInputにonChangeイベントを仕掛けuseFormのプロパティーに値を詰めるようにする。
<div className="mb-4">
<form onSubmit={submit}>
<TextInput
onChange={(e) => setData('q', e.target.value)}
placeholder={t('Search users...')}>
// 略
UserController(バックエンド)
そうすると、http://server/users/index?q=<もげもげ>みたいな形状でGETされるので、これを取得する。簡単なコードを提示するなら以下のようなものになるだろう
public function index(Request $request): Response
{
$query = $request->query('q', '');
if (!empty($query)) {
$users = User::search($query)->get();
} else {
$users = User::all();
}
return Inertia::render('Users/Index', [
'users' => $users,
'query' => $query,
]);
}
このように、search()メソッドでlaravel scoutのsearch()メソッド叩いている。その先はscoutの設定によりmeilisearchの検索結果が帰ってくる、だろう、ここまで手順の通りやってればね。
検索結果の文字をfrontendに渡す
今の状態だと何で検索したのかよくわからんくなっているので、検索queryをfrontに渡すというか実際に
return Inertia::render('Users/Index', [
'users' => $users,
'query' => $query,
]);
で渡している。ただこの状態だとqueryが空の時に
とか言われるかもしれない
public function index(Request $request): Response
{
$query = $request->input('q', '');
if (!empty($query)) {
$users = User::search($query)->get();
} else {
$users = User::all();
}
return Inertia::render('Users/Index', [
'users' => $users,
'query' => $query ?? '', // <-これ
]);
}
こうやっといた方が安全かも
resources/js/Pages/Users/Index.jsx
export default function UserIndex({ auth, users, query }) {
const { t, currentLocale } = useLaravelReactI18n();
const {
data, setData, get, processing, errors, reset,
} = useForm({
q: query,
});
同様にText Inputも改良し、これを受け取れるようにしよう
<TextInput
onChange={(e) => setData('q', e.target.value)}
value={data.q}
placeholder={t('Search users...')}>
概ね期待通りに動作してきたはずだ。
検索のクリアーボタンが欲しい
ここではreact-iconsの VscChromeClose を使ってみるけどまあパーツの選定はおまかせします
import {
VscVerifiedFilled,
VscUnverified,
VscChromeClose, // これ
} from 'react-icons/vsc';
<div className="mb-4">
<form onSubmit={submit}>
<TextInput
onChange={(e) => setData('q', e.target.value)}
value={data.q}
placeholder={t('Search users...')}
/>
<VscChromeClose />
<PrimaryButton>
{("Search")}
</PrimaryButton>
</form>
</div>
デザインは後でやっつけるとして、このXマークを押したらformをclearしたい。これは一応ボタン形式にしておく
<div className="mb-4">
<form onSubmit={submit}>
<TextInput
onChange={(e) => setData('q', e.target.value)}
value={data.q}
placeholder={t('Search users...')}
className=""
/>
<button type="button" onClick={clearSearch}>
<VscChromeClose>
{t("Clear")}
</VscChromeClose>
</button>
<PrimaryButton>
{t("Search")}
</PrimaryButton>
</form>
</div>
でclearSearchを実装する
const clearSearch = (e) => {
e.preventDefault();
router.get(route('admin.users.index', { q: '' }));
};
こっちはformを送信するんじゃなくて、router.getで空のqを入れている。これは
import { Head, useForm, router } from '@inertiajs/react';
これでimportしておくとよいかも。
機能は大体揃ったのでデザインを何とかしていく
まず、伸ばす
<TextInput
onChange={(e) => setData('q', e.target.value)}
value={data.q}
placeholder={t('Search users...')}
className="w-full">
ボタンとかが改行されてるのでflexboxにする
<form className="flex" onSubmit={submit}>
<div className="flex-grow relative flex">
<TextInput
placeholder={t('Search users...')}
value={data.q}
onChange={(e) => setData('q', e.target.value)}
className="w-full"
/>
</div>
{/*
<button type="button" onClick={clearSearch}>
<VscChromeClose>
{t("Clear")}
</VscChromeClose>
</button>
*/}
<PrimaryButton>
{("Search")}
</PrimaryButton>
</form>
(一端、clearボタンはコメントアウト)
とりあえずcloseボタンはおいといて、こんな感じで、あと微調整をかける
<form className="flex" onSubmit={submit}>
<div className="flex-grow relative flex">
<TextInput
placeholder={t('Search users...')}
value={data.q}
onChange={(e) => setData('q', e.target.value)}
className="w-full"
/>
</div>
{/*
<button type="button" onClick={clearSearch}>
<VscChromeClose>
{t("Clear")}
</VscChromeClose>
</button>
*/}
<PrimaryButton className="ml-2 px-4 py-2">
{("Search")}
</PrimaryButton>
</form>
formやら何やらもちゃんと動作するようにしてあるから、ここまではokのはず
ここにcloseアイコンを埋めていく
<div className="mb-4">
<form className="flex" onSubmit={submit}>
<div className="flex-grow relative flex">
<TextInput
value={data.q}
onChange={(e) => setData('q', e.target.value)}
type="text"
className="w-full"
placeholder={t('Search users...')}
/>
<button type="button" onClick={clearSearch}>
<VscChromeClose>
{t("Clear")}
</VscChromeClose>
</button>
</div>
<PrimaryButton className="ml-2 px-4 py-2">
{t("Search")}
</PrimaryButton>
</form>
</div>
これをinputフォームの中にメリこませていく
<div className="mb-4">
<form className="flex" onSubmit={submit}>
<div className="flex-grow relative flex">
<TextInput
value={data.q}
onChange={(e) => setData('q', e.target.value)}
type="text"
className="w-full"
placeholder={t('Search users...')}
/>
<button
type="button"
className="absolute right-0 top-1/2 transform -translate-y-1/2 text-gray-500 cursor-pointer mr-3"
onClick={clearSearch}
>
<VscChromeClose>
{t("Clear")}
</VscChromeClose>
</button>
</div>
<PrimaryButton className="ml-2 px-4 py-2">
{t("Search")}
</PrimaryButton>
</form>
</div>
まあ、あとは英語のところを日本語にすれば冒頭のようなデモになるはずだ。
課題
このようにバックエンドの検索エンジンにより、結構過剰にヒットする事が(Meilisearchの場合は特に)まあまあ、ある。algoliaはもう少しマトモなのだがまあ致し方ないという所でもあるのかな
chatgptに聞いてみたところ「タイポ耐性」あたりが臭いとは思うんだけど
まあこの辺は出ないより出すぎてた方がいいっていう設計になってるんだと思うし、異なるものが出ていたとしてもランキングで目的とするものが最上位に来てればまあいいかという所なのかもしれないね。
この記事が気に入ったらサポートをしてみませんか?