Kento
Next.js Prisma TypeScriptを使った検索機能
2026年09月20日
見出しはありません
要約を生成中...
現在個人開発(簡易的なアプリ)で、Next Prisma TypeScriptで開発中でして、 今回は検索機能についてメモを残したく書いています。
エラーログナレッジ管理アプリで、エラーログの一覧ページに、検索機能を追加。 自分のみたい内容や技術をキーワード検索して、該当したエラーログのみ絞って表示させたいから。
検索InputのUIコンポーネントを作成。
検索キーワードをクエリパラメータに変換して、検索条件付きのパスをAPIへ返す
検索機能のような文字入力が連続で発生するイベントを、デバウンスで一定時間遅延させて、操作を終えた後に実行させる。(useEffect)
API側にURLのパラメータから検索キーワードを受け取り、検索条件WHERE句でカラムごとに、部分一致で検索するようにする。
import { prisma } from "@/lib/prisma";
import { NextRequest, NextResponse } from "next/server";
import { ErrorLog } from "@/generated/prisma";
import { getCurrentUser } from "@/lib/auth";
export const GET = async (req: NextRequest) => {
try {
const userId = await getCurrentUser();
const keyword = req.nextUrl.searchParams.get("keyword")?.trim() ?? "";
const searchKeyword = `%${keyword}%`;
const errorLogListData = await prisma.$queryRaw<ErrorLog[]>`SELECT el."id",
el."title",
el."status",
el."created_at",
el."updated_at",
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'id', t."id",
'tag_name', t."tag_name"
)
)
FROM "ErrorLogTag" elt
INNER JOIN "Tag" t
ON t."id" = elt."tagId"
WHERE elt."errorLogId" = el."id"
),
'[]'::jsonb
) AS "tags"
FROM "ErrorLog" el
WHERE el."userId" = ${userId}
AND (
${keyword} = ''
OR el."title" ILIKE ${searchKeyword}
OR el."error_message" ILIKE ${searchKeyword}
OR el."cause" ILIKE ${searchKeyword}
OR el."solution" ILIKE ${searchKeyword}
OR el."framework" ILIKE ${searchKeyword}
OR el."framework_version" ILIKE ${searchKeyword}
OR el."os" ILIKE ${searchKeyword}
OR EXISTS (
SELECT 1
FROM "ErrorLogTag" elt
INNER JOIN "Tag" t
ON t."id" = elt."tagId"
WHERE elt."errorLogId" = el."id"
AND t."tag_name" ILIKE ${searchKeyword}
)
)
ORDER BY el."updated_at" DESC`;
type CountResult = {
total: number;
};
// エラーログ一覧全体の件数
const [countResult] = await prisma.$queryRaw<CountResult[]>
`SELECT COUNT(*)::int AS "total"
FROM "ErrorLog" el
WHERE el."useId" = ${userId}`;
const total = countResult.total;
const totalPage = Math.ceil(total / limit);
const response = {
status: "OK",
errorlog: errorLogListData,
};
return NextResponse.json(response, { status: 200 });
} catch (error) {
console.error(error);
if (error instanceof Error && error.message === "UNAUTHORIZED") {
return NextResponse.json(
{ message: "認証が必要です。" },
{ status: 401 },
);
}
const errorMessage =
error instanceof Error ? error.message : "サーバーエラー";
return NextResponse.json({ message: errorMessage }, { status: 500 });
}
};
API側で受け取ったkeywordを使って、WHERE句で複数のカラムを横断的に検索するようにします。
API側でクエリパラメータで検索キーワードを受け取れるようにする。
const keyword = req.nextUrl.searchParams.get("keyword") ?? "";
const searchKeyword = `%${keyword}%`;
まずは、ログイン本人のエラーログに絞る。
WHERE el."userId" = ${userId}
その上で複数の条件で検索にヒットするログを取得するようにする。 ORでどれか一つでも検索に引っ掛かるようにしています。
AND (
${keyword} = ''
OR el."title" ILIKE ${searchKeyword}
OR el."error_message" ILIKE ${searchKeyword}
...
)
リクエストで、/api/admin/errorlogs?keyword=Prismaの場合は、以下のようになる。 ILIKEは、PostgreSQLでは大文字・小文字を区別しない部分一致の検索にする。
OR el."title" ILIKE Prisma
OR el."error_message" ILIKE Prisma
OR el."cause" ILIKE Prisma
検索キーワードが空なら、ログインユーザー本人のエラーログ全てを取得する。
${keyword} = ''
EXISTSは、キーワード検索に入力されたタグが、そのエラーログに1件でも紐づいているかを確認をしている。
OR EXISTS (
SELECT 1
FROM "ErrorLogTag" elt
INNER JOIN "Tag" t
ON t."id" = elt."tagId"
WHERE elt."errorLogId" = el."id"
AND t."tag_name" ILIKE ${searchKeyword}
)
今回は、検索キーワード有無によって、APIへ送るURLを切り替える関数を実装してみました。
// 検索キーワード有無によって、APIへ送るURLを切り替えている。
export const buildErrorLogListPath = (keyword: string): string => {
const trimmedKeyword = keyword.trim();
// キーワードが空白の場合は、一覧取得APIへリクエスト
if(!trimmedKeyword) return "/public/errorlog-list";
// キーワードある場合は、キーワードをクエリパラメータへ変換し、検索条件付きのパスを返す
const searchParams = new URLSearchParams({
keyword: trimmedKeyword,
});
return `/public/errorlog-list?${searchParams.toString()}`;
};
trimメソッドで検索キーワードが空欄の場合は、エラーログ一覧APIをリクエストする。
const trimmedKeyword = keyword.trim();
// キーワードが空白の場合は、一覧取得APIへリクエスト
if(!trimmedKeyword) return "/public/errorlog-list";
キーワード入力がある場合は、キーワードをクエリパラメータに変換して、検索条件付きのパスをAPIへリクエストする。
new URLSearchParamsでクエリパラメータを作成。
trimmedKeyword が"React"の場合、searchParamsはURL用のパラメータ情報を保持する。
つまり、keyword=入力値の形で正しいクエリパラメータを作るために、「パラメータ名」と「値」で組み合わせています。
なのでオブジェクトでkeyword: trimmedKeywordにしています。
// キーワードある場合は、キーワードをクエリパラメータへ変換し、検索条件付きのパスを返す
const searchParams = new URLSearchParams({
keyword: trimmedKeyword,
});
return `/public/errorlog-list?${searchParams.toString()}`;
toStringでパラメータを文字列に変換して返す。
return `/public/errorlog-list?${searchParams.toString()}`;
今回は、検索で入力するたびにAPI・DBへのアクセスが発生する構成なので、useEffect + setTimeoutによるデバウンスで実装しました。
デバウンスとは、連続して発生するイベント(文字入力やスクロール)の処理を一定時間遅延させてから実行する これにより、連続入力の不要なAPIリクエストやDBアクセス検索を削減できます。
import { useEffect, useState } from "react";
export const useDebounce = <T>(value: T, delay = 300): T => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// delayミリ秒後に最新の値を反映する
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// delayが経過する前に値が変わったら、前回の更新予約を消す。
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
};
検索フォームの入力されたキーワードをjotaiライブラリを活用して取得。
const keyword = useAtomValue(searchKeywordAtom);
デバウンスで、入力されてから300ミリ秒後に検索で使うキーワードを更新。 入力されるたびに、APIリクエストとDBアクセスの発生を防ぐ。
const debouncedKeyword = useDebounce(keyword, 300);
デバウンス後のキーワードから、APIのリクエスト先の生成。
const searchPath = buildErrorLogListPath(debouncedKeyword);
生成したリクエストパス(検索条件付きリクエストパスorエラーログリストのパス)を使って、TanStack Query経由でエラーログ一覧をfetch。
useGetFetcher<ErrorLogListResponse>(
["errorLogs", debouncedKeyword],
searchPath,
);
"use client";
// 検査文字の入力・状態管理
import { atom, useAtom } from "jotai";
export const searchKeywordAtom = atom("");
export const SearchInput = () => {
const [keyword, setKeyword] = useAtom(searchKeywordAtom);
return (
<input
type="search"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="エラーログを検索"
className="border p-2 rounded-md"
/>
);
};今回は、検索機能についての実装メモとして残しました。
参考になれば幸いです。
要約
コメント
まだコメントはありません。