Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to Elastic Search 2.x #40

Open
wants to merge 14 commits into
base: 3-0-stable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions app/helpers/spree/base_helper_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,32 @@ module Spree
# parses the properties facet result
# input: Facet(name: "properties", type: "terms", body: {"terms" => [{"term" => "key1||value1", "count" => 1},{"term" => "key1||value2", "count" => 1}]}])
# output: Facet(name: key1, type: terms, body: {"terms" => [{"term" => "value1", "count" => 1},{"term" => "value2", "count" => 1}]})
def expand_properties_facet_to_facet_array(facet)
def expand_properties_aggregation_to_aggregation_array(aggregation)
# first step is to build a hash
# {"property_name" => [{"term" => "value1", "count" => 1},{"term" => "value2", "count" => 1}]}}
property_names = {}
facet["terms"].each do |term|
t = term["term"].split("||")
aggregation[:buckets].each do |term|
t = term[:key].split('||')
property_name = t[0]
property_value = t[1]
# add a search_term to each term hash to allow searching on the element later on
property = {"term" => property_value, "count" => term["count"], "search_term" => term["term"]}
if property_names.has_key?(property_name)
property_names[property_name] << property
else
property_names[property_name] = [property]
if property_value
# add a search_term to each term hash to allow searching on the element later on
property = { term: property_value, count: term[:doc_count], search_term: term[:key] }
if property_names.has_key?(property_name)
property_names[property_name] << property
else
property_names[property_name] = [property]
end
end
end
# next step is to transform the hash to facet objects
# this allows us to handle it in a uniform way
# format: Facet(name: "property_name", type: type, body: {"terms" => [{"term" => "value1", "count" => 1},{"term" => "value2", "count" => 1}]}])
result = {}
property_names.each do |key,value|
value.sort_by!{|h| [-h["count"],h["term"].downcase]} # first sort on desc, then on term asc
value.sort_by!{ |value| [-value[:count], value[:term].downcase] } # first sort on desc, then on term asc
# result << Spree::Search::Elasticsearch::Facet.new(name: key, search_name: facet.name, type: facet.type, body: {"terms" => value})
result[key] = {
'_type' => facet['_type'],
'terms' => value
}
end
Expand All @@ -37,19 +38,19 @@ def expand_properties_facet_to_facet_array(facet)
# Helper method for interpreting facets from Elasticsearch. Something like a before filter.
# Sorting, changings things, the world is your oyster
# Input is a hash
def process_facets(facets)
new_facets = {}
def process_aggregations(aggregations)
new_aggregations = {}
delete_keys = []
facets.map do |key, facet|
if key == "properties"
new_facets.merge! expand_properties_facet_to_facet_array(facet)
aggregations.map do |key, aggregation|
if key == 'properties'
new_aggregations.merge! expand_properties_aggregation_to_aggregation_array(aggregation)
delete_keys << :properties
else
facet
aggregation
end
end
delete_keys.each {|key| facets.delete(key) }
facets.merge! new_facets
delete_keys.each { |key| aggregations.delete(key) }
aggregations.merge! new_aggregations
end
end
end
end
72 changes: 40 additions & 32 deletions app/models/spree/product_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ module Spree
index_name Spree::ElasticsearchSettings.index
document_type 'spree_product'

mapping _all: {"index_analyzer" => "nGram_analyzer", "search_analyzer" => "whitespace_analyzer"} do
indexes :name, type: 'multi_field' do
mapping _all: { analyzer: 'nGram_analyzer', search_analyzer: 'whitespace_analyzer' } do
indexes :name, type: 'string' do
indexes :name, type: 'string', analyzer: 'nGram_analyzer', boost: 100
indexes :untouched, type: 'string', include_in_all: false, index: 'not_analyzed'
end

