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 | 45 | padding = [options[:padding].to_i, 0].max |
46 | 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 | 51 | conversions_field = searchkick_options[:conversions] |
49 | 52 | personalize_field = searchkick_options[:personalize] |
50 | 53 | |
51 | 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 | 59 | else |
70 | - if options[:autocomplete] | |
60 | + if options[:query] | |
61 | + payload = options[:query] | |
62 | + elsif options[:similar] | |
71 | 63 | payload = { |
72 | - multi_match: { | |
64 | + more_like_this: { | |
73 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 282 | end |
276 | 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 | 303 | end |
297 | 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 | 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 | 328 | end |
325 | 329 | |
326 | 330 | @body = payload | ... | ... |
test/highlight_test.rb
... | ... | @@ -19,4 +19,23 @@ class TestHighlight < Minitest::Unit::TestCase |
19 | 19 | assert_equal "<em>Cinema</em> Orange", highlight[:color] |
20 | 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 | 41 | end | ... | ... |
test/index_test.rb
... | ... | @@ -35,6 +35,12 @@ class TestIndex < Minitest::Unit::TestCase |
35 | 35 | assert_equal ["Dollar Tree"], Store.search(query: {match: {name: "Dollar Tree"}}).map(&:name) |
36 | 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 | 44 | def test_tokens |
39 | 45 | assert_equal ["dollar", "dollartre", "tree"], Product.searchkick_index.tokens("Dollar Tree") |
40 | 46 | end | ... | ... |