小技

日々の仕事の中で調べたり発見したりしたことを書き留めていくブログ

Kibana REST API を DevToos のコンソールで実行する

DevTools のコンソールでは、ログインしたユーザーの権限で Elasticsearch の API を実行できるので便利です。 しかし、以下のようにエラーになってしまうことがあります。

GET /api/data_views

# レスポンス
{
  "error": "Incorrect HTTP method for uri [/api/data_views?pretty=true] and method [GET], allowed: [POST]",
  "status": 405
}

POST は許可されているということですが、実際にやってみるとエラーになってしまうので、メソッドの間違いでもなさそうです。

POST /api/data_views

# レスポンス
{
  "error": "no handler found for uri [/api/data_views?pretty=true] and method [POST]"
}

Kibana REST API の実行方法

調べてみると、Data Views API は Elasticsearch ではなく、Kibana の API であるようです。

Get all data views API | Kibana Guide [8.12] | Elastic

Requestedit
GET <kibana host>:<port>/api/data_views

GET <kibana host>:<port>/s/<space_id>/api/data_views

この場合、ホスト名やポート番号についても、Kibana のものを指定する必要があるようです。 実際にDevTools ではなく curl で実行してみたところ、Kibana のホスト名やポート番号を指定することで実行可能でした。

# curl -s -u admin http://localhost:5601/api/data_views | jq .
Enter host password for user 'admin':
{
  "data_view": [
    {
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
      "namespaces": [
        "default"
      ],
      "title": "kibana_sample_data_ecommerce",
      "typeMeta": {},
      "name": "Kibana Sample Data eCommerce"
    }
  ]
}

しかし、DevTools では実行するホスト名やポート番号を指定しても、URI の先頭に / が補完されてしまうようで、うまく実行できません。

GET http://localhost:5601/api/data_views

# レスポンス
{
  "error": "no handler found for uri [/http%3A//localhost%3A5601/api/data_views?pretty=true] and method [GET]"
}

DevTools のコンソールにおける Kibana API の実行方法

何か方法はないものかと探してみたところ、Elastic のドキュメントに記載がありました。

REST API | Kibana Guide [8.12] | Elastic

Using the APIsedit Prepend any Kibana API endpoint with kbn: and send the request through Dev Tools > Console. For example:

GET kbn:/api/index_management/indices

どうやら URI の先頭に kbn: をつけることで、DevTool のコンソール上で Kibana API を実行できるようです。 実際に試してみると、うまく実行することができました。

GET kbn:/api/data_views

# レスポンス
{
  "data_view": [
    {
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
      "namespaces": [
        "default"
      ],
      "title": "kibana_sample_data_ecommerce",
      "typeMeta": {},
      "name": "Kibana Sample Data eCommerce"
    }
  ]
}

DevTools コンソールにおける Kibana API 実行時の注意事項

API はうまく実行できましたが、ドキュメント上では上記引用部分に続けて以下の記載もあります。

Note: this will automatically prefix s/{space_id}/ on the API request if ran from a non-default Kibana Space.

デフォルト以外のスペースで実行した場合には、スペース ID の指定が自動で追加されるようです。

Data views API は、Data view の一覧を表示するものですが、Data view は特定のスペースに対して設定するものです。 スペース ID を指定して Data views API を実行することで、指定したスペースに設定された Data view の一覧を取得することができます。

前節の実行例はデフォルトスペースで実行したもので、スペース ID の指定は行っていません。 同様にデフォルトスペースでスペース ID を指定して実行すると以下のようになります。

GET kbn:/s/default/api/data_views

# レスポンス
{
  "data_view": [
    {
      "id": "ff959d40-b880-11e8-a6d9-e546fe2bba5f",
      "namespaces": [
        "default"
      ],
      "title": "kibana_sample_data_ecommerce",
      "typeMeta": {},
      "name": "Kibana Sample Data eCommerce"
    }
  ]
}

デフォルトスペースの ID を指定すると、未指定の場合と同様の結果になります。 この場合は未指定とすることでデフォルトスペースの情報が表示されているものと考えられます。

GET kbn:/s/test1/api/data_views

# レスポンス
{
  "data_view": [
    {
      "id": "7c9b9a4e-91e5-4ea1-b2d5-7445481a588f",
      "namespaces": [
        "test1"
      ],
      "title": "test1-*",
      "typeMeta": {},
      "name": "test1"
    }
  ]
}

デフォルト以外のスペース ID を指定すると、先ほどとは異なる結果となり、指定したスペースの情報が表示されているようです。

一方で、Kibana のスペースを test1 に切り替えて実行すると以下のようになります。

GET kbn:/api/data_views

# 
{
  "data_view": [
    {
      "id": "7c9b9a4e-91e5-4ea1-b2d5-7445481a588f",
      "namespaces": [
        "test1"
      ],
      "title": "test1-*",
      "typeMeta": {},
      "name": "test1"
    }
  ]
}

ここではスペース ID の指定を行っていませんが、先ほど test1 のスペース ID を指定した場合と同様の結果となっています。これは URI の先頭に /s/test1 が追加されたためと考えられます。 一方で、明示的にスペース ID の指定を行った場合は、いずれのスペース ID でも 404 エラーとなってしまいました。

GET kbn:/s/default/api/data_views

