Fessから見る全文検索としてのElasticsearch

Elastic stack Advent Calendar 9日目の記事になります。

はじめに

全文検索サーバーFessはバージョン10からElasticsearchを検索エンジンとして利用しています。Fessでは、Elasticsearchを全文検索エンジンとして使うためにいろいろとやっているので、Elasticsearch観点で紹介します。

インストール

Fessはここからダウンロードできます。現在の最新は10.3.1です。今回は、rpmをインストールすることにします。(簡単に試すのであれば、zip版でも良いです)
インストール手順はここに書いてあります。rpm版はelasticsearchのrpmを別途インストールする必要があるので、Fess 10.3に対応するElasticsearch 2.4.2をダウンロードしてインストールします。

# rpm -ivh elasticsearch-2.4.2.rpm

Fessで利用するためには以下の設定が必要になるので、/etc/elasticsearch/elasticsearch.ymlに以下を追加します。

configsync.config_path: /var/lib/elasticsearch/config
script.engine.groovy.inline.update: on

FessはElasticsearchをプラグインをインストールして拡張しています。
以下のプラグインをインストールしてください。

# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-analysis-fess/2.4.0
# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-analysis-ja/2.4.0
# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-analysis-synonym/2.4.0
# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-configsync/2.4.2
# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-dataformat/2.4.0
# /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-langfield/2.4.1
#   /usr/share/elasticsearch/bin/plugin install org.codelibs/elasticsearch-analysis-kuromoji-neologd/2.4.1 -b
# /usr/share/elasticsearch/bin/plugin install http://maven.codelibs.org/archive/elasticsearch/plugin/kopf/elasticsearch-kopf-2.0.1.0.zip

次にFessのrpmをインストールします。

# rpm -ivh fess-10.3.1.rpm

あとは、サービス登録して、

# systemctl daemon-reload
# systemctl enable elasticsearch.service
# systemctl enable fess.service

FessとElasticsearchを起動します。

# systemctl start elasticsearch.service
# systemctl start fess.service

起動したら、ブラウザで http://localhost:8080/ にアクセスすると検索画面が表示されます。

クロール&インデクシング

セットアップが完了したら、クロール&インデクシングして、検索できるようにしてみます。検索画面の右上の「ログイン」を選択するとログイン画面が表示されます。ユーザー名:admin、パスワード:adminでログインします。

まず、クロールの設定を作成します。今回はFessのサイトをクロールしてみます。左のメニューの「クローラ」の「ウェブ」を選択して、「新規作成」をクリックしてください。以下のように設定して保存します。

クロール対象とするURLを設定することで、指定した範囲でのクロールになります。間隔も3000ミリ秒に変更しています。3スレッドなので、この設定で各スレッドは3秒に1回ページを取得することになります。(3スレッド3秒間隔だと、結構時間がかかるので、最大アクセス数を100などに設定しても良いと思います)
クロールの開始は、「システム」の「スケジューラ」を開き、「Default Crawler」を選択します。

「今すぐ開始」を押下することでクロールが開始します。最大アクセス数を指定していない場合は、かなり時間がかかります…。
「ダッシュボード」を開くと、インデックスの状況を確認することができます。下図の赤枠部分が検索用インデックスになります。

Fessでは、起動時にインデックスが存在しない場合は作成します。検索用のインデックス名はfess.[日付]になります。Fessのシステム上では、インデックス名でなく、エイリアス名でアクセスしています。検索時はfess.search、インデクシング時はfess.updateを利用しています。システム上でインデックス名を参照しないことで、analyzerを変えたい等のときに新しいインデックスに再インデクシングして、移行できたら、エイリアスを張り替えるだけで良いようにしています。検索とインデクシングのエイリアスを分けていたりもするので、様々なElasticsearchのクラスタ構成に対応できるようにしています。
ある程度、インデクシングできたら(上図のdocs数が検索対象数)、管理画面からログアウトして、検索してください。
日本語環境のブラウザで「全文検索」を検索すると以下のようになります。

Query DSL

