Amazon Elasticsearch Serviceチュートリアル

Amazon Elasticsearch Service のdeployやデータ投入、検索リクエストを試すべく、以下資料を参考に進めていきます。 今回はAWS CDKでElasticsearch Serviceドメインクラスタ)を作成する方法で行っていきます。

参考資料

インストール

まず、aws-cdkをインストールします

$ npm i -g aws-cdk
/usr/local/bin/cdk -> /usr/local/lib/node_modules/aws-cdk/bin/cdk
+ aws-cdk@1.70.0
added 189 packages from 186 contributors in 4.708s

バージョン情報

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H2
$ cdk version
1.70.0 (build c145314)

CDKプロジェクト作成

CDKプロジェクトを作成します。
今回はPythonで実装するので、 --language pythonオプションを指定します。

$ mkdir hello-cdk-es
$ cd hello-cdk-es
$ cdk init --language python
Applying project template app for python

# Welcome to your CDK Python project!

This is a blank project for Python development with CDK.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

This project is set up like a standard Python project.  The initialization
process also creates a virtualenv within this project, stored under the .env
directory.  To create the virtualenv it assumes that there is a `python3`
(or `python` for Windows) executable in your path with access to the `venv`
package. If for any reason the automatic creation of the virtualenv fails,
you can create the virtualenv manually.

To manually create a virtualenv on MacOS and Linux:

```
$ python3 -m venv .env
```

After the init process completes and the virtualenv is created, you can use the following
step to activate your virtualenv.

```
$ source .env/bin/activate
```

If you are a Windows platform, you would activate the virtualenv like this:

```
% .env\Scripts\activate.bat
```

Once the virtualenv is activated, you can install the required dependencies.

```
$ pip install -r requirements.txt
```

At this point you can now synthesize the CloudFormation template for this code.

```
$ cdk synth
```

To add additional dependencies, for example other CDK libraries, just add
them to your `setup.py` file and rerun the `pip install -r requirements.txt`
command.

