Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Amazon Bedrock の Tool Use(Function Calling)でプロンプトに応じて処理を振り分ける

はじめに

こんにちは一史です。
最高気温も10℃を下回る日も出てきて、外出する際には、マフラーをするようになりました。
皆様も体調にはお気を付けください。

さて、OpenAIのChatGPTではFunction callingという会話の流れからAIが判断して関数(メソッド)を呼び出す機能がありますが、Amazon BedrockでもTool Useという機能により関数呼び出しをすることができます。
docs.aws.amazon.com

今回はこのTool Useを使って、旅行プランの提案・予約を行う生成AIチャットを作ってみます。
AIエージェントで実現されるような内容ですが、ToolUse(Function calling)が実際にどのように使えるかを生成AIチャットを作り、見ていきます。

概要

本記事ではBedrockで旅行プランの提案・予約を行う生成AIチャットを作り、Tool Useで実際にどのようなことに使えるかを試していきます。

Tool Useとは

Tool Use は何をしてくれるのか

Tool Useとは、事前に定義してある関数をAIが任意に呼び出すことができる機能です。
これにより、インターネットの検索や、データベースの読み取り書き込み、複雑な数値計算など、AI単体ではできなかった処理を、会話の文脈からAIが選び出し、実行することができます。

Tool Use はどのように振る舞うのか

例えばAWS公式のサンプルとして、ラジオ局の人気の曲を返す関数を呼び出しています。
このサンプルでは会話の中から、AIがラジオ局の局名を抽出し、関数に渡すことで、人気の曲名を回答しています。
docs.aws.amazon.com

Tool Use を使って旅行プランの提案・予約を行うプロンプト処理を実現する

概要

今回作成する生成AIチャットの構成図はこちらです。
Bedrockとの会話を行う中で、旅行プラン提案関数、旅行プランの修正関数、予約関数を呼び出していきます。

構成図

具体的な処理の流れは以下です。

  1. ユーザーに「どこで、どんなことをしたいか、出発日はいつごろか」を入力してもらう。
  2. ユーザー入力から旅行の、場所とカテゴリを抽出し、旅行プランをユーザーに提示する。
  3. ユーザーとの会話で適宜以下の処理を呼び出す。
    1. 旅行プランの一部変更。
    2. AIに予約が必要なものを抽出させて、予約する。

実装内容

全体像

今回の実装の全体像はこちらです。全体像を示すために処理の中身は省略してあります。

class ToolUseAdapter:
    """ユーザーの入力から実行するツールを選択するクラス"""

    def define_tools(self) -> list:
        # ToolUseで使用する各関数の定義の一覧を作成する。

    def select_tool(self, prompt: str) -> tuple:
        # ユーザープロンプトに基づいてツールを選択する。
        # 選択した関数名、生成した引数を返す。

    def make_tool_use_response(self, tool_name: str, result_params: dict) -> str:
        # ツール使用結果をもとにユーザーへの回答を作成する。

class ToolUseExecutor:
    """選択されたツールを実行して実行結果をモデルに返すクラス"""

    def execute(self, prompt: str) -> str:
        # select_toolで判定されたツールを実行し、その結果のメッセージを返す。
        # ツールとしてsuggest_base_plan、arrange_plan、reserve、chatのいずれかを実行する。

    def suggest_base_plan(self, destination: str, category: str) -> list:
        # 目的地とカテゴリに基づいて旅行プランを提案する。

    def arrange_plan(self, arranged_plan: list) -> list:
        # 調整された旅行プランを受け取り、返す。

    def reserve(self, reservation_list: list):
        # 予約を行う。

    def chat(self, prompt: str) -> str:
        # ユーザー入力をもとに返答を返す。

def run():
    # 旅行プランの提案と予約処理を実行する。
Tool定義(呼び出される関数の定義)

今回呼び出す関数の定義は、define_toolsメソッド内で以下で定義しています。
ここでは旅行プランの提案、修正、予約用の関数を定義しています。
また、どの関数にも該当せず、返答を生成するだけの処理を選ばせるために、chat関数も定義しています。

    def define_tools(self) -> list:
        # ToolUseで使う各関数の定義を作成する。

        # 旅行プランの提案をする関数の定義。
        base_plan_suggester_def = {
            "toolSpec": {
                "name": "suggest_base_plan",
                "description": "指定された旅行の要件に基づいて旅行プランの一覧を作成する。",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "destination": {
                                "type": "string",
                                "description": "旅行の目的地"
                            },
                            "category": {
                                "type": "string",
                                "description": "旅行のカテゴリ、次のいずれか: 'outdoor', 'culture', 'relaxing', 'food', 'shopping'"
                            }
                        },
                        "required": ["destination", "category"]
                    }
                }
            }
        }

        # 旅行プランの修正をする関数の定義。
        arrange_plan_def = {
            "toolSpec": {
                "name": "arrange_plan",
                "description": "更新した旅行プランを出力する関数",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "arranged_plan_list": {
                                "type": "array",
                                "items": {
                                    "start_time": {
                                        "type": "string",
                                        "description": "時刻 HH:MM形式の文字列"
                                    },
                                    "content": {
                                        "type": "string",
                                        "description": "内容の文字列"
                                    }
                                }
                            }
                        },
                        "required": ["arranged_plan_list"]
                    }
                }
            }
        }

        # 予約をする関数の定義。
        reserve_def = {
            "toolSpec": {
                "name": "reserve",
                "description": "確定した旅行プランの中から、予約が必要な場所の予約を行う。常識的に考えて予約が不要な場所や予約ができない場所は予約しない。",
                "inputSchema": {
                    "json": {
                        "type": "object",
                        "properties": {
                            "reservation_list": {
                                "type": "array",
                                "items": {
                                    "reservation_datetime": {
                                        "type": "string",
                                        "description": "予約の日時 yyyy-mm-dd HH:MM:SS形式"
                                    },
                                    "reservation_dest": {
                                        "type": "string",
                                        "description": "予約した先の店"
                                    }
                                }
                            }
                        },
                        "required": ["reservation_list"]
                    }
                }
            }
        }

        # どの関数定義にも該当しなかった場合Chatを行う関数の定義。
        chat_def = {
                "toolSpec": {
                    "name": "chat",
                    "description": "suggest_base_plan、arrange_plan、reserveの呼び出しに該当しない、自由な回答が必要な場合この関数を呼び出して会話を生成する",
                    "inputSchema": {
                        "json": {
                            "type": "object",
                            "properties": {},
                            "required": []
                        }
                    }
                }
            }

        return [
            base_plan_suggester_def,
            arrange_plan_def,
            reserve_def,
            chat_def
        ]
Toolの判定処理

前節で定義したToolの判定処理と、判定後のツール実行処理について紹介します。

判定は以下コードでBedrockに関数の定義とこれまでの会話履歴(conversation_history)を渡し、判定させています。
また、Tool Useはデフォルトでは、回答の文章か関数呼び出しのためのレスポンスが混在して出力されますが、今回は処理をシンプルにするためにtoolChoiceに"any"を設定することで関数一覧のどれかが必ず選択されるようにしています。

    def select_tool(self, prompt: str) -> tuple:
        # ユーザープロンプトを元に、ツールを決定する。
        self.conversation_history.append({"role": "user", "content": [{"text": prompt}]})
        
        # Bedrock APIを呼び出し、指定されたツールとともにプロンプトを送信してレスポンスを取得する。
        tool_list = self.define_tools()

        response = self.bedrock_client.converse(
            modelId="anthropic.claude-3-haiku-20240307-v1:0",
            messages=self.conversation_history,
            toolConfig={
                "tools": tool_list,
                "toolChoice": {"any": {}}
            }
        )

        tool_use_params = response.get("output", {}).get("message", {}).get("content", [{}])[0].get("toolUse", {})

        function_name = tool_use_params.get("name", "")
        input_params = tool_use_params.get("input", {})
        return function_name, input_params


この判定処理結果の一例を以下に載せます。
以下は「横浜でおいしい中華を食べたい。出発日は1月下旬の土曜日がいいです。」とプロンプトを渡した時に、旅行プラン提案関数が選択されたときのBedrockのレスポンスになります。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_PaQLCs1yTwGy-PejdbG1jw",
          "name": "suggest_base_plan",
          "input": { "destination": "横浜", "category": "food" }
        }
      }
    ]
  }
}

レスポンスを見てみると"toolUse"の中の"name"で呼び出す関数名が、"input"で関数の引数であるdestination(旅の目的地)やcategory(旅行のカテゴリ)が定義通りに生成されていることがわかります。

このselect_toolメソッドを呼び出し、実際に各関数を実行するexecuteメソッドの実装は以下になります。

    def execute(self, prompt: str) -> list:
        # select_toolで判定されたツールを実行し、その結果のメッセージを返す。
        # ツールとしてsuggest_base_plan、arrange_plan、reserve、chatのいずれかを実行する。

        func_name, input_params = self.tool_use_adapter.select_tool(prompt)

        if func_name == "suggest_base_plan":
            # 旅行プランを提案。
            destination = input_params.get("destination", "")
            category = input_params.get("category", "")
            suggested_plan_contents = self.suggest_base_plan(destination, category)
            
            # ユーザーへの回答を生成。
            response_message = self.tool_use_adapter.make_tool_use_response(func_name, {"result": suggested_plan_contents})

        elif func_name == "arrange_plan":
            # 修正されたプランを取得し、提案プランを更新。
            arranged_plan_list = input_params.get("arranged_plan_list", [])
            arranged_plan_list = self.arrange_plan(arranged_plan_list)

            # ユーザーへの回答を生成。
            response_message = self.tool_use_adapter.make_tool_use_response(func_name, {"result": arranged_plan_list})

        elif func_name == "reserve":
            # 予約を行う。
            reservation_list = input_params.get("reservation_list", [])
            self.reserve(reservation_list)
            response_message = "以下の予約が完了しました。\n"
            for res in reservation_list:
                response_message += f"{res['reservation_dest']} ({res['reservation_datetime']})\n"

        else:
            # チャットモードの場合、チャットを行う。
            response_message = self.chat(prompt)

        return response_message

