searchkick 0.7.8 → 0.7.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb8ada9111159102b2327b5344afe0876aac137d
4
- data.tar.gz: 3e3e655a723d06f6b741b0938d3fde6d02d5c7ab
3
+ metadata.gz: e2bc8db79f3c60a5694ccd3400713d0dc8788ffd
4
+ data.tar.gz: 163dfde8a0f72d3f9584ad7e6f94b6370bafceb3
5
5
  SHA512:
6
- metadata.gz: fdb99efc9149d644dd126456c9ab2e753a6f67a77feb90e07ad20dcbea75dd7152705136a2e30cfd0fd9e1a38a0ba35b1f05d0b8dd6c986ba248aa90a82126b2
7
- data.tar.gz: 04a2c3229e5f40a866fa8cc2aed8832d361d680ef9a88fe75d1e3b8dc72875524961056f2f00bf2edd7bcd07989864118367547006198588b5f3af3e0e81ee96
6
+ metadata.gz: 83f9f13181e205843a72d3539b4ebba790c98f3abdbb9f1be1a3bfda539eb415304bd8e9eaf9acea062f64f6dbbc227db0892cd76623178d0ab20783119a91d4
7
+ data.tar.gz: 2123d48c3269eb517108f8864740bb2d1cfdc3e0130ed99c161a51d09d97f24799625fc83896912ce4351cfd678434665cefcf176935c2b08f5b5006a23d031a
@@ -1,3 +1,11 @@
1
+ ## 0.7.9
2
+
3
+ - Added `tokens` method
4
+ - Added `json` option
5
+ - Added exact matches
6
+ - Added `prev_page` for Kaminari pagination
7
+ - Added `import` option to reindex
8
+
1
9
  ## 0.7.8
2
10
 
3
11
  - Added `boost_by` and `boost_where` options
data/Gemfile CHANGED
@@ -4,6 +4,6 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "sqlite3"
7
- gem "activerecord"
7
+ gem "activerecord", ">= 3.1"
8
8
  # gem "activerecord", "~> 3.2.0"
9
9
  # gem "activerecord", "~> 3.1.0"
data/README.md CHANGED
@@ -42,7 +42,13 @@ brew install elasticsearch
42
42
  Add this line to your application’s Gemfile:
43
43
 
44
44
  ```ruby
45
- gem "searchkick"
45
+ gem 'searchkick'
46
+ ```
47
+
48
+ For Elasticsearch 1.2, use:
49
+
50
+ ```ruby
51
+ gem 'searchkick', github: 'ankane/searchkick', branch: 'elasticsearch-1.2'
46
52
  ```
47
53
 
48
54
  For Elasticsearch 0.90, use version `0.6.3` and [this readme](https://github.com/ankane/searchkick/blob/v0.6.3/README.md).
@@ -70,6 +76,8 @@ products.each do |product|
70
76
  end
71
77
  ```
72
78
 
79
+ Searchkick supports the complete [Elasticsearch Search API](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html). As your search becomes more advanced, we recommend you use the [Elasticsearch DSL](#advanced) for maximum flexibility.
80
+
73
81
  ### Queries
74
82
 
75
83
  Query like SQL
@@ -78,8 +86,6 @@ Query like SQL
78
86
  Product.search "2% Milk", where: {in_stock: true}, limit: 10, offset: 50
79
87
  ```
80
88
 
81
- **Note:** If you prefer the Elasticsearch DSL, see the [Advanced section](#advanced)
82
-
83
89
  Search specific fields
84
90
 
85
91
  ```ruby
@@ -128,16 +134,18 @@ Boost by the value of a field
128
134
 
129
135
  ```ruby
130
136
  boost_by: [:orders_count] # give popular documents a little boost
131
- boost_by: {orders_count: {factor: 10}}
137
+ boost_by: {orders_count: {factor: 10}} # default factor is 1
132
138
  ```
133
139
 
134
140
  Boost matching documents
135
141
 
136
142
  ```ruby
137
- boost_where: {user_id: 1}
143
+ boost_where: {user_id: 1} # default factor is 1000
138
144
  boost_where: {user_id: {value: 1, factor: 100}}
139
145
  ```
140
146
 
147
+ [Conversions](#keep-getting-better) are also a great way to boost.
148
+
141
149
  ### Get Everything
142
150
 
143
151
  Use a `*` for the query.
@@ -204,6 +212,12 @@ To boost fields, use:
204
212
  fields: [{"name^2" => :word_start}] # better interface on the way
205
213
  ```
206
214
 
215
+ ### Exact Matches
216
+
217
+ ```ruby
218
+ User.search "hi@searchkick.org", fields: [{email: :exact}]
219
+ ```
220
+
207
221
  ### Language
208
222
 
209
223
  Searchkick defaults to English for stemming. To change this, use:
@@ -337,7 +351,7 @@ class Product < ActiveRecord::Base
337
351
  def search_data
338
352
  {
339
353
  name: name,
340
- user_ids: orders.pluck(:user_id) # boost this product for these users
354
+ orderer_ids: orders.pluck(:user_id) # boost this product for these users
341
355
  }
342
356
  end
343
357
 
@@ -347,7 +361,7 @@ end
347
361
  Reindex and search with:
348
362
 
349
363
  ```ruby
350
- Product.search "milk", boost_where: {user_id: 8}
364
+ Product.search "milk", boost_where: {orderer_ids: current_user.id}
351
365
  ```
352
366
 
353
367
  ### Autocomplete
@@ -557,6 +571,33 @@ Animal.search "*", type: [Dog, Cat] # just cats and dogs
557
571
  Dog.search "airbudd", suggest: true # suggestions for all animals
558
572
  ```
559
573
 
574
+ ## Debugging Queries
575
+
576
+ See how Elasticsearch tokenizes your queries with:
577
+
578
+ ```ruby
579
+ Product.searchkick_index.tokens("Dish Washer Soap", analyzer: "default_index")
580
+ # ["dish", "dishwash", "washer", "washersoap", "soap"]
581
+
582
+ Product.searchkick_index.tokens("dishwasher soap", analyzer: "searchkick_search")
583
+ # ["dishwashersoap"] - no match
584
+
585
+ Product.searchkick_index.tokens("dishwasher soap", analyzer: "searchkick_search2")
586
+ # ["dishwash", "soap"] - match!!
587
+ ```
588
+
589
+ Partial matches
590
+
591
+ ```ruby
592
+ Product.searchkick_index.tokens("San Diego", analyzer: "searchkick_word_start_index")
593
+ # ["s", "sa", "san", "d", "di", "die", "dieg", "diego"]
594
+
595
+ Product.searchkick_index.tokens("dieg", analyzer: "searchkick_word_search")
596
+ # ["dieg"] - match!!
597
+ ```
598
+
599
+ See the [complete list of analyzers](lib/searchkick/reindex.rb#L86).
600
+
560
601
  ## Deployment
561
602
 
562
603
  Searchkick uses `ENV["ELASTICSEARCH_URL"]` for the Elasticsearch server. This defaults to `http://localhost:9200`.
@@ -65,6 +65,10 @@ module Searchkick
65
65
  end
66
66
  end
67
67
 
68
+ def tokens(text, options = {})
69
+ client.indices.analyze({text: text, index: name}.merge(options))["tokens"].map{|t| t["token"] }
70
+ end
71
+
68
72
  protected
69
73
 
70
74
  def client
@@ -45,276 +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
- factor = boost_fields[field] || 1
82
- shared_options = {
83
- fields: [field],
84
- query: term,
85
- use_dis_max: false,
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
  }
89
- if field == "_all" or field.end_with?(".analyzed")
90
- shared_options[:cutoff_frequency] = 0.001 unless operator == "and"
91
- queries.concat [
92
- {multi_match: shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search")},
93
- {multi_match: shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2")}
94
- ]
95
- if options[:misspellings] != false
96
- distance = (options[:misspellings].is_a?(Hash) && options[:misspellings][:distance]) || 1
97
- queries.concat [
98
- {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search")},
99
- {multi_match: shared_options.merge(fuzziness: distance, max_expansions: 3, analyzer: "searchkick_search2")}
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
+ }
96
+
97
+ if field == "_all" or field.end_with?(".analyzed")
98
+ shared_options[:cutoff_frequency] = 0.001 unless operator == "and"
99
+ qs.concat [
100
+ shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"),
101
+ shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2")
100
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)
101
116
  end
102
- else
103
- analyzer = field.match(/\.word_(start|middle|end)\z/) ? "searchkick_word_search" : "searchkick_autocomplete_search"
104
- queries << {multi_match: shared_options.merge(analyzer: analyzer)}
117
+
118
+ queries.concat(qs.map{|q| {match: {field => q}} })
105
119
  end
106
- end
107
120
 
108
- payload = {
109
- dis_max: {
110
- queries: queries
121
+ payload = {
122
+ dis_max: {
123
+ queries: queries
124
+ }
111
125
  }
112
- }
113
- end
126
+ end
114
127
 
115
- if conversions_field and options[:conversions] != false
116
- # wrap payload in a bool query
117
- payload = {
118
- bool: {
119
- must: payload,
120
- should: {
121
- nested: {
122
- path: conversions_field,
123
- score_mode: "total",
124
- query: {
125
- function_score: {
126
- boost_mode: "replace",
127
- query: {
128
- match: {
129
- 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"
130
147
  }
131
- },
132
- script_score: {
133
- script: "doc['count'].value"
134
148
  }
135
149
  }
136
150
  }
137
151
  }
138
152
  }
139
153
  }
140
- }
154
+ end
141
155
  end
142
- end
143
156
 
144
- custom_filters = []
157
+ custom_filters = []
145
158
 
146
- boost_by = options[:boost_by] || {}
147
- if boost_by.is_a?(Array)
148
- boost_by = Hash[ boost_by.map{|f| [f, {factor: 1}] } ]
149
- end
150
- if options[:boost]
151
- boost_by[options[:boost]] = {factor: 1}
152
- 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
153
166
 
154
- boost_by.each do |field, value|
155
- custom_filters << {
156
- filter: {
157
- exists: {
158
- 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)"
159
176
  }
160
- },
161
- script_score: {
162
- script: "#{value[:factor].to_f} * log(doc['#{field}'].value + 2.718281828)"
163
177
  }
164
- }
165
- end
166
-
167
- boost_where = options[:boost_where] || {}
168
- if options[:user_id] and personalize_field
169
- boost_where[personalize_field] = options[:user_id]
170
- end
171
- if options[:personalize]
172
- boost_where.merge!(options[:personalize])
173
- end
174
- boost_where.each do |field, value|
175
- if value.is_a?(Hash)
176
- value, factor = value[:value], value[:factor]
177
- else
178
- factor = 1000
179
178
  end
180
- custom_filters << {
181
- filter: {
182
- term: {field => value}
183
- },
184
- boost_factor: factor
185
- }
186
- end
187
179
 
188
- if custom_filters.any?
189
- payload = {
190
- function_score: {
191
- functions: custom_filters,
192
- query: payload,
193
- 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
194
198
  }
195
- }
196
- end
197
-
198
- payload = {
199
- query: payload,
200
- size: per_page,
201
- from: offset
202
- }
203
- payload[:explain] = options[:explain] if options[:explain]
199
+ end
204
200
 
205
- # order
206
- if options[:order]
207
- order = options[:order].is_a?(Enumerable) ? options[:order] : {options[:order] => :asc}
208
- payload[:sort] = Hash[ order.map{|k, v| [k.to_s == "id" ? :_id : k, v] } ]
209
- 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
210
210
 
211
- # filters
212
- filters = where_filters(options[:where])
213
- if filters.any?
214
- payload[:filter] = {
215
- and: filters
211
+ payload = {
212
+ query: payload,
213
+ size: per_page,
214
+ from: offset
216
215
  }
217
- end
216
+ payload[:explain] = options[:explain] if options[:explain]
218
217
 
219
- # facets
220
- facet_limits = {}
221
- if options[:facets]
222
- facets = options[:facets] || {}
223
- if facets.is_a?(Array) # convert to more advanced syntax
224
- 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] } ]
225
222
  end
226
223
 
227
- payload[:facets] = {}
228
- facets.each do |field, facet_options|
229
- # ask for extra facets due to
230
- # https://github.com/elasticsearch/elasticsearch/issues/1305
231
- 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
232
244
 
233
- if facet_options[:ranges]
234
- payload[:facets][field] = {
235
- range: {
236
- 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
+ }
237
250
  }
238
- }
239
- elsif facet_options[:stats]
240
- payload[:facets][field] = {
241
- terms_stats: {
242
- key_field: field,
243
- value_script: "doc.score",
244
- 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
+ }
245
258
  }
246
- }
247
- else
248
- payload[:facets][field] = {
249
- terms: {
250
- field: field,
251
- size: size
259
+ else
260
+ payload[:facets][field] = {
261
+ terms: {
262
+ field: field,
263
+ size: size
264
+ }
252
265
  }
253
- }
254
- end
266
+ end
255
267
 
256
- facet_limits[field] = facet_options[:limit] if facet_options[:limit]
268
+ facet_limits[field] = facet_options[:limit] if facet_options[:limit]
257
269
 
258
- # offset is not possible
259
- # 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
260
272
 
261
- facet_options.deep_merge!(where: options[:where].reject{|k| k == field}) if options[:smart_facets] == true
262
- facet_filters = where_filters(facet_options[:where])
263
- if facet_filters.any?
264
- payload[:facets][field][:facet_filter] = {
265
- and: {
266
- 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
+ }
267
280
  }
268
- }
281
+ end
269
282
  end
270
283
  end
271
- end
272
284
 
273
- # suggestions
274
- if options[:suggest]
275
- suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)
285
+ # suggestions
286
+ if options[:suggest]
287
+ suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)
276
288
 
277
- # intersection
278
- if options[:fields]
279
- suggest_fields = suggest_fields & options[:fields].map{|v| (v.is_a?(Hash) ? v.keys.first : v).to_s }
280
- 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
281
293
 
282
- if suggest_fields.any?
283
- payload[:suggest] = {text: term}
284
- suggest_fields.each do |field|
285
- payload[:suggest][field] = {
286
- phrase: {
287
- 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
+ }
288
301
  }
289
- }
302
+ end
290
303
  end
291
304
  end
292
- end
293
305
 
294
- # highlight
295
- if options[:highlight]
296
- payload[:highlight] = {
297
- fields: Hash[ fields.map{|f| [f, {}] } ]
298
- }
299
- if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag]
300
- payload[:highlight][:pre_tags] = [tag]
301
- 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
302
315
  end
303
- end
304
316
 
305
- # model and eagar loading
306
- load = options[:load].nil? ? true : options[:load]
307
-
308
- # An empty array will cause only the _id and _type for each hit to be returned
309
- # http://www.elasticsearch.org/guide/reference/api/search/fields/
310
- if load
311
- payload[:fields] = []
312
- elsif options[:select]
313
- payload[:fields] = options[:select]
314
- 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
315
324
 
316
- if options[:type] or klass != searchkick_klass
317
- @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
318
328
  end