## Useful commands

 * `cdk ls`          list all stacks in the app
 * `cdk synth`       emits the synthesized CloudFormation template
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk docs`        open CDK documentation

Enjoy!

Initializing a new git repository...
Please run 'python3 -m venv .env'!
Executing Creating virtualenv...
✅ All done!

生成ファイル確認

$ tree -L 1
.
├── README.md
├── app.py
├── cdk.json
├── hello_cdk_es
├── requirements.txt
├── setup.py
└── source.bat

1 directory, 6 files

必要なパッケージのインストール

$ python3 -m venv .env
$ source ./.env/bin/activate
$ pip list
Package    Version
---------- -------
pip        20.2.4
setuptools 49.2.1
$ pip install -r requirements.txt
:
Collecting aws-cdk.core==1.70.0
  Downloading aws_cdk.core-1.70.0-py3-none-any.whl (780 kB)
     |████████████████████████████████| 780 kB 3.3 MB/s
Collecting constructs<4.0.0,>=3.0.4
  Downloading constructs-3.1.3-py3-none-any.whl (57 kB)
     |████████████████████████████████| 57 kB 8.8 MB/s
Collecting jsii<2.0.0,>=1.13.0
  Downloading jsii-1.13.0-py3-none-any.whl (266 kB)
     |████████████████████████████████| 266 kB 11.1 MB/s
Collecting aws-cdk.region-info==1.70.0
  Downloading aws_cdk.region_info-1.70.0-py3-none-any.whl (56 kB)
     |████████████████████████████████| 56 kB 6.2 MB/s
Collecting publication>=0.0.3
  Downloading publication-0.0.3-py2.py3-none-any.whl (7.7 kB)
Collecting aws-cdk.cx-api==1.70.0
  Downloading aws_cdk.cx_api-1.70.0-py3-none-any.whl (99 kB)
     |████████████████████████████████| 99 kB 12.7 MB/s
Collecting aws-cdk.cloud-assembly-schema==1.70.0
  Downloading aws_cdk.cloud_assembly_schema-1.70.0-py3-none-any.whl (106 kB)
     |████████████████████████████████| 106 kB 12.6 MB/s
Collecting attrs~=20.1
  Downloading attrs-20.2.0-py2.py3-none-any.whl (48 kB)
     |████████████████████████████████| 48 kB 9.2 MB/s
Collecting typing-extensions~=3.7
  Downloading typing_extensions-3.7.4.3-py3-none-any.whl (22 kB)
Collecting cattrs~=1.0
  Downloading cattrs-1.0.0-py2.py3-none-any.whl (14 kB)
Collecting python-dateutil
  Using cached python_dateutil-2.8.1-py2.py3-none-any.whl (227 kB)
Collecting six>=1.5
  Using cached six-1.15.0-py2.py3-none-any.whl (10 kB)
Installing collected packages: attrs, typing-extensions, cattrs, six, python-dateutil, jsii, publication, constructs, aws-cdk.region-info, aws-cdk.cloud-assembly-schema, aws-cdk.cx-api, aws-cdk.core, hello-cdk-es
  Running setup.py develop for hello-cdk-es
Successfully installed attrs-20.2.0 aws-cdk.cloud-assembly-schema-1.70.0 aws-cdk.core-1.70.0 aws-cdk.cx-api-1.70.0 aws-cdk.region-info-1.70.0 cattrs-1.0.0 constructs-3.1.3 hello-cdk-es jsii-1.13.0 publication-0.0.3 python-dateutil-2.8.1 six-1.15.0 typing-extensions-3.7.4.3

aws-cdk.aws-elasticsearchのインストール

$ pip install aws-cdk.aws-elasticsearch
Collecting aws-cdk.aws-elasticsearch
  Downloading aws_cdk.aws_elasticsearch-1.70.0-py3-none-any.whl (103 kB)
     |████████████████████████████████| 103 kB 2.0 MB/s
Collecting aws-cdk.aws-cloudwatch==1.70.0
  Downloading aws_cdk.aws_cloudwatch-1.70.0-py3-none-any.whl (180 kB)
     |████████████████████████████████| 180 kB 6.2 MB/s
Requirement already satisfied: jsii<2.0.0,>=1.13.0 in ./.env/lib/python3.8/site-packages (from aws-cdk.aws-elasticsearch) (1.13.0)
Requirement already satisfied: constructs<4.0.0,>=3.0.4 in ./.env/lib/python3.8/site-packages (from aws-cdk.aws-elasticsearch) (3.1.3)
Requirement already satisfied: aws-cdk.core==1.70.0 in ./.env/lib/python3.8/site-packages (from aws-cdk.aws-elasticsearch) (1.70.0)
Collecting aws-cdk.aws-kms==1.70.0
  Downloading aws_cdk.aws_kms-1.70.0-py3-none-any.whl (57 kB)
     |████████████████████████████████| 57 kB 14.2 MB/s
Collecting aws-cdk.aws-logs==1.70.0
  Downloading aws_cdk.aws_logs-1.70.0-py3-none-any.whl (102 kB)
     |████████████████████████████████| 102 kB 7.6 MB/s
Requirement already satisfied: publication>=0.0.3 in ./.env/lib/python3.8/site-packages (from aws-cdk.aws-elasticsearch) (0.0.3)
Collecting aws-cdk.custom-resources==1.70.0
  Downloading aws_cdk.custom_resources-1.70.0-py3-none-any.whl (94 kB)
     |████████████████████████████████| 94 kB 5.7 MB/s
Collecting aws-cdk.aws-iam==1.70.0
  Downloading aws_cdk.aws_iam-1.70.0-py3-none-any.whl (223 kB)
     |████████████████████████████████| 223 kB 6.0 MB/s
Collecting aws-cdk.aws-secretsmanager==1.70.0
  Downloading aws_cdk.aws_secretsmanager-1.70.0-py3-none-any.whl (94 kB)
     |████████████████████████████████| 94 kB 7.0 MB/s
Collecting aws-cdk.aws-ec2==1.70.0
  Downloading aws_cdk.aws_ec2-1.70.0-py3-none-any.whl (818 kB)
     |████████████████████████████████| 818 kB 8.5 MB/s
Requirement already satisfied: python-dateutil in ./.env/lib/python3.8/site-packages (from jsii<2.0.0,>=1.13.0->aws-cdk.aws-elasticsearch) (2.8.1)
Requirement already satisfied: attrs~=20.1 in ./.env/lib/python3.8/site-packages (from jsii<2.0.0,>=1.13.0->aws-cdk.aws-elasticsearch) (20.2.0)
Requirement already satisfied: cattrs~=1.0 in ./.env/lib/python3.8/site-packages (from jsii<2.0.0,>=1.13.0->aws-cdk.aws-elasticsearch) (1.0.0)
Requirement already satisfied: typing-extensions~=3.7 in ./.env/lib/python3.8/site-packages (from jsii<2.0.0,>=1.13.0->aws-cdk.aws-elasticsearch) (3.7.4.3)
Requirement already satisfied: aws-cdk.cloud-assembly-schema==1.70.0 in ./.env/lib/python3.8/site-packages (from aws-cdk.core==1.70.0->aws-cdk.aws-elasticsearch) (1.70.0)
Requirement already satisfied: aws-cdk.cx-api==1.70.0 in ./.env/lib/python3.8/site-packages (from aws-cdk.core==1.70.0->aws-cdk.aws-elasticsearch) (1.70.0)
Requirement already satisfied: aws-cdk.region-info==1.70.0 in ./.env/lib/python3.8/site-packages (from aws-cdk.core==1.70.0->aws-cdk.aws-elasticsearch) (1.70.0)
Collecting aws-cdk.aws-s3-assets==1.70.0
  Downloading aws_cdk.aws_s3_assets-1.70.0-py3-none-any.whl (32 kB)
Collecting aws-cdk.aws-cloudformation==1.70.0
  Downloading aws_cdk.aws_cloudformation-1.70.0-py3-none-any.whl (59 kB)
     |████████████████████████████████| 59 kB 7.5 MB/s
Collecting aws-cdk.aws-lambda==1.70.0
  Downloading aws_cdk.aws_lambda-1.70.0-py3-none-any.whl (262 kB)
     |████████████████████████████████| 262 kB 9.0 MB/s
Collecting aws-cdk.aws-sns==1.70.0
  Downloading aws_cdk.aws_sns-1.70.0-py3-none-any.whl (68 kB)
     |████████████████████████████████| 68 kB 10.0 MB/s
Collecting aws-cdk.aws-sam==1.70.0
  Downloading aws_cdk.aws_sam-1.70.0-py3-none-any.whl (127 kB)
     |████████████████████████████████| 127 kB 14.7 MB/s
Collecting aws-cdk.aws-ssm==1.70.0
  Downloading aws_cdk.aws_ssm-1.70.0-py3-none-any.whl (117 kB)
     |████████████████████████████████| 117 kB 12.9 MB/s
Collecting aws-cdk.aws-s3==1.70.0
  Downloading aws_cdk.aws_s3-1.70.0-py3-none-any.whl (214 kB)
     |████████████████████████████████| 214 kB 13.4 MB/s
Collecting aws-cdk.assets==1.70.0
  Downloading aws_cdk.assets-1.70.0-py3-none-any.whl (16 kB)
Requirement already satisfied: six>=1.5 in ./.env/lib/python3.8/site-packages (from python-dateutil->jsii<2.0.0,>=1.13.0->aws-cdk.aws-elasticsearch) (1.15.0)
Collecting aws-cdk.aws-applicationautoscaling==1.70.0
  Downloading aws_cdk.aws_applicationautoscaling-1.70.0-py3-none-any.whl (99 kB)
     |████████████████████████████████| 99 kB 11.8 MB/s
Collecting aws-cdk.aws-events==1.70.0
  Downloading aws_cdk.aws_events-1.70.0-py3-none-any.whl (109 kB)
     |████████████████████████████████| 109 kB 11.9 MB/s
Collecting aws-cdk.aws-efs==1.70.0
  Downloading aws_cdk.aws_efs-1.70.0-py3-none-any.whl (63 kB)
     |████████████████████████████████| 63 kB 5.2 MB/s
Collecting aws-cdk.aws-sqs==1.70.0
  Downloading aws_cdk.aws_sqs-1.70.0-py3-none-any.whl (61 kB)
     |████████████████████████████████| 61 kB 9.3 MB/s
Collecting aws-cdk.aws-codeguruprofiler==1.70.0
  Downloading aws_cdk.aws_codeguruprofiler-1.70.0-py3-none-any.whl (31 kB)
Collecting aws-cdk.aws-autoscaling-common==1.70.0
  Downloading aws_cdk.aws_autoscaling_common-1.70.0-py3-none-any.whl (26 kB)
Installing collected packages: aws-cdk.aws-iam, aws-cdk.aws-cloudwatch, aws-cdk.aws-kms, aws-cdk.aws-events, aws-cdk.aws-s3, aws-cdk.assets, aws-cdk.aws-s3-assets, aws-cdk.aws-logs, aws-cdk.aws-autoscaling-common, aws-cdk.aws-applicationautoscaling, aws-cdk.aws-ssm, aws-cdk.aws-ec2, aws-cdk.aws-efs, aws-cdk.aws-sqs, aws-cdk.aws-codeguruprofiler, aws-cdk.aws-lambda, aws-cdk.aws-sns, aws-cdk.aws-cloudformation, aws-cdk.custom-resources, aws-cdk.aws-sam, aws-cdk.aws-secretsmanager, aws-cdk.aws-elasticsearch
Successfully installed aws-cdk.assets-1.70.0 aws-cdk.aws-applicationautoscaling-1.70.0 aws-cdk.aws-autoscaling-common-1.70.0 aws-cdk.aws-cloudformation-1.70.0 aws-cdk.aws-cloudwatch-1.70.0 aws-cdk.aws-codeguruprofiler-1.70.0 aws-cdk.aws-ec2-1.70.0 aws-cdk.aws-efs-1.70.0 aws-cdk.aws-elasticsearch-1.70.0 aws-cdk.aws-events-1.70.0 aws-cdk.aws-iam-1.70.0 aws-cdk.aws-kms-1.70.0 aws-cdk.aws-lambda-1.70.0 aws-cdk.aws-logs-1.70.0 aws-cdk.aws-s3-1.70.0 aws-cdk.aws-s3-assets-1.70.0 aws-cdk.aws-sam-1.70.0 aws-cdk.aws-secretsmanager-1.70.0 aws-cdk.aws-sns-1.70.0 aws-cdk.aws-sqs-1.70.0 aws-cdk.aws-ssm-1.70.0 aws-cdk.custom-resources-1.70.0

CDKデプロイ

$ cdk list
AwsEsStack

CDK初回デプロイ時、CloudFormationで利用するデプロイ用S3バケットの作成します。

$ cdk bootstrap
 ⏳  Bootstrapping environment aws://867065454662/us-east-1...
CDKToolkit: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (3/3)



 ✅  Environment aws://867065454662/us-east-1 bootstrapped.

Amazon Elasticsearchの設定定義

以下のpythonスクリプトで定義。アクセスポリシーは今回はIP制限のみ

CDK定義

以下のように利用するelasticsearch versionを追加してみます。

 "es": {
      "version": "7.7"
    }

https://github.com/kenji-imi/hello_cdk_es/blob/master/cdk.json

CDKデプロイ

$ cdk deploy ## ドメイン作成

hello-cdk-es: deploying...
hello-cdk-es: creating CloudFormation changeset...
 0/3 | 16:17:35 | REVIEW_IN_PROGRESS   | AWS::CloudFormation::Stack | hello-cdk-es User Initiated
 0/3 | 16:17:40 | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | hello-cdk-es User Initiated
 0/3 | 16:17:45 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | HelloCdkEs
 0/3 | 16:17:45 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata/Default (CDKMetadata)
 1/3 | 16:17:47 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata/Default (CDKMetadata) Resource creation Initiated
 1/3 | 16:17:47 | CREATE_COMPLETE      | AWS::CDK::Metadata         | CDKMetadata/Default (CDKMetadata)
 1/3 | 16:17:48 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | HelloCdkEs Resource creation Initiated
1/3 Currently in progress: hello-cdk-es, HelloCdkEs
 2/3 | 16:33:52 | CREATE_COMPLETE      | AWS::Elasticsearch::Domain | HelloCdkEs
 3/3 | 16:33:54 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | hello-cdk-es
Stack hello-cdk-es has completed updating

 ✅  hello-cdk-es

Stack ARN:
arn:aws:cloudformation:us-east-1:867065454662:stack/hello-cdk-es/7c6a6990-1824-11eb-8a28-12f8925a37c4

ドメイン作成確認

$ aws es list-domain-names
{
    "DomainNames": [
        {
            "DomainName": "hello-cdk-es"
        }
    ]
}
$ aws es list-domain-names
You must specify a region. You can also configure your region by running "aws configure".
$ curl -s https://checkip.amazonaws.com
103.5.140.182

設定

settings.json

{
  "settings": {
    "analysis": {
      "analyzer": {
        "index_analyzer": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer",
          "mode": "search",
          "char_filter": ["icu_normalizer", "kuromoji_iteration_mark"],
          "filter": [
            "cjk_width",
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "ja_stop",
            "lowercase",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        }
      },
      "tokenizer": {
        "kuromoji_tokenizer": {
          "type": "kuromoji_tokenizer",
          "mode": "normal"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "company": {
        "type": "text",
        "analyzer": "index_analyzer"
      },
      "products": {
        "type": "text",
        "analyzer": "index_analyzer"
      }
    }
  }
}

設定用リクエス

$ curl -s -XPUT "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test?pretty" -H "Content-type: application/json" -d @settings.json
{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "test"
}

設定確認リクエス

$ curl -s -XGET "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test/_mapping?pretty"
{
  "test": {
    "mappings": {
      "properties": {
        "company": {
          "type": "text",
          "analyzer": "index_analyzer"
        },
        "products": {
          "type": "text",
          "analyzer": "index_analyzer"
        }
      }
    }
  }
}

データ投入

データ投入は、bulk insertで行います。

data.json

{"index": {"_index": "test","_type": "_doc","_id": "1"}}
{"company": "SHARP", "products": "SHARP AQUOS 4T-C50BN1"}
{"index": {"_index": "test","_type": "_doc","_id": "2"}}
{"company": "Panasonic", "products": "Panasonic VIERA HZ2000"}
{"index": {"_index": "test","_type": "_doc","_id": "3"}}
{"company": "SONY", "products": "SONY BRAVIA KJ-65A8H"}

buku insertリクエス

$ curl -s -XPOST "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/_bulk?pretty" -H "Content-type: application/json" --data-binary @data.json
{
  "took": 41,
  "errors": false,
  "items": [
    {
      "index": {
        "_index": "test",
        "_type": "_doc",
        "_id": "1",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1,
        "status": 201
      }
    },
    {
      "index": {
        "_index": "test",
        "_type": "_doc",
        "_id": "2",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1,
        "status": 201
      }
    },
    {
      "index": {
        "_index": "test",
        "_type": "_doc",
        "_id": "3",
        "_version": 1,
        "result": "created",
        "_shards": {
          "total": 2,
          "successful": 1,
          "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1,
        "status": 201
      }
    }
  ]
}

検索

query.json

{
  "query": { "match": { "products": "BRAVIA" } }
}

検索リクエス

$ curl -s -XGET "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test/_search?pretty" -H "Content-type: application/json" -d @query.json
{
  "took": 308,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "test",
        "_type": "_doc",
        "_id": "3",
        "_score": 0.2876821,
        "_source": {
          "company": "SONY",
          "products": "SONY BRAVIA KJ-65A8H"
        }
      }
    ]
  }
}

インデックス削除

検索が出来たことを確認したので、インデックスを削除します。
削除する前にインデックス存在チェックします。

インデックス存在確認用に以下リクエストを行います。

$ curl -s -XGET "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test?pretty"
{
  "test" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "company" : {
          "type" : "text",
          "analyzer" : "index_analyzer"
        },
        "products" : {
          "type" : "text",
          "analyzer" : "index_analyzer"
        }
      }
    },
    "settings" : {
      "index" : {
        "number_of_shards" : "5",
        "provided_name" : "test",
        "creation_date" : "1603785392481",
        "analysis" : {
          "analyzer" : {
            "index_analyzer" : {
              "filter" : [ "cjk_width", "kuromoji_baseform", "kuromoji_part_of_speech", "ja_stop", "lowercase", "kuromoji_number", "kuromoji_stemmer" ],
              "mode" : "search",
              "char_filter" : [ "icu_normalizer", "kuromoji_iteration_mark" ],
              "type" : "custom",
              "tokenizer" : "kuromoji_tokenizer"
            }
          },
          "tokenizer" : {
            "kuromoji_tokenizer" : {
              "mode" : "normal",
              "type" : "kuromoji_tokenizer"
            }
          }
        },
        "number_of_replicas" : "1",
        "uuid" : "1e1cI4fMSfOl_KJziJ8VKQ",
        "version" : {
          "created" : "7070099"
        }
      }
    }
  }
}

削除リクエス

$ curl -XDELETE "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test?pretty"
{
  "acknowledged" : true
}

再度インデックス存在確認用のリクエストを行うと、NotFoundエラーになり、削除されたことを確認。

$ curl -s -XGET "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com/test?pretty"
{
  "error" : {
    "root_cause" : [
      {
        "type" : "index_not_found_exception",
        "reason" : "no such index [test]",
        "resource.type" : "index_or_alias",
        "resource.id" : "test",
        "index_uuid" : "_na_",
        "index" : "test"
      }
    ],
    "type" : "index_not_found_exception",
    "reason" : "no such index [test]",
    "resource.type" : "index_or_alias",
    "resource.id" : "test",
    "index_uuid" : "_na_",
    "index" : "test"
  },
  "status" : 404
}

Elasitcsearchドメイン削除

最後に、ドメインの削除を行います。

$ aws es delete-elasticsearch-domain --domain-name hello-cdk-es
{
    "DomainStatus": {
        "DomainId": "867065454662/hello-cdk-es",
        "DomainName": "hello-cdk-es",
        "ARN": "arn:aws:es:us-east-1:867065454662:domain/hello-cdk-es",
        "Created": true,
        "Deleted": true,
        "Endpoint": "search-hello-cdk-es-orziggze45iaxzd2wcaz2tdwze.us-east-1.es.amazonaws.com",
        "Processing": true,
        "UpgradeProcessing": false,
        "ElasticsearchVersion": "7.7",
        "ElasticsearchClusterConfig": {
            "InstanceType": "t2.small.elasticsearch",
            "InstanceCount": 1,
            "DedicatedMasterEnabled": false,
            "ZoneAwarenessEnabled": false,
            "WarmEnabled": false
        },
        "EBSOptions": {
            "EBSEnabled": true,
            "VolumeType": "gp2",
            "VolumeSize": 10
        },
        "AccessPolicies": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":\"es:*\",\"Resource\":\"*\",\"Condition\":{\"IpAddress\":{\"aws:SourceIp\":[\"127.0.0.1\",\"103.5.140.182\"]}}}]}",
        "SnapshotOptions": {},
        "CognitoOptions": {
            "Enabled": false
        },
        "EncryptionAtRestOptions": {
            "Enabled": false
        },
        "NodeToNodeEncryptionOptions": {
            "Enabled": false
        },
        "AdvancedOptions": {
            "rest.action.multi.allow_explicit_index": "true"
        },
        "ServiceSoftwareOptions": {
            "CurrentVersion": "R20201019",
            "NewVersion": "",
            "UpdateAvailable": false,
            "Cancellable": false,
            "UpdateStatus": "COMPLETED",
            "Description": "There is no software update available for this domain.",
            "AutomatedUpdateDate": "1970-01-01T09:00:00+09:00",
            "OptionalDeployment": true
        },
        "DomainEndpointOptions": {
            "EnforceHTTPS": false,
            "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07"
        },
        "AdvancedSecurityOptions": {
            "Enabled": false,
            "InternalUserDatabaseEnabled": false
        }
    }
}

このリクエストを行って、5分程度待った後に以下リクエストを行ってみると、Could not connectメッセージが出て、削除されたことが確認できます。

$ aws es list-domain-names

Could not connect to the endpoint URL: "https://es.us-east-1.amazonaws.com/2015-01-01/domain"

まとめ

Amazon Elasticsearch Serviceの基本的な動作について試してみました。
service定義コードとしてpythonを用いての確認を行うことも出来ました。

Elasitcsearchとkuromojiを用いての全文検索

Elasticsearchの学習用に、livedoorニュースコーパスデータのIndex登録を行い、登録された日本語ニュース記事に対して検索機能を試してみます。

バージョン情報

macOSバージョン

$ sw_ver
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H2

elasticsearchバージョン

  • 7.9.2 ※ 2020年10月時点での最新バージョン

elasticsearch起動

最初にelasticsearchを起動します。
起動するにあたって、docker-composeを用いて進めていきます。

docker-composeの内容はこちら。
今回は1ノードのみ起動するようにしています。(単一ノードクラスタ
また、kibana dev toolsを利用したく、kibanaもあわせて起動します。

docker/elasticsearch/Dockerfile

FROM docker.elastic.co/elasticsearch/elasticsearch:7.9.2

# install elasticsearch plugins
RUN elasticsearch-plugin install analysis-kuromoji
RUN elasticsearch-plugin install analysis-icu
  • kuromojiプラグインもこのタイミングでインストールします
  • インストールするとtext型フィールドに対して、defaultでkuromoji analyzerが適用されるよう。

docker-compose.yaml

version: "3.7"

services:
  elasticsearch:
    build: ./docker/elasticsearch/
    container_name: es01
    environment:
      - cluster.name=docker-cluster
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - "discovery.type=single-node"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
      - 9300:9300

  kibana:
    image: docker.elastic.co/kibana/kibana:7.9.2
    container_name: kibana01
    environment:
      ELASTICSEARCH_URL: http://elasticsearch:9200
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch

volumes:
  esdata01:
    driver: local

elasticsearchとkibana用コンテナを起動します。

$ docker-compose up -d
Creating network "es-livedoor-news-search_default" with the default driver
Building elasticsearch
Step 1/3 : FROM docker.elastic.co/elasticsearch/elasticsearch:7.9.2
 ---> caa7a21ca06e
Step 2/3 : RUN elasticsearch-plugin install analysis-kuromoji
 ---> Running in 97b65d75348b
-> Installing analysis-kuromoji
-> Downloading analysis-kuromoji from elastic
:
:
$ docker-compose ps
  Name                Command               State                       Ports
--------------------------------------------------------------------------------------------------
es01       /tini -- /usr/local/bin/do ...   Up      0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp
kibana01   /usr/local/bin/dumb-init - ...   Up      0.0.0.0:5601->5601/tcp

elasticsearch起動確認

GET /

{
  "name" : "fdfbd5f82bd6",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "brwR4DgsTPywnbHpKbciGQ",
  "version" : {
    "number" : "7.9.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "d34da0ea4a966c4e49417f2da2f244e3e97b4e6e",
    "build_date" : "2020-09-23T00:45:33.626720Z",
    "build_snapshot" : false,
    "lucene_version" : "8.6.2",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

プラグイン確認

GET /_cat/plugins

efd4f7468364 analysis-icu      7.9.2
efd4f7468364 analysis-kuromoji 7.9.2

Indexデータ

Indexデータとして、Livedoorニュースコーパスデータを利用します。
Livedoorニュースコーパスデータは株式会社ロンウイットのダウンロードページよりダウンロード・解凍して利用できる状態にします。

$ curl -OL https://www.rondhuit.com/download/ldcc-20140209.tar.gz
$ ls -lh ldcc-20140209.tar.gz | awk '{print $5}'
30M
$ tar xvzf ldcc-20140209.tar.gz

解凍するとtextディレクトリが作成されます。
textディレクトリの中には以下9カテゴリのディレクトリが存在してます。

$ tree -L 1 text/
text/
├── CHANGES.txt
├── README.txt
├── dokujo-tsushin
├── it-life-hack
├── kaden-channel
├── livedoor-homme
├── movie-enter
├── peachy
├── smax
├── sports-watch
└── topic-news

9 directories, 2 files

Indexing

elasitcsearchにデータ投入するためのコードを作成します。今回はpythonで実装します。 pythonからelasticseachを利用するため、Python Elasticsearch Clientをインストールします。

$ pip install elasticsearch
Collecting elasticsearch
  Downloading elasticsearch-7.9.1-py2.py3-none-any.whl (219 kB)
     |████████████████████████████████| 219 kB 2.8 MB/s
Requirement already satisfied: certifi in ./py3/lib/python3.8/site-packages (from elasticsearch) (2020.6.20)
Requirement already satisfied: urllib3>=1.21.1 in ./py3/lib/python3.8/site-packages (from elasticsearch) (1.25.10)
Installing collected packages: elasticsearch
Successfully installed elasticsearch-7.9.1
$ pip show elasticsearch
Name: elasticsearch
Version: 7.9.1
Summary: Python client for Elasticsearch
Home-page: https://github.com/elastic/elasticsearch-py
Author: Honza Král, Nick Lang
Author-email: honza.kral@gmail.com, nick@nicklang.com
License: Apache-2.0
Location: /Users/kimai/local/python/py3/lib/python3.8/site-packages
Requires: urllib3, certifi
Required-by:

7.9.2は存在していないよう。

$ pip install 'elasticsearch==7.9.2'
ERROR: Could not find a version that satisfies the requirement elasticsearch==7.9.2 (from versions: 0.4.1, 0.4.2, 0.4.3, 0.4.4, 0.4.5, 1.0.0, 1.1.0, 1.1.1, 1.2.0, 1.3.0, 1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.4.1, 5.0.0, 5.0.1, 5.1.0, 5.2.0, 5.3.0, 5.4.0, 5.5.0, 5.5.1, 5.5.2, 5.5.3, 6.0.0, 6.1.1, 6.2.0, 6.3.0, 6.3.1, 6.4.0, 6.8.0, 6.8.1, 7.0.0, 7.0.1, 7.0.2, 7.0.3, 7.0.4, 7.0.5, 7.1.0, 7.5.1, 7.6.0a1, 7.6.0, 7.7.0a1, 7.7.0a2, 7.7.0, 7.7.1, 7.8.0a1, 7.8.0, 7.8.1, 7.9.0a1, 7.9.0, 7.9.1, 7.10.0a1)
ERROR: No matching distribution found for elasticsearch==7.9.2

elasticsearchへのデータ投入はpythonコードを実行して行います。

$ python src/index.py

正常に実行完了したあとは、kibana dev toolsを用いてインデックスデータの確認を行っていきます。

作成されたインデックスの確認

GET /ldnews

{
  "ldnews": {
    "aliases": {},
    "mappings": {
      "properties": {
        "category": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "contents": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    },
    "settings": {
      "index": {
        "creation_date": "1602655963825",
        "number_of_shards": "1",
        "number_of_replicas": "1",
        "uuid": "mlGuWK47R1SAlrf0S9Cwew",
        "version": {
          "created": "7090299"
        },
        "provided_name": "ldnews"
      }
    }
  }
}

インデックス登録済みの文書数取得

GET /ldnews/_count

{
  "count" : 7367,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  }
}

Search

1件取得してみます。

GET /ldnews/_search?size=1

{
  "took" : 730,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 7367,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "ldnews",
        "_type" : "_doc",
        "_id" : "7e29JXUBiDTGyEjqZ8tv",
        "_score" : 1.0,
        "_source" : {
          "category" : "movie-enter",
          "title" : "movie-enter-6858134.txt",
          "contents" : """http://news.livedoor.com/article/detail/6858134/
2012-08-16T10:00:00+0900
号泣ラブストーリー『100回泣くこと』が関ジャニ∞大倉&桐谷美玲で映画化
 2002年にデビュー作「リレキショ」で第39回文藝賞を受賞し、2004年には「ぐるぐるまわるすべり台」で第26回野間文芸新人賞を受賞するなど話題の絶えない中村航。その彼の55万部発行の号泣ラブストーリー『100回泣くこと』が実写映画化されることが決定した。

 主人公、藤井を演じるのは、満を持して映画単独初主演を務める大倉忠義。今年デビュー8周年を迎える関ジャニ∞主演映画『エイトレンジャー』や秋にはコンサートツアーが控えるなど、今最も勢いのあるグループのメンバーとして活躍するだけでなく、映画デビュー作『大奥』、ドラマ「ヤスコとケンジ」、「三毛猫ホームズの推理」に出演するなど俳優としての実力も高く評価されている。

 ヒロイン、佳美には映画『荒川アンダーザブリッジ』や『逆転裁判』でヒロインを務め、今年10月に『ツナグ』や『新しい靴を買わなくちゃ』の公開を控える若手実力派女優の桐谷美玲。その他、主演舞台「新・幕末純情伝」や情報番組「NEWS ZERO」でキャスターを務めるなど、近年活躍の場を広げることで幅広い世代から支持を受けている。

 監督は、2003年に『ヴァイブレータ』で第25回ヨコハマ映画祭にて監督賞を始め5部門を受賞したほか、40以上の国際映画祭で数々の賞を受賞し、一大センセーションを巻き起こした廣木隆一。『余命一ヶ月の花嫁』『雷桜』『軽蔑』そして、2013年には『きいろいゾウ』の公開が控えるなど、これまで男女の愛の様々な形、心の機微を見つめ続けてきた廣木隆一が、お互いが相手を想うがゆえに揺れ動く、男女の姿を繊細な描写で映し出す。

 脚本は、『ソラニン』などを手がけた高橋泉。本作では、原作にはない設定へとアレンジすることで、より感動的なストーリーを作り上げている。『100回泣くこと』は2013年、全国ロードショー。

藤井役・大倉忠義 コメント
 僕自身、これ程深い恋愛ストーリーは初めての挑戦です。そして、その作品にスクリーンで初主演という形で務めさせていただくことになりました。素直に嬉しいという気持ちとともに、原作・台本を読ませていただき"本当の愛”ということについて、改めて考えさせられました。主人公の繊細な心の移り変わりや葛藤を表現できればと思います。廣木監督、共演者の方々、スタッフの皆さんにお力を借りながら、観に来て下さった方に何か”大切なメッセージ”を伝えられる素敵な作品になるように頑張ります。

佳美役・桐谷美玲 コメント
 はじめて本を読ませて頂いた時、何気ない日常の中にある幸せをとても感じました。私が演じる佳美という役は、病気と必死に闘う姿、彼を一途に思う健気な姿、彼と一緒にすごしているときの可愛らしい姿…どの姿も魅力的なキャラクターだなと感じました。私が感じた魅力をみなさんにもスクリーンを通して伝えられればと思います。また、以前から、廣木監督の作品は好きでよく観ていて、この作品の素敵な世界観をこれから一緒に創り上げていけることが本当に嬉しいし、楽しみです。

監督・廣木隆一 コメント
 悲しい題材ですが永遠になれるように主演の二人の新鮮な組み合わせに期待してます

原作者・中村航 コメント
 真摯でひたむきな大倉さんと桐谷さんに以前から注目していました。お二人を通した『100回泣くこと』を心から楽しみにしています。

『100回泣くこと』ストーリー
4年前のバイク事故で記憶の一部を失った藤井(大倉忠義)は、友人の結婚式で佳美(桐谷美玲)に出会う。佳美に運命を感じた藤井は付き合いだしてまだ日が浅いうちに、「結婚しよう」と告げる。佳美は、1年間(結婚の)練習をしようと応えた。幸せがこのままずっと続くと思っていた藤井と佳美…。しかし、佳美に病魔が忍び寄る。なぜ、佳美は1年と伝えたのか?二人の本当の出会いとは?藤井の失われた記憶が明らかになったとき、私たちはあまりにも切なく、そして美しいラブストーリーを目撃する。

・100回泣くこと - 公式サイト
"""
        }
      }
    ]
  }
}

「Sports Watch」カテゴリの「キャスター」という文字列を含んだ記事を検索すべく、And検索を試してみます。

GET /ldnews/_search?size=1
{
  "query": {
    "bool": {
      "must": [
        { "match": { "contents": "キャスター" } },
        { "match": { "category": "sports-watch" } }
      ]
    }
  }
}


{
  "took" : 26,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 416,
      "relation" : "eq"
    },
    "max_score" : 14.871006,
    "hits" : [
      {
        "_index" : "ldnews",
        "_type" : "_doc",
        "_id" : "he2-JXUBiDTGyEjqFN7R",
        "_score" : 14.871006,
        "_source" : {
          "category" : "sports-watch",
          "title" : "sports-watch-6816328.txt",
          "contents" : """http://news.livedoor.com/article/detail/6816328/
2012-08-02T12:30:00+0900
小倉智昭キャスター、柔道に苦言「試合がつまらない」
2日、フジテレビ「とくダネ!」では、イギリスの地でロンドン五輪の現地取材を行ってる小倉智昭キャスターが、ここまで行われてた柔道の試合について苦言を述べた。

解説を務める、アテネ五輪柔道金メダリスト・鈴木桂治氏に対し、「アテネの頃と比べて、柔道場に朝から晩までいると試合がつまらないんですよ。本当に一本が数えるほどしかないんだよね。あれって、そのままでいいんだろうか、講道館柔道は?」と訴えかけた小倉キャスター。

鈴木氏は「海外の選手が日本人に対して、マークが厳しくなったり、柔道スタイルを変えてでも勝ちにこだわるっていうスタイルが増えていますので、日本は日本のスタイルを貫くのと同時に勝つことも考えなきゃいけないと思います」と返答したが、小倉キャスターは「指導2本で有効で、それだけで勝ち負けが決まっちゃうというのも解せないですよね」と続け、不満を抱いている様子をうかがわせた。
"""
        }
      }
    ]
  }
}

「キャス」だと検索にヒットしないことを確認。

GET /ldnews/_search?size=1
{
  "query": {
    "bool": {
      "must": [
        { "match": { "contents": "キャス" } },
        { "match": { "category": "sports-watch" } }
      ]
    }
  }
}

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

Aggregation

Aggregation機能を用いて、9カテゴリの各カテゴリの件数を出してみます。

GET /ldnews/_search?size=0
{
  "aggs": {
    "category_aggs": {
      "terms": {
        "field": "category.keyword"
      } 
    }
  }
}

{
  "took" : 6,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10000,
      "relation" : "gte"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "category_aggs" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "sports-watch",
          "doc_count" : 1800
        },
        {
          "key" : "dokujo-tsushin",
          "doc_count" : 1740
        },
        {
          "key" : "it-life-hack",
          "doc_count" : 1740
        },
        {
          "key" : "movie-enter",
          "doc_count" : 1740
        },
        {
          "key" : "smax",
          "doc_count" : 1740
        },
        {
          "key" : "kaden-channel",
          "doc_count" : 1728
        },
        {
          "key" : "peachy",
          "doc_count" : 1684
        },
        {
          "key" : "topic-news",
          "doc_count" : 1540
        },
        {
          "key" : "livedoor-homme",
          "doc_count" : 1022
        }
      ]
    }
  }
}

まとめ

日本語記事に対しての基本的な検索機能を試すまでの流れを試すことが出来ました。 この後は、他の機能を試したり、↑の機能を深ぼっりしていきます。

prismaを使ってQraphQLからMySQLに接続したい

QraphQLからMySQLに接続したい。
以下を参考にprsmaを用いて試してみる。

prismaとは

Prisma
Home - Prisma Docs

GraphQLのスキーマ定義をするだけで、以下ができるようになる便利なツール

  • 起動用docker-compose.yml生成
  • データ管理アプリ(Prisma Admin)
  • CRUDの自動生成(GraphQLサーバの実装)
  • インデックス自動生成
  • DB(MySQL/PostgreSQL)へのMigration
  • Seed定義
  • CRUDにアクセスするクライアント(JavaScript, TypeScript, Golang)生成

1. GraphQL APIサーバー構築

prismaを用いる前に、先にGraphQLサーバーを構築する

プロジェクト作成

$ mkdir prisma-demo
$ cd prisma-demo
$ npm init -y

パッケージインストール

$ npm install graphql-yoga
$ npm install nodemon ts-node typescript --save-dev
  • ts-node: コンパイル無しで、Node.js上で直接tsファイルを実行出来るライブラリ
  • nodemon: ファイルを監視し、変更があればNodeのプロセスを再起動してくれる

package.json更新

npm start で実行できるように、scriptsにstartを追加する

:
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon --ext ts,yaml,graphql --exec 'ts-node' src/index.ts"
  },
:

スキーマファイル作成

以下内容のsrc/schema.graphqlを作成する

type Query {
  posts: [Post!]!
  post(id: ID!): Post
  description: String!
}

type Mutation {
  createDraft(title: String!, content: String): Post
  deletePost(id: ID!): Post
  publish(id: ID!): Post
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
}

index.ts作成

以下内容のsrc/index.tsを作成する

import { GraphQLServer } from 'graphql-yoga';

const resolvers = {
  Query: {
    description: () => `dummy description`,
  },
};

const server = new GraphQLServer({
  typeDefs: './src/schema.graphql',
  resolvers,
});

server.start(() =>
  console.log(`The server is running on http://localhost:4000`),
);

