【TypeScript】langchainの公式チュートリアルを実践する-Basics#3

## はじめに
langchainの公式チュートリアル(TypeScript版)を実践しています。
今回は基礎の第3回の、「Build vector stores and retrievers」を実践した結果を記事にしています。

|  | チュートリアル |実践記事  |
|---|---|---|
|Basics#1 |[Build a Simple LLM Application with LCEL](https://js.langchain.com/docs/tutorials/llm_chain) |[完了](https://qiita.com/ameG-it/items/a419146cc58f23f7bb07)  |
|Basics#2 |[Build a Chatbot](https://js.langchain.com/docs/tutorials/chatbot/)  |[完了](https://qiita.com/ameG-it/items/534c3f7f746b6633ad2b)  |
|Basics#3 |[Build vector stores and retrievers](https://python.langchain.com/docs/tutorials/retrievers/)  |今回  |
|Basics#4 |[Build an Agent with LangGraph.js](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/)  |次回  |

### 注意
堂々と宣言した直後ですが、TypeScriptのチュートリアルに「Build vector stores and retrievers」の項は現時点で存在していません！（なんで！）
[Python版のチュートリアル「Build vector stores and retrievers」](https://python.langchain.com/docs/tutorials/retrievers/)を、TypeScriptで置き換えながら実装しています。



## 前提
毎度のことですが、公式チュートリアルの内容をこなすにあたり、自分のリソースに合わせてアレンジしている部分があります。
（例えば、環境変数は dotenv を利用する。モデルは AzureOpenAI リソースを使うなど）

langchain 中心の記事なので Azure のリソース作成といった操作には触れていません。

## 環境
OS:macOS Sonoma14.3
node:v21.6.1
埋め込みモデル:text-embedding-ada-002(AzureOpenAI) 2024-02-15-preview
ベクトルデータベース:Azure AI Search

## Document クラス
ベクトル検索の内容に入る前に、検索対象になるドキュメントを用意します。

Document クラスは、テキストとテキストに関連するメタデータを格納するクラスです。
それぞれ属性`page_content`と`metadata`に格納します。

- page_content:コンテンツを表す文字列
- metadata:任意のメタデータ(ドキュメントのソース、他のドキュメントとの関係、およびその他の情報)
### パッケージ
Documentクラスはコア機能のパッケージから取得します。
```
npm install langchain-core
```

### コーディング
個々の Document オブジェクトは、多くの場合、ドキュメントの一部を表します。
例えば 3 つの異なる`source`を持つ 5 つのドキュメントを実装してみます。


```typescript:documents.ts
import { Document } from "@langchain/core/documents";

export const documents: Document[] = [
  {
    pageContent: "犬は素晴らしい仲間であり、忠誠心と友好性で知られています。",
    metadata: { source: "哺乳類-ペット-ドキュメント" },
  },
  {
    pageContent: "猫は独立したペットで、しばしば自分のスペースを楽しみます。",
    metadata: { source: "哺乳類-ペット-ドキュメント" },
  },
  {
    pageContent:
      "金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。",
    metadata: { source: "魚-ペット-ドキュメント" },
  },
  {
    pageContent: "オウムは人間の言葉を模倣することができる知能の高い鳥です。",
    metadata: { source: "鳥-ペット-ドキュメント" },
  },
  {
    pageContent:
      "ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。",
    metadata: { source: "哺乳類-ペット-ドキュメント" },
  },
];

console.log(documents);
```

```zsh:実行結果
[
  {
    pageContent: '犬は素晴らしい仲間であり、忠誠心と友好性で知られています。',
    metadata: { source: '哺乳類-ペット-ドキュメント' }
  },
  {
    pageContent: '猫は独立したペットで、しばしば自分のスペースを楽しみます。',
    metadata: { source: '哺乳類-ペット-ドキュメント' }
  },
  {
    pageContent: '金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。',
    metadata: { source: '魚-ペット-ドキュメント' }
  },
  {
    pageContent: 'オウムは人間の言葉を模倣することができる知能の高い鳥です。',
    metadata: { source: '鳥-ペット-ドキュメント' }
  },
  {
    pageContent: 'ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。',
    metadata: { source: '哺乳類-ペット-ドキュメント' }
  }
]
```

Document オブジェクトを作成することができました。
この Document オブジェクトをつかってベクトル検索を実装していきます。

## ベクトル検索

ベクトル検索は、非構造化データを保存、検索する時の一般的な方法です。

検索対象となるテキストを、数値ベクトルに変換してベクトルデータベース(VDB)に保存します。
検索する際は、クエリを同じ次元の数値ベクトルに変換し、VDB に保存された数値ベクトルとの類似性を検証し、関連データを抽出します。

数値ベクトルに検索することで意味的な類似性を評価することができるので、
キーワードマッチによる検索(フルテキスト検索)と比較し、同義語や関連するワードでもヒットしやすいです。

テキストデータを数値ベクトルに変換することは、「埋め込み（embedding）」と呼ばれ、
埋め込みを行うことで、テキストの持つ意味や文脈を数値的に表現し、評価することができます。

### 構造化データと非構造化データ

そのままですが、構造化データとは、明確なルールに従って整理されているデータのことを指します。
例えば、データベースのテーブルなど。
これに対し、非構造化データは明確な構造を持たないデータのことを指します。
例えば、テキストファイルなど。
構造化データは特定のフォーマットため解析が容易ですが、
非構造化は自由なフォーマットをもつため解析が比較的難しいです。

### ベクトル検索の実装

#### 環境変数

VDB と埋め込みモデルにアクセスするための接続情報を記述します。
チュートリアルでは VDB に Chroma を採用していますが、今回は Azure AI Search を利用します。
埋め込みモデルも AzureOpenAI 上のデプロイを利用します。

```:.env
AZURE_AISEARCH_ENDPOINT="https://YourResourceName.search.windows.net"
AZURE_AISEARCH_KEY="xxxxxxxxxxxxxx"

AZURE_OPENAI_API_KEY="xxxxxxxxxxxxxx"
AZURE_OPENAI_API_INSTANCE_NAME="xxxxxxxxxxxx"
AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME="text-embedding-ada-002"
AZURE_OPENAI_API_VERSION="2024-02-15-preview"
```

#### パッケージ

ベクトル検索実装に必要なモジュールをインストールします。

```
npm install -S @langchain/community @langchain/core @azure/search-documents
```

チュートリアル上ありませんが、インデックスを削除するために azure のパッケージを使います。
（Azure AI SearchのFree版はインデックスが3つまでしか作成できないので、実行毎に削除する。）

```
npm install @azure/search-documents
```

また、インデックスに固有の名前を付与するために用に uuid を使います。

```
npm install uuid
```

#### コーディング

先ほどの作成した Document オブジェクトを VDB のインデックスに登録し、ベクトル検索する処理を実装していきます。

```typescript:vectorSearch.ts
import "dotenv/config";
import {
  AzureAISearchVectorStore,
  AzureAISearchQueryType,
} from "@langchain/community/vectorstores/azure_aisearch";
import { OpenAIEmbeddings } from "@langchain/openai";
import { SearchIndexClient, AzureKeyCredential } from "@azure/search-documents";
import { v4 as uuid } from "uuid";

//先ほど作成したdocumetsをインポート
import { documents } from "./documents";

(async () => {
  //インデックスに固有の名前を付与するためにuuidを利用します。
  const uid = uuid();

  //langchainからはAzure AI Searchのインデックスを削除する方法がなさそうだったので、
  //azureのライブラリを使っています。
  const client = new SearchIndexClient(
    process.env.AZURE_AISEARCH_ENDPOINT || "",
    new AzureKeyCredential(process.env.AZURE_AISEARCH_KEY || "")
  );

  // ベクターストアをインスタンス化します。
  //この時点でドキュメントオブジェクトがベクターストア内に取り込まれ
  //インデックスが作成されます。
  const store = await AzureAISearchVectorStore.fromDocuments(
    documents,
    new OpenAIEmbeddings(),
    {
      indexName: uid, //インデックスの名称
      search: {
        type: AzureAISearchQueryType.SimilarityHybrid, //検索方法
      },
    }
  );

  const result1 = await store.similaritySearch("猫");
  console.log("簡単なクエリ", result1);
  const result2 = await store.similaritySearchWithScore("猫");
  console.log("スコアも返す", result2);

  // インデックスを削除する。
  await client.deleteIndex(uid);
})();

```

検索方法は以下の 3 種類が設定可能です。
`SemanticHybrid`は Microsoft の言語理解モデルを使用して検索の関連性をある程度高める機能(セマンティックランカー)がついていますが、利用には上位のプランが必要なようです。

1. SimilarityHybrid :ハイブリッド検索（フルテキスト検索+ベクトル検索）
2. Similarity :ベクトル検索
3. SemanticHybrid :ハイブリッド検索+セマンティックランカー

`similaritySearch`で検索を実行できます。
さらに`similaritySearchWithScore`でスコアと共に出力することができます。

```zsh:実行結果
簡単なクエリ [
  Document {
    pageContent: '猫は独立したペットで、しばしば自分のスペースを楽しみます。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: '犬は素晴らしい仲間であり、忠誠心と友好性で知られています。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: 'ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: '金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。',
    metadata: { source: '魚-ペット-ドキュメント', attributes: [] },
    id: undefined
  }
]
スコアも返す [
  [
    Document {
      pageContent: '猫は独立したペットで、しばしば自分のスペースを楽しみます。',
      metadata: [Object],
      id: undefined
    },
    0.03333333507180214
  ],
  [
    Document {
      pageContent: '犬は素晴らしい仲間であり、忠誠心と友好性で知られています。',
      metadata: [Object],
      id: undefined
    },
    0.016393441706895828
  ],
  [
    Document {
      pageContent: 'ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。',
      metadata: [Object],
      id: undefined
    },
    0.016129031777381897
  ],
  [
    Document {
      pageContent: '金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。',
      metadata: [Object],
      id: undefined
    },
    0.01587301678955555
  ]
]
```

### Retrievers

VectorStore オブジェクトは Runnables のサブクラスではないため、LCEL チェーンに接続することはできません。
`asRetriever`メソッドを実行することで、Runnables のサブクラスである`Retrievers`に変換することができます。
`Retrievers`はクエリを受け取り、関連するドキュメントを返す役割をになっています。

#### コーディング

先ほどの VectorStore オブジェクトを Retriever に変換し、LCEL に統合してみます。

```typescript:retriever.ts
import "dotenv/config";
import {
  AzureAISearchVectorStore,
  AzureAISearchQueryType,
} from "@langchain/community/vectorstores/azure_aisearch";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Document } from "@langchain/core/documents";
import { SearchIndexClient, AzureKeyCredential } from "@azure/search-documents";
import { v4 as uuid } from "uuid";

//先ほど作成したdocumetsをインポート
import { documents } from "./documents";

(async () => {
  //インデックスに固有の名前を付与するためにuuidを利用します。
  const uid = uuid();

  //langchainからはAzure AI Searchのインデックスを削除する方法がなさそうだったので、
  //azureのライブラリを使っています。
  const client = new SearchIndexClient(
    process.env.AZURE_AISEARCH_ENDPOINT || "",
    new AzureKeyCredential(process.env.AZURE_AISEARCH_KEY || "")
  );

  // ベクターストアをインスタンス化します。
  //この時点でドキュメントオブジェクトがベクターストア内に取り込まれます。
  const store = await AzureAISearchVectorStore.fromDocuments(
    documents,
    new OpenAIEmbeddings(),
    {
      indexName: uid, //インデックスの名称
      search: {
        type: AzureAISearchQueryType.SimilarityHybrid, //検索方法
      },
    }
  );

  //フォーマッター
  const formatDocumentsAsString = (documents: Document[]) => {
    return documents.map((document) => document.pageContent).join("\n\n");
  };

  //Retrieverに変換
  const storeRetriever = store.asRetriever();

  //Retrieverの実行
  const result3 = await storeRetriever.invoke("猫");
  console.log(result3);

  //RetrieverをLCELに統合
  const result4 = await storeRetriever
    .pipe(formatDocumentsAsString)
    .invoke("猫");
  console.log(result4);

  await client.deleteIndex(uid);
})();

```

Retriever を実行することで、関連するドキュメントを得ることができます。
`Runnables` のサブクラスなので`pipe`を使って、他の処理と接続するといったことが可能です。

```zsh:実行結果
[
  Document {
    pageContent: '猫は独立したペットで、しばしば自分のスペースを楽しみます。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: '犬は素晴らしい仲間であり、忠誠心と友好性で知られています。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: 'ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。',
    metadata: { source: '哺乳類-ペット-ドキュメント', attributes: [] },
    id: undefined
  },
  Document {
    pageContent: '金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。',
    metadata: { source: '魚-ペット-ドキュメント', attributes: [] },
    id: undefined
  }
]
猫は独立したペットで、しばしば自分のスペースを楽しみます。

犬は素晴らしい仲間であり、忠誠心と友好性で知られています。

ウサギは社交的な動物で、たくさん跳ね回るためのスペースが必要です。

金魚は初心者に人気のあるペットで、比較的簡単なケアが必要です。
```

Retrievers を活用することで、チャットモデルやプロンプトテンプレートと接続することができますが、
それに関しては RAG のチュートリアルで行いたいと思います。

### 参考

- [Azure AI 検索](https://js.langchain.com/docs/integrations/vectorstores/azure_aisearch)：
  AzureAISearch を用いたベクトル検索の実装について参考になりました。
- [LangChain.js API Reference](https://v03.api.js.langchain.com/modules/_langchain_community.vectorstores_azure_aisearch.html)
  クラス、メソッド、データ型を確認する際、参考になりました。