# レスポンス(404 Not Found)
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Not Found"
}

GET kbn:/s/test1/api/data_views

# レスポンス(404 Not Found)
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Not Found"
}

Data view API 以外での挙動は確認していませんが、DevTools のコンソールで Kibana API を実行する際の知識として覚えておいた方がよさそうです。

Elasticsearch のドキュメント更新をすっきり理解する

Elasticsearch におけるドキュメントの更新は、ドキュメントの登録の場合と同じく、いくつかやり方がありそうです。 こちらも自分なりに調べた結果をまとめてみます。

Index API の _doc エンドポイント

前回の記事で調べた通り、_doc エンドポイントでは既存のドキュメントに対して、同じドキュメント ID で PUT または POST メソッドでリクエストを行うことで、ドキュメントの更新が可能です。 ここでは、あらかじめ以下のようにドキュメントを登録しておいたものとします。

PUT test-index/_doc/1
{
    "field1": "foo"
}

この時、以下のようにリクエストを行うことで、フィールドの値を変更することができます。

PUT test-index/_doc/1
{
    "field1": "bar"
}

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

レスポンスの result フィールドが updated となっていて、更新が行われたことがわかります。POST メソッドを用いた場合も、同様の応答となります。 更新されたドキュメントを実際に取得してみると、フィールドの値が更新時に指定したものになっています。

GET test-index/_doc/1

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 2,
  "_seq_no": 1,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field1": "bar"
  }
}

では、異なるフィールド名を指定した場合はどうなるか試してみます。

PUT test-index/_doc/1
{
    "field2": "bar"
}

この場合も同様にレスポンスの result フィールドは updated となりますが、更新されたドキュメントを確認してみると、以下のようになっています。

GET test-index/_doc/1

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 3,
  "_seq_no": 2,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field2": "bar"
  }
}

更新後のドキュメントには field2 が含まれていますが、field1 は含まれていません。つまりここでのドキュメントの更新は、全体の上書き、または置き換えになっています。 更新時に既存のドキュメントのすべてのフィールドとその値を記述し、加えて追加のフィールドと値を記述することで、フィールドを追加することができます。しかし、既存のドキュメントが多数のフィールドを含む場合にはこれは大変な作業になります。 では、既存ドキュメントの一部のみを更新したい場合には、どうするのが良いのでしょうか。

Update API を使ったドキュメントの更新

既存のドキュメントの一部を更新する方法について、公式ドキュメント では以下のように紹介されています。

Update part of a documentedit

The following partial update adds a new field to the existing document:

 POST test/_update/1
 {
   "doc": {
     "name": "new_name"
   }
 }

前節の例に続けて以下のようにすることで、フィールドの追加を行うことができます。

POST test-index/_update/1
{
  "doc": {
    "field3": "baz"
  }
}

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

更新されたドキュメントを見てみると、field3 が追加され、もともとあった field2 も残っていることがわかります。

GET test-index/_doc/1

{
  "_index": "test-index",
  "_id": "1",
  "_version": 4,
  "_seq_no": 3,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field2": "bar",
    "field3": "baz"
  }
}

もちろん、既存のフィールドの値を変更することもできます。

POST test-index/_update/1
{
  "doc": {
    "field3": "foo"
  }
}

GET test-index/_doc/1

{
  "_index": "test-index",
  "_id": "1",
  "_version": 5,
  "_seq_no": 4,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field2": "bar",
    "field3": "foo"
  }
}

ここでは、更新時に指定した field3 の値が変更されている一方で、指定していない field2 については元の値のままになっています。

既存ドキュメントのフィールドを削除する方法

既存のドキュメントに対して、フィールドの値の変更やフィールドの追加を行えることがわかりました。 _doc エンドポイントを用いる方法では、更新時に指定した内容でドキュメントの上書きを行うため、必要ないフィールドについては更新時に指定しないことで、結果として削除することができます。 しかし、やはり多数のフィールドを持つドキュメントから少数のフィールドを削除したい場合には、記述する内容が多くなるため、大変な作業になります。 Update API を用いて、指定したフィールドのみを削除する方法はないものでしょうか。

まず思いつくのは、値を null にすることです。

PUT test-index/_doc/1
{
    "field2": null
}

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 6,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 5,
  "_primary_term": 1
}

しかしこの方法では、対象のフィールドの値が null に変更されるのみで、フィールドそのものは削除されません。

GET test-index/_doc/1

# レスポンス
{
  "_index": "test-index",
  "_id": "1",
  "_version": 6,
  "_seq_no": 5,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field2": null,
    "field3": "foo"
  }
}

Update APIでは、doc 要素を指定して更新を行うだけでなく、スクリプト実行を実行することができ、これによりフィールドの削除を行うことができます。 (むしろ公式ドキュメントではスクリプトの実行の方が先に記載されています)

Conversely, this script removes the field new_field:

POST test/_update/1
{
  "script" : "ctx._source.remove('new_field')"
}

前節までの例では、以下のようにして field2 を削除することができます。

POST test-index/_update/1
{
  "script": "ctx._source.remove('field2')"
}

{
  "_index": "test-index",
  "_id": "1",
  "_version": 6,
  "result": "updated",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 5,
  "_primary_term": 1
}

GET test-index/_doc/1