リクエス

http://localhost:4000 にアクセスしリクエスト実行して、期待する結果 (dummy description ) が帰ってくることを確認

{
  description
}

2. Prismaサーバー構築

GraphQLサーバーからMySQLのデータ投入・取得するにあたり、リクエスト先のPrismaサーバーを構築する

prisma docs

prismaのインストール

$ npm install -g prisma
/usr/local/Cellar/node/13.7.0/bin/prisma -> /usr/local/Cellar/node/13.7.0/lib/node_modules/prisma/dist/index.js
+ prisma@1.34.10
:
$ prisma -v
Prisma CLI version: prisma/1.34.10 (darwin-x64) node-v13.7.0

database関連のファイル生成

prisma init databaseを実行し、
deployするDBの種類や、prisma clientコードの言語を選択すると、
自動で必要なファイルを生成してくれる。

$ prisma init database
? Set up a new Prisma server or deploy to an existing server? Create new database
? What kind of database do you want to deploy to? MySQL
? Select the programming language for the generated Prisma client Prisma TypeScript Client

Created 3 new files:

  prisma.yml           Prisma service definition
  datamodel.prisma    GraphQL SDL-based datamodel (foundation for database)
  docker-compose.yml   Docker configuration file

Next steps:

  1. Open folder: cd database
  2. Start your Prisma server: docker-compose up -d
  3. Deploy your Prisma service: prisma deploy
  4. Read more about Prisma server:
     http://bit.ly/prisma-server-overview

