Jupyter NotebookからWatson Discoveryを操作する(2019-07-15最新版)

# はじめに
PythonからAPIを使ってWatson Discoveryを操作する内容の記事は、過去の書いていたのですが、昨日見直してみたところ、ライブラリ名をはじめ、まったく使えない状態になっていることがわかりました。
中途半端に直すのも面倒だったので、気分も新たに記事を全面的に書き直すことにしました。
前の記事は素のPython前提だったのですが、今はJupyterでしか仕事をしていないので、Jupyter対応の形にしています。

# 利用するNotebook
https://github.com/makaishi2/sample-data/blob/master/notebooks/discovery-api-samle.ipynb
にアップしてあります。
ダウンロードして利用する場合は[ダウンロード先](https://raw.githubusercontent.com/makaishi2/sample-data/master/notebooks/discovery-api-samle.ipynb)からダウンロードして下さい。

# 前提環境
以下のセットアップは、事前に済んでいる前提です。ここまでの手順は別記事を参照して下さい。

* IBM Cloudのサインアップ
* Discoveryのインスタンス作成
* 最初のprivate collection作成

また、コードのうち、IDを自動的に取ってくる部分は、environmentとcolelctionが一つしかない前提としています。複数のcollectionなどを持っている場合は、idの設定は手動で行うようにして下さい。

# 設定・利用方法
上の前提を満たしている場合は、読み込んだnotebookでapikeyだけを設定すれば、あとは

* API初期化
* id取得
* データロード
* 文書削除
* 検索 

の一連の機能がshift+enterで一通り試せるはずです。

# コード解説
例によって個別のコードに簡単な解説をつけます。

## APIの初期化

```py3
# 必要ライブラリの導入
!pip install ibm_watson | tail -n 1

# credential情報 (個別に設定します)

discovery_credentials = {
  "apikey": "xxxx",
  "url": "https://gateway.watsonplatform.net/discovery/api"
}

# Discovery APIの初期化

import json
import os
from ibm_watson import DiscoveryV1

version = '2019-04-30'

discovery = DiscoveryV1(
    version=version,
    iam_apikey=discovery_credentials['apikey'],
    url=discovery_credentials['url']
)
```

まず、いつの間にかライブラリの名称が変わったのに驚きました。
以前は``watson-developer-cloud``みたいな名前だったはずですが、いつの間にか``ibm_watson``に。
また、これに応じて各関数の利用方法も微妙に違ってきています。

認証に関しては、Discoveryは全地区IAMに統一されたので、このコードでどのサイトも大丈夫なはずです。

DiscoveryはAPIインスタンスを作るとき、バージョン番号を指定する必要があるのですが、2019-07-15時点での最新バージョンは``2019-04-30``でした。

最後の1行がエラーなしに実行できていれば、APIとの接続に成功しているので、以降で説明するいろいろな関数が使える形になります。

## environment_id, collection_id などの取得
まあ、これば別にAPI使ってやる必要もないのですが、せっかくなので試してみました。
冒頭で説明したように一つのインスタンスで複数の環境をすでに持っている場合は、混乱の元なので、この部分はスキップして手動でIDを設定することをお勧めします。

```py3
# environment_id、collection_id、configuration_id の取得
# すでにUIで1つのprivate collectionが作成済みであることが前提

# environment id の取得
environment_id = discovery.list_environments().get_result()['environments'][1]['environment_id']
print('environment_id: ', environment_id)

# collection id の取得
collection_id = discovery.list_collections(environment_id ).get_result()['collections'][1]['collection_id']
print('collection_id: ' , collection_id)

# configuration_idの取得
configuration_id = discovery.list_configurations(environment_id).get_result()['configurations'][0]['configuration_id']
print('configuration_id: ', configuration_id)
```

うまくいくと、こんな結果がかえってくるはずです。

```
environment_id:  2c134ad3-42c4-48fe-bf84-3731d0f8cfe3
collection_id:  601acd99-511a-4b2a-bda7-c6c8f4f35ad4
configuration_id:  1a09551f-bf55-4d4a-9741-b2612e5e61fd
```

## 文書のロードと削除
APIを使って文書のロードと削除をする関数を作ってみました。

### 文書ロード関数

```py3
# 文書ロード関数
# collection_id: 対象コレクション
# sample_data: 書き込み対象テキスト (json形式の配列)
# key_name: 文書のユニークキー名称

def load_text( collection_id, sample_data, key_name):
    for item in sample_data:
        
        # itemごとにワークのjsonファイルを作成
        print(item)
        key = item.get(key_name)
        filename = str(key) + '.json'
        f = open(filename, 'w')
        json.dump(item, f)
        f.close()
        
        # 書き込み可能かのチェック
        collection = discovery.get_collection(environment_id, collection_id).get_result()
        proc_docs = collection['document_counts']['processing']
        while True:
            if proc_docs < 20:
                break
            print('busy. waiting..')
            time.sleep(10)
            collection = discovery.get_collection(environment_id, collection_id)
            proc_docs = collection['document_counts']['processing']

        # jsonファイル名を引数にDiscoveryへデータロード
        with open(filename) as f:
            add_doc = discovery.add_document(environment_id, collection_id, file = f)
        os.remove(filename)
```

コメントにあるように3つ引数を取ります。
最初の引数はcollection_id、次の引数はpython変数形式のロード用データ、3つめの引数はユニークID項目名です。
ちなみに、中程の「書き込み可能のチェック」ですが、Discoveryのデータ取込みは非同期形式で行われます。しかし、同時処理件数のMAX値が存在してその値を超えると次の書き込みはエラーになってしまいます。大量書き込み時にエラーが起きると面倒なので、それを起こさないための工夫と理解して下さい。このあたりの実装は以前に書いた記事[Watson Discovery(フルサポート版)に日本語大量文書を投入](https://qiita.com/makaishi2/items/b5af7a9807b58cde7194)の実装をそのまま取ってきています。

利用例としては、次の形になります。

```py3
# ロードテスト用テキスト
sample_data = [
    {'app_id': 1, 'title': '最初のテキスト', 'text': 'サンプルテキストその1。'},
    {'app_id': 2, 'title': '2番目のテキスト', 'text': '新幹線はやぶさが好きです。'},
    {'app_id': 3, 'title': '3番目のテキスト', 'text': '令和元年に転職しました。'},
]

load_text(collection_id, sample_data, 'app_id')
```

sample_dataは、配列形式で用意します。API形式でデータをアップロードする場合も、いったんは1行ごとにjson形式のファイルにする必要があります。その際、ファイル名がユニークになるように、元データにがユニークID列を持たせるようにして下さい。そのID列の名称が第三引数になります。

## 文書削除関数
DiscoveryのUIは未だに削除機能がないようです。
テストするときは、コレクションの中身を丸ごと消したいということがよくありますが、そのための関数となります。

```py3
# 特定のコレクションの全文書を削除する関数
# collection_id: 対象コレクション

def delete_all_docs(collection_id):

    # 文書件数取得
    collection = discovery.get_collection(environment_id, collection_id).get_result()
    doc_count = collection['document_counts']['available']

    results = discovery.query(environment_id, collection_id, return_fields='id', count=doc_count).get_result()["results"]
    ids = [item["id"] for item in results]

    for id in ids:
        print('deleting doc: id =' + id)
        discovery.delete_document(environment_id, collection_id, id)
```

利用例はこんな感じです。

```
# 全件削除テスト
delete_all_docs(collection_id)
```

うまくいくとこんな結果になります。

```
deleting doc: id =095023d6-7b9f-4fc9-8cae-220f0dac5b64
deleting doc: id =a2d805d8-ac39-4f13-adbf-ebdf60f214b0
deleting doc: id =53cb408b-abc6-4fe0-8fa2-5753251f4cef
```

## 検索関数
最後の関数は検索です。

```py3
# 検索用関数
# collection_id: 検索対象コレクション
# query_text: 検索条件式
# return_fields: 出力項目

def query_documents(collection_id, query_text, return_fields):
    # 文書件数取得
    collection = discovery.get_collection(environment_id, collection_id).get_result()
    doc_count = collection['document_counts']['available']
    print('doc_count: ', doc_count)

    query_results = discovery.query(environment_id, collection_id, 
        query=query_text, 
        count=doc_count, 
        return_fields=return_fields).get_result()[ "results"]
    return query_results
```

使い方はこんな感じです。
下の例は「textフィールドに「サンプル」の文字を含む文書」の検索例となります。

```py3
query_text = 'text:サンプル'
return_fields = 'app_id,title,text'
query_results = query_documents(collection_id, query_text, return_fields)

print(json.dumps(query_results, indent=2, ensure_ascii=False))
```

うまくいくと、こんな結果がかえってきます。

```
doc_count:  2
[
  {
    "id": "965f5a5a-2bd1-4118-9517-3c187c47a02c",
    "result_metadata": {
      "confidence": 0.08408801890816446,
      "score": 1.0226655
    },
    "text": "サンプルテキストその1。",
    "title": "最初のテキスト",
    "app_id": 1
  }
]
```