{
  "_index": "test-index",
  "_id": "1",
  "_version": 6,
  "_seq_no": 5,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field3": "foo"
  }
}

参考

Update API | Elasticsearch Guide [8.12] | Elastic

Elasticsearch へのドキュメント登録をすっきり理解する

Elasticsearch を利用するにあたり、ドキュメントの登録の仕方を調べようと思ってググってみると、いろいろな記述が見つかります。 どうやらやり方はいくつかありそうなものの、リクエストのメソッドは PUTPOST、どちらにするのが良いのか、ドキュメント ID の指定は必要なのか等、すっきりしないことが多々ありました。 ここでは、自分なりに調べた結果をまとめてみます。

_doc_create

Elasticsearch へのドキュメントの登録について、多くの記事では _doc エンドポイントに関する記述があります。一方、Elasticsearch の Index API には、_create エンドポイントというのもあるようです。 Elastic の公式ドキュメントには、以下の記述があります。

Request
PUT /<target>/_doc/<_id>
POST /<target>/_doc/
PUT /<target>/_create/<_id>
POST /<target>/_create/<_id>

Prerequisites
(略)
- To add or overwrite a document using the PUT /<target>/_doc/<_id> request format, you must have the create, index, or write index privilege.
- To add a document using the POST /<target>/_doc/, PUT /<target>/_create/<_id>, or POST /<target>/_create/<_id> request formats, you must have the create_doc, create, index, or write index privilege.

<target> の部分には対象のインデックス名を指定します。上記の記述を見る限り、PUT /<target>/_doc/<_id> という形式でリクエストを行うことで、ドキュメントの追加、または上書きができるようです。 一方、その他の形式に関しては、ドキュメントの追加については記述がありますが、上書きについては記述がありません。

_doc エンドポイントによるドキュメント登録

_doc エンドポイントでは、メソッドとして PUTPOST を使うことができます。また、リクエスURI についても、/<target>/_doc/<_id>/<target>/_doc/ のように、ドキュメント ID を含むものと含まないものの2つのパターンがあるようです。

ドキュメント ID を含む URI を指定した場合は、PUTPOST どちらのメソッドでもリクエストを行うことができます。指定したドキュメント ID がまだ存在しない場合は、リクエストボディとして送信したドキュメントが、指定したドキュメント ID で登録されることとなります。 レスポンスコードはいずれのメソッドの場合も 201 Created となり、レスポンスボディの result フィールドは created となります。

PUT test-index/_doc/1
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}
POST /test_index/_doc/2
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "2",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

ドキュメント ID を含まない URI に対しては、POST メソッドでのみリクエストを行うことができます。この場合、ドキュメント ID は Elasticsearch により自動採番されます。 一方、ドキュメント ID を含まない URI に対して PUT メソッドでリクエストを行った場合は、下記のようにエラーとなってしまいます。

PUT /test_index/_doc/
{
  "field1": "foo"
}

# レスポンスコード 405 Not Allowed
{
  "error": "Incorrect HTTP method for uri [/test-index/_doc/] and method [PUT], allowed: [POST]",
  "status": 405
}
POST test-index/_doc/
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "s8jQ44wBuSrg6d_5yvgh",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

こうした挙動は、Elasticsearch の API の実装というより、元々の PUT メソッドの用途に起因するようです。

(Wikipediaより)

PUT
指定したURIにリソースを保存する。URIが指し示すリソースが存在しない場合は、サーバはそのURIにリソースを作成する。画像のアップロードなどが代表的。

PUT メソッドでは、リソース (ここではドキュメント) の URI を指定して保存を行うので、ドキュメント ID を含む URI を指定する必要があるものと考えられます。実際のところ、対象のドキュメントを取得する場合は、同じくドキュメント ID を含む URI を指定して GET メソッドによりリクエストを行います。

GET test-index/_doc/1

# レスポンスコード 200 OK
{
  "_index": "test-index",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "field1": "foo"
  }
}

_doc エンドポイントによるドキュメントの上書き

ドキュメント ID を含む URI を指定して PUT または POST メソッドでリクエストを行う際、指定したドキュメント ID が既に存在している場合は、ドキュメントの上書きが行われます。 この場合はいずれのメソッドでもレスポンスコードは 200 OK となり、result フィールドは updated となります。また、_version フィールドの値が 1 ずつ増加していることがわかります。

PUT test-index/_doc/1
{
  "field1": "foo"
}

# レスポンスコード 200 OK
{
  "_index": "test-index",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}
POST /test_index/_doc/1
{
  "field1": "foo"
}

# レスポンスコード 200 OK
{
  "_index": "test-index",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

ドキュメント ID を含まない URI を指定する場合は、ドキュメント ID は自動採番されるため、指定したドキュメント ID が既に存在しているということはあり得ません。このため、ドキュメントの上書きも発生し得ないことになります。 ドキュメント ID を含まない URI を指定して POST メソッドで複数回リクエストを行うと、毎回新たなドキュメント ID でドキュメントが新規作成されます。

POST test-index/_doc/
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "tMjW44wBuSrg6d_5QviD",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 5,
  "_primary_term": 1
}
POST test-index/_doc/
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "tcjW44wBuSrg6d_5kPiU",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 6,
  "_primary_term": 1
}