Fessが投げている検索がどのようなQuery DSLになっているか見てみましょう。
(結構、長いので、fields, aggregations, highlightの部分は割愛します…)

{
  "from": 0,
  "size": 20,
  "query": {
    "bool": {
      "must": {
        "function_score": {
          "query": {
            "bool": {
              "should": [
                {
                  "match": {
                    "title": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 0.2
                    }
                  }
                },
                {
                  "match": {
                    "content": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 0.1
                    }
                  }
                },
                {
                  "match": {
                    "title_ja": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 1
                    }
                  }
                },
                {
                  "match": {
                    "content_ja": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 0.5
                    }
                  }
                },
                {
                  "match": {
                    "title_en": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 1
                    }
                  }
                },
                {
                  "match": {
                    "content_en": {
                      "query": "全文検索",
                      "type": "phrase",
                      "boost": 0.5
                    }
                  }
                }
              ]
            }
          },
          "functions": [
            {
              "field_value_factor": {
                "field": "boost"
              }
            }
          ]
        }
      },
      "filter": {
        "bool": {
          "should": [
            {
              "term": {
                "role": "1guest"
              }
            },
            {
              "term": {
                "role": "Rguest"
              }
            }
          ]
        }
      }
    }
  }
}

Fessでは、検索対象のドキュメントとして、titleとcontentというプロパティにタイトルと本文を保存しています。ですので、検索画面から検索語を入力すると、「title:全文検索 OR content:全文検索」のような検索になります。さらに、ドキュメントはドキュメント自体の言語ごとに、title_[言語]とcontent_[言語]のプロパティを持っています。ですので、ブラウザが利用している言語が、日本語であれば、jaが加わり、、「title:全文検索 OR content:全文検索 OR title_ja:全文検索 OR content_ja:全文検索」という検索が行われます(実際には日本語ブラウザはenもだいたい持っているので、enも追加されます)。検索する際には以下の重みを付けて検索することで、検索スコアを調整しています(この重みはfess_config.propertiesで変更可能)。

      1. title_[言語]: 1
      1. content_[言語]: 0.5
      1. title: 0.2
    1. content: 0.1

また、ドキュメントにはboostプロパティも保持しているので、Function Queryでスコアに掛け合わせることで、ドキュメント単位のスコアを調整することもできます。boostプロパティの値はクロール設定で指定したものが入ります。
あとは、ロール検索があるので、filterで閲覧できるドキュメントをフィルタして表示しています。

インデックスの設定とマッピング

次にインデックスの設定とマッピングを見てみましょう。以下で表示することができます。zip版を利用している場合は、9201ポートになります。

$ curl localhost:9200/fess.search?pretty

結構、長いです…。
まず、fess.YYYYMMDD > mappings > dynamic_templatesで言語用のプロパティを定義しています。ですので、title_[言語]の名前で値を入れると、自動でここの定義が適用されます。日本語は以下になります。

          "lang_ja" : {
            "mapping" : {
              "type" : "string",
              "analyzer" : "japanese_analyzer"
            },
            "match" : "*_ja"
          }

japanese_analyzerが適用されます。
japanese_analyzerはsettings > analysis > analyzerにあります。

            "japanese_analyzer" : {
              "filter" : [ "truncate10_filter", "fess_japanese_baseform", "fess_japanese_stemmer", "japanese_pos_filter", "lowercase" ],
              "char_filter" : [ "mapping_ja_filter", "fess_japanese_iteration_mark" ],
              "type" : "custom",
              "tokenizer" : "japanese_tokenizer"
            },

いろいろなフィルタをかけていますが、トークナイズしているのはjapanese_tokenizerになります。

            "japanese_tokenizer" : {
              "mode" : "normal",
              "user_dictionary" : "/var/lib/elasticsearch/config/ja/kuromoji.txt",
              "reload_interval" : "1m",
              "type" : "fess_japanese_reloadable_tokenizer",
              "discard_punctuation" : "false"
            },

typeがfess_japanese_reloadable_tokenizerになっていますが、Fessではanalysis-fessプラグインを入れることで、

      1. analysis-kuromoji-neologdプラグインがインストールされていれば、NEologdのKuromojiが適用される
    1. 上記がなければ、普通のKuromojiが適用される

