弱いエンジニアの備忘録

自分的に気になった技術に関するメモや備忘録です。Elasticsearchに関する記事が多くなりそうです。

Elasticsearch の search template 一覧を取得する

Elsaticsearchでは、Search Templateという機能があります。
クエリの共通部分を事前に登録しておくことで、検索時のクエリ記述量を減らすことができるものです。
www.elastic.co

作成した Search Templateの一覧を取得するための API がなかったので、取得方法をメモ。

Search Templateの登録

POST _scripts/template_for_description_search
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "match": {
          "desctiption": "{{query_string}}"
        }
      }
    }
  }
}
POST _scripts/template_for_title_search
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "match": {
          "title": "{{query_string}}"
        }
      }
    }
  }
}

取得

スクリプトが Cluster State に保存されるのがポイントです。

GET _cluster/state?filter_path=metadata.stored_scripts
{
  "metadata" : {
    "stored_scripts" : {
      "template_for_description_search" : {
        "lang" : "mustache",
        "source" : """{"query":{"match":{"desctiption":"{{query_string}}"}}}""",
        "options" : {
          "content_type" : "application/json; charset=UTF-8"
        }
      },
      "template_for_title_search" : {
        "lang" : "mustache",
        "source" : """{"query":{"match":{"title":"{{query_string}}"}}}""",
        "options" : {
          "content_type" : "application/json; charset=UTF-8"
        }
      }
    }
  }
}

以上

vimからelasticsearchのAPIを叩く方法メモ

大変長らく放置してしまいましたが、久しぶりに更新。

はじめに

vimで作業をしている時に、
ElasticsearchのAPIを叩きたくなることが良くありますよね。(ないか)

私の場合、「logstashでelasticsearchにデータを投入している傍ら、設定を編集する」といったことがよくあります。もちろんElasticsearchのAPIはKibanaのDevToolsや、curlコマンドなどでも簡単に実行できます。
とはいえ、できることならvimで作業をしながらシームレスに呼び出したい。
作りかけですが、一旦書き残しておきます。

vim scriptをまともに書く機会がないので、お作法とかが全く分からない。 勉強がてらとりあえず書いてみたという感じなので悪しからず。

こんな感じにしたい↓

f:id:shin0higuchi:20200603013443p:plain:w600

目次

1. 前提条件
2. 関数の作成
3. keymap
4. まとめ

1. 前提条件

依存関係
  • jqが利用できること。下記のリンクからインストールが可能。環境変数PATHに追加するのを忘れないように。

stedolan.github.io

  • webapi-vimをインストールしてあること

mattnさんのwebapi-vimがあれば、vimから簡単にWEB APIを叩くことが可能です。
github.com

ディレクトリ構成

構成は下記の通りです。vim-esutilごと、~/.vimrc/plugin/の配下に置いています。

vim-esutil/
├── autoload
│   └── esutil
│       ├── api.vim
│       └── util.vim
└── plugin
    └── esutil.vim

2. 関数の作成

今回は主に vim-esutil/autoload/esutil/配下の、api.vimとutil.vimにフォーカスします。vim-esutil/plugin配下はキーマップしか書いていないので割愛します。