Generating schema... 24ms


Saving Prisma Client (TypeScript) at .../database/generated/prisma-client/

生成ファイル一覧

$ tree database
database
├── datamodel.prisma // DBのテーブル構造元となる型定義
├── docker-compose.yml
├── generated
│   └── prisma-client
│       ├── index.ts
│       └── prisma-schema.ts
└── prisma.yml // Prismaサーバーの設定ファイル

datamodel.prisma書き換え

datamodel.prisma // DBのテーブル構造元となる型定義

本家チュートリアルに沿って、次の内容でdatamodel.prismaを書き換え

type Post {
  id: ID! @id
  title: String!
  content: String!
  published: Boolean!
}

DB接続用ポートの設定

ports設定 "3306:3306"コメントアウトされているので外して有効にする

:
  mysql:
    image: mysql:5.7
    restart: always
    # Uncomment the next two lines to connect to your your database from outside the Docker environment, e.g. using a database GUI like Workbench
    ports:
    - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql
:

コンテナ起動

MySQLコンテナとprisma serverを起動する

$ cd database
$ docker-compose up -d
Creating network "database_default" with the default driver
Creating volume "database_mysql" with default driver
Creating database_mysql_1  ... done
Creating database_prisma_1 ... done

デプロイ

デプロイと同時に、DBに対してmigrationが実行される

