概要
単一のキーワードでタイトル・サマリー・タグなど複数カラムを横断検索する Prisma クエリを実装する。OR 条件と contains / mode: "insensitive" の組み合わせで部分一致を実現し、空文字・undefined を where から除外する安全なパターンを示す。
インストール
npm install prisma @prisma/client
npx prisma init
実装
Prisma スキーマ(例)
// prisma/schema.prisma
model Sample {
id String @id @default(cuid())
title String
summary String
tags String[]
category String
difficulty String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
キーワード検索クエリ関数
// lib/db/searchSamples.ts
import { prisma } from "@/lib/prisma";
type SearchParams = {
q?: string;
category?: string;
difficulty?: string;
};
export async function searchSamples(params: SearchParams) {
const { q, category, difficulty } = params;
const keywordCondition = q?.trim()
? {
OR: [
{ title: { contains: q, mode: "insensitive" as const } },
{ summary: { contains: q, mode: "insensitive" as const } },
{ tags: { has: q } },
],
}
: undefined;
return prisma.sample.findMany({
where: {
...keywordCondition,
category: category || undefined,
difficulty: difficulty || undefined,
},
orderBy: { updatedAt: "desc" },
});
}
複数キーワード(スペース区切り AND 検索)
// lib/db/searchSamplesMultiKeyword.ts
import { prisma } from "@/lib/prisma";
function buildKeywordConditions(q: string) {
const keywords = q.trim().split(/\s+/).filter(Boolean);
if (keywords.length === 0) return undefined;
// 各キーワードについて OR を作り、それを AND(配列)で結合
return keywords.map((kw) => ({
OR: [
{ title: { contains: kw, mode: "insensitive" as const } },
{ summary: { contains: kw, mode: "insensitive" as const } },
],
}));
}
export async function searchSamplesMultiKeyword(q?: string) {
const AND = q ? buildKeywordConditions(q) : undefined;
return prisma.sample.findMany({
where: { AND },
orderBy: { updatedAt: "desc" },
});
}
Server Component での使用例
// app/samples/page.tsx(抜粋)
import { searchSamples } from "@/lib/db/searchSamples";
type Props = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
export default async function SamplesPage({ searchParams }: Props) {
const params = await searchParams;
const q = typeof params.q === "string" ? params.q : undefined;
const category = typeof params.category === "string" ? params.category : undefined;
const difficulty = typeof params.difficulty === "string" ? params.difficulty : undefined;
const samples = await searchSamples({ q, category, difficulty });
return (
<div>
<p className="text-sm text-gray-500">{samples.length} 件</p>
{/* ...カード一覧 */}
</div>
);
}
count との並行取得
// lib/db/searchSamplesWithCount.ts
import { prisma } from "@/lib/prisma";
import type { Prisma } from "@prisma/client";
export async function searchSamplesWithCount(q?: string) {
const where: Prisma.SampleWhereInput = q?.trim()
? {
OR: [
{ title: { contains: q, mode: "insensitive" } },
{ summary: { contains: q, mode: "insensitive" } },
],
}
: {};
const [total, items] = await Promise.all([
prisma.sample.count({ where }),
prisma.sample.findMany({ where, orderBy: { updatedAt: "desc" } }),
]);
return { total, items };
}
ポイント
q?.trim()が空文字やundefinedのときkeywordConditionをundefinedにすることで、...spread時にOR条件がwhereに混入しない。Prisma はundefinedフィールドをクエリ条件から除外するmode: "insensitive"で大文字・小文字を区別しない検索になる。PostgreSQL ではILIKEに変換される。SQLite では未対応のため使用不可tags: { has: q }は PostgreSQL の配列型カラムを対象とした完全一致検索。部分一致が必要な場合はhasSomeや別テーブル設計を検討する- スペース区切りの複数キーワードは
AND配列(各キーワードのORを AND 結合)で実装する。「全キーワードを含む」という自然な AND 検索になる Promise.allで件数と一覧を並列取得することで、1件だけカウントするために全件フェッチするコストを回避できるPrisma.SampleWhereInput型を明示することで、whereオブジェクトの型安全性を保証し、条件の組み立てミスをコンパイル時に検出できる