NoSQL×PaaSで運用するナレッジベース+WebAPI

# これはなに

NoSQLとPaaSでナレッジベース+WebAPIを構築した際のノウハウのまとめです。

`文字列タグ指向無向グラフ型ナレッジベース` というアーキテクチャを実装する場合に、
Heroku + Redis + FastAPI で構築した例と、
AWS(DynamoDB + Lambda + API Gateway) で構築した例を紹介します。

コード部分は全て `Python3.8.0` を使用しています。

※ 現在AWS編が未完成です。ごめんなさい。

# ナレッジベースとは？

ナレッジベースには様々な定義がありますが、この記事では
「知識をコンピュータが読み取り可能な形式で格納したデータベース」を指します。
`知識ベース` `ナレッジデータベース` `KB` などの呼ばれ方もあります。

<details><summary>参考リンク(クリックで開く)</summary><div>

- [知識ベース - Wikipedia](https://ja.wikipedia.org/wiki/%E7%9F%A5%E8%AD%98%E3%83%99%E3%83%BC%E3%82%B9)
- [ナレッジベース（なれっじべーす） - ITmedia エンタープライズ](https://www.itmedia.co.jp/im/articles/0702/16/news124.html)
- [ナレッジベースソフトウェア | Atlassian](https://www.atlassian.com/ja/software/confluence/knowledge-base)
- [ナレッジベースの意味とは？社内で利用する効果と作り方 | Tayori Blog](https://tayori.com/blog/knowledge-base/)
- [人間の知識がデータとして蓄積される「ナレッジデータベース」とは？ - Data Knowledge 本当に使える国産BIツール](https://www.dataknowledge.jp/knowledge/)

</div></details>

# 文字列タグ指向無向グラフ型ナレッジベース
今回例として構築するナレッジベースです。
名前だけでは分かりにくいと思うので、イメージ図を用意しました。
(可視化は未実装のためマインドマップツール [coggle](https://coggle.it/) で作成）

<img width="1364" alt="スクリーンショット 2019-12-03 20.00.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/80f94c48-899b-de9a-3645-5a62b436d3ea.png">

## このナレッジベースの役割

「2つの文字列を格納する」という単純な操作の繰り返しだけで、
辞書的な知識体系（集合知）を形成することです。

そして爆速で育て上げるためにWebAPIが必要になります。

## 「文字列タグ指向」について
このナレッジベースでは文字列(とその集合)データのみを扱い、
全ての文字列をタグとして扱います。

上の図の例では、
`Webサービス名` `アカウントID` `URL` `記事タイトル` `概念` `プログラミング言語`
などの各文字列を1つのタグとして扱っています。

仕様上、文字列には空白や改行文字などは含まないものとします。

## 「無向グラフ型」について
このナレッジベースでは、関係のあるタグを結び付けるようにします。

例えば `フレームワーク` というタグには
`Rails` `Laravel` `Django` `Flask`
というタグが紐付いているというデータが取得可能で、

<img width="50%" alt="スクリーンショット 2019-12-03 20.43.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/48bdba2a-ca2f-f190-9fc4-251eadc5f39b.png">

例えば `Qiita` と `Python` のどちらのタグも紐付いているのは
`https://qiita.com/1ntegrale9/items/94ec4437f763aa623965`
というタグ(QiitaのPythonに関する記事URL)、のようにデータを取得できます。

<img width="50%" alt="スクリーンショット 2019-12-03 20.44.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/55b6557c-7a23-cae5-9dd2-51370cbb6ad3.png">

上の図では頂点(文字列)がタグ、辺が関係を表しています。
そして無向なので双方参照が可能です。
また包含関係などは考慮しないので、重み付けはありません。

参考記事：[グラフ理論の基礎 - Qiita](https://qiita.com/maskot1977/items/e1819b7a1053eb9f7d61)

## 補足事項

このアーキテクチャは流通しているものではなく、
GraphQLから着想を得て独自に設計したものです。

GraphDB あたりのキーワードで軽くサーベイした程度なので、
もしかしたら既存かもしれません。

# 構築例：Redis + FastAPI + Heroku

手軽に無料で運用したい場合はこちらを採用します。

Heroku の初期設定や Redis の基本操作はこちらで解説しています。
[Heroku×Redis×Python で始める NoSQL DB 入門 - Qiita](https://qiita.com/1ntegrale9/items/5115eaee1ee2f10bbf06)

## [Redis](https://redis.io/)

オンメモリで読み書きが高速なKVSです。永続化にも対応しています。
1つのタグに複数のタグを紐付けたいので、集合型のみを使います。

### ライブラリのインストール

Python で扱うため、[redis-py](https://github.com/andymccurdy/redis-py) を利用します。

```bash
python3 -m pip install redis hiredis
```

[hiredis-py](https://github.com/redis/hiredis-py) は C 実装の高速なパーサのラッパーです。
redis-py 側が hiredis を検知してパーサを切り替えてくれるので入れておきます。


### Redisに接続

以下のコードで接続の初期化を行います。
[Heroku Redis](https://jp.heroku.com/redis) が自動で設定してくれる環境変数 `REDIS_URL`を使います。

```python
import redis, os
conn = redis.from_url(os.environ['REDIS_URL'], decode_responses=True)
```

デフォルトだと日本語の表示に不具合があるので、
`decode_responses=True` は必須です。

### 全てのタグを取得

`keys()` を使って取得します。

```python
def get_all_tags():
    return sorted(conn.keys())
```

タグが一覧で見れると便利なので用意しておきます。
ただ規模が大きくなると負荷が高くなるので注意が必要です。

### 紐付くタグを取得する

`smembers(key)` 使って取得します。

```python
def get_related_tags(tag):
    r.smembers(tag) if r.exists(tag) else []
```

念のため、存在しないタグを指定された場合は空配列を返すようにします。
存在確認には `exists(key)` を使います。


### 2つのタグを紐付けて格納する

`sadd(key, value)` を使って集合型データを格納します。
双方向に紐付けたいので、key-value を入れ替えて2回実行します。

```python
def set_relation_tags(tag1, tag2):
    return conn.pipeline().sadd(t1, t2).sadd(t2, t1).execute()
```

Redis はトランザクションをサポートしており、redis-py の場合は
`pipeline()` から `execute()` までのチェーンにすることで、
トランザクション内での一括実行ができます。

また、pipeline メソッドによるアトミックな実行は、個別実行よりも高速のようです。
[PythonでRedisを効率的に使う(redis-pyのパフォーマンスをあげるには) - [Dd]enzow(ill)? with DB and Python](http://www.denzow.me/entry/2017/10/08/212059)

## [FastAPI](https://fastapi.tiangolo.com/)

FastAPI は Python の Web フレームワークの1つで、
シンプルな WebAPI を少ないコードで実装できるのと、
API ドキュメントを設定なしで自動生成してくれるのが特徴です。

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/714f4460-7af1-a382-fcab-e9273b20d608.png">

[Flask](https://palletsprojects.com/p/flask/) [Responder](https://responder.readthedocs.io/en/latest/) [Starlette](https://www.starlette.io) [DRF](https://www.django-rest-framework.org) などはオーバースペックで、
[Bottle](https://bottlepy.org/docs/dev/) は逆に機能が足りず、FastAPIがちょうどよくハマりました。

### ライブラリのインストール

```bash
python3 -m pip install fastapi uvicorn email-validator
```

[Uvicorn](https://www.uvicorn.org/) は高速なASGIサーバーです。FastAPIの起動に使います。
[Gunicorn](https://gunicorn.org/) の typo ではないです。

[email-validator](https://github.com/JoshData/python-email-validator) は入れておかないと起動時に怒られます。何故か。

### アプリケーションの初期化

非常にシンプルです。

```python:main.py
from fastapi import FastAPI
app = FastAPI()
```

引数の `title` と `discription` を設定すると、
上記画像のような自動生成される API Doc にタイトルと説明部が反映されます。

```python:main.py
app = FastAPI(
    title='collective-intelligence',
    description='文字列タグ指向無向グラフ型ナレッジベース',
)
```

また、`docs_url` を指定すると、API Doc のURLを変更できます。
デフォルトでは `/docs` ですが、ルートにしておくのも良いでしょう。

```python:main.py
app = FastAPI(docs_url='/')
```

### 全てのタグを取得

シンプルに HTTPメソッド(GET) と URL と返り値を書くだけです。
リストか辞書を返り値にすることで JSON レスポンスになります。

```python:main.py
@app.get('/api')
def read_all_tags():
    return get_all_tags()
```

この定義が API Doc に自動で反映されます。
更に右上の `Try it out` からリクエストを実行できます。

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/3177e0c8-2ed3-623c-0fd6-7a754577c9bc.png">

### 指定のタグに紐付くタグを取得

タグには記号も含む任意の文字列を想定しており、
クエリストリングでは対応できないため、POST にしておきます。

```python:main.py
@app.post('/api/pull')
def read_related_tags(tag: str):
    return get_related_tags(tag)
```

引数に指定した `tag: str` をリクエストボディから受け付けます。
型アノテーションを付けていますが、これを利用してリクエストのバリデーションを行います。
適合しない場合、`422 Validation Error` がレスポンスになります。

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/9d0f9d1b-89fb-61e8-2ec4-e813e04fee93.png">

### 2つのタグを紐付けて格納する

FastAPI は [pydantic](https://pydantic-docs.helpmanual.io/) という、
型アノテーションを活用するためのライブラリを内包しています。
これを利用して独自の型を定義し、バリデーションに利用します。

```python:main.py
from pydantic import BaseModel

class Tags(BaseModel):
    tag1: str
    tag2: str

@app.post('/api/push')
def create_tags_relationship(tags: Tags):
    set_tags_relationship(tags.tag1, tags.tag2)
    return {tag: get_related_tags(tag) for _, tag in tags}
```

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/0a187716-40c4-9b66-e19d-c3ce95619abc.png">

定義した型は Schema として API Doc に反映されます。

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/4ee5d854-f58e-3157-d6fa-258b0e17a969.png">

### FastAPI の起動

先ほど紹介した Uvicorn で起動します。
`main.py` 内の `app` で初期化した場合は `main:app` と指定します。
`--reload` オプションにより、ファイル変更時にリロードして反映してくれます。

```bash
$ uvicorn main:app --reload
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [57749]
INFO:     Started server process [57752]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
```

`http://127.0.0.1:8000` または `http://127.0.0.1:8000/docs` にアクセスすると、
API Doc が表示されることが確認できると思います。

## [Heroku](https://jp.heroku.com/)

Webアプリケーションを手軽にデプロイできるPaaSです。
多くの言語・フレームワークに対応しており、
PostgreSQL や Redis も一定枠まで無料でホスティングしてくれます。

最初に以下の手順が必要です。

- アカウント登録 [Heroku | Sign up](https://signup.heroku.com/login)
- カード登録 [Account · Billing | Heroku](https://dashboard.heroku.com/account/billing)
- アプリ作成 [Create New App | Heroku](https://dashboard.heroku.com/new-app)
- Redisアドオン追加 [Heroku Redis - Add-ons - Heroku Elements](https://elements.heroku.com/addons/heroku-redis)

### 必要なファイルの用意

以下のファイルが必要になります。
これを GitHub リポジトリに用意します。

```bash
$ tree
.
├── main.py          # アプリケーション
├── Procfile         # プロセス実行コマンド定義ファイル
├── requirements.txt # 依存ライブラリ定義ファイル
└── runtime.txt      # Pythonバージョン定義ファイル
```

```bash:Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT
```

```bash:requirements.txt
fastapi
email-validator
uvicorn
redis
hiredis
```

```bash:runtime.txt
python-3.8.0
```

[実際のディレクトリ]([1ntegrale9/collective-intelligence at heroku](https://github.com/1ntegrale9/collective-intelligence/tree/heroku)
) も参考にしてください。

### アプリケーションのデプロイ

Dashboard の Deploy タブからデプロイ作業を行います。
GitHub と連携してリポジトリを紐付け、`Manual Deploy` を実行します。
`Automatic deploys` も設定しておくと、master への push 時に自動でデプロイしてくれます。

<img width="1235" alt="スクリーンショット 2019-12-19 14.45.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/14c7787b-7d10-c4dc-acb3-9d6ade16835f.png">

build が無事に完了したら、
登録されたプロセスを `Configure Dynos` から ON にしておきます。

<img width="593" alt="スクリーンショット 2019-12-19 14.48.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/dd768389-9cbf-6286-320b-0511900bbfaa.png">

Dashboard 右上の `Open app` からデプロイされたアプリケーションを確認できます。

# 構築例：AWS(DynamoDB + Lambda + API Gateway)

<details><summary>執筆中のため公開をお待ちください</summary><div>

スケーラビリティを意識するならこちらを採用します。
柔軟にデータ構造を変えることも可能です。

[初めての、LambdaとDynamoDBを使ったAPI開発 - Qiita](https://qiita.com/miutex/items/d80c9ff0290966eb0cf8)
[API Gateway + Lambda + DynamoDB - Qiita](https://qiita.com/leomaro7/items/314a80b6d91f9e6b4060)

## [Amazon DynamoDB](https://aws.amazon.com/jp/dynamodb/)

RDBと同様に1テーブル1プライマリキーが基本です。
プライマリキーはデータを一意に識別するためのキーで、「パーティションキー」または
「パーティションキーとソートキーの複合キー」のことです。
ソートキーを追加することによってパーティションキーのユニーク制限を緩和できます。

[開始方法 - Amazon DynamoDB | AWS](https://aws.amazon.com/jp/dynamodb/getting-started/)
[初めてのサーバーレスアプリケーション開発 ～DynamoDBにテーブルを作成する～ ｜ Developers.IO](https://dev.classmethod.jp/cloud/aws/serverless-first-dynamodb/)
[無料枠で頑張るためにDynamoDBのキャパシティを理解する - ITと筋トレの二刀流](https://tatsuyashi.hatenablog.com/entry/2019/01/09/080000)

### テーブルの設計

パーティションキー：タグ
ソートキー：timestamp

### テーブルの作成

## [AWS Lambda](https://aws.amazon.com/jp/lambda/)

[初めてのサーバーレスアプリケーション開発 ～LambdaでDynamoDBの値を取得する～ ｜ Developers.IO](https://dev.classmethod.jp/cloud/aws/serverless-first-lambda/)
[GitHub Actionsを使ってAWS Lambdaへ自動デプロイ (詳説＋デモ手順付きver) - Qiita](https://qiita.com/homines22/items/412d4e81b24804d75205)

### 2つのタグを紐付けて格納する

Lambda が呼び出されると lambda_handler 関数が実行される

```python
import boto3, time
from decimal import Decimal

def lambda_handler(event, context):
    timestamp = Decimal(time.time())
    table = boto3.resource('dynamodb').Table('collective-intelligence')
    with table.batch_writer() as batch: # 複数putする場合はbatch_writerを使うと良い
        batch.put_item(Item={
            'tag': event['tag1'],
            'related_tag': event['tag2'],
            'timestamp': timestamp
        })
        batch.put_item(Item={
            'tag': event['tag2'],
            'related_tag': event['tag1'],
            'timestamp': timestamp
        })
    return {'statusCode': 201}
```

### 指定のタグに紐付くタグを取得

```python
import boto3
from boto3.dynamodb.conditions import Key

def lambda_handler(event, context):
    table = boto3.resource('dynamodb').Table('collective-intelligence')
    response = table.query(KeyConditionExpression=Key('tag').eq(event['tag'])) # tag指定で検索
    tags = set(item['related_tag'] for item in response['Items']) # set型に格納して重複を削る
    return {'statusCode': 200, 'body': list(tags)} # JSONレスポンスのためlist型にキャスト
```

## [Amazon API Gateway](https://aws.amazon.com/jp/api-gateway/)

WebAPIの作成と管理をしてくれる

[初めてのサーバーレスアプリケーション開発 ～API GatewayからLambdaを呼び出す～ ｜ Developers.IO](https://dev.classmethod.jp/cloud/aws/serverless-first-apigateway/)
[ゼロから作りながら覚えるAPI Gateway環境構築 ｜ Developers.IO](https://dev.classmethod.jp/cloud/aws/getting-start-api-gateway/)
[Amazon API Gateway チュートリアル - Amazon API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-tutorials.html)

### リソースとメソッドの作成

/push と /pull で POST を作成

### リクエストの検証を設定

Lambdaを実行する前に弾けるとコストが下がってよい

- モデル(JSON Schema)の定義
- 設定->リクエストの検証に「本文の検証」を設定
- リクエスト本文にモデルを設定

[JSON Schema Tool](https://jsonschema.net/)
[リクエストおよびレスポンスマッピングのモデルおよびマッピングテンプレートを作成する - Amazon API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/models-mappings.html)
[APIGatewayの新機能Request Validationを試してみた - エムティーアイ エンジニアブログ](https://tech.mti.co.jp/entry/2017/04/21/180029)

### メソッドの作成
<img width="603" alt="create-method.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/02bb5672-78ab-f2bc-59b8-324b64415ab6.png">

### メソッドの選択
<img width="601" alt="create-post.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/456ec9c9-8798-c13b-4b4f-bb7587466d4a.png">

### メソッド管理画面
<img width="1645" alt="do-pull.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/779c1f73-ea07-94d9-7c0b-0bd554fad813.png">

### PULLモデルの作成
<img width="1680" alt="model-pull.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/e89ba68b-c5e3-ff64-7d18-9d7eb53eacc7.png">

### PUSHモデルの作成
<img width="1680" alt="model-push.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/475f8c6b-8522-7649-4e78-3606defcb9bf.png">

### リクエストの検証を設定
<img width="1680" alt="request-pull.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/39c19083-cda3-69ad-598c-52d7126bd6c0.png">

### PULL API のテスト
<img width="1632" alt="test-pull.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/7034ebab-46b4-e1c4-0c54-a3dfbaf56225.png">

### PUSH API のテスト

<img width="1660" alt="test-push.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/6196bf2a-88fa-0c64-86e2-efd4b2609bfe.png">

## 利用料金に関して

Billing 画面の請求書から確認します。

まだ本稼働しているわけではないですが、
テストで数百件のリクエスト/レスポンスの送受信を行った結果0円だったので、
お試しで使う分には恐れることはなさそうです。

<img width="1382" alt="スクリーンショット 2019-12-05 19.39.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118830/ace3f0e0-07f1-e842-c82a-d8d8e8eed5c0.png">

## GCP vs AWS

GCP(Firestore)とAWS(DynamoDB)で悩みましたがDynamoDBを採用しました。

GCP側で選択する場合、4つのデータストアサービスから悩むことになると思いますが、
趣味で使うならFirestore一択だと思います。
[データベースを選択: Cloud Firestore または Realtime Database  |  Firebase](https://firebase.google.com/docs/database/rtdb-vs-firestore?hl=ja)

</div></details>

# 終わりに

これらはほぼ独学で得た内容ですが、
新しい技術を習得するスキルは会社のモダンな環境で得られたものだと思います。
強いエンジニアが新技術をガンガン使っていく現場で働けるというのは最高の体験です。

また、Heroku側の構成のものを公開しています。
公開時点でデータは空ですが、自由に触ってみてください。
https://collective-intelligence.herokuapp.com/