$ cd database
$ prisma deploy
Creating stage default for service default ✔
Deploying service `default` to stage `default` to server `local` 691ms

Changes:

  Post (Type)
  + Created type `Post`
  + Created field `id` of type `ID!`
  + Created field `title` of type `String!`
  + Created field `content` of type `String!`
  + Created field `published` of type `Boolean!`

Applying changes 1.1s
Generating schema 50ms
Saving Prisma Client (TypeScript) at .../prisma-demo/database/generated/prisma-client/

Your Prisma endpoint is live:

  HTTP:  http://localhost:4466
  WS:    ws://localhost:4466

You can view & edit your data here:

  Prisma Admin: http://localhost:4466/_admin

Prismaサーバー起動確認

デプロイ完了後、http://localhost:4466 にアクセス DOCSを開くと、Postテーブルに対するCRUDが実装されていることが確認できる

f:id:kimai007:20200211175309p:plain

また、http://localhost:4466/_adminPrisma用Admin画面にアクセスする事ができ、クエリを投げたりすることが出来る

f:id:kimai007:20200211175556p:plain

3. PrismaサーバーとGraphQLサーバーとの紐付け

Prismaサーバーとの紐付け用設定ファイルの作成

.graphqlconfig.yml作成を作成する

projects:
  database:
    schemaPath: src/generated/prisma.graphql
    extensions:
      prisma: database/prisma.yml
  app:
    schemaPath: src/schema.graphql
    extensions:
      endpoints:
        default: http://localhost:4000

