Elasticsearchでindexにmapping情報を追加する

私の環境
Mac OSX yosemite
Ruby 2.2.2
Rails 4.2.1

elasticsearch-modelを使ってElasticsearchのindexを作ったとき、create_index!をすればindexは作られるのだけれど、そのときに作られるmapping情報が該当modelのもの以外が登録できないため、検索が全然うまくできないという症状の悩まされてました。

Web+DB pressの87号のElasticsearchをRubyで、の記事を読んでも、どうもイメージ的にはindexをテーブルのように扱っているような気がしてなりません…(記事の中での説明ではそう書いてはないけれど、後半の扱いがそんな感じに思えました)。

Elasticsearchのキーワードをデータベース風にいうと、

Elasticsearchのindex
データベース
Elasticsearchのtype
テーブル
Elasticsearchのdocument
Elasticsearchのmapping
(DBの)インデックス

ということだったと思うので、indexは一度作って、あとはtype毎のmapping情報を追加してからdocumentを登録するのが正しいんじゃないかなーと思っていたのですが、どうもmappingだけをサクッと登録するようなのはelasticsearch-modelにはなさそうでした。あってもいいと思うんだけれど…。

そこで、type毎にmappingだけ登録する方法はないのかな?と思ってelasticsearchの本家の情報を調べていたら、ありました。

参考リンク:Put Mapping

これならあるんじゃないのかなーと思って調査していたら、elasticsearch-rubyの中にそれらしきコードがありました。できるんだったらこれを呼び出せばいいわけです。

参考リンク:elasticsearch-ruby: put_mapping.rb

ということで、以下のようにしてみました。

class Shop < Activerecord::Base
  include Elasticsearch::Model
  # index_nameは省略
  settings index: {
    # 省略
  } do
    mapping do
      # 省略
    end
  end
 
  def self.put_mapping
    __elasticsearch__.client.indices.put_mapping(
      index: index_name,
      type: document_type,
      body: mapping
    )
  end
end

これで、create_index!を実行したmodel以外のmodelのmapping情報は、put_mappingを呼び出せば行えます。ちゃんとmappingが登録されたかどうかの確認は、例えばrails consoleで以下のようにすればOKです。

Shop.__elasticsearch__.client.indices.get_mapping(index: index_name)

あとはデータをimportすればOKです。

もっと他にいい方法がありましたら教えていただけると助かります!!


Elasticsearchで、包含してるデータをソートの項目に使う

私の環境
Mac OSX Yosemite
Ruby 2.2.2
Rails 4.2.1

まだElasticsearchと戦っています。
リレーション先とのデータ連携とかまではまだわかってないのですが、今のところはデータの取り込みの際にリレーション先を含める方法を取っています。そのリレーションのデータを包含するときにmappingする際のやり方がよくなかったようで、包含したデータを使ってソートすることができませんでしたが、解決したので書き方を載せておきます。

gem elasticsearch-modelを使っています。

変更前

ここでは、Shopモデルと、Companyモデルがあるという前提とします。
変更前は、mappingでindexを定義する際に、company.nameのように書いていました。

class Shop < Activerecord::Base
  include Elasticsearch::Model
  # index_nameは省略
  settings index: {
    # 省略
  } do
    mapping do
      indexes :name,
              type: 'string',
              index: :analyzed,
              analyzer: :kuromoji_analyzer
      indexes :yomigana,
              type: 'string',
              index: :analyzed,
              analyzer: :ngram_analyzer,
              fields: {
                  raw: {
                      type: 'string',
                      index: :not_analyzed
                  }
              }
      indexes :'company.name', type: 'string', index: :analyzed, analyzer: :kuromoji_analyzer
      indexes :'company.yomigana',
              type: 'string',
              index: :analyzed,
              analyzer: :ngram_analyzer,
              fields: {
                  raw: {
                      type: 'string',
                      index: :not_analyzed
                  }
              }
    end
  end

  def as_indexed_json
    as_json(
        include: {
            company: { only: [:name, :yomigana] }
        }
    )
  end
end

この状態で、ソートの条件としてcompany.yomigana.rawと指定しても、うまく機能しませんでした。

そこでelasticsearch-modelのソースコードを読んでみたところ、indexesメソッドにブロックが渡せることがわかったので、試してみることにしました。

変更後

indexesのネストで定義します。
ネストすると、companyをobjectとして定義するので、ソートの項目として使えるようになるようです(詳細なところはよくわかってません…)。

class Shop < Activerecord::Base
  include Elasticsearch::Model
  # index_nameは省略
  settings index: {
    # 省略
  } do
    mapping do
      indexes :name,
              type: 'string',
              index: :analyzed,
              analyzer: :kuromoji_analyzer
      indexes :yomigana,
              type: 'string',
              index: :analyzed,
              analyzer: :ngram_analyzer,
              fields: {
                  raw: {
                      type: 'string',
                      index: :not_analyzed
                  }
              }
      indexes :company do
        indexes :name, type: 'string', index: :analyzed, analyzer: :kuromoji_analyzer
        indexes :yomigana,
                type: 'string',
                index: :analyzed,
                analyzer: :ngram_analyzer,
                fields: {
                    raw: {
                        type: 'string',
                        index: :not_analyzed
                    }
                }
      end
    end
  end

  def as_indexed_json
    as_json(
        include: {
            company: { only: [:name, :yomigana] }
        }
    )
  end
