Commit 709641efa5e96cd391d36d8d5ffe17ae66b285f8

Authored by Andrew Kane
1 parent 0dbbeb32

Added json option

1 ## 0.7.9 [unreleased] 1 ## 0.7.9 [unreleased]
2 2
3 - Added `tokens` method 3 - Added `tokens` method
  4 +- Added `json` option
4 - Added exact matches 5 - Added exact matches
5 6
6 ## 0.7.8 7 ## 0.7.8
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 &lt; Minitest::Unit::TestCase @@ -19,4 +19,23 @@ class TestHighlight &lt; 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 &lt; Minitest::Unit::TestCase @@ -35,6 +35,12 @@ class TestIndex &lt; 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