indexes :description, analyzer: 'snowball'
indexes :available_on, type: 'date', format: 'dateOptionalTime', include_in_all: false
indexes :price, type: 'double'
Expand All @@ -24,6 +25,7 @@ def as_indexed_json(options={})
only: [:available_on, :description, :name],
include: {
variants: {
methods: [:total_on_hand],
only: [:sku],
include: {
option_values: {
Expand Down Expand Up @@ -62,8 +64,8 @@ class Product::ElasticsearchQuery
# The idea is to always to use the following schema and fill in the blanks.
# {
# query: {
# filtered: {
# query: {
# bool: {
# must: {
# query_string: { query: , fields: [] }
# }
# filter: {
Expand All @@ -77,12 +79,18 @@ class Product::ElasticsearchQuery
# filter: { range: { price: { lte: , gte: } } },
# sort: [],
# from: ,
# facets:
# aggregations:
# }
def to_hash
q = { match_all: {} }
unless query.blank? # nil or empty
q = { query_string: { query: query, fields: ['name^5','description','sku'], default_operator: 'AND', use_dis_max: true } }
q = { query_string: {
query: query,
fields: ['name^10','description','sku', 'variants.sku', 'variant.*', 'name.*^.1'],
default_operator: 'AND',
use_dis_max: true
}
}
end
query = q

Expand All @@ -92,50 +100,50 @@ def to_hash
# to { terms: { properties: ["key1||value_a","key1||value_b"] }
# { terms: { properties: ["key2||value_a"] }
# This enforces "and" relation between different property values and "or" relation between same property values
properties = @properties.map {|k,v| [k].product(v)}.map do |pair|
and_filter << { terms: { properties: pair.map {|prop| prop.join("||")} } }
properties = @properties.map{ |key, value| [key].product(value) }.map do |pair|
and_filter << { terms: { properties: pair.map { |property| property.join('||') } } }
end
end

sorting = case @sorting
when "name_asc"
[ {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }}, "_score" ]
when "name_desc"
[ {"name.untouched" => { order: "desc" }}, {"price" => { order: "asc" }}, "_score" ]
when "price_asc"
[ {"price" => { order: "asc" }}, {"name.untouched" => { order: "asc" }}, "_score" ]
when "price_desc"
[ {"price" => { order: "desc" }}, {"name.untouched" => { order: "asc" }}, "_score" ]
when "score"
[ "_score", {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }} ]
when 'default'
[ { 'variants.total_on_hand' => { order: 'desc' } }, { price: { order: 'asc' } }, '_score' ]
when 'score'
[ '_score', { price: { order: 'asc' } }, { 'name.untouched' => { order: 'asc' } } ]
when 'price_asc'
[ { 'price' => { order: 'asc' } }, { 'name.untouched' => { order: 'asc' } }, '_score' ]
when 'price_desc'
[ { 'price' => { order: 'desc' } }, { 'name.untouched' => { order: 'asc' } }, '_score' ]
when 'new_arrival'
[ { 'available_on' => { order: 'desc' } }, { price: { order: 'asc' } }, '_score' ]
else
[ {"name.untouched" => { order: "asc" }}, {"price" => { order: "asc" }}, "_score" ]
[ '_score', { 'variants.total_on_hand' => { order: 'desc' } }, { price: { order: 'asc' } } ]
end

# facets
facets = {
price: { statistical: { field: "price" } },
properties: { terms: { field: "properties", order: "count", size: 1000000 } },
taxon_ids: { terms: { field: "taxon_ids", size: 1000000 } }
# aggregations
aggregations = {
price: { stats: { field: 'price' } },
properties: { terms: { field: 'properties', size: 1000000 } },
taxon_ids: { terms: { field: 'taxon_ids', size: 1000000 } }
}

# basic skeleton
result = {
min_score: 0.1,
query: { filtered: {} },
min_score: 1,
query: { bool: {} },
sort: sorting,
from: from,
size: Spree::Config.products_per_page,
facets: facets
aggregations: aggregations
}

# add query and filters to filtered
result[:query][:filtered][:query] = query
# add query and filters to bool
result[:query][:bool][:must] = query
# taxon and property filters have an effect on the facets
and_filter << { terms: { taxon_ids: taxons } } unless taxons.empty?
and_filter << { terms: { taxon_ids: taxons } }unless taxons.empty?
# only return products that are available
and_filter << { range: { available_on: { lte: "now" } } }
result[:query][:filtered][:filter] = { "and" => and_filter } unless and_filter.empty?
and_filter << { range: { available_on: { lte: 'now' } } }
result[:query][:bool][:filter] = and_filter unless and_filter.empty?

# add price filter outside the query because it should have no effect on facets
if price_min && price_max && (price_min < price_max)
Expand Down
44 changes: 22 additions & 22 deletions app/views/spree/shared/_filter_price.html.erb
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
<fieldset class="facet">
<h4 class="filter"><%= facet.name %></h4>
<div class="facet-box">
<div class="slider">
<div class="rangeslider_top_margin"></div>
<input type="text" id="price" />
<fieldset class='facet'>
<h4 class='filter'><%= aggregation.name %></h4>
<div class='facet-box'>
<div class='slider'>
<div class='rangeslider_top_margin'></div>
<input type='text' id='price' />
</div>
<div class="slider-input">
<input type="text" id="price-min" name="search[price][min]" value"<%= facet["min"] %>">
<div class='slider-input'>
<input type='text' id='price-min' name='search[price][min]' value="<%= aggregation['min'] %>">
<span>to</span>
<input type="text" id="price-max" name="search[price][max]" value"<%= facet["max"] %>">
<input type='text' id='price-max' name='search[price][max]' value="<%= aggregation['max'] %>">
</div>
</div>
</fieldset>

<% content_for :head do %>
<script type='text/javascript'>
$(document).ready(function(){
$("#price-min").change(function () {
$('#price-min').change(function () {
// check if not below minimum
$("#price").ionRangeSlider("update", {
$('#price').ionRangeSlider('update', {
from: $(this).val()
});
});

$("#price-max").change(function () {
$('#price-max').change(function () {
// check if not above maximum
$("#price").ionRangeSlider("update", {
$('#price').ionRangeSlider('update', {
to: $(this).val()
});
});

minimum = <%= (facet["min"] == facet["max"] ? (facet["min"] - 1) : facet["min"]).floor %>;
maximum = <%= (facet["max"] == facet["min"] ? (facet["min"] + 1) : facet["max"]).ceil %>;
minimum = <%= (aggregation['min'] == aggregation['max'] ? (aggregation['min'] - 1) : aggregation['min']).floor %>;
maximum = <%= (aggregation['max'] == aggregation['min'] ? (aggregation['min'] + 1) : aggregation['max']).ceil %>;
min_param = minimum;
<% if params[:search] && params[:search][:price] %>
min_param = <%= params[:search][:price][:min] %>
Expand All @@ -41,7 +41,7 @@
max_param = <%= params[:search][:price][:max] %>
<% end %>

$("#price").ionRangeSlider({
$('#price').ionRangeSlider({
min: minimum,
max: maximum,
from: min_param,
Expand All @@ -51,13 +51,13 @@
prefix: "<%= ::Money.new(1, Spree::Config[:currency]).symbol %>",
prettify: false,
hasGrid: false,
onLoad: function(obj) {
$("#price-min").val(obj.fromNumber);
$("#price-max").val(obj.toNumber);
onLoad: function(obj) {
$('#price-min').val(obj.fromNumber);
$('#price-max').val(obj.toNumber);
},
onChange: function(obj) {
$("#price-min").val(obj.fromNumber);
$("#price-max").val(obj.toNumber);
onChange: function(obj) {
$('#price-min').val(obj.fromNumber);
$('#price-max').val(obj.toNumber);
}
});
});
Expand Down
26 changes: 13 additions & 13 deletions app/views/spree/shared/_filter_properties.html.erb
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<% facets.each do |facet_name, facet| %>
<fieldset class="facet">
<h4 class="filter"><%= facet_name %></h4>
<div class="facet-box">
<ul class="filter_choices unstyled">
<% facet["terms"].each do |term| %>
<li class="nowrap">
<input type="checkbox"
id="<%= term["term"] %>"
name="search[properties][<%= facet_name %>][]"
value="<%= term["term"] %>"
<%= params[:search][:properties] && params[:search][:properties][facet_name] && params[:search][:properties][facet_name].include?(term["term"]) ? "checked" : "" %> />
<label class="nowrap" for="<%= term["term"] %>"> <%= term["term"] %> (<%= term["count"] %>)</label>
<% aggregations.each do |aggregation_name, aggregation| %>
<fieldset class='facet'>
<h4 class='filter'><%= aggregation_name %></h4>
<div class='facet-box'>
<ul class='filter_choices unstyled'>
<% aggregation[:terms].each do |term| %>
<li class='nowrap'>
<input type='checkbox'
id="<%= term[:term] %>"
name="search[properties][<%= aggregation_name %>][]"
value="<%= term[:term] %>"
<%= params[:search][:properties] && params[:search][:properties][aggregation_name] && params[:search][:properties][aggregation_name].include?(term[:term]) ? 'checked' : '' %> />
<label class='nowrap' for="<%= term[:term] %>"> <%= term[:term] %> (<%= term[:count] %>)</label>
</li>
<% end %>
</ul>
Expand Down
12 changes: 6 additions & 6 deletions app/views/spree/shared/_filters.html.erb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<% unless @products.empty? %>
<%= form_tag '', :method => :get, :id => 'sidebar_products_search' do %>
<%= form_tag '', method: :get, id: 'sidebar_products_search' do %>
<% params[:search] ||= {} %>
<%= hidden_field_tag 'per_page', params[:per_page] %>
<% facets = process_facets(@products.response.response['facets']) %>
<%= select_tag "sorting", options_for_select({Spree.t("search_sorting.name_asc") => "name_asc", Spree.t("search_sorting.name_desc") => "name_desc", Spree.t("search_sorting.price_asc") => "price_asc", Spree.t("search_sorting.price_desc") => "price_desc", Spree.t("search_sorting.relevancy") => "score"}, params[:sorting]), :onchange => "this.form.submit();" %>
<%= render :partial => 'spree/shared/filter_price', :locals => { :facet => facets['price'] } %>
<%= render :partial => 'spree/shared/filter_properties', :locals => { :facets => facets.select {|key,facet| ((key != "price") && (key != "taxon_ids")) && (facet['_type'] == 'terms')} || [] } %>
<%= submit_tag Spree.t(:search), :name => nil %>
<% aggregations = process_aggregations(@products.response.response['aggregations']) %>
<%= select_tag 'sorting', options_for_select({ Spree.t('search_sorting.name_asc') => 'name_asc', Spree.t('search_sorting.name_desc') => 'name_desc', Spree.t('search_sorting.price_asc') => 'price_asc', Spree.t('search_sorting.price_desc') => 'price_desc', Spree.t('search_sorting.relevancy') => 'score' }, params[:sorting]), onchange: 'this.form.submit();' %>
<%= render 'spree/shared/filter_price', aggregation: aggregations['price'] %>
<%= render 'spree/shared/filter_properties', aggregations: aggregations.select { |key, aggregation| ((key != 'price') && (key != 'taxon_ids')) } || [] %>
<%= submit_tag Spree.t(:search), name: nil %>
<% end %>
<% end %>
Loading