end

その後、ソートしてみたところ、うまくいきました。

{ 
  query: {
    # その他の検索条件
  },
  sort: [{
    :'company.yomigana.raw' => :asc
  }]
}

やりたいことが大分できてきました。


Elasticsearchのクエリを書くためのDSL用gemがあった

ほんのさっきまでRubyのハッシュを自分で組んで、Elasticsearchクラスのsearchメソッドに渡していました。なんとも辛い感じで、いい方法はないんかい!と思っていて、こうなったらgem作るか…と思ったのですが、よく考えたらすでにあってもおかしくなかろう!と思ってrubygemsを検索したところ、elasticsearch-dslというgemがありました。

github: elastic/elasticsearch-ruby/elasctisearch-dsl

elasticからの公式のDSLですかね。

こ、これじゃね…!?と思って使おうとしたのですが、例だと、Elasticsearch::DSLをincludeして使う、みたいな感じなのですが、このクラスもsearchメソッドを定義しており、elasticsearch-modelのsearchメソッドと競合します。マジで辛い。簡単にsearchメソッドとか作らないでほしい…。しかもクエリを作るだけであって検索自体はしないのにsearchメソッドっておかしくない???という疑問を抱きつつも、includeせずに使えるんじゃね?と思って試したら使えました。

まずはGemfileに書いてからbundle installでインストールしましょう。

gem 'elasticsearch-dsl'

次に、modelにてrequireします。
そして、検索用クエリを生成するメソッドを定義します。

require 'elasticsearch/dsl'
class Shop < ActiveRecord::Base
  include Elasticsearch::Model
  # 省略

  def self.elasticsearch_query(keywords, filter_conditions, sorts=[])
    definition = Elasticsearch::DSL::Search::Search.new
    definition.query do
      filtered do
        query do
          simple_query_string do
            query keywords.join(' ')
            fields %w(name yomigana)
            default_operator :and
          end
        end
        if filter_conditions.present?
          filter do
            terms filter_conditions
          end
        end
      end
    end
    if sorts.present?
      definition.sort do
        sorts.each do |sort|
          column, sort_order = sort.first
           by column, order: sort_order
        end
      end
    end
    definition.to_hash
  end

end

一応、こんな感じで検索条件を記述していくことができます。
自力でHashを作るよりは多少マシなのかな…と思います。


RspecでElasticsearchのデータのimportを楽にするヘルパー

別に大した話じゃないんだけれど、これもメモ。
Elasticsearchのindexを作って、データをインポートします。
その際に、スコープを渡せるようにしてあります。
インポート後にindexをリフレッシュするので、即テストが可能です。

def elasticsearch_import_data(model_klass, scope=nil)
  model_klass.__elasticsearch__.create_index!(force: true)
  model_klass.import(query: scope)
  model_klass.__elasticsearch__.refresh_index!
end

使い方はこう。

describe Shop, type: :model do
  before do
    pref = Pref.create(name: '岡山県')
   Shop.create!(name: "Shop A", yomigana: 'ショップエー', pref: pref)
   Shop.create!(name: "Shop B", yomigana: 'ショップビー', pref: pref)
    elasticsearch_import_data(Shop, -> { eager_load(:pref) })
  end
  # テストを書く
end

importでスコープが渡せるので、Elasticsearchにデータを取り込む時にN+1問題が発生しなくて済みます。


Elasticsearchでカタカナでソートする

前提条件

Elasticsearch + kuromoji
Ruby/Rails

前回の記事では、カタカナで検索するところまででした。

今回はカタカナで並び替え(ソート、sort)します。

前回の条件のまま、ソートの条件としてヨミガナ昇順、降順を指定したら、順番がめちゃくちゃになりました。おそらく文字コード順になってるのかなぁと推測しましたが、まぁわかりません。

追記: ここから
analyzerで分割した結果の中から採用されるそうです。詳しくはコメントをみてください。
追記: ここまで

そこで、なんかいい方法ないかなぁと調査していたら、本家のコードにありました。

elastic: String Sorting and Multifields

fieldsのrawに設定をすればよいようです。

"tweet": { 
    "type":     "string",
    "analyzer": "english",
    "fields": {
        "raw": { 
            "type":  "string",
            "index": "not_analyzed"
        }
    }
}

実験的に、これに合わせてfieldsを設定してみました。
省略されている箇所のコードを見たい人は前回の記事を見てください。

class Shop < Activerecord::Base
  include Elasticsearch::Model
  # index_nameは省略
  settings index: {
    # 省略
  } do
    mapping do
      indexes :name,
              type: 'string',
              index: :analyzed,
              analyzer: :kuromoji_analyzer
      indexes :yomigana,
              type: 'string',
              index: :analyzed,
              analyzer: :ngram_analyzer,
              fields: {
                  raw: {
                      type: 'string',
                      index: :not_analyzed
                  }
              }
    end
  end

end

その後、ソートしてみたところ、うまくいきました。

{ 
  query: {
    # その他の検索条件
  },
  sort: [{
    :'yomigana.raw' => :asc
  }]
}

やっと多少Elasticsearchの気持ちがわかってきたかな、というところです。
まだまだ分からないところだらけですが…。