Rails 整合 ElasticSearch 到现有 Rails 项目

763914974 · 发布于 2015年09月30日 · 最后由 ghn645568344 回复于 2016年12月05日 · 6952 次阅读
2455
本帖已被设为精华帖!

导言

这两天项目要求,把现有的搜索改成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

共收到 23 条回复
3757

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

2455

#1楼 @riskgod 感谢捧场~

1342

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

21371

推荐 chewy

17740

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

15999

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

9592

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

15999

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

11314

我们现在用的solr

2203

es 挺好玩, 东西很多。。

3216

solr+1

17671

赞一个

808

请教楼主一个问题,我们也使用 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
4594

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

8744

我在项目里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
1031

#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 查询, 结果是惨不忍睹的.

C5fc5e

#21楼 @zw963

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

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

16225

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

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