見出し画像

meilisearchをwebアプリケーションに組みこむ

前回tinkerでsearchに成功したと思う

今回はこれをwebアプリケーションとしていこう。

氏名でもemailでも検索ができるようにしている

検索フォームの設置

これはとりあえず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 scoutsearch()メソッド叩いている。その先はscoutの設定によりmeilisearchの検索結果が帰ってくる、だろう、ここまで手順の通りやってればね。

実は「佐々木」で絞りこまれている

検索結果の文字をfrontendに渡す

今の状態だと何で検索したのかよくわからんくなっているので、検索queryをfrontに渡すというか実際に

        return Inertia::render('Users/Index', [
            'users' => $users,
            'query' => $query,
        ]);

で渡している。ただこの状態だとqueryが空の時に

Warning: `value` prop on `input` should not be 

とか言われるかもしれない

    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ボタンはコメントアウト)

揃えたがボタンとfieldの空間が無い

とりあえず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>


ボタンとinputの空間を取った

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はもう少しマトモなのだがまあ致し方ないという所でもあるのかな

meilisearchの管理UIでも同様になっているのでlaravel scoutのバグではない

chatgptに聞いてみたところ「タイポ耐性」あたりが臭いとは思うんだけど



まあこの辺は出ないより出すぎてた方がいいっていう設計になってるんだと思うし、異なるものが出ていたとしてもランキングで目的とするものが最上位に来てればまあいいかという所なのかもしれないね。



この記事が気に入ったらサポートをしてみませんか?