1. webapi実行部分(ユーティリティー

ElasticsearchのAPIはplain textで結果が得られる _cat/* 系のものと、jsonで結果が得られるものの2種類に大別できます。
それぞれレスポンスの描画形式を変えたいので、API実行のユーティリティーを2つ用意しています。
 
※util.vimを一部抜粋

"結果がplain textのもの向け。レスポンスをquick fix windowで開く。
function! esutil#util#get(host, path, params)

    "ホストやパスを受けとってwebapi-vimの関数を呼ぶだけ
    let l:res = webapi#http#get(a:host. a:path. a:params)  

    "quick fix windowに追記する
    for res in split(l:res.content, '\n')
        caddexpr res
    endfor
    copen
endfunction

"結果がjsonのもの向け。レスポンスをjqで成形し、新規タブで開く。
function! esutil#util#getjson(host, path, params)
    let l:res = webapi#http#get(a:host. a:path. a:params)
    tabnew
    call append(0, l:res.content)
    :%!jq .
endfunction
2. API呼び出し

上記のutil.vimを呼び出すだけです。
それぞれの関数は可変長の引数を受け取って、l:hostなどを決定します。
# ここは、どう考えてももっと良いやり方があるはず。vim scriptわからぬ。

" _cat/nodesの結果を表示する
function! esutil#api#catNodes(...)
    
    "quick fix listを空にする
    call setqflist([])
    
    " 環境変数で定義されていればl:hostに設定、引数で指定されればそれを優先
    if $ESUTIL_ESHOST != ''
        let l:host = get(a:, 1, $ESUTIL_ESHOST)
    " 環境変数・引数のいずれも定義されていなければ、localhost:9200に設定
    else
        let l:host = get(a:, 1, 'localhost:9200')
    endif

    call esutil#util#get(l:host, '/_cat/nodes/', '?v')
endfunction

" _cat/indicesの結果を表示する
function! esutil#api#catIndices(...)
    call setqflist([])
    if $ESUTIL_ESHOST != ''
        let l:host = get(a:, 1, $ESUTIL_ESHOST)
    else
        let l:host = get(a:, 1, 'localhost:9200')
    endif
    let l:pattern = get(a:, 2, '*')
    call esutil#util#get(l:host, '/_cat/indices/'. l:pattern, '?v&s=index')
endfunction

function! esutil#api#search(...)
    "TODO: implement esutil#api#search()    
endfunction

function! esutil#api#putIndex(...)
    "TODO: implement esutil#api#putIndex()
endfunction


このファイルを保存すれば、vimから呼び出せるようになります。

:call esutil#api#catIndices()

3. keymap

ここまでで、やりたいこと自体は実現できるようになりました。
しかし、毎回 :call esutil#... と入力するのは骨が折れます。
私は.vimrcに下記の設定を入れています。

" Custom Key Mapping
let mapleader = "\<Space>"
noremap <Leader>escati :call esutil#api#catIndices()<CR>
noremap <Leader>escatn :call esutil#api#catNodes()<CR>

この設定により、vimの実行中に" escati"と入力すれば、indexの一覧がquick fix windowで表示されます。めでたしめでたし。
※" escati"はesutilの_cat/indicesを短縮名にしたつもり。

4. まとめ

  • 探りながら書いたので、下記の事項は改善策を教えてくれる方がいると喜びます。

- 可変長引数の扱い
- 環境変数の扱い
- レスポンスを別タブに書き込むスマートな方法
- quick fix listの使い方

  • 他のAPIも呼び出せるようにしたいところ

以上です。お読みいただきありがとうございました。

elasticsearch6.xの親子関係を整理してみた

この記事はElastic stack Advent Calendar 2017 の17日目の記事となります。

概要

elasticsearchで、親子関係を持ったデータを扱うには、幾つかの方法があります。
例えば

  1. 親の情報を冗長に保持して、親子を1対1の関係にする
  2. object配列として子供を保持する
  3. nested data typeを用いる
  4. parent-childの関係を用いる

等があると思います。

elasticsearch6.0から、_typeがindexに対して単一になりました。(ver 7では_typeが無くなる予定)
それに伴って、parent-childの使い方も変わったので、改めて整理しておきます。

目次

  1. サンプルデータの概要
  2. 親の情報を冗長に保持して、親子を1対1の関係にする
  3. object data type
  4. nested data type
  5. join data type
  6. まとめ

サンプルデータの概要

ブログサービスのデータを扱うことを想定します。
(ダミーデータはhttp://www.databasetestdata.com/で生成させてもらいました)

親子関係にある2つのindexがあり、片方はユーザーの情報を表すuser index
もう片方は、ブログの投稿単位でindexしたblog_post indexです。
ブログは一人のユーザーが複数回投稿しうるので、userとblog_postは1対Nの親子関係にあると言えます。

user情報を示すuser table。
fieldとしては、FullName,Id,Country,registered_time,Emailなどがあります。
1ドキュメント取り出してみると下記のような構造になっています。

{
        "_index": "user",
        "_type": "doc",
        "_id": "QLwKYGABLuVBi7nej178",
        "_score": 1,
        "_source": {
          "Email": "Felicita@eliane.co.uk",
          "host": "Macintosh.local",
          "Country": "Gambia",
          "registered_time": "12/17/1985",
          "Id": "81",
          "@version": "1",
          "Full Name": "Ms. Judd Tromp",
          "@timestamp": "2017-12-16T15:56:32.767Z"
        }
}


それに対して、blog_postの内容は下記のようになっています。

{
        "_index": "blog_post",
        "_type": "doc",
        "_id": "jr09YGABLuVBi7neyjL2",
        "_score": 1,
        "_source": {
          "@version": "1",
          "Summary": """
facere nobis eum minima perspiciatis
sit cum debitis commodi est quia distinctio molestiae
ut praesentium autem occaecati
 
	nemo et asperiores itaque quis optio harum
ratione ex vitae dignissimos consequatur vel aliquam accusantium
doloribus optio sint provident praesentium sunt laboriosam ullam quo
tempore ratione vel dolorum accusantium suscipit blanditiis
distinctio unde dolores commodi
 
	recusandae numquam perspiciatis
ducimus dicta blanditiis sed sint dolore unde eos distinctio
facere veritatis vitae veniam qui harum voluptatem
dolorem nam quam
""",
          "Id": 3,
          "@timestamp": "2017-12-16T16:52:30.135Z",
          "Title": """
eius a quam
sequi nulla unde et laboriosam perferendis
ut id accusamus corporis ipsam quisquam vero
""",
          "Created At": "2008-09-12T13:14:46.235Z",
          "host": "Macintosh.local",
          "Body": """
quia unde quod
distinctio odit commodi odio
non qui libero perferendis quos
 
	expedita voluptatem id odio ut consequatur
minus sit odio est aspernatur
vel numquam accusamus laborum eligendi dolore corporis voluptate itaque
vel aut repudiandae enim saepe fuga voluptatem sed quis
 
	quia voluptatem eos in commodi nisi mollitia occaecati qui
sint amet unde iusto enim qui voluptate recusandae
dolores quia quam eum
at ut et cupiditate odit dolores nulla voluptas
"""
        }
}


これらの二つのindexはId fieldで紐付いていて、user:blog_postsが1対Nになっています。

親の情報を冗長に保持して、親子を1対1の関係にする

ドキュメントの構造という観点で、この方法は非常にシンプルになります。
ただし、親ドキュメントの情報が冗長になるので、データサイズの増加や、検索時の結果重複などのデメリットが考えられます。
(今回はuserに比べてblog_postが大きかったので、ほとんどデータサイズは増えませんでした)

つまりデータ構造としては、
親ドキュメント

{"Id" : 10, "user" : "A"}

子ドキュメント

{"Id" : 10, "Title" : "aaa"}, {"Id" : 10,  "Title" : "bbb"}, {"Id" : 10,  "Title" : "ccc"} 

という2つのindexをjoinした結果として、

{"user" : "A", "Id" : 10, "Title" : "aaa"}, {"user" : "A", "Id" : 10,  "Title" : "bbb"}, {"user" : "A", "Id" : 10,  "Title" : "ccc"} 

という1つのindexに統合します。

一部のフィールドを省略していますが、userというfieldが冗長であることがわかります。
(user indexのサイズが大きくなるほどデータサイズなどへの影響が顕著です。)

検索時の結果重複という面でのデメリットは、次のような状況が想像できます。
例えば「Edytheという名前のユーザーが何人いるかを検索したい場合」です。

queryはこんな感じでしょうか

GET join/_search
{
  "query": {
    "match": {
      "Full Name": "Edythe"
    }
  }
}

レスポンスはこうです↓

"hits": {
  "total": 20,
  "max_score": 5.1217155,
  "hits":[...]
}

これだけ見ると、Edytheさんは20人登録されているように見えますが、実際は1人しか登録されていません。
Edytheさんのブログ投稿が20件あったため、20件のドキュメントが"Full Name":"Edythe"という条件に当てはまりヒットしてしまいます。

object data type

上記を改善するために思いつく方法として、
ブログ投稿のデータであるblog_postを、配列形式で1つのドキュメントに詰め込む方法があります。
つまり下記のようなデータ構造です。

{
  "_index": "object",
  "_type": "doc",
  "_id": "jL20YGABLuVBi7neE7yx",
  "_score": 1,
  "_source": {
    "Email": "Kendall@reymundo.me",
    "@timestamp": "2017-12-16T15:56:32.759Z",
    "registered_time": "6/24/2012",
    "host": "Macintosh.local",
    "@version": "1",
    "Full Name": "Kale Pouros",
    "Country": "Sudan",
    "Id": "19",
    "posts": [
      {blog_post 1},{blog_post 2},{blog_post 3}....{blog_post n}
    ]
  }
}

ある程度見通しが良くなりました。
検索結果が重複する問題も解決します。

しかし、この構造にも欠点があります。
elasticsearchのobject型は、内部的には、「各object要素の配列」が保存されます。
つまり

{
  "posts" : [ 
    {
      "Id" : "1",
      "Title" :  "AAA"
    },
    {
      "Id" : "2",
      "Title" :  "BBB"
    }
  ]
}

というドキュメントがあったとしても....

{
  "posts.Id" : [ "1", "2" ],
  "posts.Title" :  [ "AAA", "BBB" ]
}

というように保持されているのです。

結果として、このドキュメントは
"Title":"AAA" AND "Id":"2"
という条件で検索してもヒットしてしまいます。

nested data type

上で述べた、object datatypeの欠点を解消するためのものがnested datatypeです。
ユーザーから見ると、object data typeを使っている場合とドキュメント構造は全く同じに見えます。
しかし内部的にnested部分は、別のドキュメントとしてindexされます。

使い方としては、事前にデータ型の定義をしておくだけです。

PUT nested
{
  "mappings": {
    "doc":{
      "properties": {
        "posts":{
          "type": "nested"
        }
      }
    }
  }
}

こうすることで、nestした個々のオブジェクトに対して検索をかけることができます。

見かけ上は100ドキュメントでも、cat APIで確認すると、10100件保持していることがわかります↓

green  open   object    aFeBCUA0SI6xAQAgwFspdQ   5   0        100            0     11.7mb         11.7mb
green  open   nested    KMCi8GTQS6eLmofmlRgBcQ   5   0      10100            0     12.4mb         12.4mb

強いてデメリットを挙げるとすれば、nested data typeを使っている場合は、
query, aggregation, sort, highlightにおいて、nested専用のものを使う必要があるというところでしょうか。
(慣れていないと書き方が少し難しい&面倒)

join data type

ここまで挙げてきた方法はいずれも、スキーマが異なる情報を、1つのドキュメントとして統合するものでした。
(nested data typeは内部的には別ドキュメントですが)

それに対して、join data typeは同一index内の異なるドキュメント同士の親子関係を定義するためのものです。
以前は複数の_typeを用いて実現していたparent-childの関係が、
バージョン6以降ではjoin data typeのフィールドを用いて実現するように変更されました。

使い方としては、
事前に親子関係の識別子となるフィールドを定義しておきます。

PUT parent-child
{
  "mappings": {
    "doc": {
      "properties": {
        "user_or_post": { 
          "type": "join",
          "relations": {
            "user": "post" 
          }
        }
      }
    }
  }
}


あとはdocument登録の際に、親には "user_or_post":"user"というフィールドを追加し....

{
  "_index": "parent-child",
  "_type": "doc",
  "_id": "-b3jYGABLuVBi7neE9XR",
  "_score": 1,
  "_source": {
    "@version": "1",
    "user_or_post": "user",     #<= ここ
    "@timestamp": "2017-12-16T19:53:02.295Z",
    "host": "Macintosh.local",
    "Full Name": "Charity Pollich V",
    "registered_time": "11/22/2011",
    "Id": "2",
    "Country": "Svalbard and Jan Mayen",
    "Email": "Demetrius@estel.com" 
  }
}


子供側には
"user_or_post": {"name": "post", "parent": "親のドキュメントID"}
というフィールドを追加します。

{
  "user_or_post": {   
    "name": "post",   #<= ここ
    "parent": "98"     #<= ここ
  },
  "Body": "xxxxx",
  "Summary": "xxxxx",
  "@version": "1",
  "Title": "xxxxxx",
  "Id": 98,
  "@timestamp": "2017-12-16T20:12:12.361Z",
  "host": "Macintosh.local",
  "Created At": "1987-12-11T14:58:59.947Z"
}


これで親子関係を持つことができます。
検索時には、has_child queryやhas_parent queryを使います。

例えば「1996-08-12T15:28:25.327Z」にブログを投稿したユーザーを調べたければ下記のように検索します。
(要するにchildの条件で絞り込んで、parentのdocumentを返したい)

GET parent-child/_search
{
  "query": {
    "has_child": {
      "type": "post",
      "query": {
        "term": {
          "Created At": {
            "value": "1996-08-12T15:28:25.327Z"
          }
        }
      }
    }
  }
}
"hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "parent-child",
        "_type": "doc",
        "_id": "93",
        "_score": 1,
        "_source": {
          "host": "Macintosh.local",
          "@timestamp": "2017-12-16T19:57:38.872Z",
          "registered_time": "5/1/1998",
          "user_or_post": "user",
          "Country": "Indonesia",
          "Email": "Kylie_Kiehn@ernestina.com",
          "@version": "1",
          "Full Name": "Dannie Schiller",
          "Id": "93"
        }
      }
    ]
  }

無事検索できました。

まとめ

ここまで、親子関係を持つデータの構造について何パターンか整理しました。
各方法とも一長一短あると思うので、状況によって使い分けたいものです。

以上です。読んでいただきありがとうございました。

その他

  • 何かツッコミがあればコメントお願いします
  • 検索パフォーマンスとかをしっかり検証していないので、データ増やしたり階層増やすとどうなるか、そのうち試してみたい(やってみた人が既にいたら是非教えてください)

X-Pack Watcherのindex actionではまったメモ

概要

elasticsearchのWatcher/Alertingを使っていて、はまったのでメモ。
具体的には "index action" についてです。

特定の条件を満たしたデータをまとめて別indexにbulkします。

バージョン情報など

  • elasticsearch-5.6.3
  • kibana-5.6.3
  • macOS High Sierra 10.13.1

やりたいこと

今回使うデータとして、
・anomaly_score
・time
という2種類の値を持ったドキュメントを用意します。
(3件しか登録してませんが、実際には大量のログがリアルタイムで来るイメージ)

f:id:shin0higuchi:20171101233740p:plain

ここから、anomaly_scoreが50を超えた物だけ抜き出して、anomaliesという別のindexに入れたい!
というのが今回の目的です。

では早速やってみます。

手順

1. watchを作成、登録する

バージョン6ではKibanaの画面から閾値ベースの定義が登録できそうですが、
今のバージョンではAPIを頑張って叩く必要があります。
今回はKIbanaのConsoleから登録します。

PUT _xpack/watcher/watch/test
{
  "trigger": {
    "schedule": {
      "interval": "1d"
    }
  },
  "input": {
    "search": {
      "request": {
        "indices": [
          "test"
        ],
        "body": {
          "query": {
            "bool": {
              "must": [
                {
                  "range": {
                    "time": {
                      "gte": "now-1d"
                    }
                  }
                },
                {
                  "range": {
                    "anomaly_score": {
                      "gte": 50
                    }
                  }
                }
              ]
            }
          }
        }
      }
    }
  },
  "actions": {
    "index_to_elasticsearch": {
      "index": {
        "index": "anomalies",
        "doc_type": "anomaly"
      }
    }
  }
}

...定義をJSONで書くと長いですね。しかし、内容はシンプルです。
ざっくり設定内容を書くと下記のようになっています。

  • trigger : 1日ごとに実行
  • input : test indexから「直近1日のデータ」かつ「anomaly_scoreが50以上」のドキュメントをとってくる
  • actions : inputから受け取ったペイロードをanomalies indexに登録する

2. watchを実行する

実際には登録したwatchは1日待っていれば実行されますが、
流石に待っているのもアレなので、無理矢理APIを叩いて実行します。

POST _xpack/watcher/watch/test/_execute

3. 結果を確認してみる

上手く動いていれば、2件のドキュメント(anomaly_scoreが50以上)がanomalies indexに登録されているはずです。

確認してみると...
f:id:shin0higuchi:20171101235806p:plain
しっかり検知・indexingされています!😊

....しかしちょっと待てよ。
確かに2件のドキュメントが登録されているように見えるけれど、
よく見ると単一のドキュメントとして2件分のデータが登録されてしまっています。

2件を見つけてくれたのは嬉しいけど、2件のドキュメントとしてindexingして欲しかったのです...

4.複数ドキュメントとして登録する

公式のドキュメントを読むと、解決策がしっかり書いてありました。
_docというフィールドにオブジェクト配列(ここでは各ドキュメント)を入れておけば
上手く複数ドキュメントとしてbulkしてくれるようです。

実際にwatchを修正してみましょう。
f:id:shin0higuchi:20171102000622p:plain

transformという部分を追加しました。
これでドキュメントの配列が_docというフィールドに入ってくれるはず。

5.再度watchを実行

一旦、anomalies indexを削除して、再度watchを実行してみます。
f:id:shin0higuchi:20171102001056p:plain

実行に失敗しました...
右の実行結果を読み進めて行くと...

"message": "MapperParsingException[Field [_index] is a metadata field and cannot be added inside a document. Use the index API request parameters.]"

なるほど。登録しようとしているドキュメント内に_indexなどのmeta fieldがあるとエラーが出るようです。

query投げた時に_sourceという部分だけを取得できれば良いのですが、
どうやらURI Searchではできても、Request Body Searchではできないように見えます。←ここが一番自信ないです。これできるなら誰か教えてください。
そうなってくるとwatcherとしてはinputの後で_sourceを取り出さなければ駄目そう。

6. _sourceだけを取り出してみた

"actions": {
    "index_to_elasticsearch": {
      "transform": {
          "script": "for (int i = 0; i < ctx.payload.hits.hits.length; ++i) {ctx.payload.hits.hits[i] = ctx.payload.hits.hits[i]._source }  return [ '_doc' : ctx.payload.hits.hits]"
        },
      "index": {
        "index": "anomalies",
        "doc_type": "anomaly"
      }
    }
  }

厳密に言うと、各ドキュメントを_sourceで上書きするかたちになっています。

7. 再度watchを実行する

再度実行、今度こそ!
f:id:shin0higuchi:20171102002356p:plain

上手く2ドキュメントに分かれて登録されました。
めでたし、めでたし😊

まとめ

  • index actionは1つのドキュメントとしてindexingされるのがデフォルト
  • _doc にオブジェクト配列を入れると、複数ドキュメントとして扱ってくれる
  • ドキュメント内部にmeta fieldがあるとrejectされる
  • もっと良い方法があってもおかしくない気もします。(知っている人いたら教えて欲しい)

以上です。

elasticsearchのfieldを一部修正する

概要

elasticesearchに入れたデータを後で修正したくなった時の修正手順。
update by queryを使います。

バージョン情報など

elasticsearch-6.0.0-alpha2
kibana-6.0.0-alpha2

前提

ユーザーのアカウント情報を持つaccountというindexを作成しました。

fieldの内容としては、氏名や電話番号、住所などが入っています。
f:id:shin0higuchi:20170707004947p:plain

「ユーザーの増加にともなってuser_idの見直しが必要になった」というシチュエーションを想定します。(先見性の無さ....)

新たなユーザー、yokohama takashiさんが登録されましたが、このユーザーのuser_idが既存のユーザーと被ってしまいました。
もともとuser_idの命名規則が、「firstnameの頭文字」.「lastname」だったので、idが被らないように変更する必要があります。
f:id:shin0higuchi:20170707005401p:plain

user_idで検索すると2件ヒットしてしまいます。
f:id:shin0higuchi:20170707005801p:plain

この記事では、t.yokohamaのuser_idを、
lastname.firstnameの形式に書き換える手順を書きます。

手順

1. 変更したいドキュメントの条件をqueryで定義する。

今回は取り急ぎ、t.yokohamaというuser_idを持つユーザーのuser_idのみを変更します。
そのためqueryは下記のようになります。

"query": {
    "term": {
      "user_id.keyword": {
        "value": "t.yokohama"
      }
    }
  }

2. update by query

結論から書くと、下記のjsonをConsole上で実行すれば、updateができます。

POST account/_update_by_query
{
  "query": {
    "term": {
      "user_id.keyword": {
        "value": "t.yokohama"
      }
    }
  },
  "script": {
    "inline": "ctx._source.user_id = ctx._source.firstname + '.' + ctx._source.lastname",
    "lang": "painless"
  }
}

query部分で、対象とするドキュメントを絞り、script部分で処理の内容を書きます。
scriptではpainlessを使っているので、ctx._source.field名でフィールドにアクセスできます。

これを実際に実行すると...
f:id:shin0higuchi:20170707010639p:plain

無事書き換わりました
f:id:shin0higuchi:20170707010659p:plain

めでたしめでたし

その他メモ

・reindex APIでもscriptが使えるので、部分的に書き換えて別indexに入れることもできる。

追記(10/1)

reindex APIを使った方法も追記して欲しいとのコメントをいただいたので追記します。

下記のようなリクエストを実行すれば同じようなことができます。
(queryの部分を省けば、全ドキュメントを対象にできます。)

POST _reindex
{
  "source": {
    "index": "account"
  },
  "dest": {
    "index": "account_dest"
  }, 
  "script": {
    "inline": "ctx._source.user_id = ctx._source.firstname + '.' + ctx._source.lastname",
    "lang": "painless"
  }
}

※この場合、account indexが書き換えられるのでは無く、account_destという別のindexが作成されるので、もう一度reindex APIで元のindexに書き戻すか、aliasを利用してください。

elasticsearchでデータ型を修正するメモ

概要

elasticsearchに入れたデータの型を修正する手順をメモ。

バージョン情報など

elasticsearch-6.0.0-alpha2
kibana-6.0.0-alpha2

前提

・elasticsearchにデータを入れてみたけど、やっぱりデータ型が違っていた。
・データ量がそれほど大きくない

reindexAPIを使った方がスマートな場合も多々ありますが、
今回はKibanaのConsoleからmappingを修正して、Logstashなどでデータを再投入する場合を想定します。

想定するシチュエーション

ユーザーのアカウント情報を示すaccountというindexにデータを突っ込んでみました。

kibanaで可視化するためにindex patternを作成...
f:id:shin0higuchi:20170703225034p:plain

確認してみるとageがstringになっています。
平均年齢とかをグラフ化する時とかに困ります。
f:id:shin0higuchi:20170703225542p:plain

ageが数字として扱われるようにmappingを修正してみます。

手順

1. 現在のmapping情報を取得する。

GET index名 をKIbanaのConsoleから実行することで、indexの情報を取得できます。
画像を見るとageのフィールドがtext型として定義されていることがわかりますね。
f:id:shin0higuchi:20170703230540p:plain

2. mappingの内容をコピーする。

先ほど右画面に出力されたjsonを畳むと、aliases, mappings, settingsなどの項目があるのがわかりますが、
今回はmappingの部分のみをコピーします。

f:id:shin0higuchi:20170703230740p:plain
f:id:shin0higuchi:20170703231150p:plain
f:id:shin0higuchi:20170703230948p:plain

こんな感じでコピーします。

3. mappingを変更する。

今度はConsoleの左画面を使います。
PUT index名 でindexを作成でき、その際にmappingを定義することができます。

f:id:shin0higuchi:20170703231417p:plain

これだとindexが作られるだけ。
中にmappingの定義を記述することで、データ型の定義などができます。


先ほどコピーしたmappingの内容を波括弧の中に貼り付けます。
f:id:shin0higuchi:20170703231534p:plain


そして、該当箇所のデータ型を書き換えます。
今回はageをintegerに書き換えています。
f:id:shin0higuchi:20170703231844p:plain

4. 変更したmappingを適用する。

さて、mapping定義を書き換えたので、実行してみます。
f:id:shin0higuchi:20170703232215p:plain
....怒られましたね。
indexが既に存在している場合、PUT index名によってmapping定義を上書きすることはできません。
なので、「indexの削除」→「mappingの定義」→「データの再投入」という手順を踏みます。
(ダウンタイムを極力減らしたい場合は、別名のindexにデータを新しく入れて、aliasを切り替えるという手もありですね。) 

indexをDELETEして...
f:id:shin0higuchi:20170703232611p:plain

再実行
f:id:shin0higuchi:20170703232714p:plain

こんどは上手く行きました☺️

5. データを再投入して型を確認する

データ型の修正もしたところで、データをもう一度入れて、型を確認してみましょう。

index patternsの画面で一度refreshを押して
f:id:shin0higuchi:20170703233118p:plain

ageのところを見ると....
f:id:shin0higuchi:20170703233241p:plain

しっかりnumberとして認識されていました。
めでたしめでたし。

まとめ

  • データ型を修正したい時は、現在のmapping情報をコピーして必要箇所を変更して貼り付けると楽ちん
  • もちろんコピーしないで、必要なフィールドの型を定義してもOK(でもコピペなら文法覚えなくてもできる)
  • reindexAPIを使ったりする方法もある
  • kibanaから参照する場合は、index patternのrefreshをお忘れなく

ElasticsearchのAggregationをpagingする

概要

elasticsearchにはaggregationという便利な機能がありますが、
paginationを提供していません。

aggregateの仕組み上単純な話ではないようです。
そのあたりの詳細は(https://github.com/elastic/elasticsearch/issues/4915)をご覧ください。

今回はaggregateした結果を擬似的にページングします。
(ちょっと反則技が入ります。途中まで読んで憤慨しないでください。。。)
(しかも大きな制約があります。。。)

バージョン情報など

elasticsearch-6.0.0-alpha2
kibana-6.0.0-alpha2

手順

1. データの準備

今回はmetricbeatのデータを使いました。
インストールして起動するだけです。
metricbeatの使い方は本記事では説明しません。

2. データの確認

データが入っていることを確認してみます。
f:id:shin0higuchi:20170620014757p:plain
無事indexが作成されていることがわかります。

3. aggregationの適用

aggregationの結果をページングするということで、
まず普通にaggregationを使ってみましょう。

今回、aggregationで行うのは、
pidごとにbucketを分割して、その中の1件ずつをtop_hits aggregationで取得するというものです。

まずはterms aggregationでpid毎のbucketに分割して...
f:id:shin0higuchi:20170620015222p:plain

top_hits aggregationで、bucketから1件ずつ取得します。
f:id:shin0higuchi:20170620015235p:plain

4. ページングできるか?

ここで問題になってくるのが、
「bucketの数が膨大になった時にページングしたい」ということです。

通常のqueryであれば、fromとsizeを使って実現できますが、下の画像の通り、aggregationには効果がありません。
f:id:shin0higuchi:20170620015806p:plain
f:id:shin0higuchi:20170620015600p:plain

かといってaggregationの中に記述するとこの通りエラーが。
fromはサポートしていないようです...
f:id:shin0higuchi:20170620015652p:plain

5. 解決策

結局のところ、どうしてもページングしたければ別indexにaggregation結果を持つのが妥当だという結論に...
(読んでくれた方の半数がタブを閉じた気がします。)

metricbeatは常に情報が更新されるので、aggregation結果も随時updateする必要があります。
今回はwatcherを使って集計結果をindexingします。

watcherというと、メール通知やslack通知などができる便利なアラートと思われがちですが、
定期実行できる上にscriptが使える(webhookだって使える)というのは割と万能なやつです。
私はわりと好きです。

こんな感じでConsoleから登録できます。(今は一応UIも出ていますね。あまり使いやすくないですが。)
f:id:shin0higuchi:20170620020314p:plain


画像だとコピペできないと思うので
一応jsonを貼っておきます。

PUT _xpack/watcher/watch/aggregation
{
  "input": {
    "search": {
      "request": {
        "indices": [
          "metricbeat-6.0.0-alpha1-2017.06.*"
        ],
        "types": [
          "doc"
        ],
        "body": {
          "size": 0,
          "aggs": {
            "num": {
              "terms": {
                "field": "system.process.pid",
                "size": 10000
              },
              "aggs": {
                "top": {
                  "top_hits": {
                    "size": 1
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "trigger": {
    "schedule": {
      "interval": "1m"
    }
  },
  "actions": {
    "aggregate": {
      "transform": {
        "script": "return ['_doc' : ctx.payload.aggregations.num.buckets]"
      },
      "index": {
        "index": "aggregated",
        "doc_type": "aggregated"
      }
    }
  }
}

この定義で何をしているかというと、
毎分aggregationを行って、その結果をaggregatedというindexに登録しています。

動いているか確認

aggregationの結果がindexingされていることがわかります。
f:id:shin0higuchi:20170620021014p:plain

ページングする

あとはこのindexに対してfromおよびsizeを使えばOKです
f:id:shin0higuchi:20170620021614p:plain
f:id:shin0higuchi:20170620021620p:plain

こんな感じです。

まとめ

  • bucketをページングするのは現状難しい。
  • watcherを使ってaggregation結果をindexingしておくと楽ちん。
  • ただし、一段目のterms aggregationでsizeを指定するので、term(pid)のパターンが膨大だと不可。
  • ブラウザ上で100件ずつページ分けたい時などには使えそう。
  • 本当はpid毎にdocument_idを指定しないとだめです。今回は省略です。
  • (watchを止め忘れないようにしましょう)

logstashからelasticsearchへデータを送る時に最低限知っておくべき設定

概要

logstashからelasticsearchにデータを送る時の最低限の設定に関するメモ

バージョン情報など

elasticsearch-5.x
logstash-5.x

設定ファイル内容

input{
 省略
}
filter{
}
output{
  elasticsearch{
    hosts => "localhost:9200" 
    index => "test_index"
    document_type => "logs"
    user => "elastic"
    password => "changeme"

  }
}

設定項目補足説明

hosts

elasticsearchのホスト名。リモートのelasticsearchにアクセスする場合はそのホスト名を指定する必要がある。

index

elasticsearchに入れる時のindex名を指定する。デフォルトだとlogstash-yyyy-MM-ddのようになる

document_type

elasticsearchに入れる時のtype名を指定する。デフォルトはlogs

user

x-packをインストールしている時のみ必要。

password

x-packをインストールしている時のみ必要。

まとめ

elasticsearchにoffice系ファイルやPDFを入れる

概要

elasticsearchにpdfやpptx,xlsxなどのファイルを入れる方法についてメモ。
ingest-attachment-pluginを使います。

バージョン情報など

macOS Sierra 10.12.5
elasticsearch-5.4.1

手順

1.ingest-attachment-pluginをインストールする。

$ bin/elasticsearch-plugin install ingest-attachment
-> Downloading ingest-attachment from elastic
[=================================================] 100%   
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.lang.RuntimePermission getClassLoader
* java.lang.reflect.ReflectPermission suppressAccessChecks
* java.security.SecurityPermission createAccessControlContext
* java.security.SecurityPermission insertProvider
* java.security.SecurityPermission putProviderProperty.BC
See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html
for descriptions of what these permissions allow and the associated risks.

Continue with installation? [y/N]y
-> Installed ingest-attachment

2. インストールできたか確認。

$ bin/elasticsearch-plugin list                     
analysis-kuromoji
ingest-attachment ← ある
ingest-geoip
x-pack

3. elasticsearchを起動する。

$ bin/elasticsearch

4. ingest-nodeのpipelineを登録する。

f:id:shin0higuchi:20170606012308p:plain
ここでは"attachment"という名前のpipelineを登録しています。
"message" fieldにbase64エンコードされたファイル情報が入っていれば、ファイルの内容が抽出されます。

5. logstashを使ってファイルをelasticsearchに入れる。

今回使う設定ファイルは下記の通り。

$ cat attachment.conf 
input{
  stdin{}
}
output{
  elasticsearch{
    hosts => ["localhost:9200"]
    index => "attachment_test"
    pipeline => "attachment" #ここにpipelineの名前入れる。
  }
}

Logstashを実行してみましょう。

$ cat ../elasticsearch-5.4.0/base64.pptx| bin/logstash -f attachment.conf

6. kibanaからドキュメントを確認する。

f:id:shin0higuchi:20170606013457p:plain
検索してみると、しっかり内容が抽出されていることがわかります。

まとめ

  • base64エンコードして、ingest-attachment pluginを使えばofficeなども検索できる。
  • pptxでの例をあげたが、pdfやxlsxなども同様の手順でindexing可能。
  • 本当はbase64エンコードされたfieldは別のprocessorでremoveしたほうが良さそう。
  • 日本語検索をしっかりかけたい場合などはkuromoji辞書をattachment.contentに適用すると良い。

MacにLogstashをインストールする

概要

Macにlogstashをインストールします。
logstashはrpmパッケージやzipなどが提供されていますが、今回はtar.gz形式です。

バージョン情報など

OS : macOS Sierra 10.12.5
logstash : logstash-5.4.1
メモリ : 4GB

手順

1. logstashのファイルを公式サイトからダウンロードする。

$ wget https://artifacts.elastic.co/downloads/logstash/logstash-5.4.1.tar.gz

2. ダウンロードしたファイルを解凍する。

$ tar zxvf logstash-5.4.1.tar.gz

ディレクトリ構成は下記の通り。

$ tree -L 1 logstash-5.4.1
logstash-5.4.1
├── CHANGELOG.md
├── CONTRIBUTORS
├── Gemfile
├── Gemfile.jruby-1.9.lock
├── LICENSE
├── NOTICE.TXT
├── bin
├── config
├── data
├── lib
├── logstash-core
├── logstash-core-plugin-api
└── vendor

3. 実行する。

$ logstash-5.4.1/bin/logstash
Sending Logstash's logs to /Users/SHIN/Elastic/Elastic5.4/logstash-5.4.1/logs which is now configured via log4j2.properties
[2017-06-03T00:17:07,073][INFO ][logstash.setting.writabledirectory] Creating directory {:setting=>"path.queue", :path=>"/Users/SHIN/Elastic/Elastic5.4/logstash-5.4.1/data/queue"}
ERROR: No configuration file was specified. Perhaps you forgot to provide the '-f yourlogstash.conf' flag?
usage:
  bin/logstash -f CONFIG_PATH [-t] [-r] [] [-w COUNT] [-l LOG]
  bin/logstash -e CONFIG_STR [-t] [--log.level fatal|error|warn|info|debug|trace] [-w COUNT] [-l LOG]
  bin/logstash -i SHELL [--log.level fatal|error|warn|info|debug|trace]
  bin/logstash -V [--log.level fatal|error|warn|info|debug|trace]
  bin/logstash --help

...これだと怒られます。(知ってた)
 通常は "-f" オプションをつけて、logstashのconfファイル(挙動を定義したファイル)を指定します。

4. confファイル(logstash.conf)を作成する。(confファイルについては別エントリで書きます。)

テキストエディタなどで、logstash.confを作成します。
文字コードUTF-8(BOM無し)にしてください。SJISとかだとエラーが出ると思います。

$ cat logstash.conf
input{
  stdin{}
}
filter{}
output{
  stdout{}
}

5. logstash.confファイルを指定して実行する。

$ logstash-5.4.1/bin/logstash -f logstash.conf
Sending Logstash's logs to /Users/SHIN/Elastic/Elastic5.4/logstash-5.4.1/logs which is now configured via log4j2.properties
[2017-06-03T00:20:54,260][INFO ][logstash.agent           ] No persistent UUID file found. Generating new UUID {:uuid=>"d94c4cf3-ef18-404d-9dac-7df53125e1f6", :path=>"/Users/SHIN/Elastic/Elastic5.4/logstash-5.4.1/data/uuid"}
[2017-06-03T00:20:54,662][INFO ][logstash.pipeline        ] Starting pipeline {"id"=>"main", "pipeline.workers"=>4, "pipeline.batch.size"=>125, "pipeline.batch.delay"=>5, "pipeline.max_inflight"=>500}
[2017-06-03T00:20:54,707][INFO ][logstash.pipeline        ] Pipeline main started
The stdin plugin is now waiting for input:
[2017-06-03T00:20:54,867][INFO ][logstash.agent           ] Successfully started Logstash API endpoint {:port=>9600}
Hello, world!!
2017-06-02T15:22:26.676Z Macintosh.local Hello, world!!

まとめ

  • logstashをインストールして実行することができました。
  • confファイルは設定が色々複雑なので別エントリで書きます。

================================================