319
329
 
320
330
  @body = payload
@@ -386,7 +396,8 @@ module Searchkick
386
396
  per_page: @per_page,
387
397
  padding: @padding,
388
398
  load: @load,
389
- includes: options[:include] || options[:includes]
399
+ includes: options[:include] || options[:includes],
400
+ json: !options[:json].nil?
390
401
  }
391
402
  Searchkick::Results.new(searchkick_klass, response, opts)
392
403
  end
@@ -3,7 +3,9 @@ module Searchkick
3
3
 
4
4
  # https://gist.github.com/jarosan/3124884
5
5
  # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
6
- def reindex
6
+ def reindex(options = {})
7
+ skip_import = options[:import] == false
8
+
7
9
  alias_name = searchkick_index.name
8
10
  new_name = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S%L")
9
11
  index = Searchkick::Index.new(new_name)
@@ -14,7 +16,8 @@ module Searchkick
14
16
 
15
17
  # check if alias exists
16
18
  if Searchkick.client.indices.exists_alias(name: alias_name)
17
- searchkick_import(index) # import before swap
19
+ # import before swap
20
+ searchkick_import(index) unless skip_import
18
21
 
19
22
  # get existing indices to remove
20
23
  old_indices = Searchkick.client.indices.get_alias(name: alias_name).keys
