NextjsとsupabaseとOpenAI Embeddings APIでベクトル検索を実装

supabaseとOpenAIを使用するとNextjsでベクトル検索を実装できるので勉強を兼ねて開発してみました。

## 目次
[使用技術](#使用技術)
[システム構成図](#システム構成図)
[ベクトル検索とは](#ベクトル検索とは)
[supabase vector databaseとは](#supabase-vector-databaseとは)
[OpenAI Embeddings APIとは](#OpenAI-Embeddings-APIとは)
[実装](#実装)
[登壇](#登壇)

## 使用技術
- TypeScript
- Nextjs
- supabase
- OpenAI API
- Vercel（デプロイ）

## システム構成図
![ベクトル検索システム構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3721753/b8c4c9aa-01e3-5754-48d2-a7f61c457cd6.png)

## ベクトル検索とは
文章・画像などをベクトル化し、ベクトル間のコサイン類似度（どれくらい似ているか）を使用して類似性の高いコンテンツを取得する方法。
単に文字を数値に変換するだけでなく、単語や文の持つ意味も加味して数値に変換します。

## supabase vector databaseとは
DBに直接ベクトル埋め込みを保存およびクエリできるようにする PostgreSQLの拡張機能
DBのレコードの中に直接数値ベクトルを格納できます。

## OpenAI Embeddings APIとは
埋め込みはベクトルを使用して表され、各ベクトルの長さは埋め込みの次元数を表します。
テキストデータを単語やフレーズの意味から捉え、数値ベクトルに変換できます。
今回はテキストデータを1536次元のベクトルに変換します。

## 実装
### メイン機能
これらのメインの機能を繋ぎ合わせると実装することができます。
1. ベクトルデータの保存
1. 入力値をベクトル化
1. データの取得

### （準備）DBにデータを保存する（supabase vector database）

```route.ts
//ベクトル化する処理
async function generateOpenAIEmbeddings(profile: number[]) {
    const textToEmbed = Object.values(profile).join(" ");
    const response = await openai.embeddings.create({
      model: "text-embedding-ada-002",
      input: textToEmbed, //ベクトル化したい値
    });
    return response.data[0].embedding;
}
DBに保存する処理
const processedDataArray = await Promise.all(
    data.map(async (item: number[]) => {
    const embeddings = await generateOpenAIEmbeddings(item);
    const modifiedItem = { ...item, embeddings };
    const { data, error } = await supabaseClient
        .from("parson")
        .upsert([modifiedItem])
        .select();
```

### 入力値をベクトル化（OpenAI Embeddings API）
検索ボタンが押されると入力値をバックエンドに送り、バックエンドからOpenAIに送信する
返ってくる値は数値ベクトル。


```route.ts
const openAiEmbeddings = await openai.embeddings.create({
    model: "text-embedding-ada-002",
    input: query, //ベクトル化したい値
  });
const [{ embedding }]: { embedding: number[] }[] = openAiEmbeddings.data;
```
APIを使用すれば、テキストをベクトル化するだけならこのコードだけで完了！

### supabase database function（データベース関数）でデータを取得
送られてきた値をデータベース関数に送信する

```route.ts
//データベース関数を呼び出す
const { data, error } = await supabaseClient.rpc("vector_search", {
    query_embedding: embedding,     //データ
    similarity_threshold: 0.8,      //類似度
    match_count: 3,                 //最大出力数指定
  });
```
これをsupabaseのデータベース関数に設定するとNextjs側から呼び出せる。
```sql
create
or replace function vector_search (
  query_embedding vector(1536),
  similarity_threshold float,
  match_count int
) returns table (
  id int4,
  first_name text,
  last_name text,
  image text,
  occupation text,
  age int4,
  hobbies text[],
  relationship_status text,
  country_of_origin text,
  bio text,
  similarity float
) language plpgsql as $$ begin return query 
select 
parson.id, parson.first_name, 
parson.last_name,parson.image, 
parson.occupation,parson.age,
parson.hobbies,parson.relationship_status,
parson.country_of_origin,parson.bio, 
1 - (parson.embeddings <=> query_embedding) as similarity 
from parson 
where 1 - (parson.embeddings <=> query_embedding) > similarity_threshold 
order by parson.embeddings <=> query_embedding limit match_count;
end; $$;
```

## まとめ
便利なツールを組み合わせると数学の知識が無くても実装する事ができます。
**今後別のプロダクトに移植して使用することも考えています。**

## 登壇
今回開発したプロダクトについてイベントで登壇させていただきました。

https://x.com/kengo20_03/status/1836535207404671133