OpenAI の text-embedding-ada-002 でベクトル化し、コサイン類似度を計算（Angular）

## はじめに
ChatGPTを用いたアプリケーションの開発中、ChatGPTへの直列した命令数が増えるとレスポンス時間が長くなるという問題に直面しました。そこで、レスポンス時間を短縮するために、コサイン類似度を算出し、類似度が高いものだけに命令を絞り込み、ChatGPTに送る命令数を減らすというアプローチを試みました。これはその試みの備忘録となります。

以下では、入力された値をベクトル化し、そのデータと事前にベクトル化して用意していた大量のデータからそれぞれコサイン類似度を求め、コサイン類似度が高い上位10件を取得するソースコードとなっています。

## ソース
```.ts
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';

// コサイン類似度を計算するためのライブラリ
var similarity = require('compute-cosine-similarity');


// 入力された値
inputWord = "入力された値";

// 入力された値をベクトル化した値
inputWordEmbedding = "";

// 事前にベクトル化したデータ (exがベクトル値、noは紐づくユニークな番号)
embeddingData: { no: string, ex: number[] }[] = ["事前に用意していたデータ"]; 


// 類似度の高い上位10件を取得
async addUpper10(){
      //inputWordをベクトル化
      await this.callEmbedding(this.inputWord);

      // 事前に用意していたデータに対してそれぞれコサイン類似度を計算
      var similarities = this.embeddingData.map((item, index) => {
        return {
          no: item.no,
          // コサイン類似度を算出
          similarity: similarity(this.inputWordEmbedding, item.ex)
        };
      });

      // コサイン類似度でソート
      similarities.sort((a: { similarity: number; }, b: { similarity: number; }) => b.similarity - a.similarity);

      // トップ10を取得
      var top10 = similarities.slice(0, 10);

      // トップ10のnoを取得
      var top10no = top10.map((item: { no: any; }) => item.no);
  }

// embeddingAPI呼び出し
  callEmbedding(changeVectWord: string) {
    const _this = this;
    return new Promise(function (r, re) {
      _this.http.post('http://localhost:3000/api/embedding', 
      {
        text: [changeVectWord]
      })
      .subscribe({
        next(data: any) {
          _this.imputWordEmbedding = data.data[0].embedding;
          return r(data.data[0].embedding);
        },
        error(err) {
          alert('エラーが発生しました: ' + err.message);
          return re(err);
        }
      });
    })
  }
```
```server.js
const express = require('express');
const app = express();
const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");

// embedding認証情報
const embeddingEndpoint = "エンドポイント"
const embeddingAzureApiKey = "APIキー"
const embeddingDeploymentId = "デプロイ名"
const embeddingClient = new OpenAIClient(embeddingEndpoint, new AzureKeyCredential(embeddingAzureApiKey));


// embeddingAPI実行
app.post('/api/embedding', async (req, res) => {
    const input = req.body.text; // リクエストボディからテキストを取得
    const options = {
        //encoding_format: 'float' //この指定をできなかった
    };
    try {
        const embedding = await embeddingClient.getEmbeddings(embeddingDeploymentId, input, options);
        res.json(embedding); // レスポンスとして埋め込みを返す
    } catch (err) {
        console.error("The sample encountered an error:", err);
        res.status(500).send(err.message); // エラーメッセージを返す
    }
});

app.listen(3000, () => console.log('Server is listening on port 3000'));
```

## おまけ
事前データの準備中に以下の問題も発生 :cold_sweat:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3725667/b59a49f1-0776-802d-a630-845e5e15a445.png)


とのことでデータ量の多いものは、情報をキリのいい箇所で切ってベクトル化。
その後は以下の要領でベクトルを平均化した。

```.ts
// ２つのベクトルの平均値算出
  averageVectors2(v1: string | any[], v2: string | any[]) {
    if (v1.length !== v2.length) {
      throw new Error('All vectors must be the same length');
    }
    let result = [];
    for (let i = 0; i < v1.length; i++) {
      let average = (v1[i] + v2[i]) / 2;
      // 少数第9位までに丸める
      let rounded = parseFloat(average.toFixed(9));
      result[i] = rounded;
    }
    console.log("ベクトル平均化の値：" + result);
    return result;
  }
```

## 終わりに
参考になりましたら「いいね」を押していただけると励みになりますのでよろしくお願いします:sunny::camel: