Rails 整合 ElasticSearch 到现有 Rails 项目

763914974 · 2015年09月30日 · 最后由 imwildcat 回复于 2018年01月06日 · 12846 次阅读
本帖已被管理员设置为精华贴

导言

这两天项目要求,把现有的搜索改成 ElasticSearch(后面简称 es)。之前接触 过一些 es,后来就开始捣鼓。记得 railcasts 上面有讲过相关视频,重温了下就 开始弄,没弄多久发现上面用的tire已经 retire 了。为了让更多的朋友们不走冤枉路,所以才有了此文。

简介

大致说下什么是 es,详细的Wikipedia有介绍。es 其实就是一个搜索的引擎,从开源项目 lucene 出来,lucene 是 java 编写的,比较复杂,要使用它必须了解核心一大堆东西。es 就是包装了一层,然后提供 RESTFUL API 调用,从而让全文搜索变得更加简单。

过程

安装

在安装 es 之前,先安装 jdk。 Mac 环境,运行 brew install elasticsearch, 然后运行elasticsearch --config=/usr/local/opt/elasticsearch/config/elasticsearch.yml启动,访问 http://localhost:9200,访问成功就表示安装完成了。

其他环境,访问官网相关安装包下载。

使用

在 Gemfile 中加入

gem 'elasticsearch-model' 
gem 'elasticsearch-rails' 

注意:es-model 自带了分页插件,如果你在 gemfile 中有分页,如will_paginate 或者 kaminari,要把他们放到 es-model 和 es-rails 的前面。

在需要添加搜索的 model 添加以下代码:

class University < ActiveRecord::Base
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks
end

完成引用后,我们可以编写 search 方法了:

def self.search(search)
   response = __elasticsearch__.search(search)
end

这是一个很简单的 search,通过传入的参数直接进行检索。我们可以使用 DSL 来使我们的检索语句更加满足我们的业务需要,以下是我需要检索一个状态为 1,并且从栏目名为 name 的一个检索:

def self.search_filter(params)
  response = __elasticsearch__.search(
      "query": {
        "filtered": {
          "filter":   {
            "bool": {
              "must":     { "term":  { "status": 1 }},
              "must": {
                "query": { 
                  "match": { "name": params }
                }
              }
            }
          }
        }
      }  
    )
end

关于 es 的 DSL 更多写法,大家可以访问这里。里面详细的讲解了 query,filter 等一些常用查询,大家可以根据业务需要自行改装。

然后我们为 model 创建 index, 主要给 es 使用:

mapping dynamic: false do
  indexes :name
  indexes :tag
end

我们继续往下走,model 是可以 serialized 成 json 的,我们使用as_indexed_json这个方法。我们可以这样写:

def as_indexed_json(options={})
  self.as_json(
    only: [:id, :name, :description, :status],   
    include: { tags: { only: [:name]}}
  )
end

include的部分是处理 association 的,only是 model 本身的字段属性。完成了以上调整,我们的 model 搜索基本完成了。如果你现在使用搜索,我估计还是搜索不出数据。我们要把数据导入给 es,使用这个命令

rake environment elasticsearch:import:model CLASS='your_model_name' FORCE=y

好啦。基本就完成。es 默认自带中文分词,但是有些 posts 反馈说不大好用,可以使用es-rtf,集成了中文的分词插件,下载直接可以用。必须要安装 jdk,里面有详尽的使用方法。

相关资料

gem 的README,有耐心慢慢看可以了解很多

一些快速的 tutorial

http://www.sitepoint.com/full-text-search-rails-elasticsearch/ http://aaronvb.com/articles/intro-to-elasticsearch-ruby-on-rails-part-1.html http://www.spacevatican.org/2012/6/3/fun-with-elasticsearch-s-children-and-nested-documents/

可以了解,railcast 的 es 介绍 http://railscasts.com/episodes/306-elasticsearch-part-1

赞!之前一个搜索用的 arel 来做的。

感觉使用 ElasticSearch 的难点部分在于配置分词的 config 文件。。。

优化不好 性能还不如查询数据库

现在用 postgresql 自带的 full text search 也挺好的,有个 pg_search 的 gem 做的很好

#6 楼 @embbnux 中文搜索咋办呢

#7 楼 @tangmonk 分词就不行了,其他还好

我们现在用的 solr

es 挺好玩,东西很多。。

请教楼主一个问题,我们也使用 es 但是目前效果不太好。

  • 查询的结果不是很好,总觉得过滤的不是很对。
