SupabaseとLangChainを使ってベクトル化したデータを検索する



この記事は「[Supabase Advent Calendar 2023](https://adventar.org/calendars/9077)」17日目の記事です。

タイトルの通り、今回はSupabaseのVector拡張機能を使用してベクトルを格納、その後格納データのセマンティック検索を行います。

<br>

## 使用するサービス・ライブラリ

### Supabase

今回は拡張機能である"vector"を使用し、データとそのベクトル値を格納します。

### LangChain

LangChainとは、大規模言語モデル（LLM）を扱う上で必要となってくる機能を提供するPython/TypeScriptのライブラリです。

数あるLLMの中から使用するモデルを切り替えたりできる他、Retrieval[^1] や Chains[^2] など様々な機能が備わっています。

[^1]: 回答に必要な情報を外部リソースから検索・取得すること。
[^2]: プロンプトを小分けにして入力すること。複雑な問題を解決する際に中間的な回答を生成したりすることで、精度の向上を図ることができる。

### OpenAI Embedding

OpenAIが提供するAPIで、テキストデータをベクトル化することができます。
これによりテキストを単なる文字列での比較ではなく、意味的な比較を行うことができるようになります。

<br>

## 実際にやってみる
### DBの設定
SupabaseはLangChainを手軽に開始するためのSQL Editerテンプレートを提供していますので、まずは `SQLEditer > Quick Starts > LangChain` から、必要な拡張機能、テーブル、DBファンクションの追加を行っていきます。

![スクリーンショット 2023-12-17 18.48.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634417/dc5fbeb0-e552-fd5e-5bb1-da770647e815.png)

上記のページ”LangChain”をクリックするとクエリが作成されるので、それをそのままRunします。

以上でDB側の設定は終わりです。お手軽すぎてビビりました。

<br>

### データの格納・取得処理の実装

せっかくなのでSupabaseのEdgeFunctionsで実装してみます。

```ts:shared.ts
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.38.4";
import { OpenAIEmbeddings } from "https://esm.sh/langchain@0.0.195/embeddings/openai";
import { SupabaseVectorStore } from "https://esm.sh/langchain@0.0.195/vectorstores/supabase";

const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
if (!supabaseKey) throw new Error(`Expected SUPABASE_SERVICE_ROLE_KEY`);

const url = Deno.env.get("SUPABASE_URL");
if (!url) throw new Error(`Expected env var SUPABASE_URL`);

const openAiKey = Deno.env.get("OPEN_AI_API_KEY");
if (!openAiKey) throw new Error(`Expected env var OPEN_AI_API_KEY`);

const client = createClient(url, supabaseKey);

export const vectorStore = await new SupabaseVectorStore(
  new OpenAIEmbeddings({ openAIApiKey: openAiKey }),
  {
    client,
    tableName: "documents",
    queryName: "match_documents",
  },
);


```


```ts:add_documents/index.ts
// **データ追加API**

import { vectorStore } from "../_shared/shared.ts";

Deno.serve(async (req) => {
  const { docs } = await req.json();

  try {
    // データを保存
    await vectorStore.addDocuments(
      docs,
    );

    return new Response(
      "OK",
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      },
    );
  } catch (error) {
    return new Response(
      error,
      { status: 500, headers: { "Content-Type": "application/json" } },
    );
  }
});
```

```ts:search_documents/index.ts
// **検索API**

import { vectorStore } from "../_shared/shared.ts";

Deno.serve(async (req) => {
  const { searchWord } = await req.json();

  try {
    /// データを取得
    ///
    /// 第二引数で取得するデータ数を設定できる
    const result = await vectorStore.similaritySearch(searchWord, 1);

    return new Response(
      JSON.stringify(result),
      {
        status: 200,
        headers: { "Content-Type": "application/json" },
      },
    );
  } catch (error) {
    return new Response(
      error,
      { status: 500, headers: { "Content-Type": "application/json" } },
    );
  }
});
```

## 動作確認

curlコマンドで以下のようなデータを追加した。

![スクリーンショット 2023-12-18 2.10.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634417/25bfc11b-2cd5-9840-01e6-6e50ddc446ef.png)


![スクリーンショット 2023-12-18 1.53.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634417/3ccc84c9-aea4-eb56-5d96-82b463a358d1.png)

実際に検索APIを通して単語を送ってみると以下の通りだった。
|検索ワード|結果|
|-|-|
|検索|Google|
|SNS|Meta|
|ハードウェア|Apple|
|ショッピング|Amazon|
|PC|Microsoft|

## まとめ
- かなり手軽に意味的検索を実装できた。
- LangChain、もっと色々な機能があるが全然使いこなせてないので他にも色々やりたい。（ChatModelとの連携とか）

### 反省点
- 普通に期限オーバーしてしまった。
- 初めて記事書いたけど文章書くのがやはり難しい。
見出しとか構成とか毎回手順書とかどんなふうに書けばいいのかわからなくなって苦戦してるけどアウトプット増やせば自然と慣れてくるのでしょうか。













## 参考ページ
https://supabase.com/docs/guides/ai/langchain

https://zenn.dev/umi_mori/books/prompt-engineer/viewer/intro