_create エンドポイントによるドキュメント登録

_create エンドポイントでも、メソッドとして PUTPOST を使うことができます。URI については、ドキュメント ID を含むものを指定する必要があります。指定したドキュメント ID が存在しない場合は、ドキュメントの新規作成が行われます。

PUT test-index/_create/3
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "3",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 7,
  "_primary_term": 1
}
POST /test_index/_create/4
{
  "field1": "foo"
}

# レスポンスコード 201 Created
{
  "_index": "test-index",
  "_id": "4",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 8,
  "_primary_term": 1
}

_create エンドポイントでは、ドキュメントの上書きは行われません。URI の中で指定したドキュメント ID が既に存在する場合は、上書きは行われず、下記のようにエラーとなります。

PUT test-index/_create/1
{
  "field1": "foo"
}

# レスポンスコード 409 Conflict
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [3])",
        "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
        "shard": "0",
        "index": "test-index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [3])",
    "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
    "shard": "0",
    "index": "test-index"
  },
  "status": 409
}
POST /test_index/_create/1
{
  "field1": "foo"
}

# レスポンスコード 409 Conflict
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [3])",
        "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
        "shard": "0",
        "index": "test-index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [3])",
    "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
    "shard": "0",
    "index": "test-index"
  },
  "status": 409
}

また、ドキュメント ID を含まない URI を指定した場合は、いずれのメソッドを用いた場合でもエラーとなります。ただし、エラーの内容はそれぞれ異なります。 PUT メソッドでリクエストを行った場合は、メソッドが利用できないとのメッセージがレスポンスとして返されます。一方で、POST メソッドは許可されているようなので、こちらは実行できそうなものの、実際にリクエストを行ってみると Bad Request エラーとなってしまいます。

PUT test-index/_create/
{
  "field1": "foo"
}

# レスポンスコード 405 Method Not Allowed
{
  "error": "Incorrect HTTP method for uri [/test-index/_create/] and method [PUT], allowed: [POST]",
  "status": 405
}
POST test-index/_create/
{
  "field1": "foo"
}

# レスポンスコード 400 Bad Request
{
  "error": "no handler found for uri [/test-index/_create/] and method [POST]"
}

op_type パラメータの利用

ElasticSearch の Index API においてドキュメント登録時の挙動に関係する要素としては、op_type パラメータというものもあります。

(Elastic の公式ドキュメントより)

op_type (Optional, enum) Set to create to only index the document if it does not already exist (put if absent). If a document with the specified _id already exists, the indexing operation will fail. Same as using the <index>/_create endpoint. Valid values: index, create. If document id is specified, it defaults to index. Otherwise, it defaults to create.

このパラメータの値は index または create とすることができ、create とした場合は作成のみを行うことができます。リクエスURI の中でドキュメント ID を指定した場合は、パラメータのデフォルト値は index であり、そうでない場合は create となるようです。ドキュメント ID を指定しない場合は、メソッドが利用できないなどの理由でエラーとなる場合を除いては、自動採番されたドキュメント ID とともにドキュメントが新規作成されます。このため、op_type が create または index どちらの値であっても、挙動に違いはないものと考えられます。

実際の挙動を確認すると、_doc エンドポイントへのリクエストで op_typecreate とすると、指定したドキュメント ID が既存のものであった場合には以下のようにエラーとなります。

PUT /test-index/_doc/1?op_type=create
{
  "field1": "foo"
}

# レスポンスコード 409 Conflict
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [3])",
        "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
        "shard": "0",
        "index": "test-index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [3])",
    "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
    "shard": "0",
    "index": "test-index"
  },
  "status": 409
}
POST test-index/_doc/1?op_type=create
{
  "field1": "foo"
}

# レスポンスコード 409 Conflict
{
  "error": {
    "root_cause": [
      {
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, document already exists (current version [3])",
        "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
        "shard": "0",
        "index": "test-index"
      }
    ],
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [3])",
    "index_uuid": "TgaTsAzNTnq9BXGM12n8dQ",
    "shard": "0",
    "index": "test-index"
  },
  "status": 409
}

_create エンドポイントについても op_type パラメータの指定は可能ですが、create 以外の値は利用できなさそうです。

PUT /test-index/_create/1?op_type=index
{
  "field1": "foo"
}

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "opType must be 'create', found: [index]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "opType must be 'create', found: [index]"
  },
  "status": 400
}
POST /test-index/_create/1?op_type=index
{
  "field1": "foo"
}

{
  "error": {
    "root_cause": [
      {
        "type": "illegal_argument_exception",
        "reason": "opType must be 'create', found: [index]"
      }
    ],
    "type": "illegal_argument_exception",
    "reason": "opType must be 'create', found: [index]"
  },
  "status": 400
}

まとめ

以上をまとめると、op_type を明示的に指定しない場合、_doc エンドポイントへのリクエストとその結果は以下のようになります。

リクエストメソッドURIドキュメント ID
新規既存
PUT/<target>/_doc/<_id>新規作成(201 Created)上書き(200 OK)
POST/<target>/_doc/<_id>新規作成(201 Created)上書き(200 OK)
PUT/<target>/_doc/エラー(405 Method Not Allowed)-
POST/<target>/_doc/新規作成(201 Created)
* ドキュメント ID は自動採番
-

