【ガチ手軽で高コスパ】FirebaseFirestoreのベクトル対応が話題になってない割に破壊的！text-embeddingなどと絡めて解説します

あなたはFirestoreの機能『ベクトル検索』、もう試しましたか？
AI時代の基盤として進化したFirestoreを活用することで、誰でも高性能な検索・レコメンドシステムを簡単に構築できるようになりました。

ベクトル検索ができると、サクッと実装した時でも、できることの幅も可能性が広がり、firestoreの低価格と合わせると悩むよりまずは作ろうという気持ちになっちゃいますね。

---
### [前書き]
この記事は、[フラー株式会社のカレンダー | Advent Calendar 2024](https://qiita.com/advent-calendar/2024/fuller-inc)の3日目の記事です。

# 何がヤバいのか3行で解説
1. **Firestoreの課金はとても安い**のに、ついにベクトル検索まで対応してしまった。
2. **AIの進化でtext-embeddingが激安＆高精度化**。個人開発者でも気軽に使える時代に。
3. **Firestore＋AIの合わせ技**で、手軽に高精度なテキスト類似度検索が実現可能。

Firestoreといえば、少しクセのあるNoSQLデータベースですが、低価格なので個人開発者やあまりお金をかけたくない開発などで使いやすいのが大きな特徴です。

<details><summary>NoSQLとは？RDBとは何が違うのかはこちらで簡単に解説</summary>

# NoSQLとは？RDBとは何が違うのか？

データベースは大きく分けて「RDB（リレーショナルデータベース）」と「NoSQL（非リレーショナルデータベース）」の2種類があります。それぞれの特徴や違いを簡単に解説します。
## RDB（リレーショナルデータベース）
- **特徴**：
  - データを**表形式（テーブル）**で管理。
  - SQL（Structured Query Language）を使用して操作可能。
- **メリット**：
  - データの整合性を保ちやすく、複雑なデータ構造や分析に対応可能。
- **デメリット**：
  - 柔軟性が低く、スケーリングや構造変更が手間。

## NoSQL（非リレーショナルデータベース）
- **特徴**：
  - JSONのような柔軟な形式でデータを保存可能。
  - スキーマレスであり、事前にデータ構造を定義する必要がない。
- **メリット**：
  - 柔軟性が高く、スケーラビリティに優れる。
- **デメリット**：
  - データの整合性を手動で管理する必要がある場合が多い。

| 項目                 | RDB                                   | NoSQL                              |
|----------------------|---------------------------------------|------------------------------------|
| データ構造           | 固定スキーマ                         | スキーマレス                      |
| データの関係性       | 外部キーで定義                       | 必要に応じてプログラム側で管理    |
| スケーラビリティ      | 垂直スケール（高性能なハードウェア） | 水平スケール（サーバー追加で拡張）|
| 柔軟性               | 制約が多い                           | 非常に柔軟                        |
| 適した用途           | 金融、在庫管理、分析など             | スケーラブルなWebサービスやIoTなど |
</details>


# Firestoreでベクトル検索を使う方法
Firestoreの新しいベクトル検索機能を使えば、データベースのスケーラビリティ低コストを維持しながら、AIを活用した高度な検索が可能です。

公式情報
https://firebase.google.com/docs/firestore/vector-search?hl=ja

## ベクトル検索とは？
ベクトル検索は、データ（文章、画像など）をベクトル値に変換し、その類似性を数値的に計算する技術です。Firestoreはこのベクトル検索をサポートすることで、次のようなシナリオで役立ちます：

- 高精度な類似度検索（FAQや製品検索）
- パーソナライズされたレコメンドシステム
- AIを活用した多言語対応検索

このベクトル化する方法は、**後述するtext-embedding**や独自のアルゴリズムにより行います。

### text-embeddingを使ったベクトル化の例
text-embeddingは、文章や単語を数値ベクトルに変換する技術で、多くのAPIやAIモデルが利用可能です。例えば、以下のように活用されます：

- 入力データ：
    - 「この商品は高品質でコストパフォーマンスが良い。」というレビューをAPIに送信。
- 変換結果：
    - ベクトル例：[0.234, -0.512, 0.398, ...] 

### 独自アルゴリズムを活用したベクトル化
自分で考えたアルゴリズムを使って、ベクトル値を生成することももちろん可能です。特定の業界やアプリケーションに特化したアルゴリズムを設計すれば、よりニッチなユースケースに対応できます。

- 画像データのベクトル化
    - 商品画像をResNetなどのニューラルネットワークに入力し、特徴量を抽出してベクトル化。
- 音声データのベクトル化
    - ユーザーの音声をMFCC（メル周波数ケプストラム係数）を使ってベクトル値に変換。

もっと具体的にいくと、例えば顔の類似度検索を実装しようと思うならば
- 入力データ: 顔写真やビデオフレーム。
- ベクトル化手法:
    - FaceNetやDlibなどのライブラリを活用。
    - 例えば、FaceNetでは128次元のベクトルとして顔の特徴を表現。
と言う感じでベクトル化できます。

### 『つまり』
Firestoreに保存されたベクトルデータと、ユーザーが入力したデータをベクトル化したものを比較し、類似度の高い順にデータを取得できる仕組みです。これにより、高度な検索やパーソナライズが簡単に実現できるため、まさに「最強のソリューション」と言えます。

## 1.ベクトルの値を保存する
以下の例では、商品情報とその埋め込みを保存します：

```
import { Firestore, FieldValue } from "@google-cloud/firestore";

const db = new Firestore();
const coll = db.collection('products');
await coll.add({
  name: "Example Product",
  description: "Product description here.",
  embedding_field: FieldValue.vector([1.0, 2.0, 3.0])
});
```

## 2. ベクトルインデックスを作成する
Firestoreのベクトル検索では、ベクター値に対応するインデックスを事前に作成する必要があります。Google Cloud CLIを使ってインデックスを作成する方法は以下の通りです

```
gcloud firestore indexes composite create \
--collection-group=products \
--query-scope=COLLECTION \
--field-config field-path=${ここにベクトルのkeyを入れる。上記例でいうと"embedding_field"},vector-config='{"dimension":"1024", "flat": "{}"}'
```

- dimensionというのはそのベクトルの次元数、[1,2,3]のベクトルは3次元
- 例えば、Geminiのtext-embedding-004で取得できるベクトルの次元数は768次元なのでdimensionは768を指定。

## 3. ベクトル検索の実行
保存したベクトルデータを基に、類似したドキュメントを検索できます。以下は、埋め込みフィールド embedding_field を使って10件の最近傍を検索する例です。

```
import { Firestore } from "@google-cloud/firestore";

const db = new Firestore();
const coll = db.collection('products');
const vectorQuery = coll.findNearest({
  vectorField: 'embedding_field',
  queryVector: [3.0, 1.0, 2.0],
  limit: 10,
  distanceMeasure: 'EUCLIDEAN'
});

const results = await vectorQuery.get();
results.forEach(doc => {
  console.log(doc.id, doc.data());
});
```
- distanceMeasure: 距離測定方法
- limit: 取得するドキュメントの最大数。

## 距離測定の選択
Firestoreでは、以下の距離測定方法をサポートしています：

### EUCLIDEAN
ベクトル間のユークリッド距離を計算。

<details><summary>ユークリッド距離の計算方法とは</summary>
ユークリッド距離は、ベクトル間の直線距離を計算する方法です。次元空間上で2つの点がどれだけ離れているかを数値化します。

- 特徴
    - 距離が小さいほど、2つのベクトルが類似している。
    - 大きさの影響を受ける（例：ベクトルのスケールが異なる場合、類似性が正確に測定されないことがある）。
</details>


### COSINE
ベクトル間の角度を測定（大きさの影響を受けない類似性）。

<details><summary>コサイン類似度とは</summary>
コサイン類似度は、ベクトル間の角度を測定する方法です。ベクトルの大きさを無視し、向き（方向性）に基づいて類似性を計算します。

- 特徴
    - ベクトルの大きさに影響されないため、スケーリングされたデータに適している。
    - 正規化されたベクトルデータに特に効果的。
</details>


### DOT_PRODUCT 
ベクトルの大きさも考慮した測定。

<details><summary>ドット積とは</summary>
ドット積は、ベクトル間の相互作用を測定します。ベクトルの向きと大きさの両方を考慮した値を返す点が特徴です。

- 特徴
    - 値が大きいほど、類似性が高いことを示す。
    - ベクトルの大きさの影響を受けるため、スケールの異なるデータを比較する場合には適していない。
</details>


### ChatGPT(4o)による距離測定のおすすめ
- データが正規化されている場合
    - DOT_PRODUCTが計算効率に優れており、パフォーマンスを重視する場面に適しています。
- データが正規化されていない場合
    - COSINEやEUCLIDEANを選択してください。特に、コサイン類似度は大きさの影響を排除したい場合に最適です。
- 距離測定に迷った場合
    - コサイン類似度（COSINE）が一般的で汎用性が高く、テキストや埋め込みベクトルの検索に向いています。



# text embeddingとは？
text embeddingは、文章や単語を数値ベクトルに変換し、AIがそれを理解・処理しやすくする技術です。先に結論からいくとChatGPTのモデルを採用することをお勧めします。課金は必要ですが、お値段もかなり落ちており手軽に導入できるようになっています

## 文章や単語をベクトル化する仕組み
text embeddingは、文章や単語の意味や文脈を反映した数値ベクトルを生成します。例えば：

**「猫」と「犬」**　は異なる単語ですが、embedding技術では「ペット」という概念に基づいて近いベクトルとして表現されます。
一方、　**「猫」と「車」**　のように全く関連性のない単語は、遠く離れたベクトルになります。

## よく使われるtext embeddingのAPI
以下のAPIが人気です：

### OpenAIのtext-embedding-3
- **特徴**：汎用性が高く、簡単に使える。日本語もしっかり対応しててよさそう
- **価格**：特に短い文章や単語ならほぼ気にならないコスト。

| モデル名                | 通常料金          | Batch API を使った料金* |
|-------------------------|-------------------|-------------------------|
| **text-embedding-3-small** | $0.020/1M トークン | $0.010/1M トークン      |
| **text-embedding-3-large** | $0.130/1M トークン | $0.065/1M トークン      |
| **ada v2**              | $0.100/1M トークン | $0.050/1M トークン      |

- text-embedding-3-smallにて、
    - 「TWICEのモモ」 → 7トークン
    - 「海辺の街」 → 6トークン
    -「昨日はとても面白い夢を見たんだ」 → 18トークン

1000000tokenで大体3円みたいですが、この1000000を消費するまでにどのくらいのリクエストを送ればいいのだと言う話になります。

個人的には、何かより良い機能を検討できるのであれば、絶対に採用したいほどの低コストで、これくらいならばlargeモデルも良いかもしれません。

https://openai.com/ja-JP/api/pricing/
https://openai.com/index/new-embedding-models-and-api-updates/

### Google Geminiのtext-embedding-004

- **特徴**：現在(11/25)時点で、場合によっては無料で利用可能(これはすごい）
- **注意点**：現在(11/25)時点で、日本語対応はうまくできていないっぽい

レートと料金に関する情報は以下の通り

| 項目                           | 詳細                          |
|--------------------------------|-------------------------------|
| **レート上限**                 | 1,500 RPM（1分あたりのリクエスト数） |
| **入力の料金**                 | 無料                          |
| **出力の料金**                 | 無料                          |
| **コンテキスト キャッシュ保存** | 該当なし                      |
| **チューニングの料金**         | 該当なし                      |
| **サービスの改善に使用**       | はい                          |

Geminiは驚くべき低コストで使えることも多いので、他の用途では弱者の強力な味方だと思いますが、私はtext-embeddingに関しては今後もOpenAIの方を脳死で使おうと思っています。

https://ai.google.dev/pricing?hl=ja#text-embedding004

---

# Firestoreとtext embeddingを活用すると何ができるのか？
Firestoreとembeddingを組み合わせることで、AIを活用した新しい体験を提供できます。
具体的なユースケースとその活用方法を詳しく解説します。

## ユースケース1：高精度な検索エンジン
- 例：
    - ユーザーが「返品ポリシーについて知りたい」と入力。
    - FAQデータベースの回答文をembeddingでベクトル化し、クエリの意図に最も近い回答を返す。

- 実装例
    - あらかじめ同じモデルでtext-embeddingを行いデータベースの中(今回はfirestore)に入れてしまっておく
    - ユーザーの入力をqueryとして、以下のように実装する
```
const query = "返品ポリシー";
const queryEmbedding = getEmbedding(query); // text-embedding APIでベクトル化
const results = coll.findNearest({
  vectorField: 'embedding',
  queryVector: queryEmbedding,
  limit: 5,
  distanceMeasure: 'COSINE',
});

results.forEach(doc => {
  console.log('一致する回答:', doc.data().content);
});
```

## ユースケース2：文章ベースのレコメンドシステム
- 例
    - ブログ記事を閲覧しているユーザーに、閲覧中の記事と類似テーマの他の記事を提案。
    - ショッピングサイトで、商品の説明文に基づいて関連商品を推薦。
- 実装例
    - あらかじめ同じモデルでtext-embeddingを行いデータベースの中(今回はfirestore)に入れてしまっておく
    - ユーザーの入力をcurrentArticleContentとして、以下のように実装する

```
const currentArticleEmbedding = getEmbedding(currentArticleContent);
const recommendedResults = coll.findNearest({
  vectorField: 'embedding',
  queryVector: currentArticleEmbedding,
  limit: 3,
  distanceMeasure: 'EUCLIDEAN',
});

recommendedResults.forEach(doc => {
  console.log('おすすめ記事:', doc.data().title);
});
```


## ユースケース3：興味や関心をベクトル化したレコメンド
- 例
    - ユーザーが過去に購入した商品や閲覧したページを元に、関連する商品を推薦。
    - SNSでの投稿内容や「いいね！」データを解析して、似たような関心を持つユーザーをつなぐ。
- 実装例
    - 投稿データは常にvector化して検索用に保持(embeddingプロパティ)
    - ユーザーの入力をuserBehaviorDataとして、以下のように実装する

```
const userBehaviorEmbedding = getEmbedding(userBehaviorData);
const personalizedResults = coll.findNearest({
  vectorField: 'embedding',
  queryVector: userBehaviorEmbedding,
  limit: 5,
  distanceMeasure: 'DOT_PRODUCT',
});

personalizedResults.forEach(doc => {
  console.log('パーソナライズされた提案:', doc.data().name);
});
```

## その他/備考
> サポートされているエンべディング ディメンションの最大値は 2,048 です。
https://firebase.google.com/docs/firestore/vector-search?hl=ja#node.js

ベクトルは2048次元までサポートしてくれているようです。


## 所感
コンテンツの品質を向上させるには、Gemini APIやChatGPTのAPIなどを活用し、ベクトル化の前処理を強化するという方法も考えられます。高性能なモデルを用いることで、入力データの精度を高め、検索やレコメンドの結果をさらに洗練されたものにすることが可能でしょう

このようにベクトルデータを扱いやすくなったことは、とても助かり手札が増えて良いですね。個人規模のプロジェクトや、あまりお金にならないけどクオリティは落としたくない時に大活躍するかもしれません。