という感じの自動判定して切り替える仕組みを適用しています。ですので、普通に利用される場合は、kuromoji_neologd_tokenizerまたはkuromoji_tokenizerに置き換えて考えてください(reload_intervalもなくしてください)。これにより、title_jaとcontent_jaは形態素解析のプロパティのインデックスができます。
次にtitleとcontentのマッピングを見てみましょう。

          "title" : {
            "type" : "langstring",
            "term_vector" : "with_positions_offsets",
            "analyzer" : "standard_analyzer",
            "lang_field" : "lang",
            "lang_base_name" : ""
          },
          "content" : {
            "type" : "langstring",
            "term_vector" : "with_positions_offsets",
            "analyzer" : "standard_analyzer",
            "lang_field" : "lang",
            "lang_base_name" : ""
          },

typeが見慣れないlangstringという型になっていると思います。これはlangfieldプラグインで拡張した型になります。langstring型は言語判定をして、copy_toをしてくれる便利な型になります。これにより、インデクシング時には、title_jaなどに自分で入れなくても、titleとcontentだけに値を入れておけば、いい感じに*_[言語]のフィールドをコピーして作成します。ただ、自動判定で言語判定に失敗するケースがそこそこあるので、lang_fieldプロパティで明示的に指定することでその値を用いることも可能です(language-detectionをフォークしているのですが、精度問題があるので、より良いものがあれば教えてもらえると助かります)。

まとめ

Fessでは、上記のような感じでElasticsearchを利用しています。langstringなど、特殊なものもありますが、自前でやる場合はcopy_toやマルチフィールドなどでの対応も可能だと思います。FessはSolrの時代からAnalyzerの定義に取り組んでいるので、今回の説明した以外にもいろいろとやっているかと思います。ノウハウなどもいろいろとあるかと思うので、参考にしていただくなどしていただければと。

Fessを用いた検索システム構築入門

JJUG CCC 2016 Fallにて、12/3にFessについて話してきました。

中身の細かい実装について話すというよりは、Fessも7年近く作り続けているものだけあって、そもそもの機能がいろいろとあるので、その機能紹介を中心に説明している資料になります。最近、機能が多々あるので、一通り説明するようないい感じの資料がなかったので、今回、まとめられてよかったと思います。ただ、サムネイル表示など、資料に含まれていない機能についてもまだまだあるので、その資料にあるものが全てではない感じではありますが…。
Fessの認知度もまだまだな感じですので、この資料内に想定されるユースケース的なことも書いてあるので、参考にして、どんどん利用していただければと思います。機能的な要望などは、GitHubのIssueに英語で書いていただければと思います(日本語の場合は、OSDNのフォーラム等に上げてもらえれば良いかもしれません)。

Fessの歴史と今後

Fess (フェス) は「5 分で簡単に構築可能な全文検索サーバー」です。(実際に5分で構築できるかはおいておいて、それくらい簡単に構築して利用できる感じのものを目指しています) Java 実行環境があればどの OS でも実行可能で、Apache ライセンスなのです。
Fessもバージョン10になり、2009年にリリースしてから、7年近く開発を続けています。ここで、一度振り返り、今後も考えてみたいと思います。

Fess誕生の経緯

当時、主にポータルサーバ(Apache Jetspeed 2)を開発して、その導入をしていましたが、ポータルサーバを入れるときに検索システムの要求も結構ありました。検索システムへの要求に答えるために、Apache Solrが使えそうということで使い始めたものの、使ってみるとシステムとして導入して使えるようにするまでにはかなりのギャップがあることに気づきました。Solrは検索サーバであり、検索画面やクロールなどは開発する必要があり、単に検索システムをシステム全体の一部として導入したいだけだとしてもかなりの開発コストが必要でした。この問題を解決するために、2009年9月にFess 1.0をリリースして、手軽に検索システムを利用できる世界を目指し始めました。

Fessの進化