Prismaスキーマファイルの作成

graphql-cliインストール

$ npm install -g graphql-cli

実行

  • GraphQLサーバーが起動している状態で実行する
$ graphql get-schema
project database - endpoint default - No changes
project app - endpoint default - Schema file was updated: src/schema.graphql

パッケージインストール

紐付けに必要なライブラリ

$ npm install prisma-binding

src/index.tsのアップデート

import { ApolloServer, gql } from "apollo-server";
import { Prisma } from "prisma-binding";
import { IResolvers } from "graphql-middleware/dist/types";

const fs = require("fs");
const typeDefs = gql`
  ${fs.readFileSync(__dirname.concat("/schema.graphql"), "utf8")}
`;

const resolvers: IResolvers = {
  Query: {
    posts(parent, args, ctx, info) {
      return ctx.db.query.posts({}, info);
    },
    post(parent, args, ctx, info) {
      return ctx.db.query.post({ where: { id: args.id } }, info);
    }
  },
  Mutation: {
    createDraft(parent, { title, content }, ctx, info) {
      return ctx.db.mutation.createPost(
        {
          data: {
            title,
            content,
            published: true
          }
        },
        info
      );
    },
    deletePost(parent, { id }, ctx, info) {
      return ctx.db.mutation.deletePost({ where: { id } }, info);
    },
    publish(parent, { id }, ctx, info) {
      return ctx.db.mutation.updatePost(
        {
          where: { id },
          data: { published: true }
        },
        info
      );
    }
  }
};