@@ -24,7 +27,9 @@ module Searchkick
24
27
  else
25
28
  searchkick_index.delete if searchkick_index.exists?
26
29
  Searchkick.client.indices.update_aliases body: {actions: [{add: {index: new_name, alias: alias_name}}]}
27
- searchkick_import(index) # import after swap
30
+
31
+ # import after swap
32
+ searchkick_import(index) unless skip_import
28
33
  end
29
34
 
30
35
  index.refresh
@@ -1,3 +1,5 @@
1
+ require "forwardable"
2
+
1
3
  module Searchkick
2
4
  class Results
3
5
  include Enumerable
@@ -67,7 +69,7 @@ module Searchkick
67
69
  each_with_hit.map do |model, hit|
68
70
  details = {}
69
71
  if hit["highlight"]
70
- details[:highlight] = Hash[ hit["highlight"].map{|k, v| [k.sub(/\.analyzed\z/, "").to_sym, v.first] } ]
72
+ details[:highlight] = Hash[ hit["highlight"].map{|k, v| [(options[:json] ? k : k.sub(/\.analyzed\z/, "")).to_sym, v.first] } ]
71
73
  end
72
74
  [model, details]
73
75
  end
@@ -112,6 +114,7 @@ module Searchkick
112
114
  def previous_page
113
115
  current_page > 1 ? (current_page - 1) : nil
114
116
  end
117
+ alias_method :prev_page, :previous_page
115
118
 
116
119
  def next_page
117
120
  current_page < total_pages ? (current_page + 1) : nil
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.7.8"
2
+ VERSION = "0.7.9"
3
3
  end
@@ -57,4 +57,11 @@ class TestAutocomplete < Minitest::Unit::TestCase
57
57
  assert_search "dark grey", ["Dark Grey"], fields: [{name: :word_start}]
58
58
  end
59
59
 
60
+ # TODO find a better place
61
+
62
+ def test_exact
63
+ store_names ["hi@example.org"]
64
+ assert_search "hi@example.org", ["hi@example.org"], fields: [{name: :exact}]
65
+ end
66
+
60
67
  end
@@ -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).with_details.first[1][:highlight][:"name.analyzed"]
39
+ end
40
+
22
41
  end
@@ -35,6 +35,20 @@ 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
+
44
+ def test_tokens
45
+ assert_equal ["dollar", "dollartre", "tree"], Product.searchkick_index.tokens("Dollar Tree")
46
+ end
47
+
48
+ def test_tokens_analyzer
49
+ assert_equal ["dollar", "tree"], Product.searchkick_index.tokens("Dollar Tree", analyzer: "searchkick_search2")
50
+ end
51
+
38
52
  def test_record_not_found
39
53
  store_names ["Product A", "Product B"]
40
54
  Product.where(name: "Product A").delete_all
@@ -33,6 +33,9 @@ class TestSql < Minitest::Unit::TestCase
33
33
  assert_equal 2, products.limit_value
34
34
  assert_equal 3, products.offset_value
35
35
  assert_equal 3, products.offset
36
+ assert_equal 3, products.next_page
37
+ assert_equal 1, products.previous_page
38
+ assert_equal 1, products.prev_page
36
39
  assert !products.first_page?
37
40
  assert !products.last_page?
38
41
  assert !products.empty?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchkick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.8
4
+ version: 0.7.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-22 00:00:00.000000000 Z
11
+ date: 2014-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel