Ruby On Rails 之 ElasticSearch

ElasticSearch中内置了许多分词器, standard, english, chinese等, 中文分词效果不佳, 所以使用ik, 以及pinyin

elasticsearch install

brew install elasticsearch

 cd /usr/local/Cellar/elasticsearch/6.4.2/bin
 ➜  bin ./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.4.2/elasticsearch-analysis-ik-6.4.2.zip

 ➜  bin ./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-pinyin/releases/download/v6.4.2/elasticsearch-analysis-pinyin-6.4.2.zip

elasticsearch 配置文件

 /usr/local/etc/elasticsearch/elasticsearch.yml
cluster.name: 集群名字
node.name: 节点名
path.data: 数据存储目录
path.logs: 日志存储目录
action.auto_create_index: false
# /usr/local/etc/elasticsearch/jvm.options
# 指定使用内存
-Xms128m
-Xmx128m

elasticsearch 启动

brew services list
brew services stop elasticsearch
brew services start elasticsearch
http://localhost:9200/products/product/1382
{
	error: {
		root_cause: [
			{
				type: "index_closed_exception",
				reason: "closed",
				index_uuid: "opkeQfWbTKSrJzx4BxwoXA",
				index: "products"
			}
		],
		type: "index_closed_exception",
		reason: "closed",
		index_uuid: "opkeQfWbTKSrJzx4BxwoXA",
		index: "products"
	},
	status: 400
}

启用或关闭(_open/_close)

➜  work-api git:(release) ✗ curl -XPOST 'localhost:9200/products/_open?pretty'
{
  "acknowledged" : true,
  "shards_acknowledged" : true
}irb(main):02:0> Product.import

elasticsearch gem

gem 'elasticsearch-model', '~> 5.0', '>= 5.0.1'
gem 'elasticsearch-rails', '~> 5.0', '>= 5.0.1'

config/setting.yml

development:
  elasticsearch:
    host: "http://127.0.0.1:9200"

config/initializers/setting.rb

require 'yaml'
setting_path = Rails.root.join('config', 'setting.yml')
$setting = YAML.load_file(setting_path)[Rails.env]

config/initializers/elasticsearch.rb

require Rails.root.join('config', 'initializers', 'setting')
config = {
  host: $setting['elasticsearch']['host'],
  transport_options: {
    request: { open_timeout: 5, timeout: 5 }
  },
}

$elasticsearch = config
Elasticsearch::Model.client = Elasticsearch::Client.new(config)

model import

class Order
  SyncModerElasticsearch
end

app/models/concerns/sync_moder_elasticsearch.rb

require "elasticsearch/model"
module SyncModerElasticsearch
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model
    extend ElasticsearchCallback
    # include Elasticsearch::Model::Callbacks
    synchronization
    mapping{
      indexes :suggested_price, type: :float
      indexes :lowest_price, type: :float
      indexes :sales_volume, type: :integer
    }
    after_commit :sync_elasticsearch, on: [:create, :update]
  end

  def as_indexed_json(options = {})
    {
    	id: id,
    	name: name,
    	name_cn: name_cn,
    	order_item: {
    	  id: _id,
    	  name: _name
    	}
    }
  end
  
  private

  def sync_elasticsearch
    self.import
    _table_name, _id = self.class.table_name, self.id
    EsSyncJob.set(table_name: _table_name, id: _id)
  end

end
module ElasticsearchCallback
  extend ActiveSupport::Concern

  def synchronization
    after_commit on: [:create] do
      begin
        $logger.info('同步创建', self)
        self.__elasticsearch__.index_document
      rescue => e
        $logger.info(self, e)
        begin
          self.__elasticsearch__.index_document
        rescue => e
          $logger.info(self, e)
        end
      end
    end

    after_commit on: [:update] do
      begin
        $logger.info('同步更新', self)
        self.__elasticsearch__.update_document
      rescue => e
        $logger.info(self, e)
        begin
          self.__elasticsearch__.update_document
        rescue => e
          $logger.info(self, e)
        end
      end
    end

    after_commit on: [:destroy] do
      begin
        $logger.info('同步删除', self)
        self.__elasticsearch__.delete_document
      rescue => e
        $logger.info(self, e)
        begin
          self.__elasticsearch__.delete_document
        rescue => e
          $logger.info(self, e)
        end
      end
    end
	end