const server = new ApolloServer({
  typeDefs: typeDefs,
  resolvers,
  context: req => ({
    ...req,
    db: new Prisma({
      typeDefs: "src/generated/prisma.graphql", // the generated Prisma DB schema
      endpoint: "http://localhost:4466", // the endpoint of the Prisma DB service
      // secret: "mysecret123", // specified in database/prisma.yml
      debug: true // log all GraphQL queries & mutations
    })
  })
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

src/schema.graphqlの以下箇所を削除

"""The `Upload` scalar type represents a file upload."""
scalar Upload

再度サーバー起動

$ npm start

リクエス

1.データが入っていない状態で、データ取得をしてみる

f:id:kimai007:20200211173555p:plain

2.その後、データ投入

f:id:kimai007:20200211173538p:plain

3.投入データが取得できることを確認

f:id:kimai007:20200211173516p:plain

さいごに

prismaを使ってQraphQLからMySQLに接続してみた。
prismaのinitコマンドで必要な雛形ファイルを生成してくれるのは便利そう。
prismaを使いこなすには、自動生成される分、生成されるファイル群をよく把握したり、何度も使って慣れる必要がありそうと感じた。

GraphQL Apolloを試す

GraphQLのフロントエンド&バックエンドのライブラリであるApolloを試す。

Apollo

  • GraphQL のサーバー・クライアント用のライブラリ
  • Meteor を開発している Meteor Development Group 社が開発
  • FE側ではapollo-clientを、BE側ではapollo-serverを導入する

apollo server

プロジェクト作成

mkdir graphql-server-example
cd graphql-server-example
npm init -y

パッケージインストール

npm install apollo-server

サーバー作成

以下コードのindex.jsを作成する

const { ApolloServer, gql } = require("apollo-server");

// スキーマ定義
const typeDefs = gql`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
  }
`;
// データセット定義
const books = [
  {
    title: "Harry Potter and the Chamber of Secrets",
    author: "J.K. Rowling"
  },
  {
    title: "Jurassic Park",
    author: "Michael Crichton"
  }
];

// リゾルバー定義
const resolvers = {
  Query: {
    books: () => books
  }
};

// ApolloServer生成
const server = new ApolloServer({ typeDefs, resolvers });

server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

サーバー起動

$ node index.js
🚀  Server ready at http://localhost:4000/

リクエス

{
  books {
    title
    author
  }
}

f:id:kimai007:20200208160017p:plain

shcema定義を、ファイルから読み込むようにする

  • schema定義を、index.jsで定義していたが、実際はschema用ファイルを用意することが想定される。
  • そこで、schema.graphqlを読み込むようにしてみる。

schema.graphqlを用意する

type Book {
  title: String
  author: String
}

type Query {
  books: [Book]
}

index.js編集

fs.readFileSyncでgraphqlファイルを読み込むようにする

const { ApolloServer, gql } = require("apollo-server");

// スキーマ定義
const fs = require("fs");
const typeDefs = gql`
  ${fs.readFileSync(__dirname.concat("/schema.graphql"), "utf8")}
`;

// データセット定義
const books = [
:

あとはサーバーを起動しリクエストすれば、先程と同様のレスポンスを得られる

apollo client

apollo boost

今回はget-startedに沿って、こちらのパッケージを利用する

パッケージインストール

npm install apollo-boost @apollo/react-hooks graphql
npm install react react-dom react-scripts

プロジェクト作成

mkdir graphql-client-example
cd graphql-client-example
npm init -y

クライアント作成 (src/index.js)

以下コードのindex.jsを作成する。 Basic Apollo app - CodeSandbox のコードを参考に動かしてみる。

import React from "react";
import { render } from "react-dom";

import ApolloClient from "apollo-boost";
import { ApolloProvider, useQuery } from "@apollo/react-hooks";
import gql from "graphql-tag";

const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

function FetchBooks() {
  const { loading, error, data } = useQuery(gql`
    {
      books {
        title
        author
      }
    }
  `);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.books.map(({ title, author }) => (
    <div key={title}>
      <p>
        {title}: {author}
      </p>
    </div>
  ));
}

const App = () => (
  <ApolloProvider client={client}>
    <div>
      <h2>My first Apollo app</h2>
      <FetchBooks />
    </div>
  </ApolloProvider>
);

render(<App />, document.getElementById("root"));

public/index.html作成

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
  </head>

  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
  </body>
</html>

クライアント起動

package.json編集・追記

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },

以下コマンド実行

$ npm run start

Compiled successfully!

You can now view graphql-client-example in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://10.138.142.229:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

その後、自動でブラウザが開き、以下の http://localhost:3000 の画面が表示される

f:id:kimai007:20200208165709p:plain

当然だが、GraphQL Serverが起動していない状態だと、エラーとなる

f:id:kimai007:20200208165956p:plain

  • consoleエラーはこんな感じ

f:id:kimai007:20200208170120p:plain

gqlgenを試す

以下を参考に、GraphQLサーバー開発用Goライブラリであるgqlgenを試す

gqlgenとは

  • GraphQLのSchemaベースで、GraphQLサーバー開発を進めるためのライブラリ
  • 特徴
    • schema first: GraphQLスキーマ定義言語を用いてAPI定義
    • type safe: map[string]interface{} を使わない
    • codegen: コマンドで必要なコードを自動生成して、すぐにアプリケーション構築できる
  • gqlgen公式
  • github

インストール

gqlgenをインストールする

$ go get -u github.com/99designs/gqlgen 

コマンドヘルプ

$ gqlgen -help
NAME:
   gqlgen - generate a graphql server based on schema

USAGE:
   gqlgen [global options] command [command options] [arguments...]

DESCRIPTION:
   This is a library for quickly creating strictly typed graphql servers in golang. See https://gqlgen.com/ for a getting started guide.

COMMANDS:
   generate  generate a graphql server based on schema
   init      create a new gqlgen project
   version   print the version string
   help, h   Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --verbose, -v             show logs
   --config value, -c value  the config filename
   --help, -h                show help

setup project

$ mkdir gqlgen-todos
$ cd gqlgen-todos
$ go mod init github.com/[username]/gqlgen-todos
go: creating new go.mod: module github.com/[username]/gqlgen-todos
$ tree
.
└── go.mod

create the project skeleton

$ gqlgen init
Exec "go run ./server/server.go" to start GraphQL server
$ tree
.
├── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── models_gen.go
├── resolver.go
├── schema.graphql
└── server
    └── server.go

gqlgen initで作成されたファイル

ファイル 説明
schema.graphql GraphQLのスキーマが書かれている。gqlgen generate を実行すると、このスキーマから 各 .go ファイルに Goコードとして記載される
generated.go schema.graphqlで記載したスキーマ情報、それに沿ったResolverやTypeの情報など、GraphQLで必要な処理が書かれている
gqlgen.yml 設定ファイル (gqlgen.ymlの設定項目)
models_gen.go schema.graphqlに記載されたもの(Type,Input,Scalar,Enumなど)に基づいたType等の Go struct が書かれている
resolver.go 各QueryやMutationを処理するためのメソッドが書かれている
server/server.go GraphQL playground用サーバー起動するためのコードが書かれている

resolver実装

自動生成されたresolver.goのTodos関数は、not impletemntedになっている.

:
func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
  panic("not implemented")
}

ここでは、ダミーのTodoリストを返すように、変更してみる。

func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
    return []*Todo{
        {ID: "1"},
        {ID: "2"},
        {ID: "3"},
    }, nil
}    

