- 更新日: 2014年5月31日
- Elasticsearch
Rails で jQuery を使って Elasticsearch 全文検索による検索文字をハイライトさせる
Rails アプリで、Elasticsearch での全文検索による検索テキストをハイライトさせるコードを、jQuery を使って書きました。Elasticsearch 自体にも ハイライト(highlight)の機能があるみたいなのですけど、使い方がよく分からなかったのでとりあえず jQuery で代替。
Rails 側で serialize したデータを Elasticsearch のインデックスに入れたりしている都合もあって、Rails 本体の highlight ヘルパーも使い辛かったりしましたので。
highlight – リファレンス – Railsドキュメント
Rails コントローラーでの検索機能実装
まずは Rails のコントローラー側で検索フォームから検索文字列を取得して、elasticsearch-ruby で検索を実行するロジック。posts_controller.rb の search アクションを例とします。必要な箇所だけ表示するため端折ってます。
app/controllers/posts_controller.rb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class PostsController < ApplicationController ES_CLIENT = Elasticsearch::Client.new def search sanitized_word = sanitize_string_for_elasticsearch(params[:word]) # search by elasticsearch-ruby @es_result = ES_CLIENT.search index: 'blog_db', type: 'post_table', size: 30, body: { query: { simple_query_string: { fields: ["_all"], query: sanitized_word, default_operator: "and" } } } # tokenized string of searched words if params[:word] =~ /(?:\p{Hiragana}|\p{Katakana}|[ー-]|[一-龠々])/ @searched_words_string = ES_CLIENT.indices.analyze(text: sanitized_word, tokenizer: 'kuromoji_tokenizer')["tokens"].map{|i| i["token"] unless i["token"] =~ /\A\p{Hiragana}\z/}.compact.join(',') else @searched_words_string = ES_CLIENT.indices.analyze(text: sanitized_word, tokenizer: 'kuromoji_tokenizer')["tokens"].map{|i| i["token"]}.join(',') end end end |
Elasticsearch::Client.new#search メソッドは、検索結果をハッシュと配列で返しますので分かりやすいです。sanitize_string_for_elasticsearch メソッドについては以下。
elasticsearch-ruby で外部入力から検索時の json 用文字列のエスケープ処理 | EasyRamble
この search メソッド自体は、前半で Elasticsearch による検索を実行、後半で検索文字列を一旦トークンに分割して、,(コンマ)で join して文字列にしてます。これは入力された検索テキストを、Ruby から JavaScript 側に引数で渡すため。
elasticsearch-ruby でトークナイザーを指定してトークン分割 | EasyRamble
検索文字列は、入力されたキーワードが日本語の場合と英語などその他の場合で処理を分けています。日本語の場合のひらがな一文字のトークン(助詞)は、トークン文字列から除外しています。「てにをは」をハイライトから除外するため。
views/posts/search.html.erb
ビューで JavaScript を呼び出す部分。ビューファイルの先頭に以下を書きました。ここで @searched_words_string を Ruby から JavaScript 側へと渡す。
1 2 3 4 5 6 7 8 9 |
<script type="text/javascript"> $(document).ready( function() { <% if @searched_words_string =~ /(?:\p{Hiragana}|\p{Katakana}|[ー-]|[一-龠々])/ %> highlight_searched_ja_words('<%= @searched_words_string %>', <%= @es_result["hits"]["total"] <= 30 ? @es_result["hits"]["total"] : 30 %>); <% else %> highlight_searched_en_words('<%= @searched_words_string %>', <%= @es_result["hits"]["total"] <= 30 ? @es_result["hits"]["total"] : 30 %>); <% end %> }); </script> |
検索文字列が日本語か英語かで、それぞれ highlight_searched_ja_words(), highlight_searched_en_words() という jQuery メソッドを呼び出す。@es_result[“hits”][“total”] は、検索によりヒットした件数が入ってます。
続いて、ビューファイルで検索結果を出力する部分。
1 2 3 4 5 6 7 8 |
<% @es_result["hits"]["hits"].each_with_index do |record, index| %> <p><%= link_to record["_source"]["title"], { controller: "posts", action: "show", id: record["_source"]["id"] } %></p> <div id="search-result-body-<%= index %>"> <%= record["_source"]["content"] %> </div> <% end %> |
@es_result[“hits”][“hits”] に検索結果自体がハッシュの配列で入っています。これをぐるぐる回すだけ。ハイライトさせるのは、<div id="search-result-body-<%= index %>"></div> で囲まれた部分の文字列。id 指定のために each_with_index で回して index を指定しています。
キーワードをハイライトさせる jQuery 関数
検索文字列が日本語かそれ以外(英語など)で、2つ関数を作成しました。類似箇所が多いので要リファクタリングです。
app/assets/javascripts/posts.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 日本語 function highlight_searched_ja_words(searched_words_string, hit_count) { var arr = searched_words_string.split(","); var str; for (var i = 0; i < hit_count; i++) { $.each(arr, function(j) { str = $('#search-result-body-' + i).html().replace(new RegExp( "(" + arr[j] + ")", 'ig' ), '<span class="highlight">' + "$1" + '</span>'); $('#search-result-body-' + i).html(str); }); } } // 英語など日本語以外 function highlight_searched_en_words(searched_words_string, hit_count) { var arr = searched_words_string.split(","); var str; for (var i = 0; i < hit_count; i++) { $.each(arr, function(j) { str = $('#search-result-body-' + i).html().replace(new RegExp( "\\b(" + arr[j] + ")\\b", 'ig' ), '<span class="highlight">' + "$1" + '</span>'); $('#search-result-body-' + i).html(str); }); } } |
検索がヒットした件数分 for ループで回して、$(‘#search-result-body-‘ + i) の html 中の、検索キーワードを <span class="highlight"></span> で囲った文字列に置き換えます。英語など日本語以外の場合は、正規表現の \b でスペースなどの単語境界を指定しています。
CSS
あとは、css で highlight のデザインを指定。
app/assets/stylesheets/custom.css.scss
1 2 3 |
.highlight { color: red; } |
検索文字の色を変えるようにしました。background-color でも何でも都合の良いように。
以上で、複数の単語からなる文章による検索や、スペース繋ぎによる複数単語の検索などの際にも、Elasticsearch の全文検索による検索文字列が良い感じにトークン分割されて、分割された単語が全てハイライトされるようになりました。重複箇所等あるので、もう少しリファクタリングが必要ですけど、とりあえずやりたかったことが実現できたので今日はここまで。
- Elasticsearch の関連記事
- CentOS6にElasticsearchをインストールしMySQLからデータをインポート
- elasticsearch-ruby でトークナイザーを指定してトークン分割
- elasticsearch-ruby で外部入力から検索時の json 用文字列のエスケープ処理
- Elasticsearch を Ruby から使う
- ElasticsearchにMySQLからデータ挿入、JDBC River Pluginのインストールと使い方
- Elasticsearchのクエリとフィルターで簡単な検索を試す例
- ElasticsearchのインストールとCSVからのデータ挿入
Leave Your Message!