elasticsearch-facetedsearch 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ require 'forwardable'
2
+
3
+ module Elasticsearch
4
+ module FacetedSearch
5
+ class FacetGroup
6
+ extend Forwardable
7
+
8
+ OPERATOR_MAPPING = {
9
+ :and => ',',
10
+ :or => '|'
11
+ }
12
+
13
+ attr_accessor :search, :objects, :key
14
+
15
+ def_delegators :search, :class_facets, :execution_type, :search_params
16
+ # delegate :class_facets, :execution_type, :search_params, to: :search
17
+
18
+ def initialize(search, key, objects)
19
+ self.search = search
20
+ self.key = key.to_sym
21
+ self.objects = hit_count_mapping(objects['terms'])
22
+ end
23
+
24
+ def items
25
+ @items ||= build_items
26
+ end
27
+
28
+ def title
29
+ class_facets[key][:title]
30
+ rescue
31
+ key.to_s.humanize
32
+ end
33
+
34
+ def selected_values
35
+ v = search_params.fetch(key, '')
36
+ operator ? v.split(operator).map(&:downcase) : v.downcase
37
+ end
38
+
39
+ def group_params
40
+ group_params_string.split(operator).dup
41
+ end
42
+
43
+ def group_params_string
44
+ search_params.fetch(key, '')
45
+ end
46
+
47
+ def type
48
+ class_facets[key][:type]
49
+ rescue
50
+ nil
51
+ end
52
+
53
+ # Returns the string value ',' or '|'
54
+ #
55
+ def operator
56
+ @operator ||= operator_mappings(operator_for)
57
+ end
58
+
59
+ def operator_mappings(i=nil)
60
+ OPERATOR_MAPPING.fetch(i, nil)
61
+ end
62
+
63
+ private
64
+
65
+ def operator_for
66
+ execution_type(
67
+ class_facets[key][:type]
68
+ )
69
+ end
70
+
71
+ def build_items
72
+ terms_collection.map do |x|
73
+ FacetItem.new(self, x.merge(count: count(x[:id])))
74
+ end
75
+ end
76
+
77
+ def terms_collection
78
+ return mapped_objects unless search.respond_to?(:"#{key}_collection")
79
+ @terms_collection ||= search.public_send(:"#{key}_collection")
80
+ end
81
+
82
+ # Occationally ElasticSearch will return boolean facets with mix cased characters (T/t & F/f)
83
+ # This method cleans this result and combines the counts to be correct & valid
84
+ #
85
+ def hit_count_mapping(o)
86
+ @hit_count_mapping ||= o.each_with_object(Hash.new(0)) do |result, hash|
87
+ term = result['term'].to_s.downcase
88
+ hash[term] += result['count']
89
+ end
90
+ end
91
+
92
+ def count(id)
93
+ objects.fetch(id.to_s.downcase, 0)
94
+ end
95
+
96
+ def mapped_objects
97
+ objects.map{|k,v| {id: k, term: k, count: v} }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,105 @@
1
+ require 'forwardable'
2
+
3
+ module Elasticsearch
4
+ module FacetedSearch
5
+ class FacetItem
6
+ extend Forwardable
7
+
8
+ attr_accessor :group, :object
9
+
10
+ def_delegators :group, :operator_mappings, :operator, :key, :selected_values, :group_params, :type
11
+ def_delegators :object, :term, :count
12
+ # delegate :operator_mappings, :operator, :key, :selected_values, :group_params, :type, to: :group
13
+ # delegate :term, :count, to: :object
14
+
15
+ def id
16
+ object.id.to_s
17
+ end
18
+
19
+ def initialize(group, object)
20
+ self.group = group
21
+ self.object = OpenStruct.new(object)
22
+ end
23
+
24
+ def selected?
25
+ @selected ||= Array(group_params).include?(id)
26
+ end
27
+
28
+ def params_for
29
+ prefix = selected? ? :remove : :add
30
+
31
+ case type
32
+ when 'multivalue' then send(:"#{prefix}_multivalue")
33
+ when 'multivalue_and' then send(:"#{prefix}_multivalue", :and)
34
+ when 'multivalue_or' then send(:"#{prefix}_multivalue", :or)
35
+ when 'exclusive_or' then send(:"#{prefix}_singlevalue")
36
+ # else raise UnknownSelectableType.new "Unknown selectable type #{selectable_type} for #{@type}"
37
+ end
38
+ end
39
+
40
+ private
41
+ def params
42
+ @params ||= group_params.dup
43
+ end
44
+
45
+ # Removing a value from the parameters
46
+ # Example:
47
+ # group_params = ['1','3','4']
48
+ # id = 3
49
+ # => {key => '1|4'}
50
+ def remove_multivalue(op=nil)
51
+ op = operator_mappings(op) || operator
52
+ p = params.reject{|v| matches?(v) }
53
+
54
+ if p.blank?
55
+ remove_singlevalue
56
+ else
57
+ {
58
+ key => p.reject(&:blank?)
59
+ .join( op )
60
+ }
61
+ end
62
+ end
63
+
64
+ # Remove a single value from params
65
+ #
66
+ def remove_singlevalue
67
+ {
68
+ key => nil
69
+ }
70
+ end
71
+
72
+ # Adding a value to the parameters
73
+ # Example:
74
+ # group_params = ['1','3','4']
75
+ # id = 5
76
+ # => {key => '1|3|4|5'}
77
+ def add_multivalue(op=nil)
78
+ op = operator_mappings(op) || operator
79
+
80
+ if params.blank?
81
+ add_singlevalue
82
+ else
83
+ {
84
+ key => (Array(params) + Array(id.to_s)).uniq
85
+ .join( op )
86
+ }
87
+ end
88
+ end
89
+
90
+ # Adding a single value to the parameters
91
+ # Example:
92
+ # group_params = ['7']
93
+ # id = 5
94
+ # => {key => '5'}
95
+ def add_singlevalue
96
+ { key => id.to_s }
97
+ end
98
+
99
+ def matches?(v)
100
+ v.to_s == id.to_s
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,27 @@
1
+ module Elasticsearch
2
+ module FacetedSearch
3
+ module Pagination
4
+ def total_count
5
+ search['hits']['total'].to_i
6
+ rescue
7
+ 0
8
+ end
9
+
10
+ def total_pages
11
+ (total_count.to_f / limit_value.to_f).ceil
12
+ end
13
+
14
+ def limit_value
15
+ limit
16
+ end
17
+
18
+ def limit
19
+ (search_params[:limit] ||= 32).to_i
20
+ end
21
+
22
+ def current_page
23
+ (search_params[:page] ||= 1).to_i
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ module Elasticsearch
2
+ module FacetedSearch
3
+ module Sortable
4
+
5
+ # Setup by the parent class
6
+ # https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html
7
+ #
8
+ # => returns Array or Hash
9
+ # {
10
+ # label: "Relevant",
11
+ # value: "relevant",
12
+ # search: {...sort value(s) for elasticsearch...},
13
+ # default: false
14
+ # }
15
+ #
16
+ def sorts
17
+ []
18
+ end
19
+
20
+ # Returns current sort hash to use for elasticsearch query
21
+ #
22
+ def current_sort_for_search
23
+ return unless current_sort.present?
24
+ current_sort[:search]
25
+ end
26
+
27
+ # Returns entire sort hash (Label, value, search....)
28
+ #
29
+ def current_sort
30
+ sorts.select{|x| x.fetch(:value) == selected_sort_value }.first || default_sort
31
+ end
32
+
33
+ private
34
+
35
+ # Selected sort value (params || default)
36
+ # => returns String
37
+ def selected_sort_value
38
+ sort_param.present? ? sort_param : default_sort_value
39
+ end
40
+
41
+ # Returns string for sort param even if invalid
42
+ # rescue required if search params is not a hash
43
+ #
44
+ def sort_param
45
+ search_params[:sort]
46
+ rescue
47
+ nil
48
+ end
49
+
50
+ # Returns string value of the default sort signified by
51
+ # => default: true
52
+ def default_sort_value
53
+ default_sort.fetch(:value, nil)
54
+ end
55
+
56
+ # Returns entire hash for sort for the default
57
+ def default_sort
58
+ sorts.select{|x| x.fetch(:default, false) }.first
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ module Elasticsearch
2
+ module FacetedSearch
3
+ VERSION = "0.0.4"
4
+ end
5
+ end
@@ -0,0 +1,256 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyFacets
4
+ include Elasticsearch::FacetedSearch::FacetBase
5
+ facet_exclusive_or(:hd, 'media_hd', 'HD Media')
6
+
7
+ #mock
8
+ def type
9
+ ['type_here']
10
+ end
11
+
12
+ #mock
13
+ def filter_hd?
14
+ end
15
+
16
+ #mock
17
+ def hd_value
18
+ true
19
+ end
20
+ end
21
+
22
+ module Elasticsearch
23
+ module FacetedSearch
24
+ describe FacetBase do
25
+ let(:model) { DummyFacets.new({}) }
26
+
27
+ describe "#class_facets" do
28
+ it "returns class facets" do
29
+ expect(DummyFacets).to receive(:facets) { {facet: true} }
30
+ expect(model.class_facets).to eq({facet: true})
31
+ end
32
+ end
33
+
34
+ describe "#query" do
35
+
36
+ it "removes an empty items" do
37
+ expect(model.query).to eq({:size=>32, :from=>0, :facets=>{:hd=>{:terms=>{:field=>"media_hd", :size=>70}}}})
38
+ end
39
+
40
+ describe "with facets and filters" do
41
+ it "merges filters when filter_query has value" do
42
+ expect(model).to receive(:filter_query).at_least(:once) { 'hi' }
43
+ expect(model).to receive(:facet_query) { nil }
44
+ expect(model.query).to eq({:size=>32, :from=>0, filter: {and: 'hi'}})
45
+ end
46
+
47
+ it "merges facets when facets has value" do
48
+ expect(model).to receive(:filter_query) { nil }
49
+ expect(model).to receive(:facet_query).at_least(:once) { 'hi' }
50
+ expect(model.query).to eq({:size=>32, :from=>0, facets: 'hi'})
51
+ end
52
+
53
+ it "can merge both hashes" do
54
+ expect(model).to receive(:filter_query).at_least(:once) { 'hi' }
55
+ expect(model).to receive(:facet_query).at_least(:once) { 'hi' }
56
+ expect(model.query).to eq({:size=>32, :from=>0, facets: 'hi', filter: {and: 'hi'}})
57
+ end
58
+ end
59
+ describe "with limits" do
60
+ before(:each) do
61
+ expect(model).to receive(:filter_query).at_least(:once) { 'hi' }
62
+ expect(model).to receive(:facet_query).at_least(:once) { 'hi' }
63
+ end
64
+
65
+ it "returns from params" do
66
+ expect(model).to receive(:search_params).at_least(:once) { {limit: '10'}}
67
+ expect(model.query).to eq({:size=>10, :from=>0, :filter=>{:and=>"hi"}, :facets=>"hi"})
68
+ end
69
+
70
+ it "defaults to 32" do
71
+ expect(model.query).to eq({:size=>32, :from=>0, :filter=>{:and=>"hi"}, :facets=>"hi"})
72
+ end
73
+ end
74
+
75
+ describe "with page" do
76
+ before(:each) do
77
+ expect(model).to receive(:filter_query).at_least(:once) { 'hi' }
78
+ expect(model).to receive(:facet_query).at_least(:once) { 'hi' }
79
+ expect(model).to receive(:search_params).at_least(:once) { {page: 10, limit: 3} }
80
+ end
81
+
82
+ it "returns from params" do
83
+ expect(model.query).to eq({:size=>3, :from=>27, :filter=>{:and=>"hi"}, :facets=>"hi"})
84
+ end
85
+ end
86
+
87
+ describe "with sort" do
88
+ before(:each) do
89
+ expect(model).to receive(:sorts).at_least(:once) {
90
+ [
91
+ {
92
+ label: "Relevant",
93
+ value: "relevant",
94
+ search: ["_score"],
95
+ default: false
96
+ },
97
+ {
98
+ label: "updated",
99
+ value: "updated",
100
+ search: ["updated"],
101
+ default: true
102
+ }
103
+ ]
104
+ }
105
+ end
106
+ it "has a default sort" do
107
+ expect(model.query).to eq({:size=>32, :from=>0, :sort=>["updated"], :facets=>{:hd=>{:terms=>{:field=>"media_hd", :size=>70}}}})
108
+ end
109
+
110
+ it "can have a selected sort" do
111
+ expect(model).to receive(:search_params).at_least(:once) { {sort: 'relevant'} }
112
+ expect(model.query).to eq({:size=>32, :from=>0, :sort=>["_score"], :facets=>{:hd=>{:terms=>{:field=>"media_hd", :size=>70}}}})
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "#facet_query" do
118
+ it "caches the request" do
119
+ expect(model).to receive(:build_facets).once {'blah'}
120
+ 2.times { model.facet_query }
121
+ end
122
+ end
123
+
124
+ describe "#filter_query" do
125
+ it "caches the request" do
126
+ expect(model).to receive(:build_filters).once {'blah'}
127
+ 2.times { model.filter_query }
128
+ end
129
+ end
130
+
131
+ describe "#search_params" do
132
+ it "returns the params" do
133
+ expect(model).to receive(:params) { 'boom' }
134
+ expect(model.search_params).to eq('boom')
135
+ end
136
+ end
137
+
138
+ describe "#build_facets" do
139
+ it "is private" do
140
+ expect {
141
+ model.build_facets
142
+ }.to raise_error(NoMethodError)
143
+ end
144
+ describe "with filters" do
145
+ it "equals a specific format" do
146
+ expect(model).to receive(:filter_query).at_least(:once) { [{terms: {:hello => ['world']}}] }
147
+ expect(model.send(:build_facets)).to eq(
148
+ {
149
+ :hd=>{
150
+ :terms=>{
151
+ :field=>"media_hd", :size=>70
152
+ },
153
+ :facet_filter=>{
154
+ :and=>[
155
+ {:terms=>{:hello=>["world"]}}
156
+ ]
157
+ }
158
+ }
159
+ }
160
+ )
161
+ end
162
+
163
+ # Added spec due to improper merge on 0.0.2 release
164
+ # => if this spec is failing, elastic search is going to complain
165
+ it "has 2 keys" do
166
+ expect(model).to receive(:filter_query).at_least(:once) { [{terms: {:hello => ['world']}}] }
167
+ expect(model.send(:build_facets)[:hd].keys).to eq([:terms, :facet_filter])
168
+ end
169
+
170
+ it "can bypass the facet_filter declaration" do
171
+ expect(model.send(:build_facets, false)).to eq(
172
+ {
173
+ :hd=>{
174
+ :terms=>{
175
+ :field=>"media_hd", :size=>70
176
+ }
177
+ }
178
+ }
179
+ )
180
+ end
181
+ end
182
+ describe "without filters" do
183
+ before(:each) do
184
+ expect(model).to receive(:filter_query) { [] }
185
+ end
186
+ it "equals a specific format" do
187
+ expect(model.send(:build_facets)).to eq({
188
+ :hd=>{
189
+ :terms=>{:field=>"media_hd", :size=>70}
190
+ }
191
+ })
192
+ end
193
+ end
194
+ end
195
+
196
+ describe "#build_filters" do
197
+ it "is private" do
198
+ expect {
199
+ model.build_filters
200
+ }.to raise_error(NoMethodError)
201
+ end
202
+ describe "with #filter_XX? = true" do
203
+ before(:each) do
204
+ expect(model).to receive(:filter_hd?) { true }
205
+ end
206
+
207
+ it "returns filter hash for Elasticsearch" do
208
+ expect(model.send(:build_filters)).to eq([{:terms=>{"media_hd"=>[true]}}])
209
+ end
210
+ end
211
+
212
+ describe "with #filter_XX? = false" do
213
+ before(:each) do
214
+ expect(model).to receive(:filter_hd?) { false }
215
+ end
216
+
217
+ it "returns an empty array" do
218
+ expect(model.send(:build_filters)).to be_blank
219
+ end
220
+ end
221
+ end
222
+
223
+ describe "#values_for" do
224
+ it "is private" do
225
+ expect {
226
+ model.values_for(:hd)
227
+ }.to raise_error(NoMethodError)
228
+ end
229
+
230
+ it "always returns an array" do
231
+ expect(model).to receive(:temp_value) { 'hello' }
232
+ expect(model.send(:values_for, :temp)).to eq(['hello'])
233
+ end
234
+ end
235
+
236
+ describe "#execution_type" do
237
+ it "is private" do
238
+ expect {
239
+ model.execution_type('multivalue_or')
240
+ }.to raise_error(NoMethodError)
241
+ end
242
+ it "returns :or when 'multivalue_or'" do
243
+ expect(model.send(:execution_type, 'multivalue_or')).to eq(:or)
244
+ end
245
+
246
+ it "returns :and when 'multivalue_and'" do
247
+ expect(model.send(:execution_type, 'multivalue_and')).to eq(:and)
248
+ end
249
+
250
+ it "returns nil when 'exclusive_or'" do
251
+ expect(model.send(:execution_type, 'exclusive_or')).to be_nil
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end