BigQuery ML テキストエンべディングを使ったセマンティック検索

BigQueryMLを使うことで、BigQuery上のテーブルデータを元にエンベディングを生成し、生成したエンベディングを用いて類似度の計算などが行えます。
BigQuery上のテーブルの特定のカラムに対して、セマンティック検索を試します。

# 検索元テーブルを用意
[pixabay](https://pixabay.com/ja/music/search/?order=ec)というフリー音源サイトを元に、track名、サイトURL、タグ情報を持つテーブルを作成しました。

タグ情報を元にセマンティック検索できるようにします。

![music_list_table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/c0ed2811-7565-66cf-4a8b-29fd5b9059bf.png)

# BigQueryからVertexAIのモデルを使用するための準備
## クラウドリソースへの接続を作成
データセットと同じロケーションに接続を作成します。
コマンドラインで接続を作成するには以下を実行。
```
bq mk --connection --location=REGION --project_id=PROJECT_ID \
    --connection_type=CLOUD_RESOURCE CONNECTION_ID
```
次の値を自身の環境に合わせて置き換えます。
- `REGION`: データセットと同じリージョン。（asia-northeast1 など）
- `PROJECT_ID`: Google CloudのプロジェクトID
- `CONNECTION_ID`: 接続のID。 bqml_test_connection などと適当に命名してください。

作成されたサービスアカウントは以下コマンドで確認できます。
```
bq show --connection PROJECT_ID.REGION.CONNECTION_ID
```

## サービスアカウントにアクセス権を付与する
先ほど作成した接続の使用権限をサービスアカウントに付与します。
```
gcloud projects add-iam-policy-binding 'PROJECT_NUMBER' --member='serviceAccount:MEMBER' --role='roles/aiplatform.user' --condition=None
```
次の値を自身の環境に合わせて置き換えます。
- `PROJECT_NUMBER`: Google Cloudのプロジェクト番号
- `MEMBER`: 先ほど作成したサービスアカウントのID

## モデルをBigQuery上に作成
[textembedding-gecko](https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/text-embeddings?hl=ja) モデルをリモートモデルとして登録します。

![model_create.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/39934b47-edde-5d57-e24d-ebf162f6b17c.png)

```
CREATE OR REPLACE MODEL `DATASET_ID.MODEL_ID`
REMOTE WITH CONNECTION `PROJECT_ID.REASION.CONNECTION_ID`
OPTIONS (REMOTE_SERVICE_TYPE = 'CLOUD_AI_TEXT_EMBEDDING_MODEL_V1');
```
次の値を自身の環境に合わせて置き換えます。
- `DATASET_ID`: 今回使用するデータセットのID
- `MODEL_ID`: 作成するモデルのID。 embedding_model などと適当に命名してください。
- `PROJECT_ID`: Google CloudのプロジェクトID
- `REGION`: データセットと同じリージョン
- `CONNECTION_ID`: 先ほど作成した接続のID


# テーブルにEmbedding列を追加

登録したモデルを ML.GENERATE_TEXT_EMBEDDING という BigQuery ML 関数を介して呼び出すことでエンべディングを生成します。
![create_embedding_table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/bdbeca2c-fe8a-8ec0-976f-2ea4de0083f3.png)

```
CREATE OR REPLACE TABLE `DATASET_ID.music_list_embedding` AS (
  -- music_listテーブルのtag列からEmbedding列を作成
  WITH tag_embedding AS (
    SELECT *
    FROM ML.GENERATE_TEXT_EMBEDDING(
      MODEL `DATASET_ID.embedding_model`,
      (
      SELECT
        track_id,
        tag as content
      FROM
        `DATASET_ID.music_list` 
      ),
      STRUCT(TRUE AS flatten_json_output)
    )
  )

  -- music_listテーブルにtext_embedding列を追加
  SELECT
    music_list.*,
    tag_embedding.text_embedding
  FROM `DATASET_ID.music_list` as music_list
  JOIN tag_embedding
  ON music_list.track_id = tag_embedding.track_id
);
```
ML.GENERATE_TEXT_EMBEDDINGの第二引数で、変換する対象のテキストを渡します。
今回はtag列のテキストを変換します。
このとき、Embeddingする対象のカラム名を`content`として渡す必要があります。

FLOAT型	REPEATEDモードのtext_embedding列が追加されました。

![music_list_embedding.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/3d89a7a8-a577-8931-f83e-676f7115c723.png)


# セマンティック検索してみる
ML.DISTANCE という BigQuery ML 関数を使ってベクトル間の距離を計算します。
検索元テーブルの各行との距離が最も近い（数値が低い）曲を今回は検索結果として解釈します。
```
-- 質問文をベクトル化
WITH search_word_embedding AS (
  SELECT *
  FROM ML.GENERATE_TEXT_EMBEDDING(
    MODEL `DATASET_ID.embedding_model`,
    (SELECT "都会の夜道で聞きたいlofiな曲" AS content),
    STRUCT(TRUE AS flatten_json_output)
  )
)

--　質問文と　music_list　のベクトル空間上の距離を計算し、近い順に出力する
SELECT
  m_list.track_id AS track_id,
  m_list.track_name AS track_name,
  m_list.url AS track_url,
  m_list.tag AS track_tag,
  s_word.content AS search_word,
  ML.DISTANCE(m_list.text_embedding, s_word.text_embedding, 'COSINE') AS distance
FROM
  `DATASET_ID.music_list_embedding` AS m_list,
  search_word_embedding AS s_word
ORDER BY distance ASC;
```

「都会の夜道で聞きたいlofiな曲」と検索した場合が以下。
「lofi」や「都市」などの文字に反応してちゃんとlofiな曲がレコメンドされた。
検索でマッチした曲：https://pixabay.com/ja/music/good-night-160166/
![search_lofi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/1e25a6ca-cb5e-b451-d6f4-dfe81c3db7c0.png)

もう少し難易度を上げて、直接的にマッチするタグがない状態での検索も試します。
「サッカーに合う曲」と検索した場合が以下。
![search_soccer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99571/384e74ca-9368-7f26-d8f1-d242e3a2a5d2.png)
今回の検索語の場合、直接的にマッチするタグ情報を持つ曲はありませんが、
「スポーツ」や「パワー」、「過激」などのサッカーと類似してそうなタグを持つ曲が最近傍と算出されました。
検索でマッチした曲：https://pixabay.com/ja/music/for-future-bass-159125/


# 参考URL
- https://cloud.google.com/blog/ja/products/data-analytics/introducing-bigquery-text-embeddings
- https://cloud.google.com/bigquery/docs/text-embedding-semantic-search?hl=ja#console