話題のSupabaseでサクッと全文検索機能をつくってみた！

# はじめに
こんにちは！[かほ](https://twitter.com/kaho_eng)です♪🐥
現在、本業では技術広報業務やエンジニア業務などに従事しています。
今回の記事では、**Supabase**×**Next.js**を用いたアプリ開発における全文検索機能の実装方法について説明します。

※全文検索・・・一応念のため、今回のタイトルにある「全文検索」の意味は`RDBMS`側の用語から来ています。下記で行なっている内容としては、SQLのLIKE句で`%keyword%`していることと変わらないですが、LIKE句の場合はインデックスが効かない問題があるということです。`RDBMS`のFull Text Searchを使うことで、事前にインデックスを貼って活かすことができます。今回の「全文検索」というキーワードは`RDBMS`における「全文検索」です。

下記の3記事の内容はすでに実装済です🙇‍♀️
まだ未実装の方は、ご参考までに♪🐥

https://qiita.com/KAHO23340582/items/8a7faf77222a599fb31c

https://qiita.com/kaho_eng/items/cb8d735b5b6ca1b3a6c5

https://qiita.com/kaho_eng/items/6f9ac01d77ab198881f4


# この記事の読者対象
・**Supabase**×**Next.js**で全文検索機能を含んだアプリを今後作ってみたい方
・**Next.js**を使ったリアルタイム検索機能の実装方法を知りたい方
・**Supabase**を今後使ってみたい方
・**Next.js**を使って高速でアプリを作りたい方


# 開発環境
```json:package.json
 "dependencies": {
    "@supabase/supabase-js": "^2.2.1",
    "eslint": "8.28.0",
    "eslint-config-next": "13.0.6",
    "next": "13.0.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
  },
  "devDependencies": {
    "@types/node": "^18.11.13",
    "@types/react": "18.0.26",
  }
```

# リアルタイム検索機能を実装しよう
下記が`search.tsx`の全体像です。
```jsx:search.tsx
import Head from "next/head";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import { useState, useEffect } from "react";

export default function Search() {
  const [posts, setPosts] = useState([]);
  const [keyword, setKeyword] = useState("");

  useEffect(() => {
    fetchPosts();
  }, []);

  async function fetchPosts() {
    const { data } = await supabase.from("posts").select("*");
    setPosts(data);
  }

  const search = async (value: string) => {
    if (value !== "") {
      const { data: posts, error } = await supabase
        .from("posts")
        .select()
        .textSearch("title", `${value}`);//どんな検索方法を行いたいかによって、書き方が変わります。
      if (error) throw error;
      setPosts(posts);
      return;
    }else{
      await fetchPosts();
    }
  };

  const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(e.target.value);
    search(e.target.value);
  };

  return (
    <>
      <div className={styles.container}>
        <Head>
          <title>検索機能</title>
          <link rel="icon" href="/favicon.ico" />
        </Head>
        <main className={styles.main}>
          <div>
            <h1>検索機能</h1>
            <div>
              <input
                type="text"
                value={keyword}
                className="my-8  rounded border border-black"
                placeholder="search"
                onChange={(e) => handleChange(e)}
              />
              <table>
                <thead>
                  <tr>
                    <th>投稿日</th>
                    <th>タイトル</th>
                    <th>内容</th>
                  </tr>
                </thead>
                <tbody>
                  {posts.map((post) => (
                    <tr key={post.id}>
                      <td>{post.created_at.substr(0, 10)}</td>
                      <td>{post.title}</td>
                      <td>{post.content}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </main>
        <footer className={styles.footer}></footer>
      </div>
    </>
  );
}

```
コードを上から順に説明します。
`fetchPosts()`メソッドによって、投稿の一覧表示を行っています。
こちらのメソッドの説明については、下記の記事「話題のSupabaseでサクッと投稿機能をつくってみた！」の「一覧機能を実装しよう」の段落と内容が同じですので、ぜひご参考までに🙌

https://qiita.com/kaho_eng/items/6f9ac01d77ab198881f4

また、検索フォームに入れる値の状態管理を行っています。
```jsx:search.tsx
  const [posts, setPosts] = useState([]);
  const [keyword, setKeyword] = useState("");

  useEffect(() => {
    fetchPosts();
  }, []);

  async function fetchPosts() {
    const { data } = await supabase.from("posts").select("*");
    setPosts(data);
  }
```
次にsupabase.tsを呼び出し、検索部分の実装を行います。
今回は`textSearch()`を使用し、postsテーブルのデータを検索します。
`from()`の引数にテーブル名を入れ、`textSearch()`の第1引数にテーブルの対象カラム、第2引数に検索したいキーワード等を入れます。

`textSearch()`を使用する方法は、下記のsupabase公式ドキュメントにも記載されていますので、ご興味のある方はご参照ください🙌

https://supabase.com/docs/guides/database/full-text-search

今回は、if文で検索フォームに値がある場合とない場合のロジックを分けて書いています。
検索フォームに値がある場合は、上記の検索方法を用いて、データを`setPosts(posts)`により状態管理します。
検索フォームに値がない場合は、投稿が一覧表示されるために、`fetchPosts()`メソッドを記述します。

```jsx:search.tsx
 const search = async (value: string) => {
    if (value !== "") {
      const { data: posts, error } = await supabase
        .from("posts")
        .select()
        .textSearch("title", `${value}`);//どんな検索方法を行いたいかによって、書き方が変わります。
      if (error) throw error;
      setPosts(posts);
      return;
    }else{
      await fetchPosts();
    }
  };
```
最後に、検索フォームの値の状態管理を行います。
今回は検索フォームの値を入れたと同時に、検索結果の出力も行う想定です。
そのため、検索フォームの値とsearchメソッドによって出力される検索結果の値に`onChange`イベントの設定を行います。
```jsx:search.tsx
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(e.target.value);
    search(e.target.value);
  };
```
# その他の検索方法も試してみよう！
上記で説明した検索方法は、完全一致検索ですが、その他にも様々な検索方法を行うことができます。
そのうちの数個を軽く紹介します♪🐥
* 完全一致検索
上記で説明した検索方法です。完全に一致した値のみを検索します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/a8349b08-c791-f3e0-5aa0-e889704f65c5.png"  width="300" height="186" />

```jsx:search.tsx
  const { data: posts, error } = await supabase
    .from("posts")
    .select()
    .textSearch("title", `${value}`)
```
* OR検索
OR(いずれかのキーワードを含む)検索は`|`でキーワードを連結します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/e22bb23c-0e56-61b6-6436-1cc437d5590c.png"  width="300" height="186" />

```jsx:search.tsx
 const { data: posts, error } = await supabase
    .from("posts")
    .select()
    .textSearch("title", `'bbb' | 'ccc'`);
```
* AND検索
AND(すべてのキーワードを含む)検索は`＆`でキーワードを連結します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1547587/5c6631b9-8ce8-833f-f827-22bec91037e8.png"  width="300" height="186" />

```jsx:search.tsx
   const { data: posts, error } = await supabase
    .from("posts")
    .select()
    .textSearch("title", `'bbb' & 'ccc'`);
```

# 追記
上記で説明した検索機能の実装方法の他にも、**PGroonga**と呼ばれる拡張機能（Extensions）を使った実装方法もあります！ご興味がある方は下記の公式ドキュメントをチェックしてみてください♪🐥

https://supabase.com/docs/guides/database/extensions/pgroonga

https://pgroonga.github.io/ja/tutorial/

# 最後に
これで実装はおしまいです！

今回は、**Supabase**×**Next.js**を用いたアプリ開発における全文検索機能の実装方法について説明しました。今後は主にフロントエンド周辺、コミュニティ運営、Tech PR(技術広報)の記事を中心に書いていく予定ですので、気になる方はぜひフォローをよろしくお願いします♪ではでは〜🐥


Twitterアカウント:https://twitter.com/kaho_eng

# 参考資料
https://qiita.com/KAHO23340582/items/8a7faf77222a599fb31c

https://qiita.com/kaho_eng/items/cb8d735b5b6ca1b3a6c5

https://qiita.com/kaho_eng/items/6f9ac01d77ab198881f4

https://supabase.com/docs/guides/database/extensions/pgroonga

https://pgroonga.github.io/ja/tutorial/