- HOME >
- Jamstack用語集 >
- AI Embeddings
AI Embeddings
AI 埋め込み
AI Embeddings を分かりやすく
AI Embeddings(埋め込み)は、テキストや画像を「数値のリスト」に変換する技術です。
例え話をしましょう。あなたが図書館で本を整理するとします:
従来の方法: タイトルや著者名でアルファベット順に並べる
- 「Apple」と「Apricot」は近くに置かれる(文字が似ているから)
- でも、内容は全く違うかもしれない
Embeddings を使う方法: 内容の「意味」で並べる
- 「犬の飼い方」と「猫の飼い方」は近くに置く(ペット関連)
- 「リンゴの栽培」と「ミカンの栽培」は近くに置く(果物栽培)
- 「Apple Inc.の歴史」は IT コーナーに置く
Embeddings は、テキストを数値ベクトル(例: [0.2, -0.5, 0.8, ...])に変換することで、「意味の類似度」を数学的に計算できるようにします。
Embeddings の仕組み
例えば、以下の3つの文章を考えてみましょう:
- 「犬が公園で遊んでいる」
- 「猫が庭で遊んでいる」
- 「株価が急上昇している」
Embeddings に変換すると(実際はもっと多次元):
文章1: [0.8, 0.6, -0.1] ← 動物、遊び
文章2: [0.7, 0.5, -0.2] ← 動物、遊び(1と近い!)
文章3: [-0.3, -0.8, 0.9] ← 経済、数値(1, 2と遠い)ベクトル間の距離(コサイン類似度など)を計算することで、文章1と文章2が意味的に似ていることが分かります。
Next.js × TypeScript で Embeddings を実装する
ステップ 1: OpenAI Embeddings API を使う
// lib/embeddings/openai.ts
import OpenAI from 'openai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export type EmbeddingModel = 'text-embedding-3-small' | 'text-embedding-3-large'
// テキストを埋め込みベクトルに変換
export async function createEmbedding(
text: string,
model: EmbeddingModel = 'text-embedding-3-small'
): Promise<number[]> {
try {
const response = await openai.embeddings.create({
model,
input: text,
})
return response.data[0].embedding
} catch (error) {
console.error('Embedding Error:', error)
throw new Error('埋め込みの生成に失敗しました')
}
}
// 複数のテキストを一度に埋め込み
export async function createBatchEmbeddings(
texts: string[],
model: EmbeddingModel = 'text-embedding-3-small'
): Promise<number[][]> {
try {
const response = await openai.embeddings.create({
model,
input: texts,
})
return response.data.map((item) => item.embedding)
} catch (error) {
console.error('Batch Embedding Error:', error)
throw new Error('バッチ埋め込みの生成に失敗しました')
}
}
ステップ 2: ベクトルの類似度計算
// lib/embeddings/similarity.ts
// コサイン類似度を計算(-1 〜 1、1が最も類似)
export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error('ベクトルの次元が一致しません')
}
const dotProduct = a.reduce((sum, val, i) => sum + val * b[i], 0)
const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0))
const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0))
return dotProduct / (magnitudeA * magnitudeB)
}
// ユークリッド距離を計算(0が最も類似)
export function euclideanDistance(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error('ベクトルの次元が一致しません')
}
const sum = a.reduce((acc, val, i) => acc + Math.pow(val - b[i], 2), 0)
return Math.sqrt(sum)
}
// 最も類似するアイテムを見つける
export function findMostSimilar<T>(
queryEmbedding: number[],
items: Array<{ embedding: number[]; data: T }>,
topK: number = 5
): Array<{ data: T; similarity: number }> {
const similarities = items.map((item) => ({
data: item.data,
similarity: cosineSimilarity(queryEmbedding, item.embedding),
}))
return similarities.sort((a, b) => b.similarity - a.similarity).slice(0, topK)
}
ステップ 3: セマンティック検索の実装
// app/api/semantic-search/route.ts
import { createEmbedding } from '@/lib/embeddings/openai'
import { findMostSimilar } from '@/lib/embeddings/similarity'
// サンプルデータ(実際にはデータベースから取得)
type Article = {
id: string
title: string
content: string
embedding: number[]
}
// 実際のアプリでは、これらの埋め込みはデータベースに保存
const articles: Article[] = []
export async function POST(req: Request) {
try {
const { query } = await req.json()
if (!query || typeof query !== 'string') {
return new Response('query が必要です', { status: 400 })
}
// クエリを埋め込みベクトルに変換
const queryEmbedding = await createEmbedding(query)
// 類似する記事を検索
const results = findMostSimilar(
queryEmbedding,
articles.map((article) => ({
embedding: article.embedding,
data: {
id: article.id,
title: article.title,
content: article.content,
},
})),
5 // 上位5件
)
return Response.json({
query,
results: results.map((result) => ({
...result.data,
similarity: result.similarity,
})),
})
} catch (error) {
console.error('Semantic Search Error:', error)
return new Response('検索に失敗しました', { status: 500 })
}
}
ステップ 4: 記事のインデックス化
// app/api/articles/index/route.ts
import { createBatchEmbeddings } from '@/lib/embeddings/openai'
export async function POST(req: Request) {
try {
const { articles } = await req.json()
if (!Array.isArray(articles) || articles.length === 0) {
return new Response('articles 配列が必要です', { status: 400 })
}
// すべての記事のテキストを抽出
const texts = articles.map((article: any) => {
return `${article.title}\n\n${article.content}`
})
// バッチで埋め込みを生成
const embeddings = await createBatchEmbeddings(texts)
// 記事と埋め込みを紐付け
const indexedArticles = articles.map((article: any, i: number) => ({
...article,
embedding: embeddings[i],
}))
// 実際にはデータベースに保存
// await prisma.article.createMany({ data: indexedArticles })
return Response.json({
success: true,
indexed: indexedArticles.length,
})
} catch (error) {
console.error('Indexing Error:', error)
return new Response('インデックス化に失敗しました', { status: 500 })
}
}
ステップ 5: セマンティック検索のUI
// app/semantic-search/page.tsx
'use client'
import { useState } from 'react'
type SearchResult = {
id: string
title: string
content: string
similarity: number
}
export default function SemanticSearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [isLoading, setIsLoading] = useState(false)
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault()
if (!query.trim()) return
setIsLoading(true)
try {
const response = await fetch('/api/semantic-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
})
const data = await response.json()
setResults(data.results)
} catch (error) {
console.error('Search Error:', error)
alert('検索に失敗しました')
} finally {
setIsLoading(false)
}
}
return (
<div className="container mx-auto max-w-4xl p-4">
<h1 className="text-3xl font-bold mb-6">セマンティック検索</h1>
{/* 説明 */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h2 className="font-semibold mb-2">セマンティック検索とは?</h2>
<p className="text-sm">
キーワードの一致ではなく、<strong>意味の類似度</strong>で検索します。
</p>
<p className="text-sm mt-2">
例: 「犬の飼い方」と検索すると、「ペットの世話」という記事も見つかります。
</p>
</div>
{/* 検索フォーム */}
<form onSubmit={handleSearch} className="mb-6">
<div className="flex gap-2">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="検索キーワードを入力..."
className="flex-1 border border-gray-300 rounded-lg px-4 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !query.trim()}
className="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
>
{isLoading ? '検索中...' : '検索'}
</button>
</div>
</form>
{/* 検索結果 */}
{results.length > 0 && (
<div>
<h2 className="text-xl font-semibold mb-4">検索結果({results.length}件)</h2>
<div className="space-y-4">
{results.map((result) => (
<div key={result.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-semibold">{result.title}</h3>
<span className="text-sm text-gray-500">
類似度: {(result.similarity * 100).toFixed(1)}%
</span>
</div>
<p className="text-gray-700">{result.content.substring(0, 200)}...</p>
</div>
))}
</div>
</div>
)}
</div>
)
}
Embeddings の高度な活用
マルチモーダル Embeddings
// テキストと画像を同じベクトル空間に埋め込む
import { pipeline } from '@xenova/transformers'
export async function createMultimodalEmbedding(input: string | Blob) {
const extractor = await pipeline('feature-extraction', 'Xenova/clip-vit-base-patch32')
const result = await extractor(input)
return Array.from(result.data)
}
クラスタリング
// 類似するアイテムをグループ化
export function clusterEmbeddings<T>(
items: Array<{ embedding: number[]; data: T }>,
numClusters: number
): Array<Array<T>> {
// K-Means クラスタリング(簡易実装)
// 実際には専門のライブラリを使用
const clusters: Array<Array<T>> = []
// 実装は省略(K-Meansアルゴリズム)
return clusters
}
次元削減(可視化用)
// 高次元ベクトルを2次元に削減(t-SNE、PCA など)
export function reduceDimensions(embeddings: number[][]): Array<[number, number]> {
// t-SNEやPCAで次元削減
// 実装は省略
return []
}
ベクトルデータベースとの統合
実用的なアプリケーションでは、Pinecone や Weaviate などのベクトルデータベースを使います。
// lib/vector-db/pinecone.ts
import { Pinecone } from '@pinecone-database/pinecone'
import { createEmbedding } from '@/lib/embeddings/openai'
const pinecone = new Pinecone({
apiKey: process.env.PINECONE_API_KEY!,
})
const index = pinecone.index('articles')
// ベクトルをアップロード
export async function upsertVector(id: string, text: string, metadata: any) {
const embedding = await createEmbedding(text)
await index.upsert([
{
id,
values: embedding,
metadata,
},
])
}
// ベクトル検索
export async function searchVectors(query: string, topK: number = 5) {
const queryEmbedding = await createEmbedding(query)
const results = await index.query({
vector: queryEmbedding,
topK,
includeMetadata: true,
})
return results.matches
}
まとめ
AI Embeddings は、セマンティック検索、RAG、推薦システムなど、様々な AI アプリケーションの基盤技術です。Next.js と TypeScript を組み合わせることで、型安全で高性能な検索システムを構築できます。
重要なポイント:
- 意味の理解: テキストの「意味」を数値ベクトルで表現
- 類似度計算: コサイン類似度やユークリッド距離で類似度を測定
- スケーラビリティ: ベクトルデータベースで数百万件のデータを高速検索
- マルチモーダル: テキスト、画像、音声を同じ空間に埋め込み可能
- 応用範囲の広さ: 検索、分類、クラスタリング、推薦など
Embeddings を活用することで、従来のキーワード検索では実現できなかった、意味的に関連する情報の発見が可能になります。