tl;dr

Elasticsearch 6 で複数のデータを結合した状態で取得したいと思ったので, join datatype と has_child, has_parent クエリを試してみたのでメモ.

本環境で利用する環境は以下の通りで, こちらの docker-compose.yml を利用している.

$ curl -s -X GET 'http://localhost:9200/' | jq -r .version.number
6.2.2

Kibana のバージョンについても, Elasticsearch と同様に 6.2.2 を利用する. また, 本記事で記載されている Elasticsearch クエリは Kibana の Dev Tools を利用することを前提としている.

尚, 記事中の内容について誤り等がある場合にはご指摘頂けると幸いです…

参考

本記事は Elasticsearch のドキュメント以外に, 以下の記事を参考にさせて頂いた.

ありがとうございました.

join datatype と has_child, has_parent クエリ

join datatype


www.elastic.co

join datatype は同一インデックス内において, ドキュメントに親子関係を定義する為のフィールドで, マッピングの定義にて以下のように定義する (ドキュメントより引用).

PUT my_index
{
  "mappings": {
    "_doc": {
      "properties": {
        "my_join_field": { 
          "type": "join",
          "relations": {
            "question": "answer" 
          }
        }
      }
    }
  }
}

ポイントは以下の二点.

  • my_join_field キーは親子関係を定義するフィールドの名前
  • relations キーは answerquestion の親であることを定義

ドキュメントの定義は以下のように定義する (ドキュメントより引用).

PUT my_index/_doc/1?refresh
{
  "text": "This is a question",
  "my_join_field": {
    "name": "question"
  }
}

まず, 親となるドキュメントを登録する. 親となるドキュメントには, 結合する為に必要な名前 ("name": "question") も合わせて登録する.

PUT my_index/_doc/3?routing=1&refresh 
{
  "text": "This is an answer",
  "my_join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

次に子となるドキュメントを登録する. 子となるドキュメントにも, 同様に結合する為に必要な名前 ("name": "answer") と親のドキュメント ID (ここでは 1) を合わせて登録する. また, 親と子のデータは同一シャード上でインデキシングする必要がある為, ?routing パラメータは必須となる.

has_child, has_parent クエリ


www.elastic.co

has_child クエリ

子ドキュメントから親ドキュメントの情報を取得する. 後述の俳句の例では, 俳句作品から作者を得たい場合に利用する.

GET haiku_idx/doc/_search
{
  "query": {
    "has_child": {
      "type": "sakuhin",
      "query": {
        "match": {
          "sakuhin": "そこのけ"
        }
      },
      "inner_hits": {}
    }
  }
}

以下のような結果が返ってくる.

{
  "took": 16,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "3",
        "_score": 1,
        "_source": {
          "author_name": "小林一茶",
          "join_key": {
            "name": "author"
          }
        },
        "inner_hits": {
          "sakuhin": {
            "hits": {
              "total": 1,
              "max_score": 1.3189635,
              "hits": [
                {
                  "_index": "haiku_idx",
                  "_type": "doc",
                  "_id": "8",
                  "_score": 1.3189635,
                  "_routing": "3",
                  "_source": {
                    "sakuhin": "すずめの子/そこのけそこのけ/お馬が通る",
                    "kigo": "春",
                    "join_key": {
                      "name": "sakuhin",
                      "parent": "3"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
}

尚, "inner_hits": {} を付与することで, 子の情報 (俳句作品の情報) も結合された状態で検索結果が返却されている.

has_parent クエリ

has_child とは逆で, 親ドキュメントから子ドキュメントの情報を取得する. 俳句の例だと, 作者名の一部から俳句作品の情報を取得したい場合に利用する.

GET haiku_idx/doc/_search
{
  "query": {
    "has_parent": {
      "parent_type": "author",
      "query": {
        "match": {
          "author_name": "松尾"
        }
      }
    }
  }
}

以下のような結果が返ってくる.

{
  "took": 9,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "5",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "sakuhin": "五月雨を/集めてはやし/最上川",
          "kigo": "夏",
          "join_key": {
            "name": "sakuhin",
            "parent": "1"
          }
        }
      },
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "7",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "sakuhin": "しずかさや/岩にしみ入る/蝉の声",
          "kigo": "夏",
          "join_key": {
            "name": "sakuhin",
            "parent": "1"
          }
        }
      }
    ]
  }
}

ということで, 俳句作品とその作者データを利用して join を体験する

俳句作品

以下のような情報を Elasticseaerch に登録する.

作品 (sakuhin) 季語 (kigo) 作者 (author_name)
五月雨を/集めてはやし/最上川 松尾芭蕉
さらさらと/竹に音あり/夜の雪 正岡子規
しずかさや/岩にしみ入る/蝉の声 松尾芭蕉
すずめの子/そこのけそこのけ/お馬が通る 小林一茶
やせ蛙/負けるな一茶/これにあり 小林一茶
与謝蕪村

Elasticsearch クエリ

以下のようなクエリを用意した.

elasticsearch-join-sample - Elasticsearch の join datatype とhas_child, has_parent クエリを試した際に利用した各種ファイル.

github.com

長くなるので, 詳細は上記のページを確認すること.

検索

松尾芭蕉の作品は?

検索クエリ.

GET haiku_idx/doc/_search
{
  "query": {
    "has_parent": {
      "parent_type": "author",
      "query": {
        "match": {
          "author_name": "松尾芭蕉"
        }
      }
    }
  }
}

結果.

{
  "took": 14,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 2,
    "max_score": 1,
    "hits": [
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "5",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "sakuhin": "五月雨を/集めてはやし/最上川",
          "kigo": "夏",
          "join_key": {
            "name": "sakuhin",
            "parent": "1"
          }
        }
      },
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "7",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "sakuhin": "しずかさや/岩にしみ入る/蝉の声",
          "kigo": "夏",
          "join_key": {
            "name": "sakuhin",
            "parent": "1"
          }
        }
      }
    ]
  }
}

松尾芭蕉の作品が取得出来た. 検索クエリに "inner_hits": {} を付与すると, 以下のように作者の情報についても結合されて返却される.

...
        "inner_hits": {
          "author": {
            "hits": {
              "total": 1,
              "max_score": 0.5753642,
              "hits": [
                {
                  "_index": "haiku_idx",
                  "_type": "doc",
                  "_id": "1",
                  "_score": 0.5753642,
                  "_source": {
                    "author_name": "松尾芭蕉",
                    "join_key": {
                      "name": "author"
                    }
                  }
                }
              ]
            }
          }
        }
...

「やせ蛙..」を詠んだのは誰?

検索クエリ.

GET haiku_idx/doc/_search
{
  "query": {
    "has_child": {
      "type": "sakuhin",
      "query": {
        "match": {
          "sakuhin": "やせ蛙"
        }
      }
    }
  }
}

結果.

{
  "took": 15,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "3",
        "_score": 1,
        "_source": {
          "author_name": "小林一茶",
          "join_key": {
            "name": "author"
          }
        }
      }
    ]
  }
}

小林一茶でした.

ということで

join datatype で親子関係を定義したドキュメントに対して, has_child 及び has_parent クエリを使って結合したデータの取得について確認した. 思ったよりも簡単に操作することが出来たので良かった. あとは, これをどのようにギョームに取り込んでいくか検討したい.

おまけ

kuromoji による俳句作品の解析

当初は, 以下のようにインデックスを定義していた為, 作品の一部で検索した場合に正確なデータを取得出来ていなかった.

PUT haiku_idx2
{
  "mappings": {
    "doc": {
      "properties": {
        "author_name": {
          "type": "text"
        },
        "sakuhin": {
          "type": "text"
        },
        "kigo": {
          "type": "text"
        },
        "join_key": {
          "type": "join",
          "relations": {
            "author": "sakuhin"
          }
        }
      }
    }
  }
}

このインデックスに対して, 以下のような検索クエリを投げた場合…

GET haiku_idx2/doc/_search
{
  "query": {
    "has_child": {
      "type": "sakuhin",
      "query": {
        "match": {
          "sakuhin": "しずかさや"
        }
      }
    }
  }
}

松尾芭蕉が返ってきてほしいのであるが…

...
    "hits": [
      {
        "_index": "haiku_idx2",
        "_type": "doc",
        "_id": "2",
        "_score": 1,
        "_source": {
          "author_name": "正岡子規",
          "join_key": {
            "name": "author"
          }
        }
      },
      {
        "_index": "haiku_idx2",
        "_type": "doc",
        "_id": "1",
        "_score": 1,
        "_source": {
          "author_name": "松尾芭蕉",
          "join_key": {
            "name": "author"
          }
        }
      },
      {
        "_index": "haiku_idx2",
        "_type": "doc",
        "_id": "3",
        "_score": 1,
        "_source": {
          "author_name": "小林一茶",
          "join_key": {
            "name": "author"
          }
        }
      }
    ]
...

んー, なんでだろう…ということで, kuromoji を利用 (kuromoji_tokenizer) を利用して文字列をイイ感じで分割することにした.


www.elastic.co

今回は Elasticsearch の Docker イメージを利用しているので, 以下のようにコンテナイメージをビルドしている.

FROM docker.elastic.co/elasticsearch/elasticsearch:6.2.2
RUN ./bin/elasticsearch-plugin install analysis-kuromoji

後はインデックスを作成する際に, 以下のようにインデックス作成時に analyzer を定義する.

PUT haiku_idx
{
  "settings": {
    "analysis": {
      "analyzer": {
        "analysis-kuromoji": {
          "type": "custom",
          "tokenizer": "kuromoji_tokenizer"
        }
      }
    }
  },
  "mappings": {
    "doc": {
      "properties": {
        "author_name": {
          "type": "text",
          "analyzer": "analysis-kuromoji"
        },
        "sakuhin": {
          "type": "text",
          "analyzer": "analysis-kuromoji"
        },
        "kigo": {
          "type": "text"
        },
        "join_key": {
          "type": "join",
          "relations": {
            "author": "sakuhin"
          }
        }
      }
    }
  }
}

このインデックスに対してクエリを投げると, 以下のように意図したような結果が返ってきた.

{
  "took": 36,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "haiku_idx",
        "_type": "doc",
        "_id": "1",
        "_score": 1,
        "_source": {
          "author_name": "松尾芭蕉",
          "join_key": {
            "name": "author"
          }
        }
      }
    ]
  }
}

おお, Elasticsearch の検索エンジンとしての面白さが分かってきた… (いまさら…

元記事はこちら

Elasticsearch 6.x で join datatype と has_child, has_parent クエリを試した