サーバー起動

$ go run ./server/server.go
2020/02/03 00:26:42 connect to http://localhost:8080/ for GraphQL playground

リクエス

ID取得リクエス

GraphiQL等で、Endpointを http://localhost:8080/query にセットし以下リクエストしてみる。
resolverのTodos関数で指定したTodoリストが返ってくることが確認出来る。

query {
  todos {
    id
  }
}

実行結果

f:id:kimai007:20200203002941p:plain

Text取得リクエス

現在のschema.graphqlでのTodo定義は、以下のようになっている。

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
}

試しにidフィールドではなく、resolverで値を返却しないtextフィールドを取得するようにリクエストしてみる。
空文字が入ったtextフィールドのみ返ってくることが確認できる(idフィールドは含まれていない)

query {
  todos {
    text
  }
}

実行結果

f:id:kimai007:20200203002919p:plain

フィールド追加

今後は、一度コードを生成した後、GraphQLのインタフェースを変えたい場合、どう実装していくのか

  • やること
    • schema.grpahqlを編集してフィールド追加を行い、Goのコードを再生成する
    • 今回は、Todo に comment フィールド を追加する

schema.graphqlの編集

commentフィールドを追加

type Todo {
  id: ID!
  text: String!
  done: Boolean!
  user: User!
  
  comment: String!
}

gqlgenによるGoコード再生成

追加したフィールドをGoコードに反映させたい場合、以下を実行する

$ gqlgen generate
または短縮系の
$ gqlgen

以下ファイルが一度削除されて、再度作成される

  • generated.go
  • models_gen.go

resolverのTodos関数のの実装を修正する

commentの値を追加する

func (r *queryResolver) Todos(ctx context.Context) ([]*Todo, error) {
    return []*Todo{
        {ID: "1", Comment: "comment1"},
        {ID: "2", Comment: "comment2"},
        {ID: "3", Comment: "comment3"},
    }, nil
}

リクエス

f:id:kimai007:20200203002857p:plain

まとめ

GraphQLサーバー開発を行う上で、shcemaからGo等のアプリケーションコードを生成してくれるライブラリを用いることは、特にshemaに変更があった時に効果を発揮するような感じがする。
また、GraphQLを追い始めた自分としては、gqlgenを使ってのコード生成を試して、GraphQLの理解が少し進んだように感じた。
Comparing Features of Other Go GraphQL Implementations — gqlgen を見るとgqlgenに似たライブラリがいくつかあるとのことで、他も時間がある時にチェックしたと思う。

Node.js + ExpressでGraphQLを試す

この記事では、以下を参考にGraphQL.jsを試してみます。

実行環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.2
BuildVersion:   19C57
$ node -v
v13.7.0
$ npm -v
6.13.4

プロジェクト作成 + graphqlインストール

$ npm init -y
$ npm install graphql --save

server.js作成

var { graphql, buildSchema } = require('graphql');

// Construct a schema, using GraphQL schema language
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// The root provides a resolver function for each API endpoint
var root = {
  hello: () => {
    return 'Hello world!';
  },
};

// Run the GraphQL query '{ hello }' and print out the response
graphql(schema, '{ hello }', root).then((response) => {
  console.log(response);
});

スキーマ言語docs

実行

$ node server.js 
{ data: [Object: null prototype] { hello: 'Hello world!' } }
  • Tutorial通りに実行してみたが、レスポンスにnull prototypeも含まれてしまった。(なにか必要なのだろうか..)

Expressインストール

$ npm install express express-graphql graphql --save

server.js更新

expressを使うようにserver.jsを更新していきます。

var express = require('express');
var graphqlHTTP = require('express-graphql');
var { buildSchema } = require('graphql');

// Construct a schema, using GraphQL schema language
var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

// The root provides a resolver function for each API endpoint
var root = {
  hello: () => {
    return 'Hello world!';
  },
};

var app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at http://localhost:4000/graphql');

GraphQLサーバー起動、リクエス

$ node server.js
Running a GraphQL API server at http://localhost:4000/graphql

f:id:kimai007:20200202001904p:plain

GraphQL入門

この記事では、以下を中心にGraphQLの初歩について整理していきます。

GraphQLとは

  • Facebookが開発しているWeb APIのための規格
  • スキーマ言語」と「クエリ言語」からなる
    • スキーマ言語
      • GraphQL APIの仕様を記述するための言語
      • リクエストされたqueryは、スキーマ言語で記述したスキーマに従ってGraphQL処理系により実行されて、レスポンスを生成
    • クエリ言語
      • GraphQL APIのリクエストのための言語
      • これはさらに以下3種類がある
        • データ取得系のquery
        • データ更新系のmutation
        • サーバーサイドからのイベントの通知であるsubscription

起源

  • もともとFacebookが開発したもので、2015年に仕様と参照実装がOSS
  • GraphQLを開発する以前
    • FacebookはWeb APIとして、「RESTful API」と「FQL(Facebook Query Language)」を運用していた.
      • RESTful APIは、現在も“Graph API”としてFacebookソーシャルグラフへアクセスするための公開APIとして提供
      • FQL(SQL風の構文である)は、廃止
        • GraphQLに置き換えられた?
  • GraphQL開発の動機
    • モバイルアプリケーションで利用するオブジェクトグラフとAPIレスポンスの構造に乖離があり、この問題を改善するため
  • 2018年11月にGraphQL Foundationが設立される
    • 以下等のメンテナンスがなされていく
      • GraphQLの規格そのもの
      • 参照実装やエディタなどのツールチェイン
    • 安定して開発されることが期待される

特徴

  • クエリの構造とレスポンスの構造がよく似ていること
  • スキーマによる型付けにより型安全な運用ができること
  • レスポンスに含まれるデータの指定が必須であること
  • クエリからレスポンスの構造を予測できるため、Web APIに対する深い知識がなくても、GraphQLのクエリであればある程度は読み書きが出来ること
  • スキーマとそれを利用するツールによる開発サポートが受けられる (ex. GraphiQL)
  • クエリの学習コストが小さいこと

スキーマ言語

  • Web APIの仕様を記述するための言語
  • 型システムを内包
    • クエリやレスポンスのバリデーションやリゾルバの適用に利用
type Query {
  todos: [Todo!]!
}

type Todo {
  id: ID!
  name: String!
  user: User!
  priority: Int
}

type User {
  id: ID!
  name: String!
}
  • フィールド
    • 型は、スカラー型、オブジェクト型、 列挙型などを利用可能
    • Not Nullは感嘆符で表現: ex. ID!
    • リストは角カッコで表現: ex. [Todo!]`
  • 各フィールドにはリゾルバ(resolver)と呼ばれる関数がマッピングされる
    • ゾルバ: オブジェクト(ex. Userインスタンス)を引数として受け取り、そのオブジェクトのプロパティ(ex. User#name)を返す関数

クエリ

  • Web APIリクエストにおいてどのようなデータを取得するかを表現
  • オペレーション型
    • データ取得系のquery、データ更新系のmutation、pub/subモデルでサーバーサイドのイベントを受け取るsubscriptionの3種類がある

リクエス

query {
  getTodos {
    id
    name
    priority
  }
}

レスポンス

{ "id": "1", "name": "Get Milk", “priority": "1" },
{ "id": “2", "name": “Go to gym", “priority": “5" },
  • 上記のクエリのタイプ: query(=データ取得系)
    • idのname,priorityを取得する

GraphQLを触ってみる

以下Playgroundで試すことが出来る