【備忘録】Backlogのデータを検索しやすくする為のAzure活用

また、BacklogでAzure活用の備忘録です。

普段のお仕事でお客様からのご質問に[Backlog](https://backlog.com/ja/)を利用して回答するといった事があります。

その質問と回答のセットはナレッジとして非常に有益なのですが、ピンポイントで探しづらく、後から「前同じような質問あったような？」「誰かが同じような対応してたけど...どこだっけ？」とよくなっていました😓
(自分自身の記憶力の問題でもありますが...）

そこで...「これは、自然言語で類似検索できたら良さそう。」という事で作ってみることにしました。ちなみに、この記事で各工程の詳細な解説は致しません。あくまでアイディアとしてこういう使い道もあるんだな～くらいに思って頂ければ幸いです🙌

早速ですが構成はこうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/9efc6e95-930e-329a-37f7-e72daac7f685.png)


[Backlog API](https://developer.nulab.com/ja/docs/backlog/)でチケットの内容を取得 →それらを埋め込み(ベクトル)化 → CosmosDBに保存します。

GitHub Actionsの[scheduleトリガー](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule)でそれらの処理を月次で走らせ、データのリフレッシュをおこなっています。

と、まあ超シンプルです。
そもそも、とにもかくにも安く済ませたい！と思ってました。Azureでベクトル検索といえば[AI Search](https://learn.microsoft.com/ja-jp/azure/search/vector-search-overview)が代表的ですが、ここではコスト面を重視でAzure CosmosDB for NoSQLでのベクトルインデックス機能を利用することにしました。詳細は下記のリンクをご参考ください。

https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/vector-search

※現時点(2024.08) ではパブリックプレビューの段階です

## 工程
以降は少しハマった点 & ちょっぴり工夫点などを書いていきます👀

### ベクトルポリシー
CosmosDB for NoSQLを作成して、新しくデータベースを作成する際にベクトルポリシーを定義します。埋め込みには`text-embeddings-ada-002`を使うので1536次元です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/3abe947d-65b7-d14d-c784-d0264d5aca3a.png)

AOAIで埋め込みとCosmosDBへの格納処理は以下を参考に書き進めました。

https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/ai/azopenai

https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/data/azcosmos

## ベクトル検索
検索用のコマンドラインツールもGo言語で書きました。
クロスパーティションクエリ (パーティションキーの指定なしでクエリ) を実行するメソッドやプロパティを探しましたが、 どうも見当たりませんでした。

https://learn.microsoft.com/ja-jp/azure/cosmos-db/nosql/quickstart-go?pivots=devcontainer-vscode#query-items

```golang
...
    // pkをnullにしても駄目 パーティションキーなしでクエリする方法が分からない
    pK := azcosmos.NullPartitionKey 
    query := "SELECT TOP 10 c.summary, c.description, c.url, VectorDistance(c.vector,@embedding) AS SimilarityScore FROM c ORDER BY VectorDistance(c.vector,@embedding)"
    	queryOptions := azcosmos.QueryOptions{
    		QueryParameters: []azcosmos.QueryParameter{
    			{Name: "@embedding", Value: vector},
    		},
    	}
  
	pager := container.NewQueryItemsPager(query, pK, &queryOptions)
 ```

 Issueを見てると、この機能はGo言語のSDKには現時点(2024.8)で実装されていないようでした。
 
 https://github.com/Azure/azure-sdk-for-go/issues/18578

 そこで、パーティションキーを見直すことにしました。
当初 `/id` で設定していたのですが、Backlog課題チケットの登録タイプ名で設定する事にしました。

https://developer.nulab.com/ja/docs/backlog/api/2/get-issue-list/

 ```txt:json
 [
    {
        "id": 1,
        "projectId": 1,
        "issueKey": "BLG-1",
        "keyId": 1,
        "issueType": {
            "id": 2,
            "projectId" :1,
            "name": "Q&A", ← この名称でパーティションを分けるようにしてみた
            "color": "#7ea800",
            "displayOrder": 0
        },
        "summary": "first issue",
        "description": "",
        "resolution": null,
        "priority": {
            "id": 3,
            "name": "中"
        },
        ...
```
この辺は実際に使ってみながら日々改善をしていこうと思ってます。

DBを作り直して実際に検索動作をさせてみた結果はこんな感じです。
```bash:bash
Enter your query message >>
Azure FrontDoorから500エラーが返ってきます //検索したいワードを入力

Top 10 similar issues: //以下検索結果
Summary: Azureの障害に関して
URL: https://×××××××××××.backlog.com/view/×××××××××

Summary: フロントドアの5xxエラーが頻発する
URL: https://×××××××××××.backlog.com/view/×××××××××

Summary: フロントドアのバックエンドルーティングの不具合
URL: https://×××××××××××.backlog.com/view/×××××××××××

Summary: Front Door/CDNプロファイルに問題が発生する原因
URL: https://×××××××××××.backlog.com/view/×××××××××××
...
```
URLはその課題チケットのURLとなっています。
これで類似した問題を探しやすくなりました🙌

## Cosmos DBのIP制限とGitHub Actions
CosmosDBには一部のネットワークからしかアクセスできないようにIP制限をかけてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/a42e0015-1208-1817-7856-6807dca3a1cd.png)
しかし、[GitHubのIPは固定ではなく、時々変更される](https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/about-githubs-ip-addresses)ため、ワークフロー実行時のIPを取得してファイアウォールルールを更新するステップを設けることにしました。[こちらの記事](https://zenn.dev/kitoketa/articles/c554aa9076a96f)を参考にさせて頂きました。これだけの為にセルフホステッド用意するの嫌だなあ...と思っていたのでお手軽に済ませられました。

以下はワークフローの全文です。
```yaml:yml
name: knowledgebase-batch

on:
  schedule:
    - cron: '0 0 1 * *'
  workflow_dispatch:

env:
  COMOS_DB_ACCOUNT_NAME: <CosmosDB AccountName>
  RESOURCE_GROUP: <Resource_GroupName>

jobs:
  knowledgebase-update:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in with Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      
      # 現在のファイアーウォールルールを取得する
      - name: Get IP Address For CosmosDB
        id: get-ip-list
        uses: azure/CLI@v2
        with:
          azcliversion: 2.62.0
          inlineScript: |
            CURRENT_IP_LIST=$(az cosmosdb show --name $COMOS_DB_ACCOUNT_NAME --resource-group $RESOURCE_GROUP --query "ipRules[].ipAddressOrRange" -o tsv | tr '\n' ' ')
            echo "CURRENT_IP_LIST=$CURRENT_IP_LIST" >> $GITHUB_ENV

      # 実行時のIPアドレスを追加して更新する
      - name: Add IP Address For CosmosDB
        uses: azure/CLI@v2
        with:
          azcliversion: 2.62.0
          inlineScript: |
            NEW_IP=$(az rest --method get --uri http://ipinfo.io/ip)

            if [ -z "$CURRENT_IP_LIST" ]; then
              NEW_IP_LIST="${NEW_IP}/32"
            else
              NEW_IP_LIST=$(echo "$CURRENT_IP_LIST ${NEW_IP}/32" | tr ' ' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
            fi

            az cosmosdb update --name $COMOS_DB_ACCOUNT_NAME \
              --resource-group $RESOURCE_GROUP \
              --ip-range-filter "$NEW_IP_LIST"
            
            echo "Updated IP rules: $NEW_IP_LIST"
      
      # Goの処理を実行する
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'

      - name: Install dependencies
        working-directory: batch/
        run: |
          go mod tidy

      - name: Run Go script
        working-directory: batch/
        env:
          BACKLOG_API_KEY: ${{ secrets.BACKLOG_API_KEY }}
          BACKLOG_BASE_URL: ${{ secrets.BACKLOG_BASE_URL }}
          AZURE_COSMOS_ENDPOINT: ${{ secrets.AZURE_COSMOS_ENDPOINT }}
          AZURE_COSMOS_ACCOUNT_KEY: ${{ secrets.AZURE_COSMOS_ACCOUNT_KEY }}
          AZURE_COSMOS_CONTAINER_NAME: <ContainerName>
          AZURE_COSMOS_DB_NAME: <DbName>
          AOAI_API_KEY: ${{ secrets.AOAI_API_KEY }}
          AOAI_ENDPOINT: ${{ secrets.AOAI_ENDPOINT }}
        run: |
          go run .

      # CosmosDBファイアーウォールルールを元に戻す
      - name: Reset IP Address For CosmosDB
        uses: azure/CLI@v2
        with:
          azcliversion: 2.62.0
          inlineScript: |
            RESET_IP_LIST=$(echo "$CURRENT_IP_LIST" | tr ' ' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
            az cosmosdb update --name $COMOS_DB_ACCOUNT_NAME \
              --resource-group $RESOURCE_GROUP \
              --ip-range-filter "$RESET_IP_LIST"
            
            echo "Updated IP rules: $RESET_IP_LIST"
```
https://learn.microsoft.com/ja-jp/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#use-the-azure-login-action-with-a-service-principal-secret

https://learn.microsoft.com/ja-jp/cli/azure/cosmosdb?view=azure-cli-latest#az-cosmosdb-update

## さいごに
過去の質問と回答例に類似したものが無いかを探す。といったひと手間が楽になりました。
もっと凝れる箇所は色々とあるので、使いながら改善ポイントに手を入れていこうと思います。特に検索精度はもっと良くできそう...🧐
以上、BacklogとAzureを活用した業務改善ネタでした～！