上記実装の"suggest_base_plan"、"arrange_plan"の処理では、ツールの使用結果を以下メソッドで再度Bedrock側に返し、最終的なユーザーへの回答を生成させています。

    def make_tool_use_response(self, tool_name: str, result_params: dict) -> str:
        # ツール使用結果をもとにユーザーへの回答を作成する。

        # assistant, userのメッセージが交互に会話履歴に含まれる必要があるため、両者のメッセージに分けて会話履歴に追加する。
        messages = [
            {
                'role': 'assistant',
                'content': [
                    {
                        'text': f'{name}の結果を返します。'
                    }
                ]
            },
            {
                'role': 'user',
                'content': [
                    {
                        'text': 'あなたが呼び出したツールの使用結果は以下です。この結果をもとにユーザーに回答してください。'
                    },
                    {
                        'text': json.dumps(result_params, ensure_ascii=False)
                    }
                ]
            }
        ]
        
        self.conversation_history.extend(messages)
        
        response = self.bedrock_client.converse(
            modelId="anthropic.claude-3-haiku-20240307-v1:0",
            messages=self.conversation_history
        )

        response_contents = response.get("output", {}).get("message", {}).get("content", [{}])
        response_message = "\n".join([response["text"] for response in response_contents if "text" in response])
        
        return response_message
ツールとなる各関数の処理

前節のToolの判定処理により呼び出される関数は以下です。

まず旅行プラン提案の関数は、旅行先(destination)と旅行のカテゴリ(category)から旅行プランを返します。

    def suggest_base_plan(self, destination: str, category: str) -> list:
        # 目的地とカテゴリに基づいた旅行プランを返す。
        plan = []
        if destination == "横浜":
            if category == "food":
                plan = [
                        {
                            "content": "中華街の正門前で写真撮影",
                            "start_time": "10:30"
                        },
                        {
                            "content": "食べ歩き",
                            "start_time": "10:45"
                        },
                        {
                            "content": "A飯店で食事",
                            "start_time": "12:00"
                        },
                        {
                            "content": "中華街のお土産購入",
                            "start_time": "13:30"
                        },
                        {
                            "content": "赤レンガ倉庫観光",
                            "start_time": "15:00"
                        },
                        {
                            "content": "ホテルにチェックイン",
                            "start_time": "17:30"
                        }
                    ]
        elif destination == "岐阜":
            if category == "culture":
                plan = [
                        {
                            "content": "岐阜城訪問",
                            "start_time": "09:00"
                        },
                        {
                            "content": "金華山ハイキング",
                            "start_time": "11:00"
                        },
                        {
                            "content": "長良川鵜飼観覧",
                            "start_time": "14:00"
                        },
                        {
                            "content": "郡上八幡で街歩き",
                            "start_time": "16:00"
                        },
                        {
                            "content": "温泉宿で宿泊",
                            "start_time": "18:30"
                        }
                ]
        
        elif destination == "北海道":
            if category == "outdoor":
                plan = [
                        {
                            "content": "旭山動物園訪問",
                            "start_time": "08:00"
                        },
                        {
                            "content": "ラーメン村で昼食",
                            "start_time": "13:00"
                        },
                        {
                            "content": "富良野で観光",
                            "start_time": "15:00"
                        },
                        {
                            "content": "温泉ホテルで宿泊",
                            "start_time": "17:30"
                        }
                    ]

        return plan

また旅行プランの修正、予約を行う関数は以下です。
本来は複雑な処理を必要とするところですが本記事の目的ではないので、処理はかなり簡略化したものとしています。

    def arrange_plan(self, arranged_plan: list) -> list:
        # 引数としてAIに生成された、修正した旅行プランを受け取りそれを返す。
        return arranged_plan

    def reserve(self, reservation_list: list):
        # 予約用のAPIを呼び出す。
        pass

プロンプトの会話例

これら機能を使って今回作成する生成AIチャットとの会話例は以下です。

AI: 旅行プランを提案します。どこでどういったことをやりたいか、出発日はいつかを入力してください。
User: 横浜でおいしい中華を食べたい。出発日は1月下旬の土曜日がいいです。

AI: 以下のような横浜での1日の旅行プランをご提案します。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - A飯店で中華料理の昼食
13:30 - 中華街でお土産を買い物
15:00 - 赤レンガ倉庫を観光
17:30 - ホテルにチェックイン
出発日は1月下旬の土曜日を想定しています。中華街での食事やお土産探し、赤レンガ倉庫の見学など、横浜の人気スポットを効率的に回れる1日コースです。ご希望の場合は、このプラン の予約手続きを行うことができます。いかがでしょうか。
User: A飯店以外にどういうお店があるか知りたいので、他の中華料理店の名前と特色を一覧で出してください。

AI: はい、わかりました。他の中華料理店の名前と特色を一覧でお伝えします。
1. 北京ダック専門店 - 北京ダックの調理方法を一流に仕上げており、香り高く、皮はパリッと、肉は柔らかい逸品。
2. 四川料理店 - 四川料理の本格的な辛さと深い旨味が特徴。麻辣な香り豊かな料理を堪能できます。
3. 広東料理店 - 広東の伝統的な調理法を継承し、繊細な味わいと質の高い食材を使用した料理が人気。
4. 点心専門店 - 種類豊富な点心メニューを楽しめる。蒸し点心やフライ点心などの調理技術が高い。
5. 家庭料理店 - 地元の食材を使った家庭的な味わいの料理が自慢。ホームクッキングのような心地良さがある。
以上が他の中華料理店の一覧になります。ご参考にしていただければと思います。
User: 広東料理いいですね、では昼食はそこに変更してください。

AI: はい、ご要望に応じて旅行プランを更新しました。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - 広東料理店で昼食
13:30 - 中華街でお土産を買い物
15:00 - 赤レンガ倉庫を観光
17:30 - ホテルにチェックイン
昼食が A飯店からより本格的な広東料理店に変更になりました。中華街の散策、お土産探し、赤レンガ倉庫での観光などを効率的にまわれる1日コースとなっています。ご希望であれば、この旅行プランの予約手続きを行えます。いかがでしょうか。
User: 赤レンガ観光後にカフェでゆっくりする時間を取りたいです。おすすめのカフェはありますか?

AI: 赤レンガ観光後のカフェ探しですね。以下に、おすすめのカフェをいくつかご紹介します。
1. 横浜赤レンガ倉庫 1号館内のカフェ・コロール
- 赤レンガ倉庫内にあるおしゃれな雰囲気のカフェ
- テラス席もあり、赤レンガの景色を楽しめます
- 洋食メニューやスイーツが人気
2. ベイサイドマリーナ内のカフェ・ルノアール
- 港の景色を眺められる立地のカフェ
- カフェラテやケーキなどのメニューが豊富
- 落ち着いた雰囲気で寛げます
3. 山下公園周辺のスターバックス 山下公園店
- 公園を望むロケーションのスターバックス店
- 大きな窓から自然光が差し込む明るい空間
- 定番のコーヒーやフードメニューを楽しめます
いかがでしょうか。それぞれ雰囲気や眺めも異なるので、お好みに合わせてお選びください。観光のあとゆっくりカフェタイムを過ごせそうですね。
User: ゆっくりできるところなら、どれでもいいので適当に選んで予定に入れてください。

AI: わかりました。ユーザー様のご要望に応じて、以下の旅行プランを作成しました。
10:30 - 中華街の正門前で写真撮影
10:45 - 中華街を食べ歩き
12:00 - 広東料理店で昼食
13:30 - 中華街でお土産を買い物  
15:00 - 赤レンガ倉庫を観光
16:30 - ベイサイドマリーナ内のカフェ・ルノアールでゆっくり過ごす
18:00 - ホテルにチェックイン
赤レンガ倉庫観光後は、カフェ・ルノアールでゆっくりとくつろげるようプランに組み込みました。港の景色を楽しみながら、カフェタイムを過ごすことができます。
ご予約の手続きが必要な場合は、ご連絡ください。
User: いいですね、ではその中で予約が必要なものは予約お願いします。

AI: 以下の予約が完了しました。
広東料理店 (2024-01-27 12:00:00)
ベイサイドマリーナ内のカフェ・ルノアール (2024-01-27 16:30:00)
横浜ホテル (2024-01-27 18:00:00)
User: OKありがとう。

AI: 承知しました。ご旅行の際は、ゆっくりとお楽しみください。 


会話例の結果をもとに、Tool Useを使うことで、期待する処理ができているかを確認してきます。

まず最初の旅行プラン提案の部分ではログから、以下のBedrockからのレスポンスでsuggest_base_plan関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_PaQLCs1yTwGy-PejdbG1jw",
          "name": "suggest_base_plan",
          "input": { "destination": "横浜", "category": "food" }
        }
      }
    ]
  }
}

この生成させたcategoryは、outdoor、culture、relaxing、food、shoppingのどれかの値を取るように関数定義のdescriptionで指定していました。
Bedrockのレスポンスをみると、categoryは期待通り定義したものの中から選ばれているようです。

次に旅行プランの修正の部分ではログから、以下のレスポンスでarrange_plan関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_G7ZCQ6z9SReFgk3_aMdebA",
          "name": "arrange_plan",
          "input": {
            "arranged_plan_list": [
              { "content": "中華街の正門前で写真撮影", "start_time": "10:30" },
              { "content": "食べ歩き", "start_time": "10:45" },
              { "content": "広東料理店で昼食", "start_time": "12:00" },
              { "content": "中華街のお土産購入", "start_time": "13:30" },
              { "content": "赤レンガ倉庫観光", "start_time": "15:00" },
              { "content": "ホテルにチェックイン", "start_time": "17:30" }
            ]
          }
        }
      }
    ]
  }
}

今回、修正した旅行プランをJSON形式の引数で関数に渡す、という定義をしました。
レスポンスの内容をみると、関数に渡された引数は期待するJSON形式で生成されているがわかります。
また、start_timeの時刻形式も指定通りに生成できているようです。

