Amazon Elasticsearch Serviceチュートリアル
Amazon Elasticsearch Service のdeployやデータ投入、検索リクエストを試すべく、以下資料を参考に進めていきます。 今回はAWS CDKでElasticsearch Serviceドメイン(クラスタ)を作成する方法で行っていきます。
参考資料
- Elasticsearch tutorial: a quick start guide
- aws-samples/aws-cdk-examples: Example projects using the AWS CDK
- Amazon Elasticsearch Serviceについてまとめてみる - Qiita
- AWS CDKでAmazon Elasticsearch Serviceのドメイン(クラスタ)を作ってみた | Developers.IO
インストール
まず、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を使ってMySQLに接続するGraphQL APIサーバーを構築 - Qiita
- コーディング不要でGraphQLサーバが作れるPrismaを触ってみて可能性を感じた - SMARTCAMP Engineer Blog
- Build a GraphQL server from scratch | Prisma Docs
prismaとは
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のインストール
$ 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 :
コンテナ起動
$ 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が実装されていることが確認できる
また、http://localhost:4466/_admin でPrisma用Admin画面にアクセスする事ができ、クエリを投げたりすることが出来る
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.データが入っていない状態で、データ取得をしてみる
2.その後、データ投入
3.投入データが取得できることを確認
さいごに
prismaを使ってQraphQLからMySQLに接続してみた。
prismaのinitコマンドで必要な雛形ファイルを生成してくれるのは便利そう。
prismaを使いこなすには、自動生成される分、生成されるファイル群をよく把握したり、何度も使って慣れる必要がありそうと感じた。
GraphQL Apolloを試す
GraphQLのフロントエンド&バックエンドのライブラリであるApolloを試す。
Apollo
- GraphQL のサーバー・クライアント用のライブラリ
- Meteor を開発している Meteor Development Group 社が開発
- FE側ではapollo-clientを、BE側ではapollo-serverを導入する
apollo server
- まず、server側から見ていく
- apollo-serverを用いることで、GraphQL 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 } }
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
- https://www.apollographql.com/docs/react/get-started/#apollo-boost
- apollo-clientを素早くセットアップするためのパッケージ
- GraphQL Serverのエンドポイント設定するだけで動作する
- 以下のパッケージが含まれている
今回は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 の画面が表示される
当然だが、GraphQL Serverが起動していない状態だと、エラーとなる
- consoleエラーはこんな感じ
gqlgenを試す
以下を参考に、GraphQLサーバー開発用Goライブラリであるgqlgenを試す
gqlgenとは
インストール
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 } }
実行結果
Text取得リクエスト
現在のschema.graphqlでのTodo定義は、以下のようになっている。
type Todo { id: ID! text: String! done: Boolean! user: User! }
試しにidフィールドではなく、resolverで値を返却しないtextフィールドを取得するようにリクエストしてみる。
空文字が入ったtextフィールドのみ返ってくることが確認できる(idフィールドは含まれていない)
query { todos { text } }
実行結果
フィールド追加
今後は、一度コードを生成した後、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 }
リクエスト
まとめ
GraphQLサーバー開発を行う上で、shcemaからGo等のアプリケーションコードを生成してくれるライブラリを用いることは、特にshemaに変更があった時に効果を発揮するような感じがする。
また、GraphQLを追い始めた自分としては、gqlgenを使ってのコード生成を試して、GraphQLの理解が少し進んだように感じた。
Comparing Features of Other Go GraphQL Implementations — gqlgen を見るとgqlgenに似たライブラリがいくつかあるとのことで、他も時間がある時にチェックしたと思う。
- schema first: gqlgen
- schema first: https://github.com/graph-gophers/graphql-go
- run time type: https://github.com/graphql-go/graphql
- struct first: https://github.com/samsarahq/thunder
Node.js + ExpressでGraphQLを試す
この記事では、以下を参考にGraphQL.jsを試してみます。
- Getting Started With GraphQL.js | GraphQL.js Tutorial
- Running an Express GraphQL Server | GraphQL.js Tutorial
実行環境
$ 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); });
- buildSchema
- GraphQLSchemaオブジェクトを生成
- buildschema
- graphql
- 定義されたschemaに対して、'{ hello }' をリクエスト
- APIルートにマッピングされたresolverで 'Hello World'を返す
- graphql
※ スキーマ言語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
- http://localhost:4000/graphqlにアクセス
- GraphiQLで
{ hello }
をリクエストすると以下の結果が得られたことを確認。
GraphQL入門
この記事では、以下を中心にGraphQLの初歩について整理していきます。
GraphQLとは
起源
- もともとFacebookが開発したもので、2015年に仕様と参照実装がOSSに
- GraphQLを開発する以前
- GraphQL開発の動機
- モバイルアプリケーションで利用するオブジェクトグラフとAPIレスポンスの構造に乖離があり、この問題を改善するため
- 2018年11月にGraphQL Foundationが設立される
- 以下等のメンテナンスがなされていく
- GraphQLの規格そのもの
- 参照実装やエディタなどのツールチェイン
- 安定して開発されることが期待される
- 以下等のメンテナンスがなされていく
特徴
- クエリの構造とレスポンスの構造がよく似ていること
- スキーマによる型付けにより型安全な運用ができること
- レスポンスに含まれるデータの指定が必須であること
- クエリからレスポンスの構造を予測できるため、Web APIに対する深い知識がなくても、GraphQLのクエリであればある程度は読み書きが出来ること
- スキーマとそれを利用するツールによる開発サポートが受けられる (ex. GraphiQL)
- クエリの学習コストが小さいこと
スキーマ言語
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)と呼ばれる関数がマッピングされる
クエリ
- 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で試すことが出来る