Commit 709641efa5e96cd391d36d8d5ffe17ae66b285f8

Authored by Andrew Kane
1 parent 0dbbeb32

Added json option

CHANGELOG.md
1 1 ## 0.7.9 [unreleased]
2 2  
3 3 - Added `tokens` method
  4 +- Added `json` option
4 5 - Added exact matches
5 6  
6 7 ## 0.7.8
... ...
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 &lt; 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 &lt; 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
... ...