最後に予約では以下のレスポンスでreserve関数が呼ばれました。

{
  "message": {
    "role": "assistant",
    "content": [
      {
        "toolUse": {
          "toolUseId": "tooluse_6Nbd1k3FSzqTuT5rE7DCbw",
          "name": "reserve",
          "input": {
            "reservation_list": [
              {
                "reservation_datetime": "2024-01-27 12:00:00",
                "reservation_dest": "広東料理店"
              },
              {
                "reservation_datetime": "2024-01-27 16:30:00",
                "reservation_dest": "ベイサイドマリーナ内のカフェ・ルノアール"
              },
              {
                "reservation_datetime": "2024-01-27 18:00:00",
                "reservation_dest": "横浜ホテル"
              }
            ]
          }
        }
      }
    ]
  }
}

レスポンスをみると、「1月下旬の土曜日」という部分から2024/1/27を生成でき、予約が必要な場所の抽出も期待通りできていることが確認できます。

まとめ

Amazon Bedrock Tool Useを使うことで、生成AIチャットから関数を呼び出し、その結果をBedrockの回答に反映されることができました。

応用範囲が広い機能なため、是非ご利用ください。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com



Elasticsearchのハイブリッド検索を用いて高精度なRAGを簡単に実現する

こんにちは。
Acroquestのデータサイエンスチーム「YAMALEX」に所属する@shin0higuchiです😊
YAMALEXチームでは、コンペティションへの参加や自社製品開発、技術研究などに日々取り組んでいます。

はじめに

近年、生成AIの発展により、RAG(Retrieval-Augmented Generation)が注目を集めています。RAGは既存の知識ベースから関連情報を検索し、それを基に生成AIが回答を生成する手法です。本記事では、多くの企業ですでに利用されているElasticsearchを使って、シンプルなRAGシステムを構築する方法をご紹介します。

RAGの基本概念

RAGはおおまかに以下の3つのステップで構成されています:

1. 知識ベースの構築(Indexing)
2. 関連情報の検索(Retrieval)
3. LLMによる回答の生成(Generation)

Elasticsearchは特に1と2のステップで強力な機能を提供し、既存のインフラを活用してRAGを実現できる点が魅力です。



より細かく図解するなら以下のようなイメージになるかと思います。


RAGが良く用いられる事例のひとつとして、製品の「よくある質問」のような内容をデータベースに格納しておき、その情報をもとにLLMに回答させるというものがあります。
たとえば「バックアップシステムが動作しない場合の解決方法を教えてください。」「パスワードを忘れた場合のリセット方法は? 」といった、よくある質問をドキュメントとして取り込んでおくのです。
そうすると、ユーザの質問に近いものがあればLLMがその情報に基づいて答えてくれます。

今回はそういった題材をイメージしてお読みいただければと思います。

Elasticsearchを使ったRAGの実装

1. インデックスの設計

まず、ドキュメントを適切に格納するためのインデックスを設計します。
検索においては、より正確性の高いものを少数返すか、正確性の低いものも含めて関連しそうなものを広く返すか、状況によって戦略が異なります。
近年の高精度なLLMによるRAGにおいては、多少関連性の薄いものが混じったとしても、関連のありそうなものをより広く返すのがメジャーです。

そのため、今回はElasticsearchでキーワード検索だけでなく、ベクトル検索を併せて利用するための設定をおこないます。
以下はマッピングの例です。
titleおよびcontentというフィールドをテキスト検索に利用し、embeddingフィールドをベクトル検索用に利用します。

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "content": {
        "type": "text",
        "analyzer": "kuromoji"
      },
      "embedding": {
        "type": "dense_vector",
        "dims": 768
      }
    }
  }
}

2. ドキュメントの登録

テキストデータをElasticsearchに登録する際、以下の処理を行います

1. テキストの前処理(クリーニング、チャンク分割)
2. ベクトル化(文章をベクトルに変換)
3. Elasticsearchへの登録

def index_document(es_client, text, title):
    # テキストをチャンクに分割
    chunks = split_text_into_chunks(text)
    
    # 各チャンクを処理
    for chunk in chunks:
        # ベクトル化(sentence-transformersなどを使用)
        embedding = get_embedding(chunk)
        
        # ドキュメントの作成
        doc = {
            'title': title,
            'content': chunk,
            'embedding': embedding
        }
        
        # Elasticsearchに登録
        es_client.index(
            index='knowledge_base',
            document=doc
        )

3. 検索の実装

ユーザーの入力文字列をキーワード・ベクトルのハイブリッドで検索します。
キーワードとベクトルのスコアの調整は様々な手法がありますが、ここでは最もシンプルにキーワード検索とベクトル検索のスコアを足し合わせています。
RRFという手法を利用して2つの検索ランキングをマージすることなども可能したり、より細かいスコア調整を行うことも可能です。
詳細はこちらのリファレンスを参照してください→ k-nearest neighbor (kNN) search | Elasticsearch Guide [8.17] | Elastic

def hybrid_search(es_client, query):
    # クエリのベクトル化
    query_vector = get_embedding(query)
    
    # ハイブリッド検索のクエリ
    search_query = {
        "query": {
            "script_score": {
                "query": {
                    "match": {
                        "content": query
                    }
                },
                "script": {
                    "source": "cosineSimilarity(params.query_vector, 'embedding') + 1.0",
                    "params": {
                        "query_vector": query_vector
                    }
                }
            }
        }
    }
    
    return es_client.search(
        index='knowledge_base',
        body=search_query
    )


ここで、ベクトルだけを利用して検索した場合と、キーワード検索を組み合わせた場合とで、検索結果の一例を確認してみます。
次の例は、「パスワードの再設定方法を知りたい」で検索し、
「ベクトル検索のみ」と「キーワード+ベクトル検索」のそれぞれでランキング順に結果を並べたものです。

ベクトル検索だけでもある程度近い意味合いの文章が上位にランクインするのですが、パスワード関連の内容が1位に来ていないなど、精度不足が見て取れます。
一方、「キーワード+ベクトル検索」の方では、意図したとおりにパスワード関連の内容が1,2位と上位に来ています。
ベクトル検索も、「パスワード」という直接的な語を含まない類似文章をヒットさせられるという点で有用なのですが、キーワード検索と組み合わせることでさらに良い結果が得られるイメージですね。

順位 ベクトル検索のみ キーワード+ベクトル検索
1 ITサポートに問い合わせる方法を知りたいです。 パスワードを忘れた場合のリセット方法は?
2 パスワードポリシーを設定するにはどうすればよいですか? パスワードポリシーを設定するにはどうすればよいですか?
3 パスワードを忘れた場合のリセット方法は? ITサポートに問い合わせる方法を知りたいです。
4 ユーザーアカウントの権限変更手順について説明してください。 新しいソフトウェアをインストールする際の注意点は?
5 新しいソフトウェアをインストールする際の注意点は? セキュリティの更新手順について教えてください。
6 セキュリティの更新手順について教えてください。 ユーザーアカウントの権限変更手順について説明してください。
7 ネットワークのパフォーマンスが低下した際の対処法は? バックアップシステムが動作しない場合の解決方法を教えてください。
8 クラウドサービスのストレージ容量を確認するには? ネットワークのパフォーマンスが低下した際の対処法は?
9 バックアップシステムが動作しない場合の解決方法を教えてください。 クラウドサービスのストレージ容量を確認するには?
10 システムログを分析してエラーを特定する手順は? システムログを分析してエラーを特定する手順は?

4. LLMとの連携

検索結果を基に、LLMを使って回答を生成します:

def generate_answer(query, search_results):
    # コンテキストの準備
    context = "\n".join([hit["_source"]["content"] for hit in search_results["hits"]["hits"]])
    
    # プロンプトの構築
    prompt = f"""
以下のコンテキストを元に、質問に回答してください。

コンテキスト:
{context}

質問:
{query}
"""


#LLMによる回答生成
return call_llm_api(prompt)


これにより、Elasticsearchが適切な情報をLLMに渡し、回答生成を行ってくれます。

まとめ

Elasticsearchを活用することで、簡単にRAGシステムを構築する方法をご紹介しました。
Elasticsearchは全文検索とベクトル検索のハイブリッド検索を実現することができる検索エンジンです。また、精度の高い日本語検索を実現可能であるため、一般的なベクトルデータベースに比べると、日本語を扱うRAGシステムにおいて大きな強みがあると言えます。

既存のElasticsearchクラスタがある場合は、特に導入のハードルが低くなります。
是非身近なデータを用いて試してみてください。

今回の記事は以上となります。お読みいただきありがとうございました。



Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com



Amazon Bedrock Knowledge Baseのクエリフィルター自動生成で検索の精度を向上させる

こんにちは、機械学習チーム YAMALEX の駿です。
YAMALEX は Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
(詳細はリンク先をご覧ください。)

この記事は Amazon Bedrock Advent Calendar 2024 17日目の投稿です。

re:Invent2024 ではたくさんの新機能が発表されて、あんなこともできるようになった、こんなことも……と興奮が止まらない日々です。

今回はそんなre:Inventで発表されたAmazon Bedrock Knowledge Baseの新機能の一つである、クエリフィルター自動生成を試してみました。

aws.amazon.com

今回試した構成

1. はじめに

1.1. Amazon Bedrock Knowledge Baseとは

Amazon Bedrock Knowledge Base (以下、Knowledge Base)はAmazonが提供する、RAGを簡単に構築するためのサービスです。

概要についてはこちらの記事で説明しているので、RAGやKnowledge Baseになじみのない方はまずはこちらをご一読ください。

acro-engineer.hatenablog.com

1.2. ベクトル検索の欠点

Knowledge Baseではデータベースとしてベクトルデータベースを使うことが多いです。
(ただし、アップデートで構造化されたデータを扱えるRedShiftをデータベースに使用可能になっていましたね)

ベクトル検索は文章の意味を捉えるのが得意なため、多少の表記ゆれや同義語を含んだ文章などでも近しい文章として取得することができる、という利点があります。

その反面、製品番号などで検索をしたい場合は、

  1. 製品番号が完全に一致する製品の情報のみ検索してほしい
  2. 製品番号は一文字違うだけで別の製品になってしまうため、一言一句正確に検索してほしい

などの要望が出てきますが、ベクトル検索は上に書いた利点が仇となり、上記の要望を満たせません。

しかし、今回のクエリフィルター自動生成機能をベクトル検索と組み合わせることで、意味的に近しい文章を取得しながら、 製品番号など正確に検索したい部分は正確に検索することが可能になりました。

2. [新機能]クエリフィルター自動生成とは

Bedrock Knowledge Baseにはドキュメントの同期時に、そのドキュメントの属性をメタデータとして付与する仕組みが2つ用意されています。

  1. 取り込みファイル名.metadata.json という命名メタデータ付与ファイルを用意しておく
  2. 同期時に実行されるLambda内でメタデータを付与する

クエリフィルター自動生成では、上記いずれかの方法でベクトルデータベースに取り込まれたメタデータに対して、絞り込みを行うためのクエリを生成AIを使って自動で生成します。

現在、クエリフィルター自動生成には Anthropic Claude 3.5 Sonnet モデルのみが使用可能です。

たとえば

次のような構造のレコードがBedrock Knowledge Baseで作成したOpenSearch Serverlessのインデックスに保存されているとします。

{
  "AMAZON_BEDROCK_TEXT": """{"製品番号": "A2-B3-C4", "分類": "テレビ", "製品名": "プラズマテレビ", "説明": "鮮やかな色と深い黒を実現するプラズマパネル", "特筆すべき内容": "プラズマパネル", "その他": "高コントラスト"}""",
  "item_id": "A2-B3-C4",
  "category": "テレビ"
}

ここでフィルターに使用できるメタデータitem_idcategory のふたつです。

クエリフィルター自動生成は、ユーザー入力にitem_idcategoryに相当する部分が存在するか否かを判断し、存在した場合、その項目をクエリフィルターとして、検索条件に追加します。

ユーザーが「製品番号:A42-B43-C44の特長を教えて」と聞いた場合、 item_id に相当する項目があるため、生成AIは下記のようなクエリを生成します。

{
  "query": "製品番号:A42-B43-C44の特長を教えて",
  "filter": {
    {"eq": {"key": "item_id", "value": "A42-B43-C44"}}
  }
}

このクエリは、item_idA42-B43-C44と一致するレコードを抽出した後に、「製品番号:A42-B43-C44の特長を教えて」でベクトル検索を行うことを意味するため、 確実に製品番号が一致する検索結果のみが返されることが保証されます。

あくまでベクトル検索に追加するフィルターという立ち位置のため、 ベクトル検索ではヒットしなかったレコードが、この機能を使うことでヒットするようになるわけではありません。

3. 実施

実際に上のような挙動をするか、試してみました。

下記手順で検証します。

  1. データ作成
  2. メタデータ付与Lambda作成
  3. Bedrock Knowledge Base作成
  4. 同期実行
  5. 検索

■使用した環境

No 項目
1 データベース OpenSearch Serverless
2 データ 家電のカタログを想定して生成したCSV(後述)
3 メタデータ 製品番号, 分類
4 クエリフィルター生成モデル Anthropic Claude 3.5 Sonnet V1

3.1. データ作成

データ作成にはAmazonが発表した新しい生成AIモデルである Nova Lite V1を使用しました。

家電のカタログを検索するユースケースを想定し、下のようなデータを生成しました。

製品番号,分類,製品名,説明,特筆すべき内容,その他
A1-B2-C3,テレビ,スマートテレビ,4K解像度で美しい映像を体験できる最新モデル,HDR対応,Bluetooth機能付き
A4-B5-C6,洗濯機,全自動洗濯機,大容量で洗浄力抜群、忙しい朝もラクラク,除菌機能,静音設計
A7-B8-C9,冷蔵庫,冷蔵冷凍庫,新鮮さを保つ冷却技術で食材の鮮度を長持ち,自動製氷機能,エコ運転モード
...

「製品番号で絞り込みたい」、「すぐ温まるドライヤーを調べたいのに、電子レンジが検索結果に含まれて困る」などのケースに対応できるかを確認するため、 「製品番号」、「分類」を列として用意してあります。

作成したCSVはBedrock Knowledge Baseで取り込むため、S3にアップロードしておきます。

3.2. メタデータ付与Lambda作成

次に、上記CSVの一行一行にフィルターするためのメタデータを付与する方法を説明します。

今回は、Knowledge Baseの同期時にLambda関数を実行してメタデータを付与する方法を使用しました。

今回はCSVの1行を1チャンクとし、 その「製品番号」、「分類」列を抽出し、それぞれ item_idcategory としてメタデータを付与します。

下はそのコード中のメタデータ付与を行っている関数です。

def build_contents(file_content: dict) -> list:
    """行ごとに分割、メタデータを付与する"""
    content_metadata = file_content["contentMetadata"]
    content_body = file_content["contentBody"]
    content_type = file_content["contentType"]

    blob = StringIO(content_body)
    reader = csv.reader(blob)
    header = next(reader)
    new_contents = []
    for row in reader:
        # 一行ごとにJSON文字列としてレコードを作成する
        d = {k: v for k, v in zip(header, row)}
        body = json.dumps(d, ensure_ascii=False)

        # 各レコードに対して、製品番号と分類をメタデータとして付与する
        m = copy.deepcopy(content_metadata)
        m["item_id"] = d["製品番号"]
        m["category"] = d["分類"]
        file_content = {
            "contentMetadata": m,
            "contentBody": body,
            "contentType": content_type,
        }
        new_contents.append(file_content)
    return new_contents

このLambdaの中身についてはこのブログの範囲外となるため、詳細は下記ドキュメントをご参照ください。

docs.aws.amazon.com

3.3. Bedrock Knowledge Base作成

下記手順で上記Lambdaを同期時に実行するKnowledge Baseを作成します。

  1. Bedrockコンソールのナレッジベースを開き「ナレッジベースを作成」>「Knowledge Base with vector store」を押下する
  2. 「ナレッジベースの詳細を入力」画面でナレッジベース名など設定を行い「次へ」を押下する
  3. 「データソースを設定」

    1. S3 URIには「3.1.」でCSVをアップロードした先のS3 URIを指定する
    2. Transformation functionとして「3.2.」でデプロイしたLambdaを指定する

      Transformation functionを設定する

    3. 「次へ」を押下する

  4. 組み込みモデルは「Titan Text Embeddings v2」、ベクトルデータベースは「新しいベクトルストアをクイック作成」を選択し、「次へ」を押下する

3.4. 同期実行

「3.3.」で作成されたデータソースを選択し、「同期」を実行します。

同期時に、クローリング後、インデクシング前に Transformation functionに設定したLambdaが実行されます。

ステータスが「Available」になるのを待ってください。

投入したデータはOpenSearch Serverlessのインデックスから確認できます。

item_idcategory がそれぞれ付与されているのが分かります。

