Commit 709641efa5e96cd391d36d8d5ffe17ae66b285f8
1 parent
0dbbeb32
Exists in
master
and in
21 other branches
Added json option
Showing
4 changed files
with
244 additions
and
214 deletions
Show diff stats
CHANGELOG.md
lib/searchkick/query.rb
@@ -45,282 +45,286 @@ module Searchkick | @@ -45,282 +45,286 @@ module Searchkick | ||
45 | padding = [options[:padding].to_i, 0].max | 45 | padding = [options[:padding].to_i, 0].max |
46 | offset = options[:offset] || (page - 1) * per_page + padding | 46 | offset = options[:offset] || (page - 1) * per_page + padding |
47 | 47 | ||
48 | + # model and eagar loading | ||
49 | + load = options[:load].nil? ? true : options[:load] | ||
50 | + | ||
48 | conversions_field = searchkick_options[:conversions] | 51 | conversions_field = searchkick_options[:conversions] |
49 | personalize_field = searchkick_options[:personalize] | 52 | personalize_field = searchkick_options[:personalize] |
50 | 53 | ||
51 | all = term == "*" | 54 | all = term == "*" |
55 | + facet_limits = {} | ||
52 | 56 | ||
53 | - if options[:query] | ||
54 | - payload = options[:query] | ||
55 | - elsif options[:similar] | ||
56 | - payload = { | ||
57 | - more_like_this: { | ||
58 | - fields: fields, | ||
59 | - like_text: term, | ||
60 | - min_doc_freq: 1, | ||
61 | - min_term_freq: 1, | ||
62 | - analyzer: "searchkick_search2" | ||
63 | - } | ||
64 | - } | ||
65 | - elsif all | ||
66 | - payload = { | ||
67 | - match_all: {} | ||
68 | - } | 57 | + if options[:json] |
58 | + payload = options[:json] | ||
69 | else | 59 | else |
70 | - if options[:autocomplete] | 60 | + if options[:query] |
61 | + payload = options[:query] | ||
62 | + elsif options[:similar] | ||
71 | payload = { | 63 | payload = { |
72 | - multi_match: { | 64 | + more_like_this: { |
73 | fields: fields, | 65 | fields: fields, |
74 | - query: term, | ||
75 | - analyzer: "searchkick_autocomplete_search" | 66 | + like_text: term, |
67 | + min_doc_freq: 1, | ||
68 | + min_term_freq: 1, | ||
69 | + analyzer: "searchkick_search2" | ||
76 | } | 70 | } |
77 | } | 71 | } |
72 | + elsif all | ||
73 | + payload = { | ||
74 | + match_all: {} | ||
75 | + } | ||
78 | else | 76 | else |
79 | - queries = [] | ||
80 | - fields.each do |field| | ||
81 | - qs = [] | ||
82 | - | ||
83 | - factor = boost_fields[field] || 1 | ||
84 | - shared_options = { | ||
85 | - query: term, | ||
86 | - operator: operator, | ||
87 | - boost: factor | 77 | + if options[:autocomplete] |
78 | + payload = { | ||
79 | + multi_match: { | ||
80 | + fields: fields, | ||
81 | + query: term, | ||
82 | + analyzer: "searchkick_autocomplete_search" | ||
83 | + } | ||
88 | } | 84 | } |
85 | + else | ||
86 | + queries = [] | ||
87 | + fields.each do |field| | ||
88 | + qs = [] | ||
89 | + | ||
90 | + factor = boost_fields[field] || 1 | ||
91 | + shared_options = { | ||
92 | + query: term, | ||
93 | + operator: operator, | ||
94 | + boost: factor | ||
95 | + } | ||
89 | 96 | ||
90 | - if field == "_all" or field.end_with?(".analyzed") | ||
91 | - shared_options[:cutoff_frequency] = 0.001 unless operator == "and" | ||
92 | - qs.concat [ | ||
93 | - shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"), | ||
94 | - shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2") | ||
95 | - ] | ||
96 | - if options[:misspellings] != false | ||
97 | - distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1 | 97 | + if field == "_all" or field.end_with?(".analyzed") |
98 | + shared_options[:cutoff_frequency] = 0.001 unless operator == "and" | ||
98 | qs.concat [ | 99 | qs.concat [ |
99 | - shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search"), | ||
100 | - shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2") | 100 | + shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"), |
101 | + shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2") | ||
101 | ] | 102 | ] |
103 | + if options[:misspellings] != false | ||
104 | + distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1 | ||
105 | + qs.concat [ | ||
106 | + shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search"), | ||
107 | + shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2") | ||
108 | + ] | ||
109 | + end | ||
110 | + elsif field.end_with?(".exact") | ||
111 | + f = field.split(".")[0..-2].join(".") | ||
112 | + queries << {match: {f => shared_options.merge(analyzer: "keyword")}} | ||
113 | + else | ||
114 | + analyzer = field.match(/\.word_(start|middle|end)\z/) ? "searchkick_word_search" : "searchkick_autocomplete_search" | ||
115 | + qs << shared_options.merge(analyzer: analyzer) | ||
102 | end | 116 | end |
103 | - elsif field.end_with?(".exact") | ||
104 | - f = field.split(".")[0..-2].join(".") | ||
105 | - queries << {match: {f => shared_options.merge(analyzer: "keyword")}} | ||
106 | - else | ||
107 | - analyzer = field.match(/\.word_(start|middle|end)\z/) ? "searchkick_word_search" : "searchkick_autocomplete_search" | ||
108 | - qs << shared_options.merge(analyzer: analyzer) | ||
109 | - end | ||
110 | 117 | ||
111 | - queries.concat(qs.map{|q| {match: {field => q}} }) | ||
112 | - end | 118 | + queries.concat(qs.map{|q| {match: {field => q}} }) |
119 | + end | ||
113 | 120 | ||
114 | - payload = { | ||
115 | - dis_max: { | ||
116 | - queries: queries | 121 | + payload = { |
122 | + dis_max: { | ||
123 | + queries: queries | ||
124 | + } | ||
117 | } | 125 | } |
118 | - } | ||
119 | - end | 126 | + end |
120 | 127 | ||
121 | - if conversions_field and options[:conversions] != false | ||
122 | - # wrap payload in a bool query | ||
123 | - payload = { | ||
124 | - bool: { | ||
125 | - must: payload, | ||
126 | - should: { | ||
127 | - nested: { | ||
128 | - path: conversions_field, | ||
129 | - score_mode: "total", | ||
130 | - query: { | ||
131 | - function_score: { | ||
132 | - boost_mode: "replace", | ||
133 | - query: { | ||
134 | - match: { | ||
135 | - query: term | 128 | + if conversions_field and options[:conversions] != false |
129 | + # wrap payload in a bool query | ||
130 | + payload = { | ||
131 | + bool: { | ||
132 | + must: payload, | ||
133 | + should: { | ||
134 | + nested: { | ||
135 | + path: conversions_field, | ||
136 | + score_mode: "total", | ||
137 | + query: { | ||
138 | + function_score: { | ||
139 | + boost_mode: "replace", | ||
140 | + query: { | ||
141 | + match: { | ||
142 | + query: term | ||
143 | + } | ||
144 | + }, | ||
145 | + script_score: { | ||
146 | + script: "doc['count'].value" | ||
136 | } | 147 | } |
137 | - }, | ||
138 | - script_score: { | ||
139 | - script: "doc['count'].value" | ||
140 | } | 148 | } |
141 | } | 149 | } |
142 | } | 150 | } |
143 | } | 151 | } |
144 | } | 152 | } |
145 | } | 153 | } |
146 | - } | 154 | + end |
147 | end | 155 | end |
148 | - end | ||
149 | 156 | ||
150 | - custom_filters = [] | 157 | + custom_filters = [] |
151 | 158 | ||
152 | - boost_by = options[:boost_by] || {} | ||
153 | - if boost_by.is_a?(Array) | ||
154 | - boost_by = Hash[ boost_by.map{|f| [f, {factor: 1}] } ] | ||
155 | - end | ||
156 | - if options[:boost] | ||
157 | - boost_by[options[:boost]] = {factor: 1} | ||
158 | - end | 159 | + boost_by = options[:boost_by] || {} |
160 | + if boost_by.is_a?(Array) | ||
161 | + boost_by = Hash[ boost_by.map{|f| [f, {factor: 1}] } ] | ||
162 | + end | ||
163 | + if options[:boost] | ||
164 | + boost_by[options[:boost]] = {factor: 1} | ||
165 | + end | ||
159 | 166 | ||
160 | - boost_by.each do |field, value| | ||
161 | - custom_filters << { | ||
162 | - filter: { | ||
163 | - exists: { | ||
164 | - field: field | 167 | + boost_by.each do |field, value| |
168 | + custom_filters << { | ||
169 | + filter: { | ||
170 | + exists: { | ||
171 | + field: field | ||
172 | + } | ||
173 | + }, | ||
174 | + script_score: { | ||
175 | + script: "#{value[:factor].to_f} * log(doc['#{field}'].value + 2.718281828)" | ||
165 | } | 176 | } |
166 | - }, | ||
167 | - script_score: { | ||
168 | - script: "#{value[:factor].to_f} * log(doc['#{field}'].value + 2.718281828)" | ||
169 | } | 177 | } |
170 | - } | ||
171 | - end | ||
172 | - | ||
173 | - boost_where = options[:boost_where] || {} | ||
174 | - if options[:user_id] and personalize_field | ||
175 | - boost_where[personalize_field] = options[:user_id] | ||
176 | - end | ||
177 | - if options[:personalize] | ||
178 | - boost_where.merge!(options[:personalize]) | ||
179 | - end | ||
180 | - boost_where.each do |field, value| | ||
181 | - if value.is_a?(Hash) | ||
182 | - value, factor = value[:value], value[:factor] | ||
183 | - else | ||
184 | - factor = 1000 | ||
185 | end | 178 | end |
186 | - custom_filters << { | ||
187 | - filter: { | ||
188 | - term: {field => value} | ||
189 | - }, | ||
190 | - boost_factor: factor | ||
191 | - } | ||
192 | - end | ||
193 | 179 | ||
194 | - if custom_filters.any? | ||
195 | - payload = { | ||
196 | - function_score: { | ||
197 | - functions: custom_filters, | ||
198 | - query: payload, | ||
199 | - score_mode: "sum" | 180 | + boost_where = options[:boost_where] || {} |
181 | + if options[:user_id] and personalize_field | ||
182 | + boost_where[personalize_field] = options[:user_id] | ||
183 | + end | ||
184 | + if options[:personalize] | ||
185 | + boost_where.merge!(options[:personalize]) | ||
186 | + end | ||
187 | + boost_where.each do |field, value| | ||
188 | + if value.is_a?(Hash) | ||
189 | + value, factor = value[:value], value[:factor] | ||
190 | + else | ||
191 | + factor = 1000 | ||
192 | + end | ||
193 | + custom_filters << { | ||
194 | + filter: { | ||
195 | + term: {field => value} | ||
196 | + }, | ||
197 | + boost_factor: factor | ||
200 | } | 198 | } |
201 | - } | ||
202 | - end | ||
203 | - | ||
204 | - payload = { | ||
205 | - query: payload, | ||
206 | - size: per_page, | ||
207 | - from: offset | ||
208 | - } | ||
209 | - payload[:explain] = options[:explain] if options[:explain] | 199 | + end |
210 | 200 | ||
211 | - # order | ||
212 | - if options[:order] | ||
213 | - order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc} | ||
214 | - payload[:sort] = Hash[ order.map{|k, v| [k.to_s == "id" ? :_id : k, v] } ] | ||
215 | - end | 201 | + if custom_filters.any? |
202 | + payload = { | ||
203 | + function_score: { | ||
204 | + functions: custom_filters, | ||
205 | + query: payload, | ||
206 | + score_mode: "sum" | ||
207 | + } | ||
208 | + } | ||
209 | + end | ||
216 | 210 | ||
217 | - # filters | ||
218 | - filters = where_filters(options[:where]) | ||
219 | - if filters.any? | ||
220 | - payload[:filter] = { | ||
221 | - and: filters | 211 | + payload = { |
212 | + query: payload, | ||
213 | + size: per_page, | ||
214 | + from: offset | ||
222 | } | 215 | } |
223 | - end | 216 | + payload[:explain] = options[:explain] if options[:explain] |
224 | 217 | ||
225 | - # facets | ||
226 | - facet_limits = {} | ||
227 | - if options[:facets] | ||
228 | - facets = options[:facets] || {} | ||
229 | - if facets.is_a?(Array) # convert to more advanced syntax | ||
230 | - facets = Hash[ facets.map{|f| [f, {}] } ] | 218 | + # order |
219 | + if options[:order] | ||
220 | + order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc} | ||
221 | + payload[:sort] = Hash[ order.map{|k, v| [k.to_s == "id" ? :_id : k, v] } ] | ||
231 | end | 222 | end |
232 | 223 | ||
233 | - payload[:facets] = {} | ||
234 | - facets.each do |field, facet_options| | ||
235 | - # ask for extra facets due to | ||
236 | - # https://github.com/elasticsearch/elasticsearch/issues/1305 | ||
237 | - size = facet_options[:limit] ? facet_options[:limit] + 150 : 100000 | 224 | + # filters |
225 | + filters = where_filters(options[:where]) | ||
226 | + if filters.any? | ||
227 | + payload[:filter] = { | ||
228 | + and: filters | ||
229 | + } | ||
230 | + end | ||
231 | + | ||
232 | + # facets | ||
233 | + if options[:facets] | ||
234 | + facets = options[:facets] || {} | ||
235 | + if facets.is_a?(Array) # convert to more advanced syntax | ||
236 | + facets = Hash[ facets.map{|f| [f, {}] } ] | ||
237 | + end | ||
238 | + | ||
239 | + payload[:facets] = {} | ||
240 | + facets.each do |field, facet_options| | ||
241 | + # ask for extra facets due to | ||
242 | + # https://github.com/elasticsearch/elasticsearch/issues/1305 | ||
243 | + size = facet_options[:limit] ? facet_options[:limit] + 150 : 100000 | ||
238 | 244 | ||
239 | - if facet_options[:ranges] | ||
240 | - payload[:facets][field] = { | ||
241 | - range: { | ||
242 | - field.to_sym => facet_options[:ranges] | 245 | + if facet_options[:ranges] |
246 | + payload[:facets][field] = { | ||
247 | + range: { | ||
248 | + field.to_sym => facet_options[:ranges] | ||
249 | + } | ||
243 | } | 250 | } |
244 | - } | ||
245 | - elsif facet_options[:stats] | ||
246 | - payload[:facets][field] = { | ||
247 | - terms_stats: { | ||
248 | - key_field: field, | ||
249 | - value_script: "doc.score", | ||
250 | - size: size | 251 | + elsif facet_options[:stats] |
252 | + payload[:facets][field] = { | ||
253 | + terms_stats: { | ||
254 | + key_field: field, | ||
255 | + value_script: "doc.score", | ||
256 | + size: size | ||
257 | + } | ||
251 | } | 258 | } |
252 | - } | ||
253 | - else | ||
254 | - payload[:facets][field] = { | ||
255 | - terms: { | ||
256 | - field: field, | ||
257 | - size: size | 259 | + else |
260 | + payload[:facets][field] = { | ||
261 | + terms: { | ||
262 | + field: field, | ||
263 | + size: size | ||
264 | + } | ||
258 | } | 265 | } |
259 | - } | ||
260 | - end | 266 | + end |
261 | 267 | ||
262 | - facet_limits[field] = facet_options[:limit] if facet_options[:limit] | 268 | + facet_limits[field] = facet_options[:limit] if facet_options[:limit] |
263 | 269 | ||
264 | - # offset is not possible | ||
265 | - # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html | 270 | + # offset is not possible |
271 | + # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html | ||
266 | 272 | ||
267 | - facet_options.deep_merge!(where: options[:where].reject{|k| k == field}) if options[:smart_facets] == true | ||
268 | - facet_filters = where_filters(facet_options[:where]) | ||
269 | - if facet_filters.any? | ||
270 | - payload[:facets][field][:facet_filter] = { | ||
271 | - and: { | ||
272 | - filters: facet_filters | 273 | + facet_options.deep_merge!(where: options[:where].reject{|k| k == field}) if options[:smart_facets] == true |
274 | + facet_filters = where_filters(facet_options[:where]) | ||
275 | + if facet_filters.any? | ||
276 | + payload[:facets][field][:facet_filter] = { | ||
277 | + and: { | ||
278 | + filters: facet_filters | ||
279 | + } | ||
273 | } | 280 | } |
274 | - } | 281 | + end |
275 | end | 282 | end |
276 | end | 283 | end |
277 | - end | ||
278 | 284 | ||
279 | - # suggestions | ||
280 | - if options[:suggest] | ||
281 | - suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s) | 285 | + # suggestions |
286 | + if options[:suggest] | ||
287 | + suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s) | ||
282 | 288 | ||
283 | - # intersection | ||
284 | - if options[:fields] | ||
285 | - suggest_fields = suggest_fields & options[:fields].map{|v| (v.is_a?(Hash) ? v.keys.first : v).to_s } | ||
286 | - end | 289 | + # intersection |
290 | + if options[:fields] | ||
291 | + suggest_fields = suggest_fields & options[:fields].map{|v| (v.is_a?(Hash) ? v.keys.first : v).to_s } | ||
292 | + end | ||
287 | 293 | ||
288 | - if suggest_fields.any? | ||
289 | - payload[:suggest] = {text: term} | ||
290 | - suggest_fields.each do |field| | ||
291 | - payload[:suggest][field] = { | ||
292 | - phrase: { | ||
293 | - field: "#{field}.suggest" | 294 | + if suggest_fields.any? |
295 | + payload[:suggest] = {text: term} | ||
296 | + suggest_fields.each do |field| | ||
297 | + payload[:suggest][field] = { | ||
298 | + phrase: { | ||
299 | + field: "#{field}.suggest" | ||
300 | + } | ||
294 | } | 301 | } |
295 | - } | 302 | + end |
296 | end | 303 | end |
297 | end | 304 | end |
298 | - end | ||
299 | 305 | ||
300 | - # highlight | ||
301 | - if options[:highlight] | ||
302 | - payload[:highlight] = { | ||
303 | - fields: Hash[ fields.map{|f| [f, {}] } ] | ||
304 | - } | ||
305 | - if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag] | ||
306 | - payload[:highlight][:pre_tags] = [tag] | ||
307 | - payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")] | 306 | + # highlight |
307 | + if options[:highlight] | ||
308 | + payload[:highlight] = { | ||
309 | + fields: Hash[ fields.map{|f| [f, {}] } ] | ||
310 | + } | ||
311 | + if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag] | ||
312 | + payload[:highlight][:pre_tags] = [tag] | ||
313 | + payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")] | ||
314 | + end | ||
308 | end | 315 | end |
309 | - end | ||
310 | 316 | ||
311 | - # model and eagar loading | ||
312 | - load = options[:load].nil? ? true : options[:load] | ||
313 | - | ||
314 | - # An empty array will cause only the _id and _type for each hit to be returned | ||
315 | - # http://www.elasticsearch.org/guide/reference/api/search/fields/ | ||
316 | - if load | ||
317 | - payload[:fields] = [] | ||
318 | - elsif options[:select] | ||
319 | - payload[:fields] = options[:select] | ||
320 | - end | 317 | + # An empty array will cause only the _id and _type for each hit to be returned |
318 | + # http://www.elasticsearch.org/guide/reference/api/search/fields/ | ||
319 | + if load | ||
320 | + payload[:fields] = [] | ||
321 | + elsif options[:select] | ||
322 | + payload[:fields] = options[:select] | ||
323 | + end | ||
321 | 324 | ||
322 | - if options[:type] or klass != searchkick_klass | ||
323 | - @type = [options[:type] || klass].flatten.map{|v| searchkick_index.klass_document_type(v) } | 325 | + if options[:type] or klass != searchkick_klass |
326 | + @type = [options[:type] || klass].flatten.map{|v| searchkick_index.klass_document_type(v) } | ||
327 | + end | ||
324 | end | 328 | end |
325 | 329 | ||
326 | @body = payload | 330 | @body = payload |
test/highlight_test.rb
@@ -19,4 +19,23 @@ class TestHighlight < Minitest::Unit::TestCase | @@ -19,4 +19,23 @@ class TestHighlight < Minitest::Unit::TestCase | ||
19 | assert_equal "<em>Cinema</em> Orange", highlight[:color] | 19 | assert_equal "<em>Cinema</em> Orange", highlight[:color] |
20 | end | 20 | end |
21 | 21 | ||
22 | + def test_json | ||
23 | + store_names ["Two Door Cinema Club"] | ||
24 | + json = { | ||
25 | + query: { | ||
26 | + match: { | ||
27 | + _all: "cinema" | ||
28 | + } | ||
29 | + }, | ||
30 | + highlight: { | ||
31 | + pre_tags: ["<strong>"], | ||
32 | + post_tags: ["</strong>"], | ||
33 | + fields: { | ||
34 | + "name.analyzed" => {} | ||
35 | + } | ||
36 | + } | ||
37 | + } | ||
38 | + assert_equal "Two Door <strong>Cinema</strong> Club", Product.search(json: json).response["hits"]["hits"].first["highlight"]["name.analyzed"].first | ||
39 | + end | ||
40 | + | ||
22 | end | 41 | end |
test/index_test.rb
@@ -35,6 +35,12 @@ class TestIndex < Minitest::Unit::TestCase | @@ -35,6 +35,12 @@ class TestIndex < Minitest::Unit::TestCase | ||
35 | assert_equal ["Dollar Tree"], Store.search(query: {match: {name: "Dollar Tree"}}).map(&:name) | 35 | assert_equal ["Dollar Tree"], Store.search(query: {match: {name: "Dollar Tree"}}).map(&:name) |
36 | end | 36 | end |
37 | 37 | ||
38 | + def test_json | ||
39 | + store_names ["Dollar Tree"], Store | ||
40 | + assert_equal [], Store.search(query: {match: {name: "dollar"}}).map(&:name) | ||
41 | + assert_equal ["Dollar Tree"], Store.search(json: {query: {match: {name: "Dollar Tree"}}}, load: false).map(&:name) | ||
42 | + end | ||
43 | + | ||
38 | def test_tokens | 44 | def test_tokens |
39 | assert_equal ["dollar", "dollartre", "tree"], Product.searchkick_index.tokens("Dollar Tree") | 45 | assert_equal ["dollar", "dollartre", "tree"], Product.searchkick_index.tokens("Dollar Tree") |
40 | end | 46 | end |