PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

Path Hierarchy を活用した Elasticsearch におけるファイルシステム検索の実現

こんにちは、メディアサプライチェーン技術部開発第1グループの渕です。

任意に指定したパスの直下にあるフォルダ・ファイルを取得するファイルシステム検索をElasticsearchで実現するために、Path hierarchyというトークナイザーを使用してみました。

Path hierarchyとは

Path hierarchyとは、Elasticsearchにあるトークナイザーの1つでファイルパスなどの階層的な値をあらかじめ設定した区切り文字で分割し、分割した値をそれぞれ出力します。

実際にPath hierarchyを使用するとどのようになるのか試してみます。

POST _analyze 
{
  "tokenizer": "path_hierarchy",
  "text": "/test/hoge/fuga.txt"
}

tokenizerpath_hierarchyを設定し、textに分析したい文字を設定します。今回は/test/hoge/fuga.txtというパスにしました。実行した結果は以下のようになりました。

{
  "tokens": [
    {
      "token": "/test",
      "start_offset": 0,
      "end_offset": 5,
      "type": "word",
      "position": 0
    },
    {
      "token": "/test/hoge",
      "start_offset": 0,
      "end_offset": 10,
      "type": "word",
      "position": 0
    },
    {
      "token": "/test/hoge/fuga.txt",
      "start_offset": 0,
      "end_offset": 19,
      "type": "word",
      "position": 0
    }
  ]
}

Path hierarchy tokenizerのデフォルトの区切り文字は/となっているため、それぞれ["/test", "/test/hoge", "/test/hoge/fuga.txt"]と区切り文字で分割されたトークンが返されていることがわかります。

仮に/test/hoge/fuga.txtというファイルパスがElasticsearchに登録されており、Path hierarchyを使用して検索をした場合、トークンとして分割されている["/test", "/test/hoge", "/test/hoge/fuga.txt"]という文字列で検索をすることができます。つまり、このトークナイザーを使用すれば任意に指定したパスの配下にあるフォルダ・ファイルを取得できるようになります。

Path hierarchyには設定できる値があり、区切り文字を指定するdelimiterや分割する方向を逆にするreverseなどがあります。詳細な設定や説明についてはElasticsearchの公式ドキュメントをご確認ください。

Path hierarchyによる検索

インデックスへの設定

今回使用するデータを以下のように定義します。

  • name : ファイル名
  • path : ファイルパス

また、Elasticsearchに登録するデータを以下のような構造としました。

.
└── test/
    ├── folder01/
    │   ├── folder01-01/
    │   │   ├── file01-01-01.txt
    │   │   └── file01-01-02.txt
    │   ├── file01-01.txt
    │   └── file01-02.txt
    ├── folder02/
    │   ├── folder02-01/
    │   │   └── file02-01-01.txt
    │   ├── folder02-02/
    │   │   └── file02-02-01.txt
    │   └── file02-01.txt
    └── file-0.txt

そして、Path hierarchyを設定したインデックス: test-indexを作成します。

PUT test-index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "path_tree_analyzer": {
          "tokenizer": "path_hierarchy_tokenizer"
        }
      },
      "tokenizer": {
        "path_hierarchy_tokenizer": {
          "type": "path_hierarchy",
          "delimiter": "/"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "path": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          },
          "tree": {
            "type": "text",
            "analyzer": "path_tree_analyzer"
          }
        }
      }
    }
  }
}

settings.analysisPath hierarchy tokenizerの設定を定義します。

tokenizerにトークナイザー名path_hierarchy_tokenizerとしてトークナイザーを定義し type: "path_hierarhcy"としたトークナイザー名path_hierarchy_tokenizerを定義しました。区切り文字としてdelimiter: "/"と明記していますが、デフォルトの区切り文字が/なのでなくても大丈夫です。そして、analyzerにアナライザ名path_treeanalyzerとして先ほど定義したトークナイザー、path_hierarchy_tokenizerを設定しております。