{
  "took": 77,
  "timed_out": false,
  "_shards": {
    "total": 0,
    "successful": 0,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 35,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "bedrock-knowledge-base-default-index",
        "_id": "1%3A0%3Apbf3kpMB_yTemhSJpjEn",
        "_score": 1,
        "_source": {
          "AMAZON_BEDROCK_TEXT": """{"製品番号": "A4-B5-C6", "分類": "洗濯機", "製品名": "全自動洗濯機", "説明": "大容量で洗浄力抜群、忙しい朝もラクラク", "特筆すべき内容": "除菌機能", "その他": "静音設計"}""",
          "item_id": "A4-B5-C6",
          "category": "洗濯機"
        }
      },
      {
        "_index": "bedrock-knowledge-base-default-index",
        "_id": "1%3A0%3Ap7f3kpMB_yTemhSJpjEn",
        "_score": 1,
        "_source": {
          "AMAZON_BEDROCK_TEXT": """{"製品番号": "A10-B11-C12", "分類": "電子レンジ", "製品名": "オーブンレンジ", "説明": "高速加熱で時短調理が叶う、便利な機能満載", "特筆すべき内容": "フラットテーブル", "その他": "自動メニュー"}""",
          "item_id": "A10-B11-C12",
          "category": "電子レンジ"
        }
      },
...

3.5. 検索

Knowledge Baseの準備ができたので、実際に検索をしてみましょう。

検索は下記2パターンで、Pythonのboto3を使って試しました。

  1. クエリフィルター自動生成を使わない場合

     response = bedrock_agent.retrieve(
         knowledgeBaseId=KNOWLEDGE_BASE_ID,
         retrievalQuery={"text": query},
     )
    
  2. クエリフィルター自動生成を使う場合

     response = bedrock_agent.retrieve(
         knowledgeBaseId=KNOWLEDGE_BASE_ID,
         retrievalQuery={"text": query},
         retrievalConfiguration={
             "vectorSearchConfiguration": {
                 "implicitFilterConfiguration": {
                     "metadataAttributes": [
                         # ユーザークエリから抽出するメタデータの候補を記述します
                         {
                             "key": "item_id",
                             "type": "STRING",
                             "description": "id of the item.  製品番号. e.g. A45-B46-C47, A2-B3-C4, D1-E2-F3, ..."
                         },
                         {
                             "key": "category",
                             "type": "STRING",
                             "description": "category of the item. enum: テレビ, 洗濯機, 冷蔵庫, 電子レンジ, 掃除機, エアコン, 炊飯器, コーヒーメーカー, オーブンレンジ, ドライヤー"
                         },
                     ],
                     "modelArn": "anthropic.claude-3-5-sonnet-20240620-v1:0"
                 }
             }
         }
     )
    

3.4. 結果

  1. 製品番号でフィルター出来る「製品番号:A42-B43-C44の特長を教えて」

    1. 使わない場合

       [
        {
         "content": {"text": "{\"製品番号\": \"A39-B40-C41\", \"分類\": \"冷蔵庫\", \"製品名\": \"冷蔵冷凍庫\", \"説明\": \"新鮮さを保つ冷却技術で食材の鮮度を長持ち\", \"特筆すべき内容\": \"自動製氷機能\", \"その他\": \"エコ運転モード\"}"},
         "metadata": {"category": "冷蔵庫", "item_id": "A39-B40-C41"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A41-B42-C43\", \"分類\": \"掃除機\", \"製品名\": \"コードレス掃除機\", \"説明\": \"軽量で使いやすく、様々な床材に対応\", \"特筆すべき内容\": \"HEPAフィルター\", \"その他\": \"コードレス\"}"},
         "metadata": {"category": "掃除機", "item_id": "A41-B42-C43"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A2-B3-C4\", \"分類\": \"テレビ\", \"製品名\": \"プラズマテレビ\", \"説明\": \"鮮やかな色と深い黒を実現するプラズマパネル\", \"特筆すべき内容\": \"プラズマパネル\", \"その他\": \"高コントラスト\"}"},
         "metadata": {"category": "テレビ", "item_id": "A2-B3-C4"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A42-B43-C44\", \"分類\": \"エアコン\", \"製品名\": \"ルームエアコン\", \"説明\": \"快適な温度を保つ高効率モデル\", \"特筆すべき内容\": \"除湿機能\", \"その他\": \"リモコン操作\"}"},
         "metadata": {"category": "エアコン", "item_id": "A42-B43-C44"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A1-B2-C3\", \"分類\": \"テレビ\", \"製品名\": \"スマートテレビ\", \"説明\": \"4K解像度で美しい映像を体験できる最新モデル\", \"特筆すべき内容\": \"HDR対応\", \"その他\": \"Bluetooth機能付き\"}"},
         "metadata": {"category": "テレビ", "item_id": "A1-B2-C3"}
        }
       ]
      
    2. 使った場合

       [
        {
         "content": {"text": "{\"製品番号\": \"A42-B43-C44\", \"分類\": \"エアコン\", \"製品名\": \"ルームエアコン\", \"説明\": \"快適な温度を保つ高効率モデル\", \"特筆すべき内容\": \"除湿機能\", \"その他\": \"リモコン操作\"}"},
         "metadata": {"category": "エアコン", "item_id": "A42-B43-C44"}
        }
       ]
      

    使わない場合は、欲しかった製品番号の製品が4番目に来ているのに対し、クエリフィルター自動生成を使った場合は、欲しい商品のみが取得できています。

  2. カテゴリでフィルターできる「忙しい朝でも早く乾かせるドライヤー」

    1. 使わない場合

       [
        {
         "content": {"text": "{\"製品番号\": \"A36-B37-C38\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A36-B37-C38"}
         }
        },
        {
         "content": {"text": "{\"製品番号\": \"A28-B29-C30\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A28-B29-C30"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A46-B47-C48\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A46-B47-C48"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A30-B31-C32\", \"分類\": \"電子レンジ\", \"製品名\": \"オーブンレンジ\", \"説明\": \"高速加熱で時短調理が叶う、便利な機能満載\", \"特筆すべき内容\": \"フラットテーブル\", \"その他\": \"自動メニュー\"}"},
         "metadata": {"category": "電子レンジ", "item_id": "A30-B31-C32"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A40-B41-C42\", \"分類\": \"電子レンジ\", \"製品名\": \"オーブンレンジ\", \"説明\": \"高速加熱で時短調理が叶う、便利な機能満載\", \"特筆すべき内容\": \"フラットテーブル\", \"その他\": \"自動メニュー\"}"},
         "metadata": {"category": "電子レンジ", "item_id": "A40-B41-C42"}
        }
       ]
      
    2. 使った場合

       [
        {
         "content": {"text": "{\"製品番号\": \"A36-B37-C38\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A36-B37-C38"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A28-B29-C30\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A28-B29-C30"}
        },
        {
         "content": {"text": "{\"製品番号\": \"A46-B47-C48\", \"分類\": \"ドライヤー\", \"製品名\": \"ヘアドライヤー\", \"説明\": \"速乾で髪のダメージを軽減、使いやすい設計\", \"特筆すべき内容\": \"イオン機能\", \"その他\": \"軽量設計\"}"},
         "metadata": {"category": "ドライヤー", "item_id": "A46-B47-C48"}
        }
       ]
      

    使わなかった場合は、電子レンジなどドライヤーが関係のない製品が検索結果に含まれていますが、クエリフィルター自動生成のおかげて、ドライヤーのみが検索結果となっています。

まとめ

今回は、Amazon Bedrock Knowledge Baseの新機能である、クエリフィルター自動生成を試して、どれほど検索の精度が上がるのかを確認しました。

今までベクトル検索が苦手としていたキーワードでの一字一句一致する検索を検索時の設定を追加するだけで解決できるので非常に有用だと思います。

注意事項は下記2点です。

  1. メタデータは事前に付与しておく必要があります。

    ドキュメント毎に.metadata.json で付与するか、今回のようにLambdaを実行して付与する必要があります。

  2. 生成AIが生成したクエリは検索のレスポンスから知ることができません。

    精度が出ないときに実際に使われたクエリフィルターの内容が確認できないのは、デバッグする上で不便ですが、 使用しているプロンプトは公開されているため、そちらで別途出力させて確認が可能です。

re:Invent2024では、この他にも生成AI周りで多くのアップデートが発表されているため、情報キャッチアップして、取り入れていきたいですね。

Acroquest Technologyでは、キャリア採用を行っています。
  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

Amazon Bedrock の新モデル Amazon Nova の精度を確認してみた

はじめに

急に冬らしい寒さを感じるようになってきました。
データ分析エンジニアの木介です。

Amazon Bedrock Advent Calendar 2024 シリーズ2の16日目のブログ記事になります。

qiita.com

今回は12月のAWS re:Invent 2024にて発表のあったAWSの最新LLMモデルAmazon Novaを触っていきたいと思います。

www.aboutamazon.com

概要

Novaとは

Amazon Novaは12月に発表されたAmazonが新たに提供する新たな生成AIモデルファミリーです。
テキスト、画像、動画といったマルチモーダルなコンテンツの入力が可能なモデルと、画像や動画の生成が可能なモデルが発表されています。
AWSの生成AIプラットフォームであるAmazon Bedrockを通じて利用することができます。

aws.amazon.com

Novaで出来ること

Amazon Novaの生成AIモデルファミリーとして現状6種類が発表されており、それぞれ以下の形で、マルチモーダル対応のテキストの生成、画像・動画の生成などを行うことができます。

モデル名 入力可能なデータ 出力データ 概要
Amazon Nova Micro テキスト テキスト テキスト生成に特化し、迅速な応答と低コストを実現。
Amazon Nova Lite テキスト、画像、動画 テキスト マルチモーダル対応のモデルで、経済的な選択肢。
Amazon Nova Pro テキスト、画像、動画 テキスト 精度、速度、価格のバランスが取れたマルチモーダルモデルで、多様なタスクに対応。
Amazon Nova Premier - - より複雑な推論タスクに対応できるモデル。現在も学習中であり2025年に提供予定とのこと。
Amazon Nova Canvas テキスト 画像 テキストプロンプトから画像を生成し、透かし機能で責任あるAIの使用を促進。
Amazon Nova Reel テキスト、画像 動画 テキストや画像から6秒間の動画を生成し、製品紹介などに活用可能。

aws.amazon.com

使い方

Amazon Bedrockのコンソール画面より利用したいモデルのアクセス権を申請することで利用することが可能です。
執筆時(2024年12月)ではus-east-1のみで利用可能なようです。

docs.aws.amazon.com

利用方法としては、API 呼び出しでテキスト・画像・動画生成のすべてが可能となっています。
Pythonでは以下の形で実装を行うことで呼び出すことができます。

import boto3
import json

client = boto3.client("bedrock-runtime")

system = [{ "text": "あなたは便利なAIアシスタントです。" }]

messages = [
    {"role": "user", "content": [{"text": "世界で一番高い山は?"}]},
]

inf_params = {"maxTokens": 300, "topP": 0.1, "temperature": 0.3}

additionalModelRequestFields = {
    "inferenceConfig": {
         "topK": 20
    }
}

model_response = client.converse(
    modelId="us.amazon.nova-lite-v1:0", 
    messages=messages, 
    system=system, 
    inferenceConfig=inf_params,
    additionalModelRequestFields=additionalModelRequestFields
)

print("\n[Full Response]")
print(json.dumps(model_response, indent=2))

print("\n[Response Content Text]")
print(model_response["output"]["message"]["content"][0]["text"])

InvokeModel、ConverseModeに対応しているため他のBedrockの基盤モデルと同様に簡単に呼び出すことができます。

また、以下のAmazon BedrockのPlaygroundで手軽に試してみることもできます。


他モデルとの比較

以下の表がAmazon Novaのモデルファミリーと他モデルとの簡単な比較になります。

テキスト生成モデル

モデル名 入力コスト 出力コスト 画像入力 動画入力
Nova Micro $0.035/1M token $0.140/1M token × ×
Nova Lite $0.060/1M token $0.240/1M token
Nova Pro $0.800/1M token $3.200/1M token
gpt-4o-mini $0.150/1M token $0.600/1M token ×
gpt-4o $2.500/1M token $10.000/1M token ×
Claude 3.5 Sonnet $3.000/1M token $15.000/1M token ×
Claude 3.5 Haiku $0.800/1M token $4.000/1M token ×
Claude 3 Ops $15.000/1M token $75.000/1M token ×
Gemini 1.5 Flash $0.075/1M token $0.300/1M token
Gemini 1.5 Pro $1.250/1M token $5.000/1M token

画像・動画生成モデル

モデル名 生成コスト
Nova Canvas $0.04~0.06 / image
Nova Reel $0.08 / second

aws.amazon.com

Nova ProでもClaude 3.5 Haikuと同程度のコストで扱うことが出来るので、コストパフォーマンスでは群を抜いている形となっています。

また、入力のモーダルとして動画を取り扱っているのもGeminiとAmazon Novaのみであるため、動画の要約などを行う場合には選択肢として上がるのではないでしょうか?

Amazon Nova の精度を確認してみる

では次に実際にAmazon Novaモデルを使ってみましょう。
今回はテキスト生成モデルとしてNova MicroとNova Proを、画像生成、動画生成を試すためにNova Canvas、Nova Reelを利用しました。

また、比較のためにClaude 3.5 Sonnet、GPT 4o、Gemini Proでのテキスト生成例も見ていきたいと思います。

実験環境としては以下のPlaygroundでそれぞれ検証を行いました。

モデル Playground
Amazon Nova Amazon Bedrock Playground
Claude 3.5 Sonnet Amazon Bedrock Playground
GPT 4o OpenAI Platform
Gemini Pro Google AI Studio

1.特殊な日本語の理解

さて、ではまず以下の簡単な質問でAmazon Novaで日本語の回答が出来るかを確認してみます。

質問

世界で一番高い山は?

Nova Micro

世界で最も高い山はエベレスト峰(Mount Everest)です。エベレストはヒマラヤ山脈にあり、ネパールと中国(チベット自治区)の国境にまたがっています。エベレストの標高は約8,848.86メートル(29,031.7フィート)で、地球上の最も高い地点として知られています。エベレストの登頂は多くの登山家にとって挑戦的な目標であり、その登山には高度病、悪天候、そして厳しい環境条件に対処する必要があるため、多くの準備と専門的なトレーニングが必要です。

Nova Pro

世界で一番高い山は、エベレスト山(Mount Everest)です。その標高は8,848.86メートルで、ヒマラヤ山脈に位置しています。エベレスト山はネパールとチベット(中国)の国境にまたがっており、ネパール側ではサガルマタ(Sagarmatha)、チベット側ではチョモランマ(Qomolangma)と呼ばれています。
エベレスト山は地球上で最も高い峰であり、多くの登山者にとって究極の挑戦とされています。しかし、その高さゆえに登頂は非常に危険を伴い、厳しい気象条件や高所障害などのリスクが常に存在します。

日本語での質問に対しても正しく返答出来ていることが分かります。

では難易度を上げて以下の質問をしてみましょう
こちらについては他のモデルとの比較も行ってみます。

質問

回文を1つ作成して

Nova Micro

回文例:「さくらさく」。

Nova Pro

もちろんです。以下は回文の例です:
「さけますけさ」
これは前後対称の文字列で、回文の条件を満たしています。


Claude 3.5 Sonnet

はい、以下の回文を作成しました:
「なるとるなる」
これは前から読んでも後ろから読んでも同じ言葉になります。

GPT 4o

もちろんです!こちらが回文です:
たけやぶやけた
この文は前から読んでも後から読んでも同じです。

Gemini Pro

たけやぶやけた

GPT 4o、Gemini Proのみが正しく回答出来ていて、他の問題では回文になっていませんでした。
何度か試しましたが、Nova Micro , Proでは回文は作成できませんでした。

また、以下が上記の回答を得るのにかかった時間になります。

モデル 回答時間(ms)
Nova Micro 208
Nova Pro 669
Claude 3.5 Sonnet 1447
GPT 4o 919
Gemini Pro 700

環境にもよると思いますが、Nova Microが最も早く、次点でNova Pro、Gemini Proと続く形でした。

2.画像認識

次に画像を入力して説明をさせてみた結果です。
以下が入力した画像になります

上記の画像を説明させた結果が以下になります。

Nova Pro

"この画像は、牧草地で放牧されている馬の群れを捉えた風景写真です。以下に画像の詳細な説明を示します:

画像の要素
馬の群れ:

画像の中央から右側にかけて、複数の馬が放牧されています。
馬の毛色は様々で、白、黒、芦毛(あしげ、薄茶色)、鹿毛(かげ、茶色)などがあります。
馬たちは草を食べており、お互いに近い距離で放牧されています。
牧草地:

馬がいる場所は広大な牧草地で、緑の草が豊かに生えています。
地面は平坦で、特に目立った障害物や起伏はありません。
背景:

牧草地の背後には、低い山が広がっています。山の斜面は緑の植生で覆われています。
山の頂上付近には雲がかかっており、青空とのコントラストが美しいです。
木々:

山の麓には、密集した木々が生えており、牧草地を囲むように立っています。
木々の種類は特定できませんが、葉が緑で、比較的高さのある木々です。
建物:

左側の背景には、赤い屋根の小さな建物が一つ見えます。これはおそらく農家や牧場の建物でしょう。
空:

空は青く、いくつかの白い雲が浮かんでいます。雲は主に山の頂上付近に集中しており、空の残りの部分は比較的澄んでいます。
雰囲気
この画像は、のどかな田舎の風景を捉えており、馬の群れが牧草地で自由に過ごしている様子が伝わってきます。
自然の美しさと馬の優雅な姿が調和しており、平和で穏やかな雰囲気を醸し出しています。"


Claude 3.5 Sonnet

この画像は美しい田舎の風景を捉えています。
前景には、広々とした緑の牧草地があり、そこで数頭の馬が放牧されています。馬たちは様々な色をしており、白、茶色、黒などの馬が見られます。馬たちは草を食べたり、のんびりと過ごしているように見えます。
中景には、豊かな緑の木々が生い茂る森があります。左側には赤い屋根の小さな納屋または小屋が見えます。
背景には、雄大な山が見えます。山の斜面は緑に覆われており、自然の豊かさを感じさせます。
空は明るい青色で、白い綿のような雲が浮かんでいます。全体的に、この風景は平和で牧歌的な雰囲気を醸し出しています。自然の美しさと静けさが印象的な画像です。

GPT 4o

この画像には、緑豊かな牧草地で草を食べる数頭の馬が写っています。背景には緑の木々と山が見え、青い空に白い雲が浮かんでいます。景色はとても穏やかで自然の美しさを感じさせます。左側には赤い屋根の小さな建物もあります。

Gemini Pro

"この写真は、牧歌的な田園風景に馬の群れが放牧されている様子を写したものです。

前景: 草の茂った緑の牧草地には、様々な色の馬が数頭います。白、茶、黒の馬が草を食べている様子が写っています。

中景: 牧草地と背景の森との間に、フェンスらしきものが見えます。

背景: 緑が生い茂った木々のラインが見えます。木々の色は濃い緑で、健康で豊かな印象を与えます。遠くには大きな山があり、緑の斜面が青く澄んだ空に映えています。空には、白い綿雲が浮かんでいます。左側には、赤い屋根の小さな小屋または納屋が見えます。

全体的な印象: この写真は静かで平和な田園風景を捉えています。鮮やかな緑の草、健康な木々、青い空、そして自由に草を食む馬たちは、のどかで牧歌的な雰囲気を作り出しています。自然光が十分に降り注ぎ、シーン全体に暖かみと生命力をもたらしています。

写真から読み取れるその他の詳細:

時間帯: 空の青さと雲の様子から、おそらく日中の晴れた日でしょう。

場所: 山と牧草地の風景から、おそらく山間部か高原地域でしょう。

雰囲気: 写真全体から、静寂、自然、調和といった雰囲気が伝わってきます。

この写真は、自然の美しさと動物たちの平和な共存を美しく捉えた一枚と言えるでしょう。"

Amazon Novaでも画像の内容を説明できていますが、他モデルと比べても、内容が詳細に説明されていました。
逆に、GPT 4o は、間違いはないですが、だいぶ内容が薄い結果となりました。

3.画像生成

次にAmazon Novaの画像生成モデルであるNova Canvasを試していきたいと思います。

こちらは、テキストもしくは画像を入力することで画像を生成することが可能です。

docs.aws.amazon.com

今回はテキストでの指示で画像を生成してみました。
残念ながらプロンプトは英語のみを受け付けるとのことですので、以下のプロンプトを入力として生成を行いました。

プロンプト

A tranquil riverside at dawn, shrouded in soft morning mist. The crystal-clear water flows gently, creating ripples that expand across the surface. Above, a flock of birds soars in graceful arcs, their reflections dancing on the water below. The surrounding trees sway softly in the breeze, embodying the rhythm of nature.

生成された画像が以下になります。

指示通りの風景を良い感じに生成することが出来ました。

4.動画生成

最後に動画生成モデルであるAmazon Nova Reelを使っていきます。
制限として以下のモノがありました。

  • プロンプト:英語のみ、512文字以内
  • ファイル形式:png,jpeg
  • その他: pngではアルファチャネルの対応不可
  • 動画時間:最大9秒

docs.aws.amazon.com

こちらの画像と以下のプロンプトを入力することで動画を作成することが出来ました。

入力した画像

プロンプト

Create a short video of several horses grazing peacefully in a lush green meadow under a clear blue sky, with gentle wind moving the grass.

動画はS3バケットに出力される形となっています。
自然な形の動画に仕上がっていました。


※容量の関係でサイズを縮小して表示しています。

まとめ

re: Invent 2024で発表のあったAmazon Novaについて紹介をしました。
マルチモーダルなテキスト生成から画像・動画生成まで一通りのニーズには対応しているモデルであり、コストパフォーマンスのよいものとなっていることが分かりました。
2025年にはさらに音声などにも対応するとのことですので、今後の発表にも注目していきたいですね。



Acroquest Technologyでは、キャリア採用を行っています。
  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。
www.wantedly.com

Elastic CloudでObservabilityを簡単に始める 2024年版

こんにちは、Elastic認定資格3種(※)を保持しているノムラです。
※Elastic社の公式認定資格(Elastic Certified Engineer / Elastic Certified Analyst / Elastic Certified Observability Engineer)

Elastic Stack (Elasticsearch) Advent Calendar 2024の13日目のブログ記事になります。
qiita.com

Elastic CloudはElastic社が提供しているSaaSサービスで、クラウドプロバイダはAWS、Azure、GCP等をサポートしています。
最新バージョンのクラスタ構築や、既存クラスタのバージョンアップを数クリックで実施できるため、導入がお手軽です。

本記事ではElastic Cloudを利用してAWSのリソース・メトリック監視/ログ監視/死活監視を簡単に始める方法について手順ベースで紹介します。

はじめに
1. Elastic AWS Integrationのインストール
2. リソース・メトリック監視
3. ログ監視
4. 死活監視
まとめ

はじめに

以下本記事ではElastic Cloud(Elasticsearch Service)の名称を統一して「Elastic Cloud」で記載します。
またElastic Cloudと監視対象のAWS各サービスの構築手順/権限設定については割愛します。

Elastic Cloudの構築手順について不明な方は、以下の記事を参照ください。
AWSでElastic Cloudを利用する 2024年版(構築編) - Taste of Tech Topics
AzureでElastic Cloudを利用する 2024年版(構築編) - Taste of Tech Topics

構成イメージ

今回Elastic Agentを利用してAWSのリソース・メトリック監視/ログ監視/死活監視を実現します。

構成イメージ

1.Elastic AWS Integrationのインストール

(1) 関連ダッシュボード等のインストール

「Install AWS」を押下し関連資材をインストールします

インストールすると関連ダッシュボード/APIリファレンス等が表示されます

(2) EC2にElastic Agentをインストール

「Add Amazon EC2」を押下し遷移後、画面下部の「Install Elastic Agent」を押下しAgentのインストールを開始します


手順「①Install Elastic Agent on your host」に従って、EC2上でコマンドを実行しElastic Agentをインストールします。
手順通り実行すると、以下の通りAgentが登録されます。

(3) AWS Integrationの設定

そのまま画面下部の「Add the integration」を押下すると、AWS Integrationの設定画面に遷移します。
今回は以下の設定を行います。

  1. Collect EC2 logs from CloudWatchをオンに設定
  2. CloudWatch Logs の監視対象ログが登録されているARNを設定
  3. Collect EC2 metricsのCollection Periodを「1m」に設定
  4. Advanced Settingからアクセスキーとシークレットキーを入力

「Save and Continue」を押下後、「Save and deploy changes」を押下しAWS Integrationの設定をElastic Agentに反映します。

(4) AWS Integrationによるデータ登録の確認

上記までの操作でElastic Agent経由でEC2のメトリクスと、CloudWatch Logsに格納されているEC2のログが確認可能になりました。

2.リソース・メトリック監視

まずは登録したEC2のリソース・メトリクスを確認していきます。

(1) Kibanaダッシュボード画面にアクセス

Kibana画面左のメニューから[Dashboards]を選択し「[Elastic Agent] Input Metrics」を押下すると以下のダッシュボードが表示されます。

このダッシュボードからサービス毎のダッシュボードに遷移可能です。
今回はEC2とCloudWatchのみを対象にリソース・メトリックを収集していますが、他クラウド環境/オンプレ環境のリソース情報も集約して監視/分析可能な点がElastic Cloud(Elastic Stack)の利点だと思います。

(2) EC2のリソース・メトリックを確認

同様に「[Metrics AWS] EC2 Overview」ダッシュボードを利用すると、より詳細にEC2インスタンス毎のCPU使用率等のメトリクスを確認することが可能です。

3.ログ監視

ログ監視にはLogs Explorerを利用します。

(1) Logs Explorerによるログ監視

Logs Explorerでは以下のようにログのタイムスタンプとそのSummaryが表示されます。何か問題が発生した際にはログメッセージやステータスを絞りこんで分析していくことで状況確認に利用可能です。
キャプチャでは特定ホストのErrorログのみを表示しています。

Elasticsearchの特徴になりますが、大量のログに対して絞り込みを行っても素早くレスポンスが返ってくるのは業務上快適だと思います。
このようにLogs Explorerで簡単にログの確認/分析が可能です。

ただし実際の運用では常時ダッシュボード/Logs Explorerを監視しているわけにはいきません。
そのためAlertsを設定することで特定条件での検知/通知が可能です。
Alertsの詳細/設定方法については以下ドキュメントをご参照ください。
Create and manage rules | Elastic Observability [8.16] | Elastic

4.死活監視

最後に死活監視も実施していきます。死活監視にはSynthetic monitoringの機能を利用します。
Synthetic monitoringの詳細については以下のドキュメントをご参照ください。
Synthetic monitoring | Elastic Observability [8.16] | Elastic

(1) Synthetic monitoring画面へアクセス

Kibana画面左のメニューから[Applications] ⇒ [SYNTHETICS] ⇒ [Monitors]を選択しSynthetic monitoringの画面へ遷移します。

(2) 監視対象を設定

画面上部の「select a different monitor type」を押下し、以下の通り設定し「Create Monitor」を押下します。

項目名 設定値 今回の設定値
Select a monitor type HTTP PingTCP Ping HTTP Ping
URL <監視対象URL> LogstashサーバURL
Monitor name 任意の名前 LogstashServer
Locations 監視対象が起動している地域 US East

(3) 監視結果を確認

以下のように監視対象の死活状態が表示されます。

Synthetic monitoringによってKibana画面の設定のみで対象ホストの死活監視が可能になり、簡単に死活監視が実現可能になりました。
(これまではHeartbeatというBeatsを監視対象サーバにインストールする必要がありました)

(4) 検知/通知の設定

Synthetic monitoringではKibana画面から簡単にAlertsのオン・オフが可能です。
画面の「LogstashServer」を押下するとキャプチャのように詳細画面が表示されます。

詳細画面右上の「Enable(all location)」のトグルをオン・オフすることでAlertsを有効化・無効化できます。
メールやSlackなどで通知が簡単に飛ばすことができ便利です。

まとめ

Elastic Cloudで簡単にAWS上のサービスに対してObservabilityを実現できました。
途中にも記載しましたが他クラウド環境/オンプレ環境の情報も集約してリソース・メトリック監視/ログ監視/死活監視を実現できるのはElastic Cloud(Elastic Stack)の利点だと思います。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com



ElasticsearchでLIKE検索のような部分一致検索を高速に実現する方法

この記事は
Elastic Stack (Elasticsearch) - Qiita Advent Calendar 2024 - Qiitaの11日目の記事です。

はじめまして。テクニカルコンサルタントの江見と申します。
普段はElasticsearchに関するコンサルティング業務に携わっております。

業務の中で、RDBMySQLPostgreSQLなど)の検索機能に関する課題として、「LIKE検索の速度が遅い」という声を多くいただきます。
特に、大量のデータを扱うシステムでは、LIKE検索が原因でパフォーマンスが低下し、検索レスポンスの遅延が問題となることが少なくありません。その解決策として、RDBからElasticsearchへの移行を検討されるケースが増えています。