end

model import worker

class EsSyncJob < ApplicationJob
  queue_as :default

  def perform table_name,id
    $logger.info('begin sync_elasticsearch queue')
    begin
      table_name.classify.safe_constantize.where(id: id).import
    rescue => e
      $logger.info("sync_elasticsearch error: #{e.message}")
    end
  end
end

Elasticsearch Service

class ElasticsearchService

  def initialize model
    @model = model
  end

  def search_without_paginate query, sort=nil, page = 1, per_page = 10, is_highlight = false
    _query = query.is_a?(Hash) ? hash_query(query) : multi_match_query(query)
    _highlight = is_highlight ? fetch_highlight_hash : {}
    _sort = sort.nil? ? {id: :desc} : sort
    begin
      response = @model.search query: _query,
                               highlight: { fields: _highlight },
                               sort: _sort,
                               from: per_page * (page - 1),
                               size: 1000 #TODO

      return response.results
    rescue => e
      $logger.info("es search_without_paginate error", e)
      return []
    end
  end

  def search query, sort=nil, page = 1, per_page = 10, is_highlight = false
    _query = query.is_a?(Hash) ? hash_query(query) : multi_match_query(query)
    _highlight = is_highlight ? fetch_highlight_hash : {}
    _sort = sort.nil? ? {id: :desc} : sort
    begin
      response = @model.search query: _query,
                               highlight: { fields: _highlight },
                               sort: _sort,
                               from: per_page * (page - 1),
                               size: per_page
      return [response.response['hits']['total'], response.results]
    rescue => e
      $logger.info("es search error", e)
      struct = Struct.new :results
      return [0, struct.new([])]
    end
  end

  private

  def hash_query query
    filtered = {
      bool: {
        must: query[:filter].select{|key, value| !value.include?(' OR ')}.keys.map do |key|
          { match: query[:filter].slice(key) }
        end
      }
    }
    if query[:filter].select{|key, value| value.include?(' OR ')}.present?
      should = query[:filter].select{|key, value| value.include?(' OR ')}.map do |key, values|
        values.split(" OR ").map do |value|
          {match: {key.to_sym => value} }
        end
      end.flatten
      filtered[:bool][:must] << { bool: { should: [should] } }
    end
    filtered[:bool].merge!({filter: { range: query[:range] }}) if query[:range].present?
    filtered[:bool].merge!({must_not: { term: {status: 'preparing'} }}) if query[:only_show]
    return filtered
  end

  def multi_match_query query
    fileds = fetch_multi_match_fields
    {
      bool: {
        must: {
          multi_match:{
            query: query,
            type: "best_fields",
            fields: fileds,
            operator: "and"
          }
        },
        # filter: filter_term,
        must_not: must_not_term
      }
    }
  end

  def filter_term
    case @model.to_s
    when 'Product'
      {
        not:{
          filter:{
            term: {status: 'selling'}
          }
        }
      }
    end
  end

  def must_not_term
    case @model.to_s
    when 'Product'
      {
        term: {status: 'preparing'}
      }
    end
  end


  def fetch_multi_match_fields
    case @model.to_s
    when 'Model'
      return [
        'name_cn^7', "name_cn.ik^6", "name_cn.pinyin^5", "name_cn.standard^4", "name_cn.english^3"
      ]
    end
  end

  def fetch_highlight_hash
    case @model.to_s
    when 'Model'
      return {
        name_cn: {}, "name_cn.ik"=>{}, "name_cn.pinyin"=>{}, "name_cn.standard"=>{}, "name_cn.english"=>{}
      }
    end
  end

end
filter_hash = {"order_no": order_no, "category.id": category_id }
range_hash = {"order_amount": {"lte": params[:order_amount]}}
sort = {id: :desc}
page = 1
per_page = 30
total, products = ElasticsearchService.new(Model).search({filter: filter_hash, range: range_hash, only_show: true}, sort, page.to_i, per_page.to_i)

你可能感兴趣的:(ruby,ruby,on,rails)