re:Invent 2019 で発表された Kendra を早速使ってドキュメント検索 slack コマンドを作ってみた

こんにちは，[ABEJA Advent Calendar](https://qiita.com/advent-calendar/2019/abeja) の8日目です．

## はじめに
先日の re:Invent 2019 で面白いサービスが発表されました．そう，エンタープライズ向け検索サービス [Amazon Kendraのリリース](https://aws.amazon.com/jp/about-aws/whats-new/2019/12/announcing-amazon-kendra-reinventing-enterprise-search-with-machine-learning/)です．
![Screenshot from 2019-12-08 07-56-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/1e587471-6abd-f438-f454-afad7d384b31.png)
社内にはさまざまなコミュニケーションツールが存在します．ただし，どれもフロー情報となりがちで整理されておらず情報格差が生じやすいという課題が発生してきています．まさしく Kendra おあつらえ向きな課題ですね．発表された時，これはやってみるしかない！とビビッと来ました．そこでこんな感じで自然文でドキュメントを検索できる slack コマンドをびゃびゃっと作ってみました．
![Screenshot_2019-12-08 Slack yuta nakagawa ABEJA, Inc .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/1505308a-cf2e-1dc0-76d0-cd1dd5916767.png)
以下では Kendra を早速触りつつどう作っていったかを紹介していきます．

## システム構成
Kendra は2019/12/8現在プレビュー版です．そのため，データソースとして利用できるサービスは以下に絞られています．

- Amazon S3
- SharePoint Online
- Amazon RDS

そこで社内のドキュメントを一度 S3 に集めて Kendra を使うことで，自然文検索を可能にする方法を思いつきました．
しかし，一筋縄では行かないのが世の常です．まぁ想像はしていましたが，Kendra は日本語をサポートしていません．
![Screenshot_2019-12-08 Amazon Kendra FAQs - Amazon Web Services.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/120fd9a1-a509-8d69-230c-b13b44457813.png)
そこで AWS の翻訳サービスである [Amazon Translate](https://aws.amazon.com/jp/translate/) と組み合わせる方法で解決しようと思います．ドキュメントを S3 に投入するところと，クエリをかけるところで Translate を使えば，内部的には英語の処理だけで IF は何の言語でもよくなります．(ホントは早く Kendra に日本語対応してほしいけど)
IF に関しては，普段のコミュニケーションが slack なのと，コマンド実装が容易なので slack を選択しました．

まぁ若干節操はないけど，以上をまとめるとこんなアーキテクチャになりました．
![Architecture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/3cb40cec-075e-9820-f3e5-396f829b214b.png)

## ドキュメント準備
システム構成の上部枠の話です．基本的には指定のドキュメントをダウンロードしてきて翻訳して S3 にアップロードするだけですが，Translate は一度に翻訳できる文章のサイズに制限があるので1行ずつ翻訳するようにしています．そのためか微妙に変な英語に翻訳されることもありますが，まぁ検証なので気にせず進みます．また，slack コマンドを実行した際に元ドキュメントへのリンクが欲しかったので S3 Object の metadata で連携するようにしています．

```python
def translate_and_upload(url, title, content):
    client = boto3.client('translate')
    bucket = boto3.resource('s3').Bucket('ynaka-kendra')
    # Translate content to English.
    translated = ''
    for line in content.splitlines():
        if len(line) > 0:
            response = client.translate_text(
                Text=line,
                SourceLanguageCode='ja',
                TargetLanguageCode='en'
            )
            translated += (response['TranslatedText'] + os.linesep)
        else:
            translated += os.linesep
    # Upload to S3.
    file_obj = io.BytesIO(translated.encode('utf-8'))
    bucket.upload_fileobj(file_obj, title, ExtraArgs={'Metadata': {'url': url}})
```

## ドキュメント検索
システム構成の下部枠の話です．ここから本日の主役である Kendra の登場です．

### Kendra の構築
まずは今回の検索の根幹をなす Kendra を構築していきます．基本的には [Getting Started with the Console](https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/gs-console.html) に従っていけば構築できます．

まずは Index の生成．
![Screenshot_2019-12-07 Kendra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/df9fd4a2-f976-0d36-2594-03aa704a69c7.png)
Kendra 用に新しい IAM Role を作ると生成まで30秒ほどかかります．
![Screenshot_2019-12-07 Kendra (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/e348dd45-1113-25d2-a593-852ac3b882c4.png)
さらにインデックス生成までに30分ほどかかるので長めのコーヒーブレイクを取りましょう 笑
![Screenshot_2019-12-07 Kendra(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/e81b51ee-8c3e-8e59-a7e4-0e1749088d85.png)
次はデータソースの選択です．今回は S3 を選択します．
![Screenshot_2019-12-07 Kendra(2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/d5e032c9-6f0d-5fbb-49ba-8abe00f4c778.png)
![Screenshot_2019-12-07 Kendra(3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/e5cfaafb-f768-e511-c680-d223ae92fc8f.png)
連携する S3 のパスを指定します．どのくらいの頻度で Sync するかも設定できるのですが，今回は作ってみたなので「Run on demand」にしました．
![Screenshot_2019-12-07 Kendra(4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/6cfa3c20-4fff-c280-d3ca-b77210b51e7d.png)
設定のレビュー画面が出てきて，
![Screenshot_2019-12-07 Kendra(5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/a60e1df3-5021-466d-32dc-2b673b613f34.png)
またちょっと長めのコーヒーブレイクを取れば構築完了です．
![Screenshot_2019-12-07 Kendra(6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/2e94b3f5-cdc3-36f0-5c94-bc2eb71f19c5.png)
![Screenshot_2019-12-07 Kendra(7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/0d2496d5-77cc-37ae-31e9-eec055bdd57f.png)
すでに S3 にドキュメントが溜まっているので，Kendra との Sync を始めます．
![Screenshot_2019-12-07 Kendra(8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/9151a58d-77ba-efe4-b20d-65cc084ae305.png)
ただし，このままだと IAM が足りずエラーになります．
![Screenshot_2019-12-07 Kendra(9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/cc359908-568b-9b3f-b130-7c81eb7267a0.png)
そこで Kendra 自身の権限と S3 の権限を足してやります．
![Screenshot_2019-12-07 IAM Management Console(4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/2a3c48b0-6225-9782-8d5c-06675fd83089.png)
これでようやく Sync が成功しました．
![Screenshot_2019-12-07 Kendra(11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/cb55385a-a828-a142-c6af-0613c416dd12.png)

### ドキュメント検索 API
slack との連携は [Slash Commands](https://api.slack.com/interactivity/slash-commands) で実装していきます．よくある API Gateway + Lambda の構成です．Lambda を実行するロールには Kendra の権限を追加することを忘れずに．また，最新の `boto3` でないと Kendra は使えないので注意です．

```python
import boto3
import base64
import json
import logging
import os
import urllib


logger = logging.getLogger()
logger.setLevel(logging.INFO)


def respond(err, res=None):
    return {
        'statusCode': '400' if err else '200',
        'response_type': 'in_channel',
        'body': err.message if err else json.dumps({
            'response_type': 'in_channel',
            'text': res 
        }), 
        'headers': {
            'Content-Type': 'application/json',
        },  
    }   


def handler(event, context):
    # Prepare clients.
    translate = boto3.client('translate')
    kendra = boto3.client('kendra')
    s3 = boto3.resource('s3')
    # Parse request.
    body = event['body']
    params = urllib.parse.parse_qs(base64.b64decode(body).decode('utf-8'))
    token = params['token'][0]
    if token != os.environ['VERIFY_TOKEN']:
        logger.error(f'Request token ({token}) does not match expected.')
        return respond(Exception('Invalid request token.'))
    if 'text' not in params:
        logger.error(f'The text should be included in command.')
        return respond(Exception('Need text.'))
    query = params['text'][0]
    # Translate query to English.
    response = translate.translate_text(
        Text=query,
        SourceLanguageCode='ja',
        TargetLanguageCode='en'
    )
    query = response['TranslatedText']
    # Find the most relevant document.
    response = kendra.query(IndexId=os.environ['INDEX_ID'], QueryText=query)
    # Create response message.
    message = ''
    for i, item in enumerate(response['ResultItems'][:3]):
        title = item['DocumentTitle']['Text']
        excerpt = item['DocumentExcerpt']['Text']
        s3_uri = item['DocumentId'].replace('s3://', '').split('/')
        url = s3.Object(s3_uri[0], '/'.join(s3_uri[1:])).metadata['url']
        message_i = f'{i + 1}. *{title}*{os.linesep}url: {url}{os.linesep}```{excerpt}```{os.linesep}{os.linesep}'
        message += message_i
    # Return result.
    return respond(None, message)
```

Kendra の SDK には `IndexId` が必要なので環境変数に設定しておきましょう．工夫点としては，以下3点です．

- Kendra が英語のみ対応なのでクエリに関しても Translate をかけている．
- 他人の探したいドキュメントが実は自分も探していたなんて状況もあるので `in_channel` で投稿している．
- English メンバーも多いので検索でヒットした翻訳ドキュメントの一部も表示する．

## 結果
Kendra + Translate を使ってこんな感じで自然文で検索できるようになりました．
![Screenshot_2019-12-08 Slack yuta nakagawa ABEJA, Inc .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/48bb07bf-aa93-7e98-d3f2-b94dbdcd4b12.png)
「ABEJA のスキルマップは？」という質問に対して，1番目にまさしく整理中のスキルマップを答え，2番目に先日リリースした[技術スタック](https://tech-blog.abeja.asia/entry/tech-stack-201911)に関する記事を出してくるあたりそれっぽく動作しているようです．さすがにドキュメントの一部はぼかしていますが，雰囲気察してください．

ちなみに当然ですが，英語での検索もできます．
![Screenshot_2019-12-08 Slack yuta nakagawa ABEJA, Inc (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/10e410a1-ba98-7f08-d4f2-abafbb9d3385.png)
なお，Kendra では同一ドキュメント内の異なる場所がヒットする場合もあるので，これをどう扱うかはコマンドで実現したいことを踏まえて検討する必要がありそうです．

また社内の反応も上々で中々うれしいです．
![Screenshot_2019-12-08 Slack proj_tech_branding ABEJA, Inc .png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/6637c37e-6e10-c810-004a-2c13397fcb0d.png)

## まとめ
先日の re:Invent 2019 で発表された Amazon Kendra を早速使ってみて，社内のドキュメント検索 slack コマンドを作ってみました．待ち時間が長かったり，謎エラーで解明に時間がかかることもありましたが，Kendra は概して使いやすく自然文での検索ができるのは非常によいサービスだなぁと感じました．今回は時間とコストの関係から社内の一部のドキュメントだけを使って作ってみましたが，ありとあらゆる情報を集めて slack から検索できて，その結果を他の人が見ることによって学び，検索履歴から情報流通が可視化され情報が構造化されていく世界観も描けそうで夢が広がります．早く日本語対応もしてくれないかなぁ．
ちなみに，日本語ドキュメントなど想定していないドキュメントを Kendra につっこむと Internal Server Error が帰ってくることがあるのでご注意を．でも，なんかエラーがあってもリリースしていいんだなと勇気が湧いてきました．
![Screenshot_2019-12-04 Kendra(21).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130762/347c9acc-bc9a-7a6a-412d-35df40b4162c.png)