Elasticsearchは、高速で柔軟な全文検索が可能な強力な検索エンジンです。ただし、その性能を十分に引き出すためには、適切なデータ設計やクエリ設計が重要です。

本記事では、「SQLのLIKE検索による部分一致検索をElasticsearchで高速に実現する方法」に焦点を当てて解説します。

1.データ型の違いについて

Elasticsearchで文字列検索をする前に、まず理解しておきたいのはkeyword型とtext型の違いです。
Elasticsearchに文字列型フィールドを登録する際にはどちらの型として登録するかを事前に設定する必要があります。

keyword型:正確な文字列一致向け

keyword型は文字列をそのままの形でインデックスに登録する型です。
完全一致検索やソート、集計といった操作を高速に行うことができるため、IDや商品カテゴリのような短い文字列などはkeyword型で登録します。

text型:フルテキスト検索向け

text型は自然言語処理やフルテキスト検索を得意とする型です。
この型に設定されたフィールドはインデックスに登録時に、Analyzerによって文章を単語やフレーズごとに分割され登録されます。

どのような規則で文章をトークン化するかはAnalyzerによって決まり、検索要件に応じて適切なAnalyzer設計が必要になります。
analyzer | Elasticsearch Guide [8.16] | Elastic

文字列がどのようにトークン化されて登録されるかはAnalyze APIを使って確認ができます。
以下はデフォルトのAnalyzer(standard Analyzer)でトークン化をした例です。

POST _analyze
{
  "text":     "Elasticsearch is powerful"
}

以下のように、「elasticsearch」「is」「powerful」のように分割されているのがわかります。

{
  "tokens": [
    {
      "token": "elasticsearch",
      "start_offset": 0,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "is",
      "start_offset": 14,
      "end_offset": 16,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "powerful",
      "start_offset": 17,
      "end_offset": 25,
      "type": "<ALPHANUM>",
      "position": 2
    }
  ]
}

部分一致検索の対象となるフィールドはtext型で登録することが基本となります。

2.部分一致検索を実現する方法と特徴

keyword型での部分一致検索

keyword型のフィールドでもwildcardクエリを使用することで部分一致での検索は可能です。
wildcardクエリはLIKE検索と同様に特定のパターンに一致する文字列を検索するクエリでSQLのLIKE検索と直感的に近い検索方法です。
ただし検索時の計算コストが非常に高いためインデックスサイズが大きくなればなるほど、検索システムに悪影響を及ぼす可能性が高くなります。

wildcardクエリの例(”Elasticsearch”から始まる文字列を検索する場合)

GET test_index/_search
{
  "query": {
    "wildcard": {
      "message": {
        "value": "Elasticsearch*"
      }
    }
  }
}

text型での部分一致検索

text型で部分一致検索を行う場合はmatchクエリまたはmatch_phraseクエリを使用するのが一般的です。
text型のフィールドがトークン化されてインデックスに登録されるのと同じく、
検索文字列もトークン分割し、検索対象フィールドと検索文字列それぞれのトークン同士が一致していればヒットするクエリです。
matchクエリは単語検索、match_phraseクエリはフレーズ検索に適しています。

matchクエリの例(”Elasticsearch”から始まる文字列を検索する場合)

GET test_index/_search
{
  "query": {
    "match": {
      "message": "Elasticsearch"
    }
  }
}

3.適切なAnalyzerの設計

Elasticsearchで文字列検索を行う場合はmatchやmatch_phraseクエリを使うのが基本ですが、Analyzerを適切に設定していない場合想定している結果が返ってこない、逆に多くの検索結果が返ってきてしまうなどの問題が発生します。

例えば1の例で登録したtest_indexに対して、”power"という文字列で検索した場合はヒットしません。

”Elastic”で検索したクエリ

GET test_index/_search
{
  "query": {
    "match": {
      "message": "power"
    }
  }
}

検索結果

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  }
}

ヒットしないのはインデックスに登録されているのは「elasticsearch」「is」「powerful」のトークンであり、
「power」というトークンは登録されていないためです。
そのため意図した検索結果を実現するためには

  1. データ登録時のAnalyzer
  2. 検索文字列のAnalyzer

をそれぞれ適切に設計する必要があります。

4.text型フィールドに対して、LIKE句と似た検索をする方法

LIKE句のような、特定のパターンの文字列が含まれる文字列を検索をtext型フィールドに対して実現するためには、
N-gram tokenizerを含むAnalyzerを適用したフィールドに対して、match_phraseをかけることで実現可能です。

N-gram tokenizerを使用すると、登録された文字列を任意の文字列ずつ機械的トークン化します。
例えば「Elasticsearch」という文字列を2文字ずつのN-gram(bi-gram)でトークン化すると
「El」「la」「as」・・・「rc」「ch」のように2文字ずつトークン化されインデックスに登録されます。

以下のようにインデックス作成時にAnalyzerを設定することで、N-gram Tokenizerを含むAnalyzerを特定のフィールドに設定することができます。

PUT test_index
{
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "analyzer": "my_analyzer"
      }
    }
  },
  "settings": {
    "analysis": {
      "analyzer": {
        "my_analyzer": {
          "tokenizer": "my_tokenizer"
        }
      },
      "tokenizer": {
        "my_tokenizer": {
          "type": "ngram",
          "min_gram": 2,
          "max_gram": 2
        }
      }
    }
  }
}


match_phraseで「lastic」と検索した場合でも「la」「as」「st」「ti」「ic」とトークン化され
上記のトークンがすべて一致する文字列のみがヒットします。
これによりLIKE検索に相当する部分一致検索を実現することが可能になります。

GET test_index/_search
{
  "query": {
    "match_phrase": {
      "message": "lastic"
    }
  }
}
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.1507283,
    "hits": [
      {
        "_index": "test_index",
        "_id": "ESDup5MBGJks9_KvUZZQ",
        "_score": 1.1507283,
        "_source": {
          "message": "Elasticsearch is powerful"
        }
      }
    ]
  }
}

5.まとめ

今回はElasticsearchでLIKE検索のような部分一致をさせるための方法を紹介しました。
部分一致検索を実現するだけであればwildcardクエリを使えば可能ですが、
システムの運用時の検索パフォーマンスなどを考えると、事前のAnalyzer設定などの手間が必要となるものの
N-gram × match_phraseクエリを使うのが基本となります。

もちろん検索要件によってより細かなAnalyzeやクエリの設計が必要となります。
特に、検索パフォーマンスと精度のバランスをどう取るかは、Elasticsearchを活用する上で避けて通れないポイントです。
今回の記事が、Elasticsearchにおける検索設計の参考になれば幸いです。

アクロクエストでは幅広いユースケースに対してElastic Cloud/Elastic Stackをご利用頂く際の導入検討から運用まで幅広くサービスを提供しています。
Elaticsearchコンサルティングサービス

実際にElastic Cloudのご利用を検討される場合は是非お気軽にお問い合わせください。
ENdoSnipe/Elasticsearchお問い合わせフォーム

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com



生成AI×アプリ開発!bolt.newでフロントエンドが苦手な私でもWebアプリ開発

こんにちは。バックエンドエンジニアの前田です。
最近はかなり冷え込んできて、冬が近づいてきたなと感じます。
社内では、肉まんを販売しています。
寒い日に熱々の肉まん、良いですよね。

さて、今回はbolt.newを用いて、ゼロからアプリケーションを作成していきます。
(私は、フロントエンドの経験があまりなく、苦手分野なのですが・・・)

1. bolt.newとは

bolt.newは自然言語のみで、フルスタックアプリケーションを開発、実行、編集、デプロイできるAIツールです。
さらに、パッケージやライブラリのインストールなどもすべてWebブラウザ上で行ってくれるため、ローカルでの面倒な環境構築も不要なことが特徴的です。
github.com
bolt.newはアカウントを作成すれば、無料でも利用可能です。
各プランとその料金は、以下のようになっています。

  • Personal:0$ /month
  • Pro:18$ /month
  • Teams:29$ /member /month

※商用利用は、Teams以上のプランでないとできないようです。

stackblitz.com

2. 今回作成するアプリについて

以下のようなチェックリスト管理アプリをWebブラウザ上に構築していきます。
以下がbolt.newで実際に作成したアプリケーション画面です。

チェックリストの要件は以下です。

  1. 画面はヘッダー、サイドメニュー、ダッシュボード画面が存在すること。
  2. サイドメニューに「テンプレート一覧」というメニューが存在すること。
  3. 「大項目」、「中項目」、「小項目」の3つの項目が存在すること。
  4. 「確認」ボタンが存在すること。
  5. チェック項目にチェックを入れると、色が変わること。
  6. チェックをしていない項目がある状態で確認ボタンを押すと、その項目が赤くなること。
  7. 新規作成のポップアップが表示されること。

今回のアプリに利用した言語、フレームワークやライブラリは以下です。

言語/DB ライブラリ/フレームワーク
フロントエンド TypeScript React
バックエンド Python FastAPI
DB SQLite sqlite3

3. UIを作成する

3.1. 基本機能を実装する

まずは、チェックリストの要件を満たすように、基本機能を実装してもらいましょう。
以下のプロンプトを入力します。

チェックリスト管理システムを作成したい。
以下の条件を満たすように作成してください。

# 条件
・ヘッダーとサイドメニューとダッシュボード画面を作成すること。
 サイドメニューには、「テンプレート一覧」というメニューを追加すること。
・チェックリストは「大項目」、「中項目」、「小項目の3つの項目に分けること。
 (チェックを入れるのは小項目のみとする)
・「確認」ボタンを作成すること。
 チェックしていない項目がある状態で「確認」ボタンを押したとき、チェックしていない項目を赤くしすること。
・チェックをした項目は、緑色に色を変えること。
・「新規作成」ボタンを作成し、押したら「タイトル」を入力し、「大項目」、「中項目」、「小項目」の3つをそれぞれ自由な数追加できるようなポップアップを表示すること。
・新規作成したら、テンプレート一覧に追加したものを表示すること。

実際にプロンプトを入力している様子が以下の画像です。

プロンプトを入力し、Enterを押すと、以下の画像のように実際にコードを生成している様子がわかります。

2~3分で出てきた画面が以下。

以下のように新規作成画面のポップアップも出てきました。

せっかくなので、チェックリストの機能要件をどの程度満たしているかのチェックリストを新規で作成し、確認してみたところ、ダッシュボード画面以外は実装されていました。

3.2. 使いやすいUIにする

新規作成画面において、文字が入力欄の左に寄りすぎているので、直してもらうことにしました。
ここでも、具体的なプログラミングの知識は必要ありません。

上のようなチャット欄に以下のようなプロンプトを入力するだけ。

新規作成画面や編集画面の入力欄が見づらいので、見やすくしてほしい。
入力欄の左側に文字が寄りすぎなので、もう少し広くとってほしい。

「もう少し」という曖昧な表現を用いましたが、どのように実装するのでしょうか。
返答は以下のようなものでした。

AIが自動でパディングなどを設定してくれたようです。

Before
After

このように、チャットをする感覚でアプリを実装できます。
さて、ダッシュボード画面も作ってもらいましょう。
同じく、チャット欄に以下のようなプロンプトを入力しました。

ダッシュボード画面を作成すること。
 ダッシュボード画面には最近使ったチェックリストを表示すること。
・ヘッダーの文字の部分に、ダッシュボードへのリンクを貼ること。
・テンプレート一覧にあるチェックリストを選択したら、そのチェックリストを大きく表示すること。
 (テンプレート一覧に表示するものは、チェックリストのタイトルのみで良い。)

ダッシュボードには、最近使ったチェックリストを表示してもらうようにしました。

おまけで、日付まで表示してくれています。

さらに、少しカスタマイズしたいときも以下のようにチャットをする感覚で変更してくれます。

テンプレート一覧画面の「新規作成」ボタンの色を青からオレンジ色に変更してください。
また、「新規作成」から「新規テンプレート作成」に変更してください。


4. バックエンドも実装してみる

フロントエンドができたので、バックエンドも実装しようと思います。
こちらもチャット感覚で以下のように入力しました。

バックエンドも実装してください。

これだけで、バックエンドはNode.js、DBはSQLiteを用いて、自動生成してくれました。

私はPython派なので、PythonSQLiteを用いて実装するように変更してもらいました。

どうやら、FastAPIを使ってフロントと疎通し、checklist.dbというファイルにデータを保存するようです。
そこで、ローカルに一旦ダウンロードし、checklist.dbが作成されるか実際に確認してみました。

まずは、右上のDownloadボタンでローカルにダウンロード。

解凍して環境構築をしたら(環境構築のやり方も書いてくれていました)、新規テンプレート作成ボタンを押し、新規作成。

テンプレート一覧に追加され、リロードしても残っていたので、DBにちゃんと登録されていそう。

ローカルのファイルを見に行くと、checklist.dbが生成されていたので、バックエンドも実装できていることを確認できました。

5. まとめ

bolt.newを用いてアプリ作成をしてみました。
作成するのにかかった時間は25~30分程度。
コード生成にかかっていた時間は10分程度でした。
私は普段ほとんどフロントエンドを触らないので、ちゃんとできるのか不安でしたが、自然言語のみで実装できるので、簡単でした。
今回私が行ったように、自動生成されたアプリケーションはダウンロードしてローカルで起動させることもできますし、
フレームワークなど指定できることも多いようなので、構成をしっかり練れば、もっと複雑なこともできそうです。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com