gem "searchkick"(4500✨) 智能搜索(使用Elastic search)(Gorails:创建一个侧边搜索栏)

Searchkick

🚀 Intelligent search made easyhtml

相似的gem还有ransackjava


 

GoRails视频 介绍 和 应用  two episodesgit


 gitgithub


初始化:

首先使用brew安装配套java(用java写的),  elasticsearch,数据库

#mac
brew cask install homebrew/cask-versions/java8
#期间提示输入mac经过的密码,成功后再安装elasticsearch
brew install elasticsearch

 

而后还要启动:brew services start elasticsearch编程

 

增长gem 'searchkick'后端

而后:缓存

class Product < ApplicationRecord
  searchkick
end

 

而后执行代码:  Product.reindex  (能够在控制台执行),增长Product数据到search indexruby

 

而后就可使用查询语句了。app

⚠️部署还要单独设置。

开发环境下,会占用localhost:9200端口。

 

Querying

相似SQL语法: 

 

fields是用于搜索指定的字段 

fields: [:name, :brand]

 

⚠️: 好像很差用。我设置了一个Television表格,只能搜索默认的:brand字段,其余字段都无论用。不知道❌在哪里? 

 

Result

搜索返回一个Searchkick::Results对象。

This responds like an array to most methods. 这个对象可使用大多数array的方法。

results = Product.search("milk")
results.size
results.any?
results.each { |result| ... }

默认, ids从Elasticsearch取得,records从你的数据库取得。

 

Get Everything

使用 "*", 查询,获得全部数据.

Product.search "*"

 

能够和gem 'kaminari' ‘will_paginate' 良好兼容。

 

部分匹配搜索

默认,结果会匹配在查询内的完整的单词:(匹配搜索条件option是 :word,即所有单词)

Product.search "fresh honey" # fresh AND honey

 

可使用operator: "or"

Product.search "fresh honey", operator: "or" # fresh OR honey

 

经过设置option,能够改变匹配的方式:

Option Matches Example
:word entire word apple matches apple
:word_start start of word app matches apple
:word_middle any part of word ppl matches apple   (经常使用的匹配方式)
:word_end end of word ple matches apple

 

 

 

 

 

2种设置方法:

  • 第一种,在model层: 添加searchkick word_middle: [:field_name]
  • 第二种,query语法,添加:
    • Product.search "someword", fields: [:name], match: :word_middle

 

精确匹配

User.search query, fields: [{email: :exact}, :name]

#query是params的值
#fields用来指定搜索的字段。

 

其余匹配:见(git)

  • 还有短语匹配,赞成思的词的匹配,语言的匹配。
  • 能够设置错误拼写(英文用户)
  • 坏匹配(相似模糊匹配)
  • 还有标签tag的匹配:如搜索 🍨🍰 ,获得"ice cream cake!" 

 

 

Indexing

使用search_data方法来控制让什么类型的数据被索引。what data is indexed.

在改变这个方法后,在控制台调用Product.reindex来执行。

⚠️:无需重启app,但每次改变,须要重启控制台或者执行reload!命令。是由于要防止缓存。从新加载环境。

# 2个关联的表格,使用search_data方法,明确指定要加入索引的表格属性:fileds

class Product < ApplicationRecord
belongs_to :department
def search_data { name: name, department_name: department.name, on_sale: sale_price.present? } end end

#文档的习惯写法,由于类是动态声明的,不是一次声明就结束(ruby元编程)。
#因此文档中只写新增的语法语句。

#本块的代码中虽然没写searchkick,但其实是由于以前的文档已经写了这行代码,
# searchkick是必须存在的。


 

为了主动加载关联,须要使用search_import scope

让产生的对象中包含关联表格的数据:

class Product < ApplicationRecord
  scope :search_import, -> { includes(:department) }
end

 

 

默认全部的records被加上index。 能够控制什么记录加index,什么不加,方式是:

search_import和should_index方法配合使用:

class Product < ApplicationRecord
  scope: :search_import, -> { where(active: true)}

  def should_index?
    active   #只有active records能够被索引index. active是一个字段filed
  end
end

 

 

何时用Reindex:

  • 安装更新searchkick后
  • 改变search_data方法后
  • 改变searchkick方法后

 

保持同步:

让索引和数据库同步的4种方法:

第一种: 默认,任何一条记录的修改删除插入。

第二种: 异步,使用backgrounds jobs. 须要修改:

class Product < ApplicationRecord
  searchkick callbacks: :async
end

 

第三种:人队列Queuing ✅

把要更新的记录的id集合放入队列。而后在后端批量(in batches)reindex。比异步方法更好!

具体见:See how to set up.

第四种,手动。

 

关联表格的数据同步:

当一个关联的数据被更新,searchkick生成的Data不会自动的同步更新。

若是但愿这么作,加一个回调方法来reindex.

class Image < ApplicationRecord
  belongs_to :product

  after_commit :reindex_product

  def reindex_product
    product.reindex
  end
end

 

 


 


 

 

实例:仿照视频的代码步骤: (git)

创建rails g scaffold Television brand name display price:float size:integer year:integer

输入数据:rails db:seed,

此时必须事先搭建好 elasticsearch 的环境(见安装和部署)

brands  = 1.upto(10).map{ FFaker::Product.brand }
display = ["LCD", "LED", "QLED"]
sizes   = [42, 50, 55, 60, 65, 75, 80, 85]

Television.delete_all

ApplicationRecord.transaction do
  1000.times do
    Television.create(
      brand: brands.sample,
      name:  FFaker::Lorem.word,
      display: display.sample,
      price: rand(200..4000),
      size: sizes.sample,
      year: rand(2010..2019),
    )
  end
end

Television.reindex

 

class TelevisionsController < ApplicationController
  before_action :set_television, only: [:show, :edit, :update, :destroy]

  def index
    @televisions = Television.search "*", aggs: {brand:{}, year:{}, size:{}, display:{}}
  end

 

⚠️ Aggregations provide aggregated search data.提供搜索集合的搜索数据。

 

在index.html.erb中:

<h6>Year</h6>
 <%= @televisions.aggs%>  

 #获得一个对象的结构相似:
 #{ "size":{...}, year:{...}, display:{...}, "brand":{...}}

 

获得一个对象:

