Amazon KendraとAzureChatOpenAIで日本語での文書検索を実行してみた（Python）

---
## ゴール
[amazon-kendra-langchain-extensions](https://github.com/aws-samples/amazon-kendra-langchain-extensions)を参考に、Amazon KendraとOpenAIを使って文書検索をPythonで実行することができます。日本語化にも対応しています。

## スタートライン
- AWSアカウントを持っていること
- AWS CLIの設定が完了していること

## 環境
- Windows 11
- Python 3.11.5
- AWS CLI

## ステップ1：IAMユーザーにAmazonKendraFullAccessをアタッチする
任意のIAMユーザーを選択し、「許可を追加」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/f636585e-33a4-dbf5-6a88-370e01c4e36c.png)
　　
「AmazonKendraFullAccess」と検索し、「次へ」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/3bb60907-a242-ba47-067c-02eb3517c158.png)
　　
「許可を追加」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/822a0713-754d-3f3a-e41f-333ec11cefa7.png)

これで任意のIAMユーザーにAmazonKendraFullAccessがアタッチされました。

## ステップ2：Amazon Kendra インデックスを設定する
[Amazon Kendra console](https://console.aws.amazon.com/kendra/)にアクセスし、「Create an Index」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/011f758d-0530-3ae2-09bc-fb588f79bbfe.png)
　　　
以下のように、Index nameとIAM role、Role nameを入力します。各項目名はご自身で
決めてください。入力したら「Next」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/d4fe3ebc-70f6-756d-174a-9647ab8941b3.png)
　　
次の画面では、何も変更せずに「Next」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/f111bb22-1843-3264-e70d-cf897d36ffd4.png)
　　　　
「Developer editions」を選択し、「Next」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/0dcc3e14-74df-3f3b-14d1-ad5bbbbab583.png)
　　
「Create」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a80e66b4-a13d-b8c0-872b-cdc9bdac3eae.png)
　　
Statusが「Active」になったら、作成完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/655c7b4d-6b23-71c5-374b-02f842aae063.png)


## ステップ3：IAMロールを更新する
[IAM console](https://console.aws.amazon.com/iam/home?region=us-east-1#/home)で先ほど作成したRoleを選択し、「ポリシーをアタッチ」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/f10ba6da-6348-ad2a-97e9-4e3615fbd44c.png)
　　
今回はS3を使うため、「AmazonS3ReadOnlyAccess」と検索し、ヒットしたポリシーにチェックを入れた状態で、「許可を追加」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/02f40b29-6129-166a-b220-a8d713eaf168.png)
　　
許可ポリシーに追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/5b441d86-cd12-09a1-5ea7-4cfae0e1c2ab.png)


## ステップ4：データソースへのS3コネクタを構成する
S3のページに移動し、「バケットを作成」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/41b8c8d1-0d60-9a7f-bb8b-96651e57572b.png)
　　
AWSリージョンは「米国東部（バージニア北部）us-east-1、バケットタイプは「汎用」を選択し、バケット名を入力してバケットを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/7eb89bd7-5c84-8d76-1c3c-aa9dd8ad034a.png)
　　
作成したバケットに移動し、「アップロード」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/4fc0625f-ea1d-c439-0eea-d1644c0dc02c.png)
　　
今回は以下のAmazon Kendraのデベロッパーガイドをアップロードします。

https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/kendra-dg.pdf


次に、データソースを作成します。作成したインデックスの画面に移動し、「Data management」の「Data sources」タブを選択すると以下のような画面が表示されるので、「Add data sources」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/d4a5fb8c-8472-0b5e-ff80-d2ca884123bd.png)
　　
Amazon S3 connecterの「Add connecter」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/d689f4bb-6df1-a5d5-edb1-d45462212378.png)
　　
Data sourse nameを入力し、Default Languageは「Japanese(ja)」を選択し、「Next」で移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/ac8c02de-9bed-777f-8fbf-4c997b6f213b.png)
　　
「Create a new Role(Recommended)」を選択し、任意のRole nameを入力して「Next」で移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/0bf38407-2c87-135d-554b-0fa299337acc.png)

Enter the data source locationには先ほど作成したs3へのパスを通し、Sync modeは「Full sync」、Sync run scheduleは「Run on demand」を選択して「Next」で移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/ebc228e1-8a30-4b8d-294e-ae0e5b483620.png)

そのまま「Next」で移動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/b9ee684e-ad06-1ae3-2655-adc77d0f6b73.png)
　　
確認画面では「Add data source」を選択してください。データソースが作成されます。

データソースの作成が完了したら、以下の「Sync now」を選択してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/2ad0e7d7-9a6d-0641-d92c-a9990480c28b.png)
　　
StatusがCompletedになったら完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/9ef693d5-65fc-d765-8976-6535048bab7b.png)


## ステップ5：Search console上で実行する
「Data management」の「Search indexed content」タブに移動し、右側のSettingsのDefault language of sourse documentsから「Japanese(ja)」を選択して、「Kendraの料金」というクエリを実行すると、以下のように結果が返ってきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/3a6b5162-8ea9-4342-7832-08ec4dd4ad3b.png)

## ステップ6：Pythonから実行する
### ライブラリのインストール
以下のコマンドで必要なライブラリをインストールします。

```terminal
pip install langchain openai python-dotenv boto3
```

### 環境変数の設定
.envファイルを作成して、以下の変数をご自身の環境に合わせて設定します。