mappingsにはファイルの名前とパスをそれぞれnamepathとして定義しております。そして、path.fieldstreeというフィールドを追加し、フィールドのanalyzersettingsで定義したアナライザ、path_tree_analyzerを設定することでPath hierarchyを使用した検索ができるようになります。

Path hierarchyを使用した検索

先ほど作成したインデックスにデータを追加し、検索してみます。

GET test-index/_search
{
  "query": {
    "term": {
      "path.tree": "任意のパス"
    }
  }
}

試しにtestフォルダを指定して検索します。検索結果で取得したファイルは以下のとおりです。

[
  "test",
  "test/folder01",
  "test/folder01/folder01-01",
  "test/folder01/folder01-01/file01-01-01.txt",
  "test/folder01/folder01-01/file01-01-02.txt",
  "test/folder01/file01-01.txt",
  "test/folder01/file01-02.txt",
  "test/folder02",
  "test/folder02/folder02-01",
  "test/folder02/folder02-01/file02-01-01.txt",
  "test/folder02/folder02-02",
  "test/folder02/folder02-02/file02-02-01.txt",
  "test/folder02/file02-01.txt",
  "test/file-0.txt"
]

ということで、testフォルダとその配下にあるフォルダ・ファイル全てが取得できました。

次に、test/folder01/folder-01-01と指定して検索します。

[
  "test/folder01/folder01-01",
  "test/folder01/folder01-01/file01-01-01.txt",
  "test/folder01/folder01-01/file01-01-02.txt"
]

test/folder01/folder-01-01フォルダとその配下にあるファイルが取得できました。

Path hierarchyを使用しない方法

Path hierarchy tokenizerを使用せずに指定したパスの配下にあるフォルダ・ファイルを取得する方法として考えられるのは、Prefix Queryを使用する方法とワイルドカードを使用する方法の2種類が考えられます。

Prefix Query は前方一致で検索できるクエリとなります。指定したパスの配下にあるフォルダ・ファイルを取得する際には、パスの前方一致で取得することができます。先ほど登録したtest/folder01/folder01-01Prefix Queryで検索すると、以下のようになります。

[
  "test/folder01/folder01-01",
  "test/folder01/folder01-01/file01-01-01.txt",
  "test/folder01/folder01-01/file01-01-02.txt"
]

ワイルドカードは*を使用して部分一致検索ができるため、指定したパスの末尾に*をつけることで前方一致検索ができます。 同様にtest/folder01/folder01-01で検索をすると、以下のようになります。

GET test-index/_search
{
  "query": {
    "wildcard": {
      "path.keyword": {
        "value": "test/folder01/folder01-01*"
      }
    }
  }
}

取得した結果は以下のとおりです。

[
  "test/folder01/folder01-01",
  "test/folder01/folder01-01/file01-01-01.txt",
  "test/folder01/folder01-01/file01-01-02.txt"
]

Prefix Queryによる前方一致検索とワイルドカードを使用した検索のいずれも、指定したフォルダ配下のデータが取得できました。 この2つの検索の方が設定も少なく簡単に実現することができます。

ですが、両者とも前方一致検索となるため前方が一致する結果を全て返すことになります。先ほど登録したデータに新しくtest/folder01/folder01というフォルダを追加し検索すると、test/folder01/folder01フォルダ配下のデータだけではなく、test/folder01/folder01-01フォルダ配下のデータも取得されることになります。

また、Elasticsearchにはsearch.allow_expensive_queriesという設定があり、この設定をFalseにすると実行速度が遅くなるクエリを実行できないようにするため、Prefix Queryやワイルドカードといったクエリの実行ができなくなります。

このことから、Prefix Queryやワイルドカードの実行は速度が遅くなりElasticsearchクラスタの安定性にも影響を与える可能性があるため、これら2つの検索方法は極力避けた方が良いと考えられます。

Path hierarchyと階層数によるファイルシステム検索の実現

