ore88ore memo

プログラミング関連のTipsをメインに書いていきます。どなたかのお役に立てれば幸いです。

Elasticsearch Joinデータ型で親子データの定義とクエリ

Joinデータ型を利用することで、同一インデックス内に親子関係を持ったドキュメントを作成することができます。
ユースケースとしては、1つのエンティティが他のエンティティを大幅に上回っている1対多の関係がデータに含まれている場合と公式ページに記載されています。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/parent-join.html

また、クエリの際は、パフォーマンス的な負担が大きいと記載されているので、注意が必要です。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/parent-join.html#_parent_join_and_performance

実行環境

  • Elasticsearch
    • 7.4.2
  • Kibana
    • 7.4.2

定義

フィールド名をjoin_fieldとして、Joinデータ型を定義してみます。relations内で指定している、questionanswerはそれぞれ親ドキュメント名、子ドキュメント名を任意の名前を指定することができます。

PUT /join_test
{
  "mappings": {
    "properties": {
      "join_field": {
        "type": "join",
        "relations": {
          "question": "answer" 
        }
      }
    }
  }
}

インデクシング

Joinデータ型を定義したインデックスに親ドキュメント(質問)、子ドキュメント(解答)をインデクシングする。

親ドキュメント

Joinデータ型で指定した、親ドキュメント名(今回の例だとquestion)を指定してインデクシングする。また、nameの指定は省略することができるので、以下の2パターンは同じ意味となる。

PUT /join_test/_doc/1
{
  "question_text": "好きな野球チームは?",
  "join_field": {
    "name": "question" 
  }
}

PUT /join_test/_doc/2
{
  "question_text": "好きなサッカーチームは?",
  "join_field": "question" 
}

子ドキュメント

Joinデータ型で指定した、子ドキュメント名(今回の例だとanswer)を指定してインデクシングする。以下の例だと1つ目の質問の子ドキュメント(解答)を2つインデクシングしている。

PUT /join_test/_doc/3?routing=1
{
  "answer_text": "日ハム",
  "join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

PUT /join_test/_doc/4?routing=1
{
  "answer_text": "巨人",
  "join_field": {
    "name": "answer", 
    "parent": "1" 
  }
}

クエリ

上記でインデクシングしたデータを取得する。親子関係というと、親レコードに子レコードリストがぶら下がっている事を想像してしまいますが、親ドキュメントと子ドキュメントは同じレベルのドキュメントとしてフラットになっていることを意識する必要がある。

✗こっちじゃない
- 親1
    - 子1
    - 子2
- 親2
    - 子1

○こっち
- 親1
- 親1の子1
- 親1の子2
- 親2
- 親2子1

親ドキュメントのみ取得

GET /join_test/_search
{
  "query": {
    "term": {
      "join_field": "question"
    }
  }
}

子ドキュメントのみ取得

GET /join_test/_search
{
  "query": {
    "term": {
      "join_field": "answer"
    }
  }
}

子ドキュメントの条件に合致する親ドキュメントを取得

answer_text日ハムの子ドキュメントを持つ親ドキュメントリストを取得する。inner_hitsを付与することで、条件に合致する子ドキュメントもレスポンスとして含まれる。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-has-child-query.html

GET /join_test/_search
{
  "query": {
    "has_child": {
      "type": "answer",
      "query": {
        "term": {
          "answer_text.keyword": "日ハム"
        }
      },
      "inner_hits": {}
    }
  }
}

親ドキュメントが同じ子ドキュメントを取得

https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-parent-id-query.html https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-has-parent-query.html

GET /join_test/_search
{
  "query": {
    "parent_id": { 
      "type": "answer",
      "id": "1"
    }
  }
}

GET /join_test/_search
{
  "query": {
    "has_parent": { 
      "parent_type" : "question",
      "query": {
        "term": {
          "question_text.keyword": "好きな野球チームは?"
        }
      }
    }
  }
}

Typescriptでオブジェクトのnullやundefinedの扱い方

なにも考慮せずに書くと、以下のコメント部分のようにコンパイラに怒られる。

type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return { hoge: "hogehoge" };
}
const sampleObject = getObject();
console.log(sampleObject.hoge); // Object is possibly 'null' or 'undefined'.

ifでNullチェック

type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return { hoge: "hogehoge" };
}
const sampleObject = getObject();
if (sampleObject != null) {
    console.log(sampleObject.hoge);
}
// "hogehoge"

Optional Chainingの?

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining

type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return { hoge: "hogehoge" };
}
const sampleObject = getObject();
console.log(sampleObject?.hoge);
// "hogehoge"

null または undefinedの場合

nullundefinedいずれの場合も結果はundefined

type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return null;
}
const sampleObject = getObject();
console.log(sampleObject?.hoge);
// undefined

fooを参照する場合

Nullish Coalescingと組み合わせると設定されていない場合の初期値などを設定できる。
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing

type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return { hoge: "hogehoge", foo: 1 };
}
const sampleObject = getObject();
console.log(sampleObject?.foo ?? 10);
// 1
type nullableObject = null | undefined | { hoge: string, foo?: Number };
const getObject = (): nullableObject => {
    return null;
}
const sampleObject = getObject();
console.log(sampleObject?.foo ?? 10);
// 10

Elasticsearch 基本的なQuery

KibanaのDev Tools上のconsoleから実行できます。

  • Elasticsearch
    • 7.4.2
  • Kibana
    • 7.4.2

Term-level queries

検索キーワードに完全一致するフィールドを検索する際に利用するクエリです。文字列の完全一致、日付の範囲、数値の検索などに利用できそうです。
また、文字列の完全一致に利用する場合は、keyword型でフィールドを作成します。

text型ではアナライザによって単語の分割が行われて転置インデックスが形成されるが、keyword型では、アナライザによる単語の分割が行われません。

term

指定されたフィールドの値と完全一致するドキュメントを取得する https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-term-query.html

GET /blog_test/_search
{
  "query": {
    "term": {
      "id": {
        "value": "id1"
      }
    }
  }
}

terms

termクエリと同様の検索で、valueを複数指定して完全一致するドキュメントを取得する。SQLのIN句のイメージでしょうか。なお、デフォルトの最大terms数は、65,536個とのことです。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-terms-query.html

GET /blog_test/_search
{
  "query": {
    "terms": {
      "id": ["id1", "id2"]
    }
  }
}

range

指定された範囲内の値を持つドキュメントを取得する https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-range-query.html

GET /blog_test/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 10,
        "lte": 20
      }
    }
  }
}

この例だとageが10以上20以下を取得するクエリとなる。なお、大小関係の表現としては以下を利用することができる。

gt
〜より大きい
gte
〜以上
lt
〜より小さい
lte
〜以下

ids

指定されたドキュメントIDに合致するドキュメントを取得する。 https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-ids-query.html

GET /blog_test/_search
{
  "query": {
    "ids": {
      "values": ["1", "3"]
    }
  }
}

prefix

指定されたプレフィックスを含むドキュメントを取得する
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-prefix-query.html

GET /blog_test/_search
{
  "query": {
    "prefix": {
      "id": {
        "value": "id"
      }
    }
  }
}

wildcard

指定されたワイルドカードパターンに一致するドキュメントを取得する。SQLのLike検索のイメージでしょうか。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-wildcard-query.html

GET /blog_test/_search
{
  "query": {
    "wildcard": {
      "id": {
        "value": "id*"
      }
    }
  }
}

Full text queries

検索キーワードで全文検索を実行します。転置インデックスに対しての検索となるので、検索対象のフィールドはtext型で作成します。
なお、指定されたキーワードもインデクシング時同様に分割(Analyzer)されて一致条件に利用されます。

match

指定されたキーワードに一致するドキュメントを取得する。オペレーター(ANDとかOR)を変更したり、 キーワードのAnalyzerをインデクシング時と異なるものを指定することもできる。詳細は以下のドキュメントを参照。 https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-match-query.html

GET /blog_test/_search
{
  "query": {
    "match": {
      "name": {
        "query": "名前 太郎"
      }
    }
  }
}

名前または太郎nameフィールドに含まれるドキュメントが取得される。

match phrase

指定されたキーワードが、指定された語順に一致するドキュメントを取得する。
https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl-match-query-phrase.html

GET /blog_test/_search
{
  "query": {
    "match_phrase": {
      "name": {
        "query": "名前 太郎"
      }
    }
  }
}

名前太郎の語順でnameフィールドに一致するドキュメントが取得される。

Elasticsearch 基本的なCRUD操作

KibanaのDev Tools上のconsoleから実行できます。

  • Elasticsearch
    • 7.4.2
  • Kibana
    • 7.4.2

登録

ドキュメントIDを指定して登録
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html

PUT /blog_test/_doc/1
{
  "id": "id1",
  "name": "名前 太郎"
}

ドキュメントIDを自動生成して(指定しないで)登録
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html

POST /blog_test/_doc
{
  "id": "id2",
  "name": "名前 花子"
}

取得・検索

ドキュメントIDを指定して取得
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html

GET /blog_test/_doc/1

条件を指定して検索
https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html

GET /blog_test/_search
{
  "query": {
    "match": {
      "name": "太郎"
    }
  }
}

更新

ドキュメント全体を置き換える更新
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html

PUT /blog_test/_doc/1
{
  "id": "id_update",
  "name": "名前 更新"
}

ドキュメントの一部を更新
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

POST /blog_test/_update/1
{
  "doc": {
   "name": "部分 更新" 
  }
}

削除

ドキュメントの削除
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html

DELETE /blog_test/_doc/1

Javascriptで配列を便利に操作

配列の中がオブジェクトで構成されているような配列を操作したい時のTipsです。

const array = [
    {id: "id1", name: "hoge"},
    {id: "id2", name: "foo"},
];

nameだけ取得する

const array = [
    {id: "id1", name: "hoge"},
    {id: "id2", name: "foo"},
];
const names = array.map((currentValue, index, array) => {
    // currentValueは、配列内のアイテム
    return currentValue.name;
});
console.log(names);
// ["hoge", "foo"]

nameだけの配列から重複を削除する

const array = ["hoge", "foo", "foo2", "hoge"];
const newArray = Array.from(new Set(array));

console.log(array);
// ["hoge", "foo", "foo2", "hoge"]

console.log(newArray);
// ["hoge", "foo", "foo2"]

idがid1のアイテムだけ取得する

const array = [
    {id: "id1", name: "hoge"},
    {id: "id2", name: "foo"},
];
const id1 = array.find((element, index, array) => {
    // elementは、配列内のアイテム
    return element.id === "id1";
});
console.log(id1);
// {id: "id1", name: "hoge"}

idがid1のアイテムだけ配列で取得する

1つの特定のオブジェクトを取得する場合は、上記のfindのほうが良いかと思います。

const array = [
    {id: "id1", name: "hoge"},
    {id: "id2", name: "foo"},
];
const id1 = array.filter((currentValue, index, array) => {
    // currentValueは、配列内のアイテム
    return currentValue.id === "id1";
});
console.log(id1);
// [{id: "id1", name: "hoge"}]

idがid1のアイテムが存在するか?

const array = [
    {id: "id1", name: "hoge"},
    {id: "id2", name: "foo"},
];
const isExists = array.some((element, index, array) => {
    // elementは、配列内のアイテム
    return element.id === "id1";
});
console.log(isExists);
// true

Javascriptでオブジェクトを結合・マージする

Javascriptでオブジェクトを結合する方法をいくつかメモっておきます。個人的には、簡潔に書けるのでスプレッド構文がオススメ。

Object.assign

const object1 = { A: "objectA", B: "objectB" };
const object2 = { C: "objectC", D: "objectD" };
console.log(Object.assign(object1, object2));
// → {A: "objectA", B: "objectB", C: "objectC", D: "objectD"}

Object.assign()でマージされるが、最初に指定されたオブジェクト(例だとobject1)も変更されるので注意が必要。 キーが重複している場合は、あとに指定されているオブジェクト(例だとobject2)が優先される。 上記の破壊的変更を防ぐには、空のオブジェクトを指定することで回避可能。

const object1 = { A: "objectA", B: "objectB" };
const object2 = { C: "objectC", D: "objectD" };
console.log(Object.assign({}, object1, object2));    // 最初の引数に空オブジェクトを指定する
// → {A: "objectA", B: "objectB", C: "objectC", D: "objectD"}

スプレッド構文

const object1 = { A: "objectA", B: "objectB" };
const object2 = { C: "objectC", D: "objectD" };
console.log({...object1, ...object2});
// → {A: "objectA", B: "objectB", C: "objectC", D: "objectD"}

指定したオブジェクトの破壊的変更はなし。キーが重複していた場合は、あとに指定されているオブジェクト(例だとobject2)が優先される。

Vuetifyのv-selectのドロップダウンアイコンを変更する方法

Vuetifyのv-selectコンポーネントを利用する際に、デフォルトだと以下のようなアイコンです。

f:id:ore88ore:20200418112445j:plain

このアイコンを別のアイコンに変更する方法を2パターン試してみました。

対象のコンポーネントのみ変更する

v-selectコンポーネントappend-iconプロパティに変更したいアイコンを指定します。 ちなみにデフォルトはmdi-menu-downというアイコンが設定されています。 以下の通り実装することで、対象のコンポーネントのアイコンのみ指定されたアイコンに変更されます。

<v-select append-icon="mdi-chevron-down">

v-selectすべてを変更する

一部のコンポーネントのみ変更するよりも、サイト全体として変更することのほうが多いのかもしれません。 そんな時は、v-selectが利用しているのデフォルトのアイコン設定を変更します。 iconsのvaluesに変更したいアイコンを指定することで変更することができます。

// src/plugins/vuetify.js

import Vue from 'vue'
import Vuetify from 'vuetify/lib'

Vue.use(Vuetify)

export default new Vuetify({
  icons: {
    iconfont: 'mdi',
    values: {
      dropdown: 'mdi-chevron-down'
    },
  },
})

nuxt.jsのbuildModulesを使って指定する場合は、以下のように指定することができます。

// nuxt.config.js

[
  "@nuxtjs/vuetify",
    icons: {
      iconfont: "mdi", // default
      values: {
        dropdown: "mdi-chevron-down"
      }
    }
  }
]

変更後

以下のようにドロップダウンのアイコンが変更されます。 f:id:ore88ore:20200418112756j:plain