```.env
OPENAI_API_KEY = "Azure OpenAIのキー"
OPENAI_API_BASE = "Azure OpenAIのエンドポイント"
OPENAI_API_VERSION = "Azure OpenAIのバージョン"

AWS_REGION = "インデックスを作成したリージョン"
KENDRA_INDEX_ID = "インデックスのID"
```
### コードの追加
https://github.com/aws-samples/amazon-kendra-langchain-extensions/blob/main/kendra_retriever_samples/kendra_chat_open_ai.py を参考に、いくつか変更点を加えた以下のコードを追加します。

```test.py
from langchain.retrievers import AmazonKendraRetriever
from langchain.chains import ConversationalRetrievalChain
from langchain.prompts import PromptTemplate
from langchain.chat_models import AzureChatOpenAI
import sys
import os

from dotenv import load_dotenv
load_dotenv()

MAX_HISTORY_LENGTH = 5

def build_chain():
  region = os.environ["AWS_REGION"]
  kendra_index_id = os.environ["KENDRA_INDEX_ID"]

  llm = AzureChatOpenAI(
      temperature=0,
      openai_api_version=os.environ["OPENAI_API_VERSION"],
  )

  attribute_filter = {"EqualsTo": {"Key": "_language_code", "Value": {"StringValue": "ja"}}}
      
  retriever = AmazonKendraRetriever(
    index_id=kendra_index_id, attribute_filter=attribute_filter, region_name=region
  )

  prompt_template = """
  <documents>タグには参考文書が書かれています。
  <documents>{context}</documents>
  \n\nHuman: 上記参考文書を元に、<question>に対して説明してください。言語の指定が無い場合は日本語で答えてください。
    もし<question>の内容が参考文書に無かった場合は「文書にありません」と答えてください。
  <question>{question}</question>
  \n\nAssistant:
  """
  PROMPT = PromptTemplate(
      template=prompt_template, input_variables=["context", "question"]
  )

  condense_qa_template = """
  Given the following conversation and a follow up question, rephrase the follow up question 
  to be a standalone question.

  Chat History:
  {chat_history}
  Follow Up Input: {question}
  Standalone question:"""
  standalone_question_prompt = PromptTemplate.from_template(condense_qa_template)

  qa = ConversationalRetrievalChain.from_llm(
        llm=llm, 
        retriever=retriever, 
        condense_question_prompt=standalone_question_prompt, 
        return_source_documents=True, 
        combine_docs_chain_kwargs={"prompt":PROMPT})
  return qa

def run_chain(chain, prompt: str, history=[]):
  return chain({"question": prompt, "chat_history": history})


if __name__ == "__main__":
  class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

  qa = build_chain()
  chat_history = []
  print(bcolors.OKBLUE + "Hello! How can I help you?" + bcolors.ENDC)
  print(bcolors.OKCYAN + "Ask a question, start a New search: or CTRL-D to exit." + bcolors.ENDC)
  print(">", end=" ", flush=True)
  for query in sys.stdin:
    if (query.strip().lower().startswith("new search:")):
      query = query.strip().lower().replace("new search:","")
      chat_history = []
    elif (len(chat_history) == MAX_HISTORY_LENGTH):
      chat_history.pop(0)
    result = run_chain(qa, query, chat_history)
    chat_history.append((query, result["answer"]))
    print(bcolors.OKGREEN + result['answer'] + bcolors.ENDC)
    if 'source_documents' in result:
      print(bcolors.OKGREEN + 'Sources:')
      for d in result['source_documents']:
        print(d.metadata['source'])
    print(bcolors.ENDC)
    print(bcolors.OKCYAN + "Ask a question, start a New search: or CTRL-D to exit." + bcolors.ENDC)
    print(">", end=" ", flush=True)
  print(bcolors.OKBLUE + "Bye" + bcolors.ENDC)
```
変更点
- dotenvで.envファイルにある環境変数を読み込む
- OpenAIではなくAzureChatOpenAIを使用
- attribute_filterで日本語のドキュメントに対応
- [こちらの記事](https://qiita.com/cyberBOSE/items/5cdec3580eb22b567d39) を参考にしたprompt_templateに

### 実行

```terminal
Hello! How can I help you?
Ask a question, start a New search: or CTRL-D to exit.
> Kendraの料金
Amazon Kendraの料金については、参考文書に詳細な情報が記載されています。料金は、Amazon Kendraの使用状況に応じて請求されます。最初の30日間は、最大750時間のAmazon Kendraの使用が無料で利用できるデベロッパーエディションがあります。ただし、試用期間が終了すると、プロビジョニングされたインデックスが空でクエリが実行されていない場合でも、Amazon Kendraプロビジョニングされたすべてのインデックスに対して料金が請求されます。また、試用期間が終 了すると、Amazon Kendraデータソースを使用してドキュメントをスキャンおよび同期するための追加料金がかかります。詳細な料金と価格については、Amazon Kendra料金表をご覧ください。
Sources:
https://t-tsuchiyama-test.s3.amazonaws.com/kendra-dg.pdf
https://t-tsuchiyama-test.s3.amazonaws.com/kendra-dg.pdf
https://t-tsuchiyama-test.s3.amazonaws.com/kendra-dg.pdf
```
回答結果が日本語で取得できました。

## フィニッシュ
お疲れ様でした。
値段が高いので、継続的に使用しない場合は今回作成したリソースは削除しましょう。
以下のXでも情報発信してます！

https://twitter.com/t___tatsuki

## 参考文献
https://github.com/aws-samples/amazon-kendra-langchain-extensions/blob/main/kendra_retriever_samples/kendra_chat_open_ai.py

https://dev.classmethod.jp/articles/full-text-search-of-powerpoint-excel-and-word-with-amazon-kendra/

https://qiita.com/cyberBOSE/items/5cdec3580eb22b567d39