Path hierarchyによる検索を実行してみましたが、1つ問題があります。それは、指定したパスの配下全てのフォルダ・ファイルを取得してしまうことです。ファイルエクスプローラーやそのほか一般的なファイルシステム検索を使用してみると、選んだフォルダの直下にあるフォルダ・ファイルが一覧で表示されていると思います。

ここでは、フォルダの階層数を追加してフォルダ直下にあるフォルダ・ファイルを取得します。

階層数の追加

フォルダの階層数は、区切り文字/で区切った時の配列の長さになります。フォルダtest/folder01/folder01-01/で区切った時は["test", "folder01", "folder01-01"]となり、この配列の長さ3が階層数となります。

ファイルtest/folder01/folder01-01/file01-01-01.txtは階層数が4となるため、任意のフォルダ直下のフォルダ・オブジェクトを取得するには、Path hierarchyによるフォルダの一致かつ指定したフォルダの階層数 + 1と等しいフォルダ・オブジェクトというクエリにすると、取得することができます。

先ほど作成したIndexに階層数 hierarchyを追加します。

PUT test-index/_mapping
{
  "properties": {
    "hierarchy": {
      "type": "short"
    }
  }
}

そして、painlessというElasticsearch上で使用できるスクリプトを使用し、登録されているドキュメントを更新するという形で階層数を追加します。

POST test-index/_update_by_query
{
  "script": {
    "lang": "painless",
    "source": "def split_key = ctx._source.path.splitOnToken('/');int i = 0;for (item in split_key) {if (item !== '') {i = i + 1;}}ctx._source.hierarchy = i;"
  },
  "query": {
    "match_all": {}
  }
}

登録されている全てのドキュメントを更新するために、_update_by_queryでクエリに該当するドキュメントを更新するようにし、match_all: {}とすることで全てのドキュメントを取得するクエリとしました。

階層数を取得しているスクリプトは、改行を追加すると以下のようなスクリプトとなっております。

def split_key = ctx._source.path.splitOnToken('/');
int i = 0;
for (item in split_key) {
  if (item !== '') {
    i = i + 1;
  }
}

ctx._source.hierarchy = i;

ctx._source.pathで登録されているパスを取得し、splitOnToken('/')で分割しています。分割してできたリストの長さをforループで取得し、その長さをctx._source.hierarchyに入れることで階層数を設定しています。

その他にもpainlessは検索時に使用したりと様々なことができます。詳細はElasticsearchの公式ドキュメントをご確認ください。

指定したフォルダ直下にあるフォルダ・ファイルの取得

追加した階層数を使用して、フォルダ直下にあるフォルダ・ファイルを取得します。

まずは、testフォルダの直下にあるフォルダ・ファイルを取得します。検索のクエリは以下のようになります。

GET test-index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "path.tree": "test"
          }
        },
        {
          "term": {
            "hierarchy": 2
          }
        }
      ]
    }
  },
  "size": 1000
}

testフォルダの階層数に1を足した値を階層数のフィルタとして追加します。検索結果は以下のようになりました。

[
  "test/folder01",
  "test/folder02",
  "test/file-0.txt"
]

Path hierarchyだけのクエリだと配下にあるフォルダ・ファイルすべてが取得されていましたが、階層数を追加することで指定したフォルダの直下にあるフォルダ・ファイルのみを取得することができました。

最後に

今回はElasticsearchでファイルシステム検索を実現するために、Path hierarchyというトークナイザーを使用してみました。

私自身今回初めてElasticsearchを触ってみたのですが、想像以上に様々な機能があり、ほかの機能を使うとより複雑なことができるのではと思いました。そういった様々な機能を試して複雑なことを実行してみるのも面白そうだと思いました。

個人的にはElasticsearchはジオメトリデータも扱えるということなので、ジオメトリデータを活用してどのようなことができるのか、どのくらいの検索速度なのかが気になったので、色々試してみたいと思いました。

最後までご覧いただきありがとうございました。