こんにちは、メディアサプライチェーン技術部開発第1グループの渕です。
任意に指定したパスの直下にあるフォルダ・ファイルを取得するファイルシステム検索をElasticsearchで実現するために、Path hierarchy
というトークナイザーを使用してみました。
Path hierarchyとは
Path hierarchy
とは、Elasticsearchにあるトークナイザーの1つでファイルパスなどの階層的な値をあらかじめ設定した区切り文字で分割し、分割した値をそれぞれ出力します。
実際にPath hierarchy
を使用するとどのようになるのか試してみます。
POST _analyze { "tokenizer": "path_hierarchy", "text": "/test/hoge/fuga.txt" }
tokenizer
にpath_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.analysis
にPath hierarchy tokenizer
の設定を定義します。
tokenizer
にトークナイザー名path_hierarchy_tokenizer
としてトークナイザーを定義し type: "path_hierarhcy"
としたトークナイザー名path_hierarchy_tokenizer
を定義しました。区切り文字としてdelimiter: "/"
と明記していますが、デフォルトの区切り文字が/
なのでなくても大丈夫です。そして、analyzer
にアナライザ名path_treeanalyzer
として先ほど定義したトークナイザー、path_hierarchy_tokenizer
を設定しております。
mappings
にはファイルの名前とパスをそれぞれname
、path
として定義しております。そして、path.fields
にtree
というフィールドを追加し、フィールドのanalyzer
にsettings
で定義したアナライザ、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-01
でPrefix 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はジオメトリデータも扱えるということなので、ジオメトリデータを活用してどのようなことができるのか、どのくらいの検索速度なのかが気になったので、色々試してみたいと思いました。
最後までご覧いただきありがとうございました。