「あのマニュアル、どこにあったっけ?」——この一言に覚えがある方は少なくないのではないかと思います。ファイルサーバーの奥深く、あるいは誰かのデスクトップに。社内マニュアルや手順書は存在するのに、必要なときに見つからない。結果として、隣の席の人に聞く、過去のチャットを遡る、あるいは自分の記憶を頼りに作業する。こうした「探す時間」は1回あたりは小さくても、部門全体で積み重なると無視できない量になります。この記事では、AIを使った「意味で探せる検索」——ベクトル検索を、Next.js 16のアプリケーションに組み込む方法を、技術者でない方にも分かるようにお伝えします。
従来のキーワード検索とベクトル検索は何が違うのか?
社内マニュアルの検索というと、多くの方がイメージするのは「キーワードを入れて、一致するページが出てくる」という仕組みだと思います。いわゆる全文検索です。これはこれで有用なのですが、一つ大きな弱点があります。検索する側が「正しい言葉」を知っていないと、目的の文書にたどり着けないという点です。
たとえば、「出張の申請方法」を知りたいとします。しかし社内マニュアルのタイトルが「旅費精算規程」だった場合、「出張 申請」で検索しても引っかからないことがあります。書いた人と探す人の「使う言葉」が違うだけで、情報にたどり着けなくなる。これがキーワード検索の構造的な限界です。
ベクトル検索は、この問題を「意味」で乗り越えようとする仕組みです。ここで、図書館の司書に例えてみます。
優秀な司書は、来館者が「海外出張のときの経費の出し方を知りたい」と言ったら、「旅費精算規程」の棚に案内してくれます。言葉がぴったり一致していなくても、「意味が近い」ことを理解しているからです。ベクトル検索は、この「意味が近い」をコンピュータに判断させる技術です。
具体的には、文章を「数百個の数字の並び(ベクトル)」に変換します。意味が近い文章同士は、この数字の並びも近くなるように設計されています。「出張の申請方法」と「旅費精算規程」は、言葉は違っても意味が近いため、ベクトルの距離が近くなり、検索結果に出てくるようになります。
キーワード検索が得意なこと、ベクトル検索が得意なこと
誤解のないように補足すると、ベクトル検索がすべての場面でキーワード検索より優れているわけではありません。
- キーワード検索が向いている場面:型番、社員番号、固有名詞など「その言葉そのもの」で探したいとき
- ベクトル検索が向いている場面:「こういうことが知りたい」と曖昧な意図で探したいとき
実務では両方を組み合わせる「ハイブリッド検索」が現実的な構成になることが多いです。
Next.js × OpenAI Embeddings × ベクトルDBでどう構成するのか?
技術的な構成の全体像をお伝えします。非エンジニアの方は「こういうパーツで成り立っているのか」という理解で十分です。
3つの主要パーツ
- Next.js 16(App Router) —— ユーザーが検索窓に質問を入力し、結果を表示するWebアプリケーション本体
- OpenAI Embeddings API —— 文章を「数字の並び(ベクトル)」に変換するAPI。先ほどの司書の「意味を理解する力」にあたる部分
- ベクトルDB —— 変換されたベクトルを保存・検索するデータベース。Pinecone、Supabase pgvector、Qdrantなど複数の選択肢がある
データの流れ
事前準備の段階と、ユーザーが検索する段階の2つに分かれます。
事前準備(データの登録)
社内マニュアル(テキスト)
↓ チャンクに分割(後述)
↓ OpenAI Embeddings API でベクトル化
↓ ベクトルDBに保存
検索時
ユーザーの質問
↓ OpenAI Embeddings API でベクトル化
↓ ベクトルDBで「近いベクトル」を検索
↓ 該当するマニュアルの文章を取得
↓ Next.js の画面に結果を表示
この仕組みは「RAG(Retrieval-Augmented Generation)」と呼ばれるパターンの基本形です。RAGを先ほどの司書の例えに戻すと、司書が本棚から関連する資料を引っ張り出して、利用者の質問に合わせて要約してくれるようなイメージです。
Next.js 16 で組む利点
Next.js 16 の App Router では、Server Components を使ってサーバー側でベクトルDBへの問い合わせを処理できます。API キーをブラウザに露出させずに済むため、セキュリティ面でも合理的な構成です。Route Handlers を使えば、検索APIのエンドポイントを app/api/search/route.ts のようなファイル1つで定義できます。
// app/api/search/route.ts のイメージ(簡略化)
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { query } = await request.json();
// 1. ユーザーの質問をベクトル化
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: query,
});
// 2. ベクトルDBで類似検索
const results = await vectorDB.query({
vector: embedding.data[0].embedding,
topK: 5,
});
// 3. 結果を返す
return NextResponse.json({ results });
}
上のコードは概念を伝えるための簡略版です。実際のプロダクションコードでは、エラーハンドリングや認証チェックなどが加わります。
