🚀 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端口。
fields是用於搜索指定的字段
fields: [:name, :brand]
⚠️: 好像很差用。我設置了一個Television表格,只能搜索默認的:brand字段,其餘字段都無論用。不知道❌在哪裏?
搜索返回一個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從你的數據庫取得。
使用 "*", 查詢,獲得全部數據.
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種設置方法:
Product.search "someword", fields: [:name], match: :word_middle
User.search query, fields: [{email: :exact}, :name]
#query是params的值
#fields用來指定搜索的字段。
其餘匹配:見(git)
使用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
讓索引和數據庫同步的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
創建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的對應的數據的和。結構:
經過了解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個功能:
<% @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 %>