# coding: utf-8
class SearchController < ApplicationController
  def index
    @topics = Topic.search(
        sort: [
            {updated_at: {order: "desc", ignore_unmapped: true}},
            {excellent:  {order: "desc", ignore_unmapped: true}}
        ],
        query: {
            multi_match: {
                query: params[:q],
                fields: %w(title body^10),
                fuzziness: 2,
                prefix_length: 5,
                operator: :and
            }
        },
        highlight: {
            fields: {
                title: {},
                body: {}
            }
        }
    ).paginate(page: params[:page], per_page: 10).records
    @count = @topics.total_entries
  end
end

  • 另外个问题就是,删除了的文章,如果恢复,怎么去更新 index 呢?
def undestroy
  @topic = Topic.unscoped.find(params[:id])
  @topic.update_attribute(:deleted_at, nil)
  redirect_to(cpanel_topics_path)
end

#9 楼 @shawnyu 看上去很强大,有没有具体的比较?

我在项目里 monkey patch 了一下,实现了根据 mapping 自动生成 as_indexed_json

# 用法
settings index: { number_of_shards: 5 } do
      mappings do
        indexes :title,                type: 'string', :boost => 100, analyzer: "ik"
        indexes :content,              type: 'string', :boost => 50,  analyzer: "ik_smart",      as: ->(_){ self.content_text }
        indexes :current_state,        type: 'string', index: :not_analyzed, analyzer: :keyword, as: ->(_){ self.current_state.to_s }
    end

# monkey

# 去掉请求中的 as 部分
class Elasticsearch::Model::Indexing::Mappings
  def to_hash
    { @type.to_sym => @options.merge( properties: @mapping.as_json(except: :as) ) }
  end
end

# 自动生成 as_indexed_json
module Elasticsearch::Model::Serializing::InstanceMethods

  def as_indexed_json(options={})
    build_indexed_json(
      target.class.mappings.instance_variable_get(:@mapping),
      target,
      {id: target.id.to_s}
    ).as_json(options.merge root: false)
  end

private

  def build_indexed_json(mappings, model, json)
    return json unless model.respond_to? :[]

    if model.kind_of? Array
      build_array_json(mappings, model, json)
    else
      build_hash_json(mappings, model, json)
    end

    json
  end

  def build_array_json(mappings, model, json)
    return json unless model.respond_to?(:[]) && json.kind_of?(Array)

    model.each do |elem|
      elem_json = if elem.kind_of? Array then [] else {} end
      json << elem_json
      build_indexed_json(mappings, elem, elem_json)
    end
  end

  def build_hash_json(mappings, model, json)
    return json unless model.respond_to?(:[]) && json.kind_of?(Hash)

    mappings.each_pair do |field, option|

      # Custom transformer
      if option.has_key?(:as) && option[:as].kind_of?(Proc)
        json[field] = target.instance_exec(get_field(model, field), &option[:as])

      # A nested field
      elsif option.has_key?(:properties)
        json[field] = if get_field(model, field).kind_of? Array then [] else {} end
        build_indexed_json(option[:properties], get_field(model, field), json[field])

      # Normal case
      else
        json[field] = get_field(model, field)
      end
    end
  end

  def get_field(model, field_name)
    model.try(:[], field_name) || model.try(field_name)
  end
end

#18 楼 @lihuazhang

我看到你查询时没有指定使用的分析器,我觉得你的问题可能是以下原因造成的。

  1. 在 mapping 中指定正确的分析器,es 在创建索引时,需要知道采用的分析器的类型。
  2. 在 query 时,也要指定的正确的分析器,否则,query 不知道该才用什么方式来查询。

简单点就一句话:使用正确的分析器创建索引,并且在 query 时,指定索引的分析器。

下面是一个 elasticsearch-rails 的例子:

添加 ik 分析器到 es

settings index: {
                   analysis: {
                     analyzer: {
                       ik_max_word: {
                         type: 'ik',
                         use_smart: false
                       },
                       ik_smart: {
                         type: 'ik',
                         use_smart: true
                       }
                     }
                   }
                 }

通过 mapping 设定告诉 es 当创建索引时,使用 ik 分析器可以理解的方式创建索引。

mapping do
  indexes field,
                  type: 'string',
                  analyzer: "ik_smart",
                  searchAnalyzer: "ik_smart",
                  boost: 10
   end
end

查询时,告诉 es, index 使用的分析器类型。

search(
          query: {
            multi_match: {
              query: '关键字',
              fields: ['title', 'description'],
              type: 'best_fields',
              analyzer:  'ik_smart'
            },
          }
        )

如果你不指定 mapping, 直接使用 ik 查询,结果是惨不忍睹的。

#21 楼 @zw963

根据文档,只要 settings 里面设置了 default, 搜索的时候可以不用指定 analyzer

https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html#_specifying_a_search_time_analyzer

如果 ES 不在本地,怎么修改请求的默认本地连接?

shawnyu 回复

这个真的好用,但是没搜到中文分词怎么配置。。。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号