同様に _create エンドポイントについては以下のとおりです。

リクエストメソッドURIドキュメント ID
新規既存
PUT/<target>/_create/<_id>新規作成(201 Created)エラー(409 Conflict)
POST/<target>/_create/<_id>新規作成(201 Created)エラー(409 Conflict)
PUT/<target>/_create/エラー(405 Method Not Allowed)-
POST/<target>/_create/エラー(400 Bad Request)-

まず、ドキュメント ID を含まない URI に対しては、_doc エンドポイントに POST メソッドでリクエストを行う以外の方法ではエラー (400 Bad Request405 Method Not Allowed) となるため、やり方は一通りしかないことがわかります。また、リクエストの結果については、ドキュメント ID が新規に自動採番されるため、ドキュメント新規作成のみ、となります。

次に、ドキュメント ID を含む URI へのリクエストについては、それぞれ op_type がどのようになっているかにより挙動が異なります。op_typecreate または index の値をとることができ、create とした場合はドキュメントの新規作成のみ、index とした場合は、ドキュメントの新規作成、または上書きのいずれかが行われます。

_create エンドポイントでは op_type の値を index にしようとするとエラー(400 Bad Request)となるため、op_type の値は常に create となっているものと考えられます。このため、ドキュメントの新規作成のみを行うことができ、URI の中で既存のドキュメント ID を指定してリクエストを行うとエラー(409 Conflict)となります。

_doc エンドポイントでは、URI の中でドキュメント ID を指定する場合は op_type はデフォルトで index となるため、指定したドキュメント ID が既に存在するかどうかに応じて、ドキュメントの新規作成、または上書きのいずれかが行われます。一方、_doc エンドポイントでもリクエスト時に op_type の値を create とすることができ、この場合は既存のドキュメント ID を指定して上書きが発生するような状況ではエラー (409 Conflict) が発生します。

結局どうするのが良いのか

以下は私見となりますが、利用する場面に応じて以下のように使い分けするのが良さそうです。

ドキュメントの登録時にドキュメント ID にはこだわらない場合

POST /<target>/_doc/

_doc エンドポイントに POST メソッドでリクエストを行う必要があり、そのほかの方法ではエラーとなります。 ドキュメント ID を指定しなくても自動採番されるため、指定が必要な情報が最も少なく、スムーズかと思います。

ドキュメントの登録時にドキュメント ID を明示的に指定したい場合

PUT /<target>/_doc/<_id>

POST /<target>/_doc/<_id>

いずれの方法でも違いはなさそうなので、ドキュメント ID を指定しない場合と同様、POST メソッドのみ使うのでもよいように思えます。

ドキュメントの登録時にドキュメント ID を明示的に指定したい、かつ新規作成のみを行いたい場合

PUT /<target>/_create/<_id>

POST /<target>/_create/<_id>

PUT /<target>/_doc/<_id>?op_type=create

POST /<target>/_doc/<_id>?op_type=create

指定したドキュメント ID が既に存在する場合はエラー(409 Conflict)応答を受け取ることができます。 こちらもいずれの方法でも違いはなさそうですが、_create エンドポイントと op_type の指定はやりやすい方を選べばよいかと思います。

参考

Index API | Elasticsearch Guide [8.11] | Elastic

Hypertext Transfer Protocol - Wikipedia

PUT と POST どっち使う?5分で使い分けを確実に覚えよう! #API - Qiita

q で正しいカラム名を指定しても "no such column" というエラーが出るときの対処

q コマンドで CSV ファイルの解析を行っていたところ、下記のようなエラーが出力されることがありました。

$ q -O -H -d , 'select timestamp, username, remotehost from test.csv'
query error: no such column: timestamp
Warning - There seems to be a "no such column" error, and -H (header line) exists. Please make sure that you are using the column names from the header line and not the default (cXX) column names. Another issue might be that the file contains a BOM. Files that are encoded with UTF8 and contain a BOM can be read by specifying `-e utf-9-sig` in the command line. Support for non-UTF8 encoding will be provided in the future.

カラム名の間違いではないのか

カラム名を間違えたのかな?と思い確認してみると。。

$ q -O -H -d , 'select * from test.csv'
timestamp,username,remotehost
1675857600,foo,192.168.0.1
1675861200,bar,192.168.0.2
1675818000,baz,192.168.0.3
$ cat test.csv
timestamp,username,remotehost
1675857600,foo,192.168.0.1
1675861200,bar,192.168.0.2
1675818000,baz,192.168.0.3

timestamp というカラム名は確かに存在しているようです。だけど、実際にカラム名を指定するとエラーになってしまう。

$ q -O -H -d , 'select username, remotehost from test.csv'
username,remotehost
foo,192.168.0.1
bar,192.168.0.2
baz,192.168.0.3

timestamp というカラム名以外はどうやら問題ありません。 となると、何か別の問題があるのかもしれません。

何が問題なのか

エラーメッセージの後半に注目してみます。

Another issue might be that the file contains a BOM. Files that are encoded with UTF8 and contain a BOM can be read by specifying `-e utf-9-sig` in the command line. Support for non-UTF8 encoding will be provided in the future.

UTF8 でエンコードされていて、"BOM" を含むファイルを読むためには、オプションの指定が必要とのこと。 BOM とは何なのかを調べてみます。

(Wikipediaより)

バイト順マーク (バイトじゅんマーク、英: byte order mark) あるいはバイトオーダーマークとは、通称BOM(ボム)といわれるUnicodeの符号化形式で符号化したテキストの先頭につける数バイトのデータのことである。このデータを元にUnicodeで符号化されていることおよび符号化の種類の判別に使用する。

BOM とはどうやら、Unicode のテキストの先頭に付与されて、符号化の種類の判別に使用されるデータのようです。

cat コマンドでは出力されないのに、本当にそんなデータが入っているのか? od コマンドでダンプしてみます。

$ od -tx1z test.csv
0000000 ef bb bf 74 69 6d 65 73 74 61 6d 70 2c 75 73 65  >...timestamp,use<
0000020 72 6e 61 6d 65 2c 72 65 6d 6f 74 65 68 6f 73 74  >rname,remotehost<
0000040 0a 31 36 37 35 38 35 37 36 30 30 2c 66 6f 6f 2c  >.1675857600,foo,<
0000060 31 39 32 2e 31 36 38 2e 30 2e 31 0a 31 36 37 35  >192.168.0.1.1675<
0000100 38 36 31 32 30 30 2c 62 61 72 2c 31 39 32 2e 31  >861200,bar,192.1<
0000120 36 38 2e 30 2e 32 0a 31 36 37 35 38 31 38 30 30  >68.0.2.167581800<
0000140 30 2c 62 61 7a 2c 31 39 32 2e 31 36 38 2e 30 2e  >0,baz,192.168.0.<
0000160 33 0a                                            >3.<
0000162

確かに、3 バイト分のデータが入っています。 0xEF 0xBB 0xBF は、どうやら UTF-8 の BOM のようです。

BOM 付きのファイルを読み込む方法

エラーメッセージにあるように、BOM が含まれているのが原因のようなので、オプション指定すれば読み込めるはず。

$ q -O -H -d , -e utf-9-sig 'select timestamp from test.csv'
Encoding utf-9-sig could not be found

utf-9-sig というエンコーディングは見つからないといわれています。 確かに。 UTF8 ではないのか?

q - Text as Dataq - Text as Data によると、下記の記載があるので、どうやら -e utf-8-sig とするのが正しいようです。

Files with BOM: Files which contain a BOM (Byte Order Mark) are not properly supported inside python's csv module. q contains a workaround that allows reading UTF8 files which contain a BOM - Use -e utf-8-sig for this. I plan to separate the BOM handling from the encoding itself, which would allow to support BOMs for all encodings.

実際に指定すると、エラーなく読み込むことができました。

$ q -O -H -d , -e utf-8-sig 'select timestamp from test.csv'
timestamp
1675857600
1675861200
1675818000

ファイルから BOM を取り除く方法

読み込むときにオプションを指定してもいいですが、毎回指定するのも手間なので、取り除いておいてオプションなしでも読み込めるようにしたいところです。 sed コマンドで置換することで取り除くことができます。

$ sed -e $'1s/^\xef\xbb\xbf//' test.csv > test2.csv
$ od -tx1z test2.csv
0000000 74 69 6d 65 73 74 61 6d 70 2c 75 73 65 72 6e 61  >timestamp,userna<
0000020 6d 65 2c 72 65 6d 6f 74 65 68 6f 73 74 0a 31 36  >me,remotehost.16<
0000040 37 35 38 35 37 36 30 30 2c 66 6f 6f 2c 31 39 32  >75857600,foo,192<
0000060 2e 31 36 38 2e 30 2e 31 0a 31 36 37 35 38 36 31  >.168.0.1.1675861<
0000100 32 30 30 2c 62 61 72 2c 31 39 32 2e 31 36 38 2e  >200,bar,192.168.<
0000120 30 2e 32 0a 31 36 37 35 38 31 38 30 30 30 2c 62  >0.2.1675818000,b<
0000140 61 7a 2c 31 39 32 2e 31 36 38 2e 30 2e 33 0a     >az,192.168.0.3.<
0000157

BOM を取り除いた後は、-e utf-8-sig オプションなしでも読み込むことができます。

$ q -O -H -d , 'select timestamp from test2.csv'
timestamp
1675857600
1675861200
1675818000

参考

jq で JSON オブジェクトのキーをソートする

複数の JSON データを作成する中で、途中でオブジェクトのキーの並びが変わってしまった!

$ cat sample_a.json
{
    "key0": "a0",
    "key1": "a1",
    "key2": "a2"
}
$ cat sample_b.json
{
    "key0": "b0",
    "key2": "b2",
    "key1": "b1"
}

そんなことが実際に起こり得るのかわからないですが、この場合まとめて CSV として出力 しようとすると、行によって並びが異なる状態のまま出力されてしまいます。

$ jq . *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key2": "b2",
  "key1": "b1"
}
$ jq -r '[.[]] | @csv'  *.json
"a0","a1","a2"
"b0","b2","b1"

キーの並びを指定する

キーの並びを指定してオブジェクトを作り直すと、いずれの行でも同じ並び順で出力することができます。 下記のように {} の中にキーを列挙することで実現できます。

$ jq '{key0, key1, key2}' *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}
$ jq -r '{key0, key1, key2} | [.[]] | @csv' *.json
"a0","a1","a2"
"b0","b1","b2"

なお、オブジェクトを作るときは {<key>: <value>, ...} と書くものですが、元のオブジェクトからキー名そのままで取り出す場合は、上記のようにショートカットが使えるようです。

(jq のマニュアルより)

if the input is an object with "user", "title", "id", and "content" fields and you just want "user" and "title", you can write

{user: .user, title: .title}
Because that is so common, there's a shortcut syntax for it: {user, title}.

オブジェクトのキーをソートする

サンプルだとキーの数が連番で3つだけなので列挙するのも簡単ですが、数が多くなるとショートカットを使っても大変そうです。 to_entries でオブジェクトをキーと値の組み合わせの配列に変換することで、ソートができるようになります。

$ jq 'to_entries' *.json
[
  {
    "key": "key0",
    "value": "a0"
  },
  {
    "key": "key1",
    "value": "a1"
  },
  {
    "key": "key2",
    "value": "a2"
  }
]
[
  {
    "key": "key0",
    "value": "b0"
  },
  {
    "key": "key2",
    "value": "b2"
  },
  {
    "key": "key1",
    "value": "b1"
  }
]

sort_by でキーをもとにソートした後、from_entries で再度オブジェクトに戻せます。

$ jq 'to_entries | sort_by(.key) | from_entries' *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}

sort_by の部分を工夫してやることで、キー名以外でのソートもできるようです。

jq の --sort-keys(-S) オプションを使う

長々と書いてきましたが、jq には -S(--sort-keys) というオプションがあり、オブジェクトのキーをソートした状態で出力することができます。

(jq のマニュアルより)

--sort-keys / -S:
Output the fields of each object with the keys in sorted order.

$ jq --sort-keys . *.json
{
  "key0": "a0",
  "key1": "a1",
  "key2": "a2"
}
{
  "key0": "b0",
  "key1": "b1",
  "key2": "b2"
}

これを使っておけば万事解決と思いきや、そう簡単には行きません。

$ jq --sort-keys -r '[.[]]' *.json
[
  "a0",
  "a1",
  "a2"
]
[
  "b0",
  "b2",
  "b1"
]

どうも、オブジェクトのキーのソートは最終的な出力に対してのみ行われるように思えます。 一旦オブジェクトのキーをソートして出力し、パイプで再度 jq コマンドに入力として渡してやる必要があります。

$ jq --sort-keys . *.json | jq -r '[.[]]'
[
  "a0",
  "a1",
  "a2"
]
[
  "b0",
  "b1",
  "b2"
]

参考

jq で オブジェクトのキーまたは値を取得する

前回の記事 の中で、オブジェクトの全てのキーや値を取得する方法に触れました。 やり方がいくつかありそうだったので、まとめてみました。

オブジェクトの全てのキーの取得

下記のような JSON に対して、keys 関数を用いることで、キーの一覧を取得できます。

$ cat sample.jsson
{
    "key0": "value0",
    "key2": "value2",
    "key1": "value1"
}
$ jq keys sample.json
[
  "key0",
  "key1",
  "key2"
]

しかしこれは、よく見るとソートされて順番が変わっています。 keys に対して keys_unsorted という関数もあり、これを用いることで、元の順序通りにキーの一覧を得ることができます。

(jq のマニュアルより)

keys, keys_unsorted
The builtin function keys, when given an object, returns its keys in an array.

The keys are sorted "alphabetically", by unicode codepoint order. This is not an order that makes particular sense in any particular language, but you can count on it being the same for any two objects with the same set of keys, regardless of locale settings.

When keys is given an array, it returns the valid indices for that array: the integers from 0 to length-1.

The keys_unsorted function is just like keys, but if the input is an object then the keys will not be sorted, instead the keys will roughly be in insertion order.

$ jq keys_unsorted sample.json
[
  "key0",
  "key2",
  "key1"
]

オブジェクトの全ての値の取得

前回の記事 でも触れたように、.[] をオブジェクトに対して使うことで、全ての値を得ることができます。 この場合は結果が配列とはならないので、keyskeys_unsorted のように配列としたい場合は [] で囲んでやる必要があります。

$ jq .[] sample.json
"value0"
"value2"
"value1"
$ jq [.[]] sample.json
[
  "value0",
  "value2",
  "value1"
]

また、こちらは値の順番がそのままで出力されます。 ソートした結果が欲しい場合は、当然ですが | sort を追加する必要があります。

全てのキーと値の組み合わせを配列として取得

キーと値で取得の仕方があまりにも違うので、両方に対応できる汎用的な方法がないか調べたところ、to_entries という関数があります。

These functions convert between an object and an array of key-value pairs. If to_entries is passed an object, then for each k: v entry in the input, the output array includes {"key": k, "value": v}.

$ jq to_entries sample.json
[
  {
    "key": "key0",
    "value": "value0"
  },
  {
    "key": "key2",
    "value": "value2"
  },
  {
    "key": "key1",
    "value": "value1"
  }
]

これだと、キーと値どちらにも対応できそうです。 対応するキーと値の組み合わせとして出力されるので、片方だけソートされているということもありません。

$ jq 'to_entries[].key'  sample.json
"key0"
"key2"
"key1"
$ jq 'to_entries[].value'  sample.json
"value0"
"value2"
"value1"

大量のデータを処理する場合の効率という点ではわかりませんが、覚えやすさとしてはこのやり方だけ覚えておいてもいいかもしれません。

参考

jq で CSV 出力する

jq を使って JSON を整形したところから、CSV 出力するまでに意外と手こずることが多いので整理してみました。

CSV 出力自体は、フィルタの末尾に | @csv をつけるだけで容易に行うことができます。 そう思って、下記のような JSON からそのまま CSV 出力しようとすると、エラーになってしまいます。

sample.json

[
  {"name": "foo", "ip": "192.168.0.1"},
  {"name": "bar", "ip": "192.168.0.2"},
  {"name": "baz", "ip": "192.168.0.3"}
]

実行結果

$ jq -r '. | @csv' sample.json
jq: error (at sample.json:5): object ({"name":"fo...) is not valid in a csv row

@csv による CSV 出力

jq のマニュアル を見ると、@csv の入力は配列である必要があることがわかります。

@csv:
The input must be an array, and it is rendered as CSV with double quotes for strings, and quotes escaped by repetition.

例えば、下記のように配列を渡すと、CSV として出力されます。

$ echo '[1, 0, 2]' | jq  -r '. | @csv'
1,0,2

複数行の CSV を出力するには、単純に複数の配列を入力として渡します。

$ echo '[1, 0, 2] [4, 3, 5]' | jq .
[
  1,
  0,
  2
]
[
  4,
  3,
  5
]
$ echo '[1, 0, 2] [4, 3, 5]' | jq -r '. | @csv'
1,0,2
4,3,5

上記は配列の配列とは異なることに注意が必要です。

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq .
[
  [
    0,
    1,
    2
  ],
  [
    3,
    4,
    5
  ]
]

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq -r '. | @csv'
jq: error (at <stdin>:1): array ([1,0,2]) is not valid in a csv row

これはエラーとなります。 外側の配列を CSV の一行として出力しようとしているようです。 下記のように外側の配列を外してやるとうまく出力できるようになります。

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq .[]
[
  1,
  0,
  2
]
[
  4,
  3,
  5
]
$ echo '[[1, 0, 2], [4, 3, 5]]' | jq -r '.[] | @csv'
1,0,2
4,3,5

つまり、@csv の入力は(1 つまたは複数の)ネストされていないフラットな配列である必要があります。

CSV 出力するための JSON の変形

冒頭の sample.json の場合、以下 2 点の変更が必要です。

  1. 外側の配列を外す。
  2. CSV の一行がオブジェクトではなく、一つのフラットな配列になるようにする。

1. については容易に行うことができます。

jq .[] sample.json
{
  "name": "foo",
  "ip": "192.168.0.1"
}
{
  "name": "bar",
  "ip": "192.168.0.2"
}
{
  "name": "baz",
  "ip": "192.168.0.3"
}

2. については、それぞれのオブジェクトをキー name および ip の値の配列とする必要があります。 これも下記のように行うことができます。

jq '.[] | [.name, .ip]' sample.json
[
  "foo",
  "192.168.0.1"
]
[
  "bar",
  "192.168.0.2"
]
[
  "baz",
  "192.168.0.3"
]

あるいは、最初から CSV での出力が目的なら、整形の段階でオブジェクトではなく、配列として整形したほうがいいのかもしれません。

多数のキーを持つオブジェクトの値の配列化

キーが 2, 3 個なら手書きで列挙することも容易ですが、数が多くなってくるとそうもいきません。 オブジェクトのキーの一覧を取得する関数として keys がありますが、同様に values としてもうまく行きません。

$ jq '.[0] | keys'  sample.json
[
  "ip",
  "name"
]
$ jq '.[0] | values'  sample.json
{
  "name": "foo",
  "ip": "192.168.0.1"
}

結論から言うと、オブジェクトに対してもフィルタ .[] を通してやることで、値を列挙することができます。

$ jq '.[0] | .[]'  sample.json
"foo"
"192.168.0.1"

(2023/1/22 追記) jq のマニュアル によると、.[] はオブジェクトに対しても使うことができ、オブジェクトに含まれる全ての値を返す、とあります。

Array/Object Value Iterator: .
If you use the .[index] syntax, but omit the index entirely, it will return all of the elements of an array. Running .
with the input [1,2,3] will produce the numbers as three separate results, rather than as a single array.
You can also use this on an object, and it will return all the values of the object.

この時の出力は配列になっていないので、@CSV に渡す場合は出力を [] で囲んで配列にする必要があります。

$ jq '.[0] | [.[]]'  sample.json
[
  "foo",
  "192.168.0.1"
]

まとめ

JSONCSV として出力するために @csv を使う場合、各行をフラットな配列として渡してやる必要があります。

下記のような JSON はそのままでは CSV として出力できません。

sample.json

[
  {"name": "foo", "ip": "192.168.0.1"},
  {"name": "bar", "ip": "192.168.0.2"},
  {"name": "baz", "ip": "192.168.0.3"}
]

以下のようにフィルタを通してやる必要があります。

$ jq -r '.[] | [.[]] | @csv'  sample.json
"foo","192.168.0.1"
"bar","192.168.0.2"
"baz","192.168.0.3"

参考