Fess 1.0はウェブとファイルシステムをクロールして、検索できるようにするくらいのシンプルなものでした。Fess 2.0〜3.0あたりから商用としても導入をいろいろと始め、ここからが案件ベースの要求により、機能拡張されていきました。SambaやFTPに対応したり、Active Directory/LDAPなどの認証システムと連携して、社内のドキュメントのセキュアな検索とか、実践でかなり鍛えられていくことで、よほどの要件がない限りは簡単に導入していくことができるようになったと思います。あとは、Solrも進化していったので、それに合わせて検索システムとしても強化されていったと思います。

Fess 9での停滞

機能拡張して、5年近く開発し続けているので、ベースのフレームワーク等が古くなっていってしまいました。Java 6やSeasar2/SAStrutsなどの過去の遺産を捨てる必要があるのに加えて、近年の検索システムへの要求が検索対象の大規模化が進み、検索システムのクラスタが必要な状況になり、アーキテクチャから見直しが必要になりました。フレームワーク等の選定において、以下のことを軸に考えました。

  • DBFluteを使う。依存するライブラリの中で、他のライブラリは変更しても良いのですが、DBFluteは使いやすさや信頼性の観点でこれなしの開発が考えられません。なので、DBFlute中心のフレームワーク選定を行いました。Springの利用も考えましたが、Seasar2からの移行のしやすさなどを考えて、LastaFluteという新しいフレームワークで開発することにしました。
  • SolrとDBをElasticsearchに移行する。当時、Solrで分散検索をするためには、SolrCloudを頑張るか、分散検索の設定を頑張るかになる気がするのですが、これらが案件で導入する作業としては現実感が全くありませんでした…。そのときに、Elasticsearchも十分にナレッジを持っていたこともあり、スケールアウトのしやすさ観点でElasticsearchへの移行を決めました。また、Fess 9まで設定データ等をDB(MySQLなど)に入れていたのですが、導入の際にDBまでの冗長化等を考えると、設計・構築コストが発生するので、すべてをElasticsearchに入れるようにしました。

Fess 10の誕生

新しいアーキテクチャが決まったので、Java 8, DBFluteやLastaFluteなどの〜Fluteシリーズ, Elasticsearchというものをベースに2015年あたりに開発を始めていきました。1年近くかかりましたが、2016年2月にFess 10をリリースすることができました。商用サービスとしても開始しているので、すでにいろいろな企業様で稼働しています。あとは、Fess 10からは日本にとどまらず、グローバルに展開するため、githubでの開発を中心に行っています。
機能的なところでは、Active Directory連携やロール検索を見直すことで、今まで商用でしか導入が難しかった機能が設定だけでできるようになったり、より利用しやすくなっているかと思います。あとは、Elasticsearchクラスタを適切に構築すれば、スケールアウトもしやすい感じになっていると思います。個人的には、Fess 9からFess 10へは劇的な進化になっていると思います。その進化の中で、DBの利用だったり、JavaEEサーバへのデプロイだったり、いくつか捨てたものはありますが、捨てることで得たものはかなり大きいと思います。

今後

今後も開発し続けます。細かい機能追加はあると思いますが、Elasticsearchのバージョンが上がるのに合わせた更新が増えるかと思います。
あとは、近年の要求としては、検索対象の大規模化、機械学習や自然言語処理的な要求が増えてきているので、このあたりでの機能拡張をしていくと思います。
まずは、年内か年明け辺りにElasticsearch 5に対応したFess 11がリリースすることになると思います。
あと、補足的な感じですが、Fessはオープンソースで提供し続けてきていますが、これは自社用独自カスタマイズや構築のコンサルティング等をリクエストしていただいているお客様がいることで成り立っていると思います。これらのビジネス上、重要な場所でいろいろと利用していただいていることに大変感謝しております。Fessは自由にご利用いただけると思いますが、検索のノウハウがないと1000万ドキュメントの検索システムの構築は難しかったり、セキュアな検索が求められるところではこれもノウハウが必要になるかと思います。この辺のことを様々な形で支援できると思いますので、気軽に商用サポートにご相談していただければと思います。(という感じで、今後も開発し続けることができるように宣伝…)