movielens recommendation with Vertex AI Agent Builder Media recommendation

## 概要

Vertex AI Agent Builder Media recommendationを使ってmovielensのレコメンドエンジンを試してみる。

内容は、[Get started with media recommendations](https://cloud.google.com/generative-ai-app-builder/docs/try-media-recommendations)の中身をやってみるものです。

## まとめ

- VertexAI Agent Builderで簡単にRecommendationエンジンを作成できる✅
    - Recommendation typesは`Others You May Like`、`Recommended for You`、`More Like This`、`Most Popular` があるのである程度カバーできている
- Data SourceはGCSやBQに対応している✅
    - 必要な形への変換はBQのViewを使うことで簡単に利用可能
    - 定期インポートがサポートされているので設定後の運用が楽 :thumbsup: ([Import from BigQuery - Periodic ingestion](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es#bigquery))
- Searchも同様のデータソースを利用できるので、検索とレコメンデーションを高速に実装できる✅ → Searchは[こちら](https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5)
- Applicationはlibraryがあるので他のGoogle Service同様に簡単にクライアントで呼び出しができる✅
- Support されているmedia_typeは `movie, show, concert, event, live-event, broadcast, tv-series, episode, video-game, clip, vlog, audio, audio-book, music, album, articles, news, radio, podcast, book, and sports-game` ([ref](https://cloud.google.com/generative-ai-app-builder/docs/media-documents)) :warning: 



## Prepare BQ Dataset

:::note info
https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5 ですでにデータを準備した人はSkip可
:::


bqのdataset `movielens`作成

```
bq mk $PROJECT_ID:movielens
```

csvからmovies テーブルをload

```
bq load --skip_leading_rows=1 movielens.movies \
  gs://cloud-samples-data/gen-app-builder/media-recommendations/movies.csv \
  movieId:integer,title,genres
```

csvからratingテーブルをload

```
bq load --skip_leading_rows=1 movielens.ratings \
  gs://cloud-samples-data/gen-app-builder/media-recommendations/ratings.csv \
  userId:integer,movieId:integer,rating:float,time:timestamp
```

view `movies_view`を作成

```
bq mk --project_id=$PROJECT_ID \
 --use_legacy_sql=false \
 --view "
  WITH t AS (
    SELECT
      CAST(movieId AS string) AS id,
      SUBSTR(title, 0, 128) AS title,
      SPLIT(genres, \"|\") AS categories
      FROM \`$PROJECT_ID.movielens.movies\`)
    SELECT
      id, \"default_schema\" as schemaId, null as parentDocumentId,
      TO_JSON_STRING(STRUCT(title as title, categories as categories,
      CONCAT(\"http://mytestdomain.movie/content/\", id) as uri,
      \"2023-01-01T00:00:00Z\" as available_time,
      \"2033-01-01T00:00:00Z\" as expire_time,
      \"movie\" as media_type)) as jsonData
    FROM t;" \
$PROJECT_ID:movielens.movies_view
```

![Screenshot 2024-08-30 at 7.59.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/7b1aa49b-83ec-a74c-cd72-7d86ecbb21b7.png)


view `user_events_for_search` (`user_events` is already used in https://qiita.com/nakamasato/items/012ea7159d3e3fc8e30e)

```
bq mk --project_id=$PROJECT_ID \
 --use_legacy_sql=false \
 --view "
 WITH t AS (
  SELECT
    MIN(UNIX_SECONDS(time)) AS old_start,
    MAX(UNIX_SECONDS(time)) AS old_end,
    UNIX_SECONDS(TIMESTAMP_SUB(
    CURRENT_TIMESTAMP(), INTERVAL 90 DAY)) AS new_start,
    UNIX_SECONDS(CURRENT_TIMESTAMP()) AS new_end
  FROM \`$PROJECT_ID.movielens.ratings\`)
  SELECT
    CAST(userId AS STRING) AS userPseudoId,
    \"view-item\" AS eventType,
    FORMAT_TIMESTAMP(\"%Y-%m-%dT%X%Ez\",
    TIMESTAMP_SECONDS(CAST(
      (t.new_start + (UNIX_SECONDS(time) - t.old_start) *
      (t.new_end - t.new_start) / (t.old_end - t.old_start))
    AS int64))) AS eventTime,
    [STRUCT(movieId AS id, null AS name)] AS documents,
  FROM \`$PROJECT_ID.movielens.ratings\`, t
  WHERE rating >= 4;" \
$PROJECT_ID:movielens.user_events_for_search
```

document nameは常にnullが入り、document idにはmovie idが入っている

![Screenshot 2024-08-30 at 8.01.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/053b3e90-50bf-459c-1a01-c03518c9cf03.png)

## Create a recommendation app

[Agent Builder App](https://console.cloud.google.com/gen-app-builder/engines)からRecommendationを選択してappを作成。

![Screenshot 2024-09-01 at 7.57.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/555e1455-d646-f516-7a78-2f79e0e4b553.png)

![Screenshot 2024-09-01 at 7.58.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/0fb3bed7-c12f-d233-9ddd-4b255fccdd40.png)

- app name: `quickstart-media-recommendations`
- Content: `Media`
- Recommendation type: `Others you may like` (詳細画面などで出すような推薦タイプ)
- Business Objective: `Click-through rate (CTR)`

Configuration:

:::note info
すでに https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5 データを作成済みの場合は、流用すれば良い
![Screenshot 2024-09-01 at 8.03.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/050ef6f1-cabc-cffd-697d-7949cbfaeed1.png)
:::

Data storeの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/39917017-016e-5f69-5a40-50ac4b2ea2cb.png)

作成したdata sourceを選択してappの作成を完了

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/42e3cb99-b5f6-7cc8-53d0-309353fb908b.png)

## データのインポート

### Documentのインポート

:::note info
https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5 ですでにインポートの場合はSkipしてください
:::

BigQueryまたはCloud Storageが選択できるので、今回はbigqueryを選択

![Screenshot 2024-08-30 at 7.31.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/e1a99b17-13d4-ecc5-55f1-07eba2c60a68.png)

Datasetを選択

![Screenshot 2024-08-30 at 7.32.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/f3f15ef1-93f8-0d3c-f031-22283de7d1bc.png)

先ほど作成したmovielensの`movies_view` を選択してimportする

![Screenshot 2024-08-30 at 7.34.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/f9d72ae4-bef2-f596-a1de-48e00ebac53e.png)

importには15分くらいかかるので完了を待つ

![Screenshot 2024-08-30 at 7.36.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/15abe72d-d6dc-9dcb-777e-b3fb4370c2ef.png)

`86,537` documents importされる

![Screenshot 2024-08-30 at 7.49.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/e9094c6a-369b-4087-194e-5327a22833af.png)

### User Dataのインポート

:::note info
ここでは`user_events_for_search`となっているのは、https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5 ですでにインポート済みのデータを利用しているため
:::


次にEventタブから `Import Events`をクリックする

![Screenshot 2024-08-30 at 7.50.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/4ec05142-c039-a7d5-b06b-5444deffff5f.png)

同様にBigQueryを選択して、`user_events_for_search` (上でインポートしたテーブル)を指定して、インポート

<!-- ![Screenshot 2024-08-30 at 7.33.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/219bef00-7fe9-403c-8a95-2c4bc667ff57.png) -->

![Screenshot 2024-08-30 at 7.51.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/69a26e49-b974-e3ad-63a8-f6b1038e4c64.png)

完了

![Screenshot 2024-08-30 at 8.09.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/1a2b2c6b-79d5-642d-132f-0141d5b35aaa.png)

## モデルの学習 (~24 hours)

![Screenshot 2024-09-01 at 8.11.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/ad5325d1-f233-5be1-d748-02a52e418888.png)

- Recommendation demotion: 今まで見たコンテンツや古いコンテンツの順位を下げるかどうか
- Result Diversification: 多様性をData-Driven or Rule-Basedを選べる

これで学習を開始できる。

TrainingタブからStatusを確認することができる

![Screenshot 2024-09-01 at 8.14.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/6cf5f154-bf1e-3766-cdce-d8fbd39f65a7.png)

完了 ✅

![Screenshot 2024-09-02 at 7.37.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/41cc3820-d5f5-9fbb-c4e0-cc65bbcfac69.png)

## 結果の確認

`Preview` tabで、Document IDに `4993` ("The Lord of the Rings: The Fellowship of the Ring (2001)") を入れてGet Recommendationをしてみる

![Screenshot 2024-09-02 at 7.38.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7059/80c821da-b936-2208-6f98-19e2e19b8467.png)


## Applicationに組み込む

https://cloud.google.com/generative-ai-app-builder/docs/preview-recommendations#discoveryengine_v1beta_generated_RecommendationService_Recommend_sync-go

こちらを参考にADCと一緒にClientを実装するとできる


```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	discoveryengine "cloud.google.com/go/discoveryengine/apiv1beta"
	discoveryenginepb "cloud.google.com/go/discoveryengine/apiv1beta/discoveryenginepb"
)

func main() {
	ctx := context.Background()
	// This snippet has been automatically generated and should be regarded as a code template only.
	// It will require modifications to work:
	// - It may require correct/in-range values for request initialization.
	// - It may require specifying regional endpoints when creating the service client as shown in:
	//   https://pkg.go.dev/cloud.google.com/go#hdr-Client_Options
	c, err := discoveryengine.NewRecommendationClient(ctx)
	if err != nil {
		os.Exit(1)
	}
	defer c.Close()
	// {"params":{"returnDocument":true},"userEvent":{"eventType":"view-item","userPseudoId":"b60dd0af-a09f-9b7b-46dd-6219ee3a817f","documents":[{"id":"1"}]},"useLatestServingConfig":true}
	projectNumber := os.Getenv("PROJECT_NUMBER")
	engine := os.Getenv("ENGINE")
	servingConfig := os.Getenv("SERVING_CONFIG")
	req := &discoveryenginepb.RecommendRequest{
		ServingConfig: fmt.Sprintf("projects/%s/locations/global/collections/default_collection/engines/%s/servingConfigs/%s", projectNumber, engine, servingConfig),
		UserEvent: &discoveryenginepb.UserEvent{
			EventType:    "view-item",
			UserPseudoId: "user123",
			Documents: []*discoveryenginepb.DocumentInfo{
				{
					DocumentDescriptor: &discoveryenginepb.DocumentInfo_Id{
						Id: "2",
					},
				},
			},
		},
	}
	resp, err := c.Recommend(ctx, req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp)
}
```

```bash
export PROJECT_NUMBER=xxx 
export ENGINE=quickstart-media-recommendations 
export SERVING_CONFIG=quickstart-media-recommendations
```
```bash
go run main.go
results:{id:"79132"} results:{id:"1367"} results:{id:"260"} results:{id:"480"} results:{id:"586"} results:{id:"50872"} results:{id:"58559"} results:{id:"356"} results:{id:"837"} results:{id:"166528"} results:{id:"858"} results:{id:"4306"} results:{id:"1"} results:{id:"318"} results:{id:"5952"} results:{id:"780"} results:{id:"527"} results:{id:"1544"} results:{id:"1196"} results:{id:"4993"} attribution_token:"xxxx"
go run main.go
results:{id:"356"} results:{id:"586"} results:{id:"1367"} results:{id:"79132"} results:{id:"480"} results:{id:"158"} results:{id:"8961"} results:{id:"837"} results:{id:"736"} results:{id:"277"} results:{id:"260"} results:{id:"485"} results:{id:"4306"} results:{id:"58559"} results:{id:"1544"} results:{id:"588"} results:{id:"780"} results:{id:"594"} results:{id:"318"} results:{id:"364"} attribution_token:"xxxx"
```


## DataのImport

[Import from BigQuery](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es#bigquery)、[Import from Cloud Storage](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es#cloud-storage)では `Periodic ingestion` (Public Preview) がサポートされているので、定期的にデータを更新することも可能 (1,3,5日に1回のどれか)

> Periodic ingestion: You import data from one or more BigQuery tables, and you set a sync frequency that determines how often the data stores are updated with the most recent data from the BigQuery dataset.
> Data updates automatically every 1, 3, or 5 days. Data cannot be manually refreshed.

詳細は、[Create a search data store](https://cloud.google.com/generative-ai-app-builder/docs/create-data-store-es#cloud-sql-procedure)

## Ref

- [Get started with media recommendations](https://cloud.google.com/generative-ai-app-builder/docs/try-media-recommendations): 今回の内容
- [Get Recommendations](https://cloud.google.com/generative-ai-app-builder/docs/preview-recommendations): APIなどを使ってRecommendationを取得する部分 (推論を利用する)
- [Filter Recommendations](https://cloud.google.com/generative-ai-app-builder/docs/filter-recommendations): まだ見てないが
- [About media app recommendations types](https://cloud.google.com/generative-ai-app-builder/docs/about-media-recommendations-types)
- [movielens search with Vertex AI Agent Builder Media search](https://qiita.com/nakamasato/items/295a2a3a4d5ed2aab2d5): Recommendationとは別に同じData Sourceを使ってSearch機能も提供できるのでこちらも参考に