{"size"=>{"doc_count_error_upper_bound"=>0, 
"sum_other_doc_count"=>0,
      "buckets"=>[{"key"=>75, "doc_count"=>31}, {"key"=>55, "doc_count"=>27}, {"key"=>65, "doc_count"=>27}, {"key"=>60, "doc_count"=>25}, {"key"=>80, "doc_count"=>24}, {"key"=>85, "doc_count"=>24}, {"key"=>42, "doc_count"=>21}, {"key"=>50, "doc_count"=>21}]},

"year"=>{"doc_count_error_upper_bound"=>0, "sum_other_doc_count"=>0, "buckets"=>[{"key"=>2015, "doc_count"=>26}, {"key"=>2010, "doc_count"=>23}, {"key"=>2011, "doc_count"=>23}, {"key"=>2016, "doc_count"=>22}, {"key"=>2017, "doc_count"=>20}, {"key"=>2018, "doc_count"=>20}, {"key"=>2013, "doc_count"=>19}, {"key"=>2014, "doc_count"=>17}, {"key"=>2012, "doc_count"=>15}, {"key"=>2019, "doc_count"=>15}]},

 

对每一个fields都进行了计算:好比size有8个不一样值的data, 便设置了8个key, 并计算出每一个key的对应的数据的和。结构:

  • size
    • buckets
      • key
      • doc_count

 

经过了解aggregatived对象的结构,拿出须要的数据:

    <h6>Year</h6>
    <% @televisions.aggs["year"]["buckets"].each do |bucket| %>
      <div>
        <%= bucket["key"] %>
        (<%= bucket["doc_count"]%>)
      </div>
    <% end %>

 

而后加上link_to, 这是为了经过添加request的参数year和对应的值,来搜索符合条件的数据:

      <div>
        <%= link_to bucket["key"], request.params.merge(year: bucket["key"]) %>
        (<%= bucket["doc_count"]%>)
      </div>

 

同时修改index方法,添加上搜索条件where

  def index
    @televisions = Television.search "*", where: {year: params[:year]}, aggs: {brand:{}, year:{}, size:{}, display:{}}
  end

 

可是,这样就写死了,咱们还要使用其余fileds来增长搜索条件,同时以上写法失去了显示所有的功能。

所以还要修改:

  def index
 args = {} args[:year] = params["year"] if params["year"]
    @televisions = Television.search "*", where: args, aggs: {brand:{}, year:{}, size:{}, display:{}}
  end

#这样会根据args来绝对where子句的值是什么

 

这样就能够为添加其余fields条件的目的,提供了扩展的代码。

而后修改index.html.erb,添加2个功能:

  1. 点击某个搜索后,增长可视效果。
  2. 再次点击这个搜索连接,取消这个搜索条件。
    <% @televisions.aggs["year"]["buckets"].each do |bucket| %>
      <div>
 <% if params["year"] == bucket["key"].to_s %> <strong><%= link_to bucket["key"], request.params.except("year") %></strong> <% else%> <%= link_to bucket["key"], request.params.merge(year: bucket["key"]) %> <% end %> (<%= bucket["doc_count"]%>)  </div>
    <% end %>

#若是请求参数的值和搜索结果的值相等,即点击了这个连接后,增长可视觉效果!
#再次点击连接,由于使用except方法,去掉以前的要搜索的条件参数,因此就恢复以前的搜索条件状态了。

view内的其余的搜索条件的代码结构同样。

另外,须要修改index方法

    args[:year] = params["year"] if params["year"]
+    args[:size] = params["size"] if params["size"].present?
+    args[:brand] = params["brand"] if params["brand"].present?

#这样where: args, 就会获得通过多个筛选条件后的搜索结果。
#使用present?肯定参数(它是string格式)存在且值不为空。

 

再次观察发现,搜索的排列顺序不对,增长sort_by方法。

  <% @televisions.aggs["brand"]["buckets"].sort_by{ |b| b["key"] }.each do |bucket| %>

 

实现新的价格(数字范围)搜索功能 :在index方法中添加:

price_ranges = [{to: 500}, {from: 500, to: 1000}, {from:1000}]
@televisions = Television.search "*", where: args,
aggs: {brand:{}, year:{}, size:{}, price: {ranges: price_ranges}}

 

修改index.html.erb:

    <h6>Price</h6>
    <% @televisions.aggs["price"]["buckets"].sort_by{|b| b.fetch("from", 0)}.each do |bucket| %>
      <div>
        <% if (params["price_from"] == bucket["from"].to_s && params["price_to"] == bucket["to"].to_s) ||
              (params["price_from"] == bucket["from"].to_s && bucket["to"] == nil) ||
              (params["price_to"] == bucket["to"].to_s && params.include?("price_from") == false) %>
          <strong><%= link_to price_range_name(bucket), request.params.except("price_from", "price_to")%></strong>
        <% else %>
          <%= link_to price_range_name(bucket), request.params.merge(price_from: bucket["from"], price_to: bucket["to"] ) %>
        <% end %>
        (<%= bucket["doc_count"]%>)
      </div>
    <% end %>

 解释:

@televisions.aggs
 => {"size"=>{"doc_count_error_upper_bound"=>0, 
"sum_other_doc_count"=>0,
"buckets"=>[{"key"=>75, "doc_count"=>31}, {"key"=>55, "doc_count"=>27}, {"key"=>65, "doc_count"=>27}, {"key"=>60, "doc_count"=>25}, {"key"=>80, "doc_count"=>24}, {"key"=>85, "doc_count"=>24}, {"key"=>42, "doc_count"=>21}, {"key"=>50, "doc_count"=>21}]},
"year"=>{"doc_count_error_upper_bound"=>0, "sum_other_doc_count"=>0, "buckets"=>[{"key"=>2015, "doc_count"=>26}, {"key"=>2010, "doc_count"=>23}, {"key"=>2011, "doc_count"=>23}, {"key"=>2016, "doc_count"=>22}, {"key"=>2017, "doc_count"=>20}, {"key"=>2018, "doc_count"=>20}, {"key"=>2013, "doc_count"=>19}, {"key"=>2014, "doc_count"=>17}, {"key"=>2012, "doc_count"=>15}, {"key"=>2019, "doc_count"=>15}]},
      "price"=>{"buckets"=>[{"key"=>"*-2000.0", "to"=>2000.0, "doc_count"=>37}, {"key"=>"2000.0-5000.0", "from"=>2000.0, "to"=>5000.0, "doc_count"=>59}, {"key"=>"5000.0-*", "from"=>5000.0, "doc_count"=>104}]},
...

 

1. 由此可知 关键字是每一个bucket["from"]和["from"]

   把这2个值设置为参数:

  • price_from: bucket["from"]
  • price_to: bucket["to"]

2 。连接的名字经过定义一个帮助方法price_range_name来设置。传入bucket对象为参数:

module TelevisionsHelper
  def price_range_name(bucket)
    if bucket["from"] && bucket["to"]
      "#{number_to_currency bucket["from"]} & #{number_to_currency bucket["to"]}"
    elsif bucket["from"]
      "#{number_to_currency bucket["from"]}"
    elsif bucket["to"]
      "#{number_to_currency bucket["to"]}"
    else
      bucket[:key]
    end
  end
end

# 共3种形式,number_to_currency根据locale来自动给予对应的符号$/¥

 

3. 根据参数是否存在来,完善高亮连接和再次点击连接返回原先的搜索的功能。

  

 

添加新功能,自定义输入价格进行查询

添加一个form。便可。

    <!-- 注意用了local: true -->
    <%= form_with( method: :get, local: true, class: "form-inline") do |f| %>
      <div class="input-group mb-3" style="width: 80%">
        <%= f.number_field "price_from", value: params["price_from"], placeholder: "mix", class:"form-control"%>
        <%= f.number_field "price_to", value: params["price_to"], placeholder: "max", class: "form-control" %>
        <div class="input-group-append">
          <%= f.button "Go"%>
        </div>
      </div>
    <% end %>
相关文章
相关标签/搜索