cerebroを使ってみた

簡単な紹介です。以前はElasticsearchの定義の確認やクラスタの状態を見るのにheadプラグインなど使っていました。 が、複数の記事で「cerebro」を紹介していたので、実際みてみました。

セットアップ

試しに使ってみようと思ったので、zip形式のものをローカルで展開して以下のようにbin/cereborするだけです

$ bin/cerebro
[info] play.api.Play - Application started (Prod) (no global state)
[info] p.c.s.AkkaHttpServer - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

どんな感じか

最初にindexを指定する欄があるので、そこで指定するだけです。あとはcerebroが設定を読み込んでくれて、それを画面で綺麗に表示してくれます。

f:id:nakaearth:20220328003110p:plainf:id:nakaearth:20220328003113p:plain f:id:nakaearth:20220328003147p:plainf:id:nakaearth:20220328003150p:plain

使ってみた感じ、headと同じような感じで使いやすかったです。 簡単ですが、紹介は以上です。

興味があれば、使ってみると良いと思います。

Mysqlからlogstashを使ってElasticsearchに同期させるをやってみた

MysqlなどのRDBからlogstashを使ってElasticsearchに同期させるというのは既に様々な方が実践されているようだが、自分はやってみたことなかったのでやってみました。 検証の動機としては、今まではRDBに入った値をElasticsearchに同期させるために自前で定期的にElasticsearchにドキュメントを追加または更新する処理を書いていたが、それをしなくも良いとなると随分楽になるので、実際どういう風に書けるのか知りたかったからです。

環境

自分が今回試した環境は以下です。

  • Elasticsearch: 7.10.1
  • Mysql: 8.0
  • logstash: 7.10.1

設定

Mysql

今回同期させたいテーブルは以下のような構成です。

CREATE TABLE `tickets` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` varchar(80) COLLATE utf8mb4_general_ci NOT NULL,
  `description` varchar(1024) COLLATE utf8mb4_general_ci NOT NULL,
  `point` int DEFAULT '0',
  `user_id` bigint DEFAULT NULL,
  `created_at` datetime(6) NOT NULL,
  `updated_at` datetime(6) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_tickets_on_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

Elasticsearch

Elaticsearchのマッピングは以下のような定義にしています。

$ curl -XGET 'localhost:9200/es_tickets/_mapping?pretty'

{
  "es_tickets" : {
    "mappings" : {
      "properties" : {
        "@timestamp" : {
          "type" : "date"
        },
        "@version" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "created_at" : {
          "type" : "date"
        },
        "creator_name" : {
          "type" : "text",
          "fields" : {
            "raw" : {
              "type" : "keyword"
            }
          },
          "analyzer" : "kuromoji_analyzer"
        },
        "description" : {
          "type" : "text",
          "analyzer" : "kuromoji_analyzer"
        },
        "description2" : {
          "type" : "text",
          "analyzer" : "ngram_analyzer"
        },
        "id" : {
          "type" : "long"
        },
        "point" : {
          "type" : "long"
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "raw" : {
              "type" : "keyword"
            }
          },
          "analyzer" : "kuromoji_analyzer"
        },
        "title2" : {
          "type" : "text",
          "analyzer" : "ngram_analyzer"
        },
        "unix_ts_in_secs" : {
          "type" : "float"
        },
        "updated_at" : {
          "type" : "date"
        },
        "user_id" : {
          "type" : "long"
        }
      }
    }
  }
}

今回はdescriptionとtitleで二つのanalyzerを定義しているmappingで、このindexにdocumentを同期させるようにlogstashを設定したいと思いました。

logstash

logstashの定義です。

Dockerfileを以下のように書きました。

FROM docker.elastic.co/logstash/logstash:7.17.1
RUN rm -rf logstash/config
RUN rm -rf logstash/pipeline
COPY config /user/share/logstash/config
COPY pipeline /user/share/logstash/pipeline
COPY --chmod=755 mysql-connector-java-8.0.17.jar /user/share/logstash/mysql-connector-java-8.0.17.jar

permissionを指定しないと、permissionのエラーが出てdriverを読み込めなかったので指定しています。 続いてpipeline直下に配置したjdbcドライバの設定をfilterの設定を記述したconfファイルです。

input {
  jdbc {
    jdbc_driver_library => "/user/share/logstash/mysql-connector-java-8.0.17.jar"
    jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
    jdbc_connection_string => "jdbc:mysql://db:3306/hogehoge_db"
    jdbc_default_timezone => "Asia/Tokyo"
    jdbc_user => "root"
    jdbc_password => ""
    jdbc_default_timezone => "Asia/Tokyo"
    tracking_column => 'id'
    tracking_column_type => "numeric"
    use_column_value => true
    schedule => "* * * * *"
    statement => "SELECT id, title, description, point, user_id, created_at, updated_at FROM tickets where id > :sql_last_value order by id asc"
  }
}

filter {
    mutate {
        add_field => { "description2" => "description" }
        add_field =>  { "title2" => "title" } 
    }
}

output {
  elasticsearch {
    hosts => ["elasticsearch"]
    index => "es_tickets"
    document_id => "%{id}"
  }
}

inputのstatementのところでmysqlから同期するときに、どの条件のデータを取得しElaticsearchに入れるのかsqlで指定します。「:sql_last_value」で最後に取得したレコード以降を取得すること条件で指定しています。今回はidカラムを指定しているので、tracking_columnでidを指定し、use_column_valueでidカラムのようにテーブルで定義したカラムををsql_last_valueにセットするように指定します。またscheduleで同期タイミングを指定できます。今回の場合は検証のため「 * * * * *」を指定しています。

filterでは今回ElasticsearchのマッピングMysqlのテーブルカラムと完全には一致しておらず追加分があるためmutateのadd_fieldで追加した分の指定をしています。mutateではfieldの追加や変更、削除ができます。

outputでは今回データの同期先がElasticsearchなので、Elasticsearchのhostsの指定、document_idにセットするカラムの値やIndexの指定をしています。 参照

続いてconfigの下にある設定ファイルです。

pipeline.ordered: auto

http.host: "0.0.0.0"
xpack.monitoring.elasticsearch.hosts: ["http://elasticsearch:9200"]
- pipeline.id: tickets
  path.config: "/usr/share/logstash/pipeline/tickets_jdbc.conf"
  queue.type: persisted

動作確認

設定は以上です。これでMysql、Elasticsearch、logstashを全て立ち上げてエラーが出ていなければ、設定に問題ないことは確認できます。 Mysqlに以下のようにデータを入れるとちゃんとlogstashに同期されるのが確認できると思います

f:id:nakaearth:20220327080246p:plain
mysql

感想

自前で同期させるよりも、このようなミドルウェアの組み合わせで自動で出来る様にするのは筋が良いと感じました。 条件が合えば、この方式を採用するのもありだなと思います。

ここ数年使っている振り返り方法

はじめに

ここ数年スクラムで開発することが多いのだが、スプリント毎の振り返りで幾つかの方法を採用していたので、そのうちメインで使っているものを自分が忘れないためにも整理しておく。あくまでも個人の感想、感覚であるので実際は違うというのもありそう。

振り返り

レトロスペクティブ(以降はレトロと省略する)をスプリントの終わりで実施し、それを元に次にスプリントをより良くなるようにまわしていくのだが、そのレトロで毎回同じ振り返り方法だとマンネリ化してしまうので、実施前にどれが良いか投票して決めるようにしている 投票ではあるが、大体使うものが決まってきていて最近多いのが以下の3つだった。

  • KPT
  • YWT
  • Fun Done Learn

KPT

Keep, Problem, Tryの頭文字をとってKPT(ケプトと呼んでいる)。

特徴

  • 良かったこと、次のスプリントでも続けたいこと、課題や問題点などスプリント内であったこと、感じたことをそれぞれK, Pの視点で挙げていく。それをもとに次のスプリントでやること、Try、を出していく。
  • KとPの間に関連性がない。

f:id:nakaearth:20220315100024p:plain
KPT

YWT

やったこと(Y)、分かったこと(W)、次にやること(T)の頭文字をとってYWT。

特徴

  • やったことを出し、それに対し分かったことを挙げていき、それを踏まえて次にやることを考えるので、段階を踏んで振り返ることができる。

f:id:nakaearth:20220315101010p:plain
YWT

Fun Done Learn

やって楽しかったこと(Fun)、やったこと(Done)、学んだこと(Learn)を挙げていく

特徴

  • 今回のスプリントを実施しみて、実際どうだったのか、ただひたすらタスクをこなしていたのか、その中で学んだことが多かったのか、学んだ事はそれほどないけど楽しかったのか、学びが多いし楽しかったのかなどなど。スプリントの状況が分かる
  • 課題は出にくい
  • これを幾つかのスプリントを続けているとチームの特徴が見れて、面白いです。

f:id:nakaearth:20220315101500p:plain
FunDoneLearn

結局何を使うか

何を使うかは、その時にスプリントをどう振り返りたいかでいつも決めています。最近はYWTかKPTを使うことが多いです。この二つの方がスプリントの課題が出しやすく、改善をしていけるからです。 でも個人的にはFun Done Learnが好きでが。

推理小説をグルーピングするために特徴語を抽出してみる

この記事はClassi developers Advent Calendar 2021の22日目の記事です。

私は推理小説をよく読むのですが、昔から結構読んでいるためストーリ展開、トリック、などにパターンがあると思うようになりました。 そして読んできた各小説を自分の独断と偏見によってパターン分けしました。例えばアガサ・クリスティーの小説のような王道の推理小説パターン、最近多いですが最後の最後で全てをひっくり返すような展開の大どんでん返しパターン、そして犯人視点で書かれたパターンなどです。 こうやってパターン訳すると何が嬉しいのかというと、「お勧めの推理小説は?」と聞かれた時にその人の好みを聞いてそれに近しいものを推薦しやすくなる点です。そして、これらパターン分けを人の手でなくプログラムで自動で出来ないか考えました。 ではそれをどう実現するか。考えた方法が次のような流れです。 それはまず各小説のデータを読み込み、その小説を表す特徴語を抽出します。その抽出した特徴語で近しいものをグルーピングしていくという方法を考えました。今回の記事では特徴語を抽出するところを書こうと思います。

特徴語の抽出

グルーピングをするために、まずは各推理小説の特徴を表す語句の抽出をします。今回はTF・IDFを使って各単語がその文章の中でどれほど重要な言葉であるかを数値化して表そうと思います。

TF・IDF

TFはTerm Frequencyの頭文字をとった言葉です。日本語でいうと単語出現頻度になります。意味は文章の中にどのくらい単語が出現しているかを表したものです。その単語がこの文章の中でどのくらい重要な単語なのかを示す指標の一つになります。

続いてIDFについてです。IDFはInverse Document Fequencyの頭文字を取った言葉です。日本語でいうと「逆文章頻度」となります。これは単語の出現の珍しさを表した物です。全文章の中で対象となる単語が使われている文章の数をwとし文章の数をDとした時、「log(w/D)」で表したものになります。

やってみる

では実際幾つか推理小説のTF・IDFを計算してみます。

前準備

今回小説の中の各文章からワードを抽出するために、Mecabを使おうと思います。Mecab京都大学情報学研究科と日本電信電話株式会社コミュニケーション科学基礎研究所の共同研究で開発された形態素解析器の1つです。 このMecabRubyで使用するためnattoというgemを使います。

Gemfileを以下のように書いて「bundle install」しておきます。

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.0.0'
gem 'mecab'
gem 'natto'

文章から言葉を抽出する

nattoを使って文章からワードを抽出してみます。文章は様々な品詞から成り立ちますが、今回は名詞だけに注目します。以下のサンプルのように してparseメソッドを使うと引数で与えた文章(この場合textの中身)をMecabを使って語句に分割し、そのうち名詞だけ@nounsという変数に格納しています。

require 'natto'

class WordTokenize
  def initialize
    @natto = Natto::MeCab.new
    @nouns = []
  end

  def tokenize(text)
    @words = @natto.parse(text) do |nm|
      feature = nm.feature.split(',')
      @nouns << nm.surface if feature[0] == '名詞'
    end
  end

  def nouns
    @nouns
  end
end

natto gem。便利です。 ネバネバ系のメカブ(Mecab)だから同じくネバネバ系の納豆(natto)ですね。gemの命名は面白いものが多いですね!

TFの計算

語句抽出できたら、小説全体の中で各語句がどのくらいの頻度で出現しているか計算してみます。上のサンプルコードのWordTokenizeクラスを使い、小説の各文章をtokenizeメソッドを使って語句に分解し、分解した後にnounsメソッドを使って名詞だけが格納された配列にし、countメソッドを使って気になる語句の登場回数をカウントします。そして全ての語句の中でどのくらい割合を占めるのか、計算します。

  def tf(documents, word)
    word_tokenizer = WordTokenize.new
    documents.each do |document_txt|
      word_tokenizer.tokenize(document_txt)
    end

    word_tokenizer.nouns.count(word) / word_tokenizer.nouns.count.to_f
  end

IDFの計算

IDFは上の説明で「全文章の中で対象となる語句が使われている文章の数をwとし文章の数をDとした時、「log(w/D)」で表したもの」と書いています。その通りの計算式で算出していきます。 以下のサンプルでは文章全体がdocumentsの中に入っています。このdocumentsは配列になっていて一文章が一要素になっています。そしてその一要素一要素をWordTokenizeクラスのtokenizeメソッドを使って語句に分解し、nounsメソッドで名詞の配列を取得します。その名詞の配列の中に対象となる語句が入っているか、各文章毎にチェックしていきます。そして最終的には以下のように対数を使って算出します。

 def idf(documents, word)
    df = 0
    documents.each do |document_txt|
      word_tokenizer = WordTokenize.new
      word_tokenizer.tokenize(document_txt)
      if word_tokenizer.nouns.any?(word)
        df += 1
      end
    end

    return 0 if df == 0

    Math.log(documents.size/df.to_f)
  end

実際にカウントしてみる

それでは実際幾つか小説を読み込んでTF・IDFの数値を算出して特徴語を抽出したいと思います。 今回使用するのは青空文庫推理小説の中からシャーロック・ホームズの「踊る人形」とアルセーヌ・ルパンが主人公の「奇巌城」を例として使います。

やることは、小説の中身をを読み込ませて幾つかの言葉の特徴値を出していきたいと思います。 まずは「踊る人形」からです。この話はシャーロック・ホームズの話なので、「ホームズ」「シャーロック」「ワトソン」でみてみます。すると以下のような結果になりました。

========== シャーロックホームズ: 踊る人形 ==========-
"====================="
"word: ホームズ"
"tf: 0.021139468935292602"
"idf: 1.776723303403941"
"ホームズの特徴値: 0.03755898707891806"
"====================="
"word: シャーロック"
"tf: 0.0015467904098994587"
"idf: 5.279223643933125"
"シャーロックの特徴値: 0.008165852504150231"
"====================="
"word: ワトソン"
"tf: 0.003351379221448827"
"idf: 4.163746426513188"
"ワトソンの特徴値: 0.013954293257198104"

やはり「ホームズ」という言葉が他の二つよりも特徴語として高い数値を示しています。続いて「人形」という言葉と「暗号」という言葉をみてみます。この「踊る人形」という話は人形を表したような文字が実は暗号を表していたということなのですが、実際数値上はどうなのか出してみます。

"====================="
"word: 暗号"
"tf: 0.0015467904098994587"
"idf: 5.542258049766918"
"暗号の特徴値: 0.008572711600567546"
"====================="
"word: 人形"
"tf: 0.0041247744263985565"
"idf: 3.957295549045762"
"人形の特徴値: 0.016322951478404794"

「人形」については物語の軸となるワードなので、「ワトソン」と同じくらいの数値を出しました。一方「暗号」については謎を解いてから判明する事実なので、特徴語としてはそれほど高く出ませんでした。

次にもう一つの方、「奇巌城(一部抜粋版)」をみてみたいと思います。

========== アルセーヌ・ルパン: 奇巌城 ==========-
"====================="
"word: ルパン"
"tf: 0.011904761904761904"
"idf: 3.538623786054287"
"ルパンの特徴値: 0.042126473643503415"
"====================="
"word: スパルミエント"
"tf: 0.010582010582010581"
"idf: 3.5892498591242545"
"スパルミエントの特徴値: 0.03798147999073285"
"====================="
"word: ガニマール"
"tf: 0.010582010582010581"
"idf: 3.5892498591242545"
"ガニマールの特徴値: 0.03798147999073285"
"====================="
"word: 強盗"
"tf: 0.0006613756613756613"
"idf: 7.396604781181859"
"強盗の特徴値: 0.00489193437908853"
"====================="
"word: 怪盗"
"tf: 0.0"
"idf: 0"
"怪盗の特徴値: 0.0"

登場人物でそれぞれTF・IDFの計算をしてみました。加えて「強盗」と「怪盗」という言葉についても算出してみました。 やはり主人公である「ルパン」という言葉が他の言葉に比べて数値が高いですね。また「怪盗」という言葉はなく、「強盗」の数値が僅かにありました。抜粋版だからかもしれませんが、「怪盗ルパン」の「怪盗」は0で「強盗」の方が数値が出ているのが個人的には面白かったです。機会があれば、一部抜粋でなく全部のデータで見てみたいです。

まとめ

今回は特徴語を導き出すためTF・IDFの計算処理を作成し、実際に小説の内容を幾つかパースして数値の算出を行いました。この後計画していることは、各小説の特徴語、TF・IDFの値を使って各小説のグルーピングをしていきたいと思います。

明日のClassi developers Advent Calendar 2021はホアンミンクアンさんです。

情報検索を学び始める:クラス分類 その1

始めに

Elasticsearchを学び始めてからそれなりに経つが、そもそも情報検索という分野についてちゃんと学んできておらず、今後のためを思い入門書籍を買って学ぶことにした。 読み進めて少し頭の整理をしたい章に差し掛かったので、まずは内容やキーワードを羅列することにした。

クラス分類について

クラス分類とはクラスタリングとは違う。分類するという点では一緒だが、クラスタリングはデータの特徴をもとに分け方を発見、クラス分類は正しく分類することを目指す。

クラス分類の一つが感情分類。いわゆる口コミって奴。 分類を行うシステムは分類器と呼ばれる。

ナイーブベイズ

ナイーブベイズでは条件付き確率によって表現する 条件付き確率 - ある条件Aを満たすことから事象Bが言える時の表現 => P(B|A)

Elasticsearch 困ったときに使ってみると良さそうな3つのAPI

これはClassi Advent Calendar 2020の18日目の記事です。よろしくお願いします。

Classiでサーバサイドエンジニアをしている@s_nakamuraです。 今年はあまりElasticsarchについて触れることが少なかったので、また定期的に触れて行こうと思います。今回紹介するのは、困ったときに使ってみるのが良さそうなAPIについてです。

Explain API

「なんか幾ら検索してもデータがヒットしないなー。どうしてだろう?」や「このXXXって文字だったら検索に出てくるのにYYYだと出てこないのはどうしてですか?」ということありませんか?ありますよね。 そんな時はExplain API を使ってはどうでしょう。

例えばあるqueryでscoreの最小値を定義していたとします。Queryの修正した後に今まで検索でヒットしていたデータが出てこなくなった。そんな時に以下のようにExplain apiを使えば実際データがヒットしているのか、ヒットしているならElasticsearch側でどのようにscoreが計算されているのか分かります。

$curl -X GET "localhost:9200/book_index/_explain/210?pretty" -H 'Content-Type: application/json' -d'
{
  "query" : {
    "match" : { "title" : "星人" }
  }                  
}
'

{
    "_index": "book_index",
    "_type": "_doc",
    "_id": "210",
    "matched": true,
    "explanation": {
        "value": 2.6731732,
        "description": "sum of:",
        "details": [
            {
                "value": 1.3365866,
                "description": "weight(title:星 in 13) [PerFieldSimilarity], result of:",
                "details": [
                    {
                        "value": 1.3365866,
                        "description": "score(freq=1.0), computed as boost * idf * tf from:",
                        "details": [
                            {
                                "value": 2.2,
                                "description": "boost",
                                "details": []
                            },
                            {
                                "value": 1.3862944,
                                "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
                                "details": [
                                    {
                                        "value": 2,
                                        "description": "n, number of documents containing term",
                                        "details": []
                                    },
                                    {
                                        "value": 9,
                                        "description": "N, total number of documents with field",
                                        "details": []
                                    }
                                ]
                            }.
  {
                                "value": 0.43824703,
                                "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
                                "details": [
                                    {
                                        "value": 1.0,
                                        "description": "freq, occurrences of term within document",
                                        "details": []
                                    },
                                    {
                                        "value": 1.2,
                                        "description": "k1, term saturation parameter",
                                        "details": []
                                    },
                                    {
                                        "value": 0.75,
                                        "description": "b, length normalization parameter",
                                        "details": []
                                    },
                                    {
                                        "value": 4.0,
                                        "description": "dl, length of field",
                                        "details": []
                                    },
                                    {
                                        "value": 3.6666667,
                                        "description": "avgdl, average length of field",
                                        "details": []
                                    }
                                ]
                            }
                        ]
                    }
                ]
            },
            {
                "value": 1.3365866,
                "description": "weight(title:人 in 13) [PerFieldSimilarity], result of:",
                "details": [
                    {
                        "value": 1.3365866,
                        "description": "score(freq=1.0), computed as boost * idf * tf from:",
                        "details": [
                            {
                                "value": 2.2,
                                "description": "boost",
                                "details": []
                            },
                            {
                                "value": 1.3862944,
                                "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
                                "details": [
                                    {
                                        "value": 2,
                                        "description": "n, number of documents containing term",
                                        "details": []
                                    },
                                    {
                                        "value": 9,
                                        "description": "N, total number of documents with field",
                                        "details": []
                                    }
                                ]
                            },
                            {
                                "value": 0.43824703,
                                "description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
                                "details": [
                                    {
                                        "value": 1.0,
                                        "description": "freq, occurrences of term within document",
                                        "details": []
                                    },
                                    {
                                        "value": 1.2,
                                        "description": "k1, term saturation parameter",
                                        "details": []
                                    },
                                    {
                                        "value": 0.75,
                                        "description": "b, length normalization parameter",
                                        "details": []
                                    },
                                    {
                                        "value": 4.0,
                                        "description": "dl, length of field",
                                        "details": []
                                    },
                                    {
                                        "value": 3.6666667,
                                        "description": "avgdl, average length of field",
                                        "details": []
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
}

上の例の場合だとscoreの計算に様々な処理が入っていることが分かります。"description": "sum of:",という記述があるように今回のQueryでは各score計算処理で算出されたscoreの合計値を合算してドキュメントのscoreとしています。 上で書いた例のようにQueryでscoreの最小値を指定している場合に検索にヒットすると思っていたドキュメントが実は想定していたほどscoreが出ていなかったなどあるかもしれません。 該当ドキュメントが検索結果に出てこない時、Explain apiで確認してみてはどうでしょうか

Validate API

このQueryで問題なく動くのか?それを確認したい場合はValidation APIを使うとQueryのチェックをしてくれます。

curl -X GET "localhost:9200/test-index/_doc/_validate/query?explain=true&pretty" -H 'Content-Type: application/json' -d'
{
  "query" : {
    "bool" : {
      "must" : {
        "query_string" : {
          "querys" : "title:1"
        }
      }
    }
  }
}
'
{
  "valid" : false,
  "error" : "ParsingException[Failed to parse]; nested: XContentParseException[[7:22] [bool] failed to parse field [must]]; nested: ParsingException[[query_string] query does not support [querys]];; org.elasticsearch.common.xcontent.XContentParseException: [7:22] [bool] failed to parse field [must]"

Queryが間違っていれば、上のようにエラー表示されす。正しいQueryであれば以下のようなresponseが返ります。クエリパラメータに「explain=true」をつけることでexplanations以下の項目が出力されます。

{
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "valid" : true,
  "explanations" : [
    {
      "index" : "test-index,
      "valid" : true,
      "explanation" : "+(+title:1) #DocValuesFieldExistsQuery [field=_primary_term]"
    }
  ]
}

思った通りの検索が出来ない時、実行しているQueryが正しく動作するかチェックしたい時にこのAPIを使うと解決の糸口になるかもしれません。

Profile API

実行したQueryがどのくらいパフォーマンスを出せているのか?それを知るためにProfile APIを使ってみてはどうでしょうか?

curl -X GET "localhost:9200/albums/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "profile": true,
  "query" : {
    "match" : { "title" : "星人" }
  }
}
'

検索API"profile": trueを追加します。

{
  "took" : 1223,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 2.6731732,
    "hits" : [
      {
        "_index" : "albums",
        "_type" : "_doc",
        "_id" : "210",
        "_score" : 2.6731732,
        "_source" : {
          "id" : 210,
          "title" : "ホゲホゲ星人の冒険",
          "user_id" : 62,
          "created_at" : "2020-12-13T02:52:03.000Z",
          "updated_at" : "2020-12-13T02:52:03.000Z",
          "photos" : [
            {
              "id" : 89,
              "image" : "#<Rack::Test::UploadedFile:0x0000560c33207048>",
              "description" : "冒険の記録1",
              "user_id" : 62,
              "group_id" : 0,
              "album_id" : 210,
              "photo_geo_id" : null,
              "good_point" : 0,
              "created_at" : "2020-12-13T02:52:03.000Z",
              "updated_at" : "2020-12-13T02:52:03.000Z",
              "description2" : null
            }
          ],
          "title2" : "ホゲホゲ星人の冒険",
          "tags" : [
            {
              "id" : 62,
              "label_name" : "犬",
              "album_id" : 210,
              "group_id" : 0,
              "created_at" : "2020-12-13T02:52:03.000Z",
              "updated_at" : "2020-12-13T02:52:03.000Z"
            }
          ],
          "total_point" : 0
        }
      },
      {
        "_index" : "albums",
        "_type" : "_doc",
        "_id" : "203",
        "_score" : 2.0209837,
        "_source" : {
          "id" : 203,
          "title" : "映画仮面ライダーとホゲホゲ星人の戦い",
          "user_id" : 61,
          "created_at" : "2020-12-13T02:52:03.000Z",
          "updated_at" : "2020-12-13T02:52:03.000Z",
          "photos" : [
            {
              "id" : 87,
              "image" : "#<Rack::Test::UploadedFile:0x0000560c3330b3e0>",
              "description" : "ホゲホゲ星人との場面1",
              "user_id" : 61,
              "group_id" : 0,
              "album_id" : 203,
              "photo_geo_id" : null,
              "good_point" : 0,
              "created_at" : "2020-12-13T02:52:03.000Z",
              "updated_at" : "2020-12-13T02:52:03.000Z",
              "description2" : null
            }
          ],
          "title2" : "映画仮面ライダーとホゲホゲ星人の戦い",
          "tags" : [
            {
              "id" : 61,
              "label_name" : "犬",
              "album_id" : 203,
              "group_id" : 0,
              "created_at" : "2020-12-13T02:52:03.000Z",
              "updated_at" : "2020-12-13T02:52:03.000Z"
            }
          ],
          "total_point" : 0
        }
      }
    ]
  },
  "profile" : {
    "shards" : [
      {
        "id" : "[g6sJGk0mTB6vV5yAPPIfMw][albums][0]",
        "searches" : [
          {
            "query" : [
              {
                "type" : "BooleanQuery",
                "description" : "title:星 title:人",
                "time_in_nanos" : 109041800,
                "breakdown" : {
                  "set_min_competitive_score_count" : 0,
                  "match_count" : 2,
                  "shallow_advance_count" : 0,
                  "set_min_competitive_score" : 0,
                  "next_doc" : 151300,
                  "match" : 31300,
                  "next_doc_count" : 2,
                  "score_count" : 2,
                  "compute_max_score_count" : 0,
                  "compute_max_score" : 0,
                  "advance" : 288300,
                  "advance_count" : 1,
                  "score" : 206300,
                  "build_scorer_count" : 2,
                  "create_weight" : 20768900,
                  "shallow_advance" : 0,
                  "create_weight_count" : 1,
                  "build_scorer" : 87595700
                },
                "children" : [
                  {
                    "type" : "TermQuery",
                    "description" : "title:星",
                    "time_in_nanos" : 7838400,
                    "breakdown" : {
                      "set_min_competitive_score_count" : 0,
                      "match_count" : 0,
                      "shallow_advance_count" : 3,
                      "set_min_competitive_score" : 0,
                      "next_doc" : 0,
                      "match" : 0,
                      "next_doc_count" : 0,
                      "score_count" : 2,
                      "compute_max_score_count" : 3,
                      "compute_max_score" : 2323200,
                      "advance" : 42600,
                      "advance_count" : 3,
                      "score" : 59500,
                      "build_scorer_count" : 3,
                      "create_weight" : 464000,
                      "shallow_advance" : 156300,
                      "create_weight_count" : 1,
                      "build_scorer" : 4792800
                    }
                  },
                  {
                    "type" : "TermQuery",
                    "description" : "title:人",
                    "time_in_nanos" : 1237500,
                    "breakdown" : {
                      "set_min_competitive_score_count" : 0,
                      "match_count" : 0,
                      "shallow_advance_count" : 3,
                      "set_min_competitive_score" : 0,
                      "next_doc" : 0,
                      "match" : 0,
                      "next_doc_count" : 0,
                      "score_count" : 2,
                      "compute_max_score_count" : 3,
                      "compute_max_score" : 64700,
                      "advance" : 54200,
                      "advance_count" : 3,
                      "score" : 33600,
                      "build_scorer_count" : 3,
                      "create_weight" : 872000,
                      "shallow_advance" : 58600,
                      "create_weight_count" : 1,
                      "build_scorer" : 154400
                    }
                  }
                ]
              }
            ],
            "rewrite_time" : 143100,
            "collector" : [
              {
                "name" : "SimpleTopScoreDocCollector",
                "reason" : "search_top_hits",
                "time_in_nanos" : 2089300
              }
            ]
          }
        ],
        "aggregations" : [ ]
      }
    ]
  }
}

検索結果の後の"profile"が今回のQueryに関するprofileの結果です。Queryセクションの中のtime_in_nanosでそのQueryに掛かった時間を示しています。breakdown以下に詳細であらわしています。childrenセクションでsub Queryの分析結果をあらわしています。 breakdownの内容はLuceneのlow levelの項目になります。 Queryの実行時にどのような処理にどのくらい時間が掛かっているのか、Luceneでどのクラスが使われているのかなど詳細を知るのにはProfile apiを使ってみると良さそうです。

以上です。 Elasticsearchは機能が豊富で様々なAPIや機能があります。個人的には非同期検索も面白そうだなと思っていて、今度試してみようと思います。

明日はhxrxchangさんです。

闘病生活から学んだこと

この記事は

qiita.com

の12月14日分です。

今回技術的なテーマを書こうかと思ったのですが、それはこちらで書いたので、ここでは技術から離れて健康をテーマに書こうかと思います。健康大事ですからね!

実は今年生まれて初めて入院をしました。今まで健康にはそれなりに自身もあり、病気になっても入院することはないだろうと思っていました。ところがある日入院することに。

入院の経緯、そしてその時あったこと、退院してから分かったことなど、これから同じようなことを経験されるかもしれない方のためにも記していこうと思います。

 

始まり

時は2019年10月。リプレースプロジェクトでQAチームによるテストも終盤にさしかかり、疲労も溜まってきた時でした。「あとこの対応さえ終われば、全ての問題も解決されリリース判定へ出せる」ちょうどそんな局面の時に身体の不調が起きました。

「あれ、なんか腰が痛いな」私は若干背骨が曲がっていることもあって腰に負担をかけやすかったので、「疲労もあって腰に痛みがきたのかな?すぐ治るだろう」と軽くみてました。ところが翌日リモート勤務をしているとき、腰に加えて両足の筋肉が筋肉痛のようになり背中に痛みがはしりました。「これは風邪だな」と思い熱を測ると予想通り熱が!「37.5」。平熱が35度台なので、37度でもしんどく感じました。妻に「風邪だから2階で休む」と告げて休み、夕食も2階でとりました。

で、23時半頃。歯磨きでもして寝るようと1階に降りて2階に戻ろうとした時、腰にビリビリビリ!!!!という激痛を覚え、立ち上がることが出来なくなりました。で仕方なく1階で寝たのですが、激痛で起き上がることも出来ず、起き上がれないのでトイレにもいけずで苦しい夜を過ごしました

 

入院日

翌日朝起きて、朝食を食べようと思って立ち上がろうとしても激痛が全く治っておらず、立ち上がるも起き上がることすら出来ませんでした。その様子をみて妻がただ事でないと察知してくれたが良かったのです。救急車を呼び病院に直行することになりました。息子は幼稚園に行く日で、救急車に運ばれた時から暫く会うことが出来ませんでした。会えない期間が結構あったので、寂しかったです。

救急車で運ばれ近くの病院に運ばれました。この最初に運ばれた病院でこのあと二週間ほど過ごすことになるとは、全く予想していませんでした。

救急車で運ばれて様々な検査をした結果、腰に細菌がついて炎症をおこしているから入院するとなりました。で、部屋まで行きましょうとなったのですが、歩けもしないし車椅子も乗れずで、ベットで寝たまま運ばれました。「腰の筋肉いためると、本当不便だな、腰って大事だな」と感じました。辛かった。

 

入院生活

入院、当初は食欲もなく怠くて辛いものでした。腰の痛みもあり本当不便でした。

実際血液検査でCRP(血液の炎症を表す数字)を見ても正常時が0.1とかなのに対し私は40とかでした。正常値の400倍。確かに腰痛いだろうって思いました。また脱水症状&腎臓機能不全もあり、投薬を続けて時間がかかるが悪いところを順に治していくという治療方針でひたすら投薬&寝る&血液検査を繰り返していきした。

そうしていくと徐々に身体も良くなり動けるようになってきました。そして食欲も段々戻ってきました。

お風呂も入ることが出来て、久々のお風呂に入った時は、気持ちの良いものでした。

 

入院中エピソード

入院中のエピソードとして覚えていることの一つが採血です。もともと採血が難しい体質のようで(血管が細く、採血しずらいみたいです)、特に点滴漏れした時は腕が晴れて全く血がとれなかったようです。腕の甲などあらゆる箇所刺したのですがダメな時がありました。そんな時は脚の付け根が最終手段だということを知りました。ただそれは太い血管のため看護士ではとれず医師でないとダメで覆いかぶさるように医者が脚の付け根から血を採っていきました。結構痛かったです。

もう一つは寝ながらシャンプーが出来るということです。まだ立つことが出来ない時にずっと風呂にもいけなかったので、頭を洗いたいが出来ないなと思っていたのですが、看護士さんの一人が寝ながらシャンプー出来るということでやっていただきました。そんな技があるとは知らなかったので、驚きました。

 

退院

退院する時期ですが、実は勉強会でLTする予定があり、それまでには退院したいと時期を交渉させていただき少し前倒して退院しました。退院の際に担当医に「今回腰に炎症をおこした細菌は何か?」と聞いたところ「事例がなく正直分からないが、血液からは溶連菌が検出された」と聞きました。担当医から明確な原因は告げられなかったのですが、溶連菌が関連しているだろうということでした。

はっきりと分からないまま数日が経った時、私の妻も同じような症状が別の箇所で発生しました。で、複数の病院を渡り歩いて検査した結果分かったのですが原因はやはり溶連菌でした。

原因は「溶連菌」だったのです。大事なことなので繰り返してしまいました。

この細菌、大抵喉の痛みと高熱をおこすのですが、完治しないと関節や筋肉に炎症をおこすようです。知らないうちに溶連菌に感染し、それが筋肉の炎症を引き起こしていたようで怖い細菌です、こいつは!

 

最後に

この溶連菌について詳しい医者がほとんどいないみたいです。詳しかったその医者は年齢が80歳くらいの高齢な方で、薬の本など出していた方でした。他の病院の若い医者や大学病院の医者はこの細菌の知識があまりなく「どうして溶連菌と判断してこの薬を出せたのか」と不思議に思っているようでした。そこで感じるのは、こういった症例に関するナレッジ共有が医学界ではあまり出来てないのかなと感じてしまいました。入院までした身としては、そこは共有して欲しいものだと強く思いました。

今回の入院で改めて健康を意識しました。健康であることのありがたみを感じました。

ということで、皆様も健康第一で!

 

明日はyoko-yanさんです。