Commit 12caa1e64fb234fb355324efb576925d605b7571
Exists in
master
and in
21 other branches
Merge branch 'master' into raise_dangerous_operations
Showing
51 changed files
with
489 additions
and
234 deletions
Show diff stats
.travis.yml
... | ... | @@ -4,7 +4,7 @@ services: |
4 | 4 | - elasticsearch |
5 | 5 | - mongodb |
6 | 6 | before_install: |
7 | - - ./ci/before_install.sh | |
7 | + - ./test/ci/before_install.sh | |
8 | 8 | script: bundle exec rake test |
9 | 9 | before_script: |
10 | 10 | - psql -c 'create database searchkick_test;' -U postgres |
... | ... | @@ -14,14 +14,14 @@ notifications: |
14 | 14 | on_failure: change |
15 | 15 | gemfile: |
16 | 16 | - Gemfile |
17 | - - gemfiles/activerecord41.gemfile | |
18 | - - gemfiles/activerecord40.gemfile | |
19 | - - gemfiles/activerecord32.gemfile | |
20 | - - gemfiles/activerecord31.gemfile | |
21 | - - gemfiles/mongoid2.gemfile | |
22 | - - gemfiles/mongoid3.gemfile | |
23 | - - gemfiles/mongoid4.gemfile | |
17 | + - test/gemfiles/activerecord41.gemfile | |
18 | + - test/gemfiles/activerecord40.gemfile | |
19 | + - test/gemfiles/activerecord32.gemfile | |
20 | + - test/gemfiles/activerecord31.gemfile | |
21 | + - test/gemfiles/mongoid2.gemfile | |
22 | + - test/gemfiles/mongoid3.gemfile | |
23 | + - test/gemfiles/mongoid4.gemfile | |
24 | 24 | matrix: |
25 | 25 | include: |
26 | - - gemfile: gemfiles/nobrainer.gemfile | |
26 | + - gemfile: test/gemfiles/nobrainer.gemfile | |
27 | 27 | env: NOBRAINER=true | ... | ... |
CHANGELOG.md
1 | -## 0.9.1 [unreleased] | |
1 | +## 0.9.2 [unreleased] | |
2 | + | |
3 | +- Added support for Elasticsearch 2.0 | |
4 | +- Added support for aggregations | |
5 | +- Added ability to use misspellings for partial matches | |
6 | +- Added `fragment_size` option for highlight | |
7 | +- Added `took` method to results | |
8 | + | |
9 | +## 0.9.1 | |
2 | 10 | |
3 | 11 | - `and` now matches `&` |
4 | 12 | - Added `transpositions` option to misspellings |
5 | 13 | - Added `boost_mode` and `log` options to `boost_by` |
14 | +- Added `prefix_length` option to `misspellings` | |
15 | +- Added ability to set env | |
6 | 16 | |
7 | 17 | ## 0.9.0 |
8 | 18 | ... | ... |
README.md
... | ... | @@ -45,7 +45,7 @@ Add this line to your applicationโs Gemfile: |
45 | 45 | gem 'searchkick' |
46 | 46 | ``` |
47 | 47 | |
48 | -For Elasticsearch 0.90, use version `0.6.3` and [this readme](https://github.com/ankane/searchkick/blob/v0.6.3/README.md). | |
48 | +For Elasticsearch 2.0, use the `elasticsearch2` branch and [this readme](https://github.com/ankane/searchkick/blob/elasticsearch2/README.md). For Elasticsearch 0.90, use version `0.6.3` and [this readme](https://github.com/ankane/searchkick/blob/v0.6.3/README.md). | |
49 | 49 | |
50 | 50 | Add searchkick to models you want to search. |
51 | 51 | |
... | ... | @@ -117,6 +117,35 @@ Limit / offset |
117 | 117 | limit: 20, offset: 40 |
118 | 118 | ``` |
119 | 119 | |
120 | +### Results | |
121 | + | |
122 | +Searches return a `Searchkick::Results` object. This responds like an array to most methods. | |
123 | + | |
124 | +```ruby | |
125 | +results = Product.search("milk") | |
126 | +results.size | |
127 | +results.any? | |
128 | +results.each { ... } | |
129 | +``` | |
130 | + | |
131 | +Get total results | |
132 | + | |
133 | +```ruby | |
134 | +results.total_count | |
135 | +``` | |
136 | + | |
137 | +Get the time the search took (in milliseconds) [master] | |
138 | + | |
139 | +```ruby | |
140 | +results.took | |
141 | +``` | |
142 | + | |
143 | +Get the full response from Elasticsearch | |
144 | + | |
145 | +```ruby | |
146 | +results.response | |
147 | +``` | |
148 | + | |
120 | 149 | ### Boosting |
121 | 150 | |
122 | 151 | Boost important fields |
... | ... | @@ -240,6 +269,8 @@ end |
240 | 269 | ```ruby |
241 | 270 | class Product < ActiveRecord::Base |
242 | 271 | searchkick synonyms: [["scallion", "green onion"], ["qtip", "cotton swab"]] |
272 | + # or | |
273 | + # searchkick synonyms: Proc.new { CSV.read("/some/path/synonyms.csv") } | |
243 | 274 | end |
244 | 275 | ``` |
245 | 276 | |
... | ... | @@ -282,7 +313,7 @@ Or turn off misspellings with: |
282 | 313 | Product.search "zuchini", misspellings: false # no zucchini |
283 | 314 | ``` |
284 | 315 | |
285 | -Swapping two letters counts as two edits. To count the [transposition of two adjacent characters as a single edit](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance), use: [master] | |
316 | +Swapping two letters counts as two edits. To count the [transposition of two adjacent characters as a single edit](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance), use: | |
286 | 317 | |
287 | 318 | ```ruby |
288 | 319 | Product.search "mikl", misspellings: {transpositions: true} # milk |
... | ... | @@ -706,6 +737,12 @@ Dog.search "airbudd", suggest: true # suggestions for all animals |
706 | 737 | |
707 | 738 | ## Debugging Queries |
708 | 739 | |
740 | +See how Elasticsearch scores your queries with: | |
741 | + | |
742 | +```ruby | |
743 | +Product.search("soap", explain: true) | |
744 | +``` | |
745 | + | |
709 | 746 | See how Elasticsearch tokenizes your queries with: |
710 | 747 | |
711 | 748 | ```ruby |
... | ... | @@ -759,6 +796,28 @@ Then deploy and reindex: |
759 | 796 | heroku run rake searchkick:reindex CLASS=Product |
760 | 797 | ``` |
761 | 798 | |
799 | +### Amazon Elasticsearch Service | |
800 | + | |
801 | +You must use an [IP-based access policy](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg-search.html) for Searchkick to work. | |
802 | + | |
803 | +Include `elasticsearch 1.0.14` or greater in your Gemfile. | |
804 | + | |
805 | +```ruby | |
806 | +gem "elasticsearch", ">= 1.0.14" | |
807 | +``` | |
808 | + | |
809 | +Create an initializer `config/initializers/elasticsearch.rb` with: | |
810 | + | |
811 | +```ruby | |
812 | +ENV["ELASTICSEARCH_URL"] = "http://es-domain-1234.us-east-1.es.amazonaws.com" | |
813 | +``` | |
814 | + | |
815 | +Then deploy and reindex: | |
816 | + | |
817 | +```sh | |
818 | +rake searchkick:reindex CLASS=Product | |
819 | +``` | |
820 | + | |
762 | 821 | ### Other |
763 | 822 | |
764 | 823 | Create an initializer `config/initializers/elasticsearch.rb` with: |
... | ... | @@ -1009,6 +1068,18 @@ Reindex all models - Rails only |
1009 | 1068 | rake searchkick:reindex:all |
1010 | 1069 | ``` |
1011 | 1070 | |
1071 | +Turn on misspellings after a certain number of characters | |
1072 | + | |
1073 | +```ruby | |
1074 | +Product.search "api", misspellings: {prefix_length: 2} # api, apt, no ahi | |
1075 | +``` | |
1076 | + | |
1077 | +**Note:** With this option, if the query length is the same as `prefix_length`, misspellings are turned off | |
1078 | + | |
1079 | +```ruby | |
1080 | +Product.search "ah", misspellings: {prefix_length: 2} # ah, no aha | |
1081 | +``` | |
1082 | + | |
1012 | 1083 | ## Large Data Sets |
1013 | 1084 | |
1014 | 1085 | For large data sets, check out [Keeping Elasticsearch in Sync](https://www.found.no/foundation/keeping-elasticsearch-in-sync/). Searchkick will make this easy in the future. | ... | ... |
ci/before_install.sh
... | ... | @@ -1,14 +0,0 @@ |
1 | -#!/usr/bin/env bash | |
2 | - | |
3 | -wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.3.2.deb | |
4 | -sudo dpkg -i elasticsearch-1.3.2.deb | |
5 | -sudo service elasticsearch restart | |
6 | - | |
7 | -if [ -n "$NOBRAINER" ]; then | |
8 | - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list | |
9 | - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - | |
10 | - sudo apt-get update -q | |
11 | - sudo apt-get install rethinkdb | |
12 | - sudo cp /etc/rethinkdb/default.conf.sample /etc/rethinkdb/instances.d/instance1.conf | |
13 | - sudo service rethinkdb restart | |
14 | -fi |
gemfiles/activerecord31.gemfile
gemfiles/activerecord32.gemfile
gemfiles/activerecord40.gemfile
gemfiles/activerecord41.gemfile
gemfiles/mongoid2.gemfile
gemfiles/mongoid3.gemfile
gemfiles/mongoid4.gemfile
gemfiles/nobrainer.gemfile
lib/searchkick.rb
lib/searchkick/index.rb
... | ... | @@ -100,7 +100,7 @@ module Searchkick |
100 | 100 | |
101 | 101 | def similar_record(record, options = {}) |
102 | 102 | like_text = retrieve(record).to_hash |
103 | - .keep_if { |k, v| !options[:fields] || options[:fields].map(&:to_s).include?(k) } | |
103 | + .keep_if { |k, _| !options[:fields] || options[:fields].map(&:to_s).include?(k) } | |
104 | 104 | .values.compact.join(" ") |
105 | 105 | |
106 | 106 | # TODO deep merge method |
... | ... | @@ -118,9 +118,7 @@ module Searchkick |
118 | 118 | |
119 | 119 | def search_model(searchkick_klass, term = nil, options = {}, &block) |
120 | 120 | query = Searchkick::Query.new(searchkick_klass, term, options) |
121 | - if block | |
122 | - block.call(query.body) | |
123 | - end | |
121 | + block.call(query.body) if block | |
124 | 122 | if options[:execute] == false |
125 | 123 | query |
126 | 124 | else |
... | ... | @@ -317,7 +315,9 @@ module Searchkick |
317 | 315 | max_gram: 50 |
318 | 316 | }, |
319 | 317 | searchkick_stemmer: { |
320 | - type: "snowball", | |
318 | + # use stemmer if language is lowercase, snowball otherwise | |
319 | + # TODO deprecate language option in favor of stemmer | |
320 | + type: options[:language] == options[:language].to_s.downcase ? "stemmer" : "snowball", | |
321 | 321 | language: options[:language] || "English" |
322 | 322 | } |
323 | 323 | }, |
... | ... | @@ -347,6 +347,9 @@ module Searchkick |
347 | 347 | |
348 | 348 | # synonyms |
349 | 349 | synonyms = options[:synonyms] || [] |
350 | + | |
351 | + synonyms = synonyms.call if synonyms.respond_to?(:call) | |
352 | + | |
350 | 353 | if synonyms.any? |
351 | 354 | settings[:analysis][:filter][:searchkick_synonym] = { |
352 | 355 | type: "synonym", |
... | ... | @@ -377,7 +380,7 @@ module Searchkick |
377 | 380 | end |
378 | 381 | |
379 | 382 | if options[:special_characters] == false |
380 | - settings[:analysis][:analyzer].each do |analyzer, analyzer_settings| | |
383 | + settings[:analysis][:analyzer].each do |_, analyzer_settings| | |
381 | 384 | analyzer_settings[:filter].reject! { |f| f == "asciifolding" } |
382 | 385 | end |
383 | 386 | end |
... | ... | @@ -385,8 +388,8 @@ module Searchkick |
385 | 388 | mapping = {} |
386 | 389 | |
387 | 390 | # conversions |
388 | - if options[:conversions] | |
389 | - mapping[:conversions] = { | |
391 | + if (conversions_field = options[:conversions]) | |
392 | + mapping[conversions_field] = { | |
390 | 393 | type: "nested", |
391 | 394 | properties: { |
392 | 395 | query: {type: "string", analyzer: "searchkick_keyword"}, |
... | ... | @@ -559,6 +562,5 @@ module Searchkick |
559 | 562 | obj |
560 | 563 | end |
561 | 564 | end |
562 | - | |
563 | 565 | end |
564 | 566 | end | ... | ... |
lib/searchkick/logging.rb
... | ... | @@ -62,7 +62,8 @@ module Searchkick |
62 | 62 | end |
63 | 63 | |
64 | 64 | def self.reset_runtime |
65 | - rt, self.runtime = runtime, 0 | |
65 | + rt = runtime | |
66 | + self.runtime = 0 | |
66 | 67 | rt |
67 | 68 | end |
68 | 69 | |
... | ... | @@ -122,7 +123,8 @@ module Searchkick |
122 | 123 | |
123 | 124 | module ClassMethods |
124 | 125 | def log_process_action(payload) |
125 | - messages, runtime = super, payload[:searchkick_runtime] | |
126 | + messages = super | |
127 | + runtime = payload[:searchkick_runtime] | |
126 | 128 | messages << ("Searchkick: %.1fms" % runtime.to_f) if runtime.to_f > 0 |
127 | 129 | messages |
128 | 130 | end | ... | ... |
lib/searchkick/model.rb
... | ... | @@ -2,7 +2,6 @@ module Searchkick |
2 | 2 | module Reindex; end # legacy for Searchjoy |
3 | 3 | |
4 | 4 | module Model |
5 | - | |
6 | 5 | def searchkick(options = {}) |
7 | 6 | raise "Only call searchkick once per model" if respond_to?(:searchkick_index) |
8 | 7 | |
... | ... | @@ -18,12 +17,11 @@ module Searchkick |
18 | 17 | class_variable_set :@@searchkick_callbacks, callbacks |
19 | 18 | class_variable_set :@@searchkick_index, options[:index_name] || [options[:index_prefix], model_name.plural, Searchkick.env].compact.join("_") |
20 | 19 | |
21 | - define_singleton_method(Searchkick.search_method_name) do |term = nil, options = {}, &block| | |
22 | - searchkick_index.search_model(self, term, options, &block) | |
23 | - end | |
24 | - extend Searchkick::Reindex # legacy for Searchjoy | |
25 | - | |
26 | 20 | class << self |
21 | + def searchkick_search(term = nil, options = {}, &block) | |
22 | + searchkick_index.search_model(self, term, options, &block) | |
23 | + end | |
24 | + alias_method Searchkick.search_method_name, :searchkick_search | |
27 | 25 | |
28 | 26 | def searchkick_index |
29 | 27 | index = class_variable_get :@@searchkick_index |
... | ... | @@ -43,12 +41,13 @@ module Searchkick |
43 | 41 | class_variable_get(:@@searchkick_callbacks) && Searchkick.callbacks? |
44 | 42 | end |
45 | 43 | |
46 | - def reindex(options = {}) | |
44 | + def searchkick_reindex(options = {}) | |
47 | 45 | if respond_to?(:current_scope) && current_scope && current_scope.to_sql != default_scoped.to_sql |
48 | 46 | raise Searchkick::DangerousOperation, "Only call reindex on models, not relations" |
49 | 47 | end |
50 | 48 | searchkick_index.reindex_scope(searchkick_klass, options) |
51 | 49 | end |
50 | + alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex) | |
52 | 51 | |
53 | 52 | def clean_indices |
54 | 53 | searchkick_index.clean_indices |
... | ... | @@ -65,8 +64,8 @@ module Searchkick |
65 | 64 | def searchkick_index_options |
66 | 65 | searchkick_index.index_options |
67 | 66 | end |
68 | - | |
69 | 67 | end |
68 | + extend Searchkick::Reindex # legacy for Searchjoy | |
70 | 69 | |
71 | 70 | if callbacks |
72 | 71 | callback_name = callbacks == :async ? :reindex_async : :reindex |
... | ... | @@ -97,9 +96,7 @@ module Searchkick |
97 | 96 | def should_index? |
98 | 97 | true |
99 | 98 | end unless method_defined?(:should_index?) |
100 | - | |
101 | 99 | end |
102 | 100 | end |
103 | - | |
104 | 101 | end |
105 | 102 | end | ... | ... |
lib/searchkick/query.rb
... | ... | @@ -92,24 +92,41 @@ module Searchkick |
92 | 92 | shared_options = { |
93 | 93 | query: term, |
94 | 94 | operator: operator, |
95 | - boost: factor | |
95 | + boost: 10 * factor | |
96 | 96 | } |
97 | 97 | |
98 | + misspellings = | |
99 | + if options.key?(:misspellings) | |
100 | + options[:misspellings] | |
101 | + elsif options.key?(:mispellings) | |
102 | + options[:mispellings] # why not? | |
103 | + elsif field == "_all" || field.end_with?(".analyzed") | |
104 | + true | |
105 | + else | |
106 | + # TODO default to true and remove this | |
107 | + false | |
108 | + end | |
109 | + | |
110 | + if misspellings != false | |
111 | + edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1 | |
112 | + transpositions = | |
113 | + if misspellings.is_a?(Hash) && misspellings[:transpositions] == true | |
114 | + {fuzzy_transpositions: true} | |
115 | + elsif below20? | |
116 | + {} | |
117 | + else | |
118 | + {fuzzy_transpositions: false} | |
119 | + end | |
120 | + prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0 | |
121 | + max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || 3 | |
122 | + end | |
123 | + | |
98 | 124 | if field == "_all" || field.end_with?(".analyzed") |
99 | - shared_options[:cutoff_frequency] = 0.001 unless operator == "and" | |
125 | + shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false | |
100 | 126 | qs.concat [ |
101 | - shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"), | |
102 | - shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2") | |
127 | + shared_options.merge(analyzer: "searchkick_search"), | |
128 | + shared_options.merge(analyzer: "searchkick_search2") | |
103 | 129 | ] |
104 | - misspellings = options.key?(:misspellings) ? options[:misspellings] : options[:mispellings] # why not? | |
105 | - if misspellings != false | |
106 | - edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1 | |
107 | - transpositions = (misspellings.is_a?(Hash) && misspellings[:transpositions] == true) ? {fuzzy_transpositions: true} : {} | |
108 | - qs.concat [ | |
109 | - shared_options.merge(fuzziness: edit_distance, max_expansions: 3, analyzer: "searchkick_search").merge(transpositions), | |
110 | - shared_options.merge(fuzziness: edit_distance, max_expansions: 3, analyzer: "searchkick_search2").merge(transpositions) | |
111 | - ] | |
112 | - end | |
113 | 130 | elsif field.end_with?(".exact") |
114 | 131 | f = field.split(".")[0..-2].join(".") |
115 | 132 | queries << {match: {f => shared_options.merge(analyzer: "keyword")}} |
... | ... | @@ -118,6 +135,10 @@ module Searchkick |
118 | 135 | qs << shared_options.merge(analyzer: analyzer) |
119 | 136 | end |
120 | 137 | |
138 | + if misspellings != false | |
139 | + qs.concat qs.map { |q| q.except(:cutoff_frequency).merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: max_expansions, boost: factor).merge(transpositions) } | |
140 | + end | |
141 | + | |
121 | 142 | queries.concat(qs.map { |q| {match: {field => q}} }) |
122 | 143 | end |
123 | 144 | |
... | ... | @@ -134,7 +155,7 @@ module Searchkick |
134 | 155 | if below12? |
135 | 156 | {script_score: {script: "doc['count'].value"}} |
136 | 157 | else |
137 | - {field_value_factor: {field: "count"}} | |
158 | + {field_value_factor: {field: "#{conversions_field}.count"}} | |
138 | 159 | end |
139 | 160 | |
140 | 161 | payload = { |
... | ... | @@ -143,13 +164,13 @@ module Searchkick |
143 | 164 | should: { |
144 | 165 | nested: { |
145 | 166 | path: conversions_field, |
146 | - score_mode: "total", | |
167 | + score_mode: "sum", | |
147 | 168 | query: { |
148 | 169 | function_score: { |
149 | 170 | boost_mode: "replace", |
150 | 171 | query: { |
151 | 172 | match: { |
152 | - query: term | |
173 | + "#{conversions_field}.query" => term | |
153 | 174 | } |
154 | 175 | } |
155 | 176 | }.merge(script_score) |
... | ... | @@ -169,11 +190,9 @@ module Searchkick |
169 | 190 | if boost_by.is_a?(Array) |
170 | 191 | boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }] |
171 | 192 | elsif boost_by.is_a?(Hash) |
172 | - multiply_by, boost_by = boost_by.partition { |k,v| v[:boost_mode] == "multiply" }.map{ |i| Hash[i] } | |
173 | - end | |
174 | - if options[:boost] | |
175 | - boost_by[options[:boost]] = {factor: 1} | |
193 | + multiply_by, boost_by = boost_by.partition { |_, v| v[:boost_mode] == "multiply" }.map { |i| Hash[i] } | |
176 | 194 | end |
195 | + boost_by[options[:boost]] = {factor: 1} if options[:boost] | |
177 | 196 | |
178 | 197 | custom_filters.concat boost_filters(boost_by, log: true) |
179 | 198 | multiply_filters.concat boost_filters(multiply_by || {}) |
... | ... | @@ -188,12 +207,10 @@ module Searchkick |
188 | 207 | boost_where.each do |field, value| |
189 | 208 | if value.is_a?(Array) && value.first.is_a?(Hash) |
190 | 209 | value.each do |value_factor| |
191 | - value, factor = value_factor[:value], value_factor[:factor] | |
192 | - custom_filters << custom_filter(field, value, factor) | |
210 | + custom_filters << custom_filter(field, value_factor[:value], value_factor[:factor]) | |
193 | 211 | end |
194 | 212 | elsif value.is_a?(Hash) |
195 | - value, factor = value[:value], value[:factor] | |
196 | - custom_filters << custom_filter(field, value, factor) | |
213 | + custom_filters << custom_filter(field, value[:value], value[:factor]) | |
197 | 214 | else |
198 | 215 | factor = 1000 |
199 | 216 | custom_filters << custom_filter(field, value, factor) |
... | ... | @@ -206,7 +223,7 @@ module Searchkick |
206 | 223 | if !boost_by_distance[:field] || !boost_by_distance[:origin] |
207 | 224 | raise ArgumentError, "boost_by_distance requires :field and :origin" |
208 | 225 | end |
209 | - function_params = boost_by_distance.select { |k, v| [:origin, :scale, :offset, :decay].include?(k) } | |
226 | + function_params = boost_by_distance.select { |k, _| [:origin, :scale, :offset, :decay].include?(k) } | |
210 | 227 | function_params[:origin] = function_params[:origin].reverse |
211 | 228 | custom_filters << { |
212 | 229 | boost_by_distance[:function] => { |
... | ... | @@ -252,7 +269,7 @@ module Searchkick |
252 | 269 | # filters |
253 | 270 | filters = where_filters(options[:where]) |
254 | 271 | if filters.any? |
255 | - if options[:facets] | |
272 | + if options[:facets] || options[:aggs] | |
256 | 273 | payload[:filter] = { |
257 | 274 | and: filters |
258 | 275 | } |
... | ... | @@ -272,9 +289,7 @@ module Searchkick |
272 | 289 | # facets |
273 | 290 | if options[:facets] |
274 | 291 | facets = options[:facets] || {} |
275 | - if facets.is_a?(Array) # convert to more advanced syntax | |
276 | - facets = Hash[facets.map { |f| [f, {}] }] | |
277 | - end | |
292 | + facets = Hash[facets.map { |f| [f, {}] }] if facets.is_a?(Array) # convert to more advanced syntax | |
278 | 293 | |
279 | 294 | payload[:facets] = {} |
280 | 295 | facets.each do |field, facet_options| |
... | ... | @@ -310,7 +325,7 @@ module Searchkick |
310 | 325 | # offset is not possible |
311 | 326 | # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html |
312 | 327 | |
313 | - facet_options.deep_merge!(where: options[:where].reject { |k| k == field }) if options[:smart_facets] == true | |
328 | + facet_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_facets] == true | |
314 | 329 | facet_filters = where_filters(facet_options[:where]) |
315 | 330 | if facet_filters.any? |
316 | 331 | payload[:facets][field][:facet_filter] = { |
... | ... | @@ -322,6 +337,41 @@ module Searchkick |
322 | 337 | end |
323 | 338 | end |
324 | 339 | |
340 | + # aggregations | |
341 | + if options[:aggs] | |
342 | + aggs = options[:aggs] | |
343 | + payload[:aggs] = {} | |
344 | + | |
345 | + aggs = aggs.map { |f| [f, {}] }.to_h if aggs.is_a?(Array) # convert to more advanced syntax | |
346 | + | |
347 | + aggs.each do |field, agg_options| | |
348 | + size = agg_options[:limit] ? agg_options[:limit] : 100_000 | |
349 | + | |
350 | + payload[:aggs][field] = { | |
351 | + terms: { | |
352 | + field: agg_options[:field] || field, | |
353 | + size: size | |
354 | + } | |
355 | + } | |
356 | + | |
357 | + agg_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_aggs] == true | |
358 | + agg_filters = where_filters(agg_options[:where]) | |
359 | + | |
360 | + if agg_filters.any? | |
361 | + payload[:aggs][field] = { | |
362 | + filter: { | |
363 | + bool: { | |
364 | + must: agg_filters | |
365 | + } | |
366 | + }, | |
367 | + aggs: { | |
368 | + field => payload[:aggs][field] | |
369 | + } | |
370 | + } | |
371 | + end | |
372 | + end | |
373 | + end | |
374 | + | |
325 | 375 | # suggestions |
326 | 376 | if options[:suggest] |
327 | 377 | suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s) |
... | ... | @@ -355,6 +405,10 @@ module Searchkick |
355 | 405 | payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")] |
356 | 406 | end |
357 | 407 | |
408 | + if (fragment_size = options[:highlight][:fragment_size]) | |
409 | + payload[:highlight][:fragment_size] = fragment_size | |
410 | + end | |
411 | + | |
358 | 412 | highlight_fields = options[:highlight][:fields] |
359 | 413 | if highlight_fields |
360 | 414 | payload[:highlight][:fields] = {} |
... | ... | @@ -379,9 +433,7 @@ module Searchkick |
379 | 433 | end |
380 | 434 | |
381 | 435 | # routing |
382 | - if options[:routing] | |
383 | - @routing = options[:routing] | |
384 | - end | |
436 | + @routing = options[:routing] if options[:routing] | |
385 | 437 | end |
386 | 438 | |
387 | 439 | @body = payload |
... | ... | @@ -477,9 +529,7 @@ module Searchkick |
477 | 529 | value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last} |
478 | 530 | end |
479 | 531 | |
480 | - if value.is_a?(Array) | |
481 | - value = {in: value} | |
482 | - end | |
532 | + value = {in: value} if value.is_a?(Array) | |
483 | 533 | |
484 | 534 | if value.is_a?(Hash) |
485 | 535 | value.each do |op, op_value| |
... | ... | @@ -505,9 +555,11 @@ module Searchkick |
505 | 555 | when :regexp # support for regexp queries without using a regexp ruby object |
506 | 556 | filters << {regexp: {field => {value: op_value}}} |
507 | 557 | when :not # not equal |
508 | - filters << {not: term_filters(field, op_value)} | |
558 | + filters << {not: {filter: term_filters(field, op_value)}} | |
509 | 559 | when :all |
510 | - filters << {terms: {field => op_value, execution: "and"}} | |
560 | + op_value.each do |value| | |
561 | + filters << term_filters(field, value) | |
562 | + end | |
511 | 563 | when :in |
512 | 564 | filters << term_filters(field, op_value) |
513 | 565 | else |
... | ... | @@ -559,7 +611,7 @@ module Searchkick |
559 | 611 | def custom_filter(field, value, factor) |
560 | 612 | { |
561 | 613 | filter: { |
562 | - and: where_filters({field => value}) | |
614 | + and: where_filters(field => value) | |
563 | 615 | }, |
564 | 616 | boost_factor: factor |
565 | 617 | } |
... | ... | @@ -595,6 +647,10 @@ module Searchkick |
595 | 647 | below_version?("1.4.0") |
596 | 648 | end |
597 | 649 | |
650 | + def below20? | |
651 | + below_version?("2.0.0") | |
652 | + end | |
653 | + | |
598 | 654 | def below_version?(version) |
599 | 655 | Gem::Version.new(Searchkick.server_version) < Gem::Version.new(version) |
600 | 656 | end | ... | ... |
lib/searchkick/reindex_job.rb
lib/searchkick/reindex_v2_job.rb
lib/searchkick/results.rb
... | ... | @@ -15,27 +15,24 @@ module Searchkick |
15 | 15 | @options = options |
16 | 16 | end |
17 | 17 | |
18 | + # experimental: may not make next release | |
19 | + def records | |
20 | + @records ||= results_query(klass, hits) | |
21 | + end | |
22 | + | |
18 | 23 | def results |
19 | 24 | @results ||= begin |
20 | 25 | if options[:load] |
21 | 26 | # results can have different types |
22 | 27 | results = {} |
23 | 28 | |
24 | - hits.group_by { |hit, i| hit["_type"] }.each do |type, grouped_hits| | |
25 | - records = type.camelize.constantize | |
26 | - if options[:includes] | |
27 | - if defined?(NoBrainer::Document) && records < NoBrainer::Document | |
28 | - records = records.preload(options[:includes]) | |
29 | - else | |
30 | - records = records.includes(options[:includes]) | |
31 | - end | |
32 | - end | |
33 | - results[type] = results_query(records, grouped_hits) | |
29 | + hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits| | |
30 | + results[type] = results_query(type.camelize.constantize, grouped_hits).to_a.index_by { |r| r.id.to_s } | |
34 | 31 | end |
35 | 32 | |
36 | 33 | # sort |
37 | 34 | hits.map do |hit| |
38 | - results[hit["_type"]].find { |r| r.id.to_s == hit["_id"].to_s } | |
35 | + results[hit["_type"]][hit["_id"].to_s] | |
39 | 36 | end.compact |
40 | 37 | else |
41 | 38 | hits.map do |hit| |
... | ... | @@ -78,6 +75,23 @@ module Searchkick |
78 | 75 | response["facets"] |
79 | 76 | end |
80 | 77 | |
78 | + def aggs | |
79 | + @aggs ||= begin | |
80 | + response["aggregations"].dup.each do |field, filtered_agg| | |
81 | + buckets = filtered_agg[field] | |
82 | + # move the buckets one level above into the field hash | |
83 | + if buckets | |
84 | + filtered_agg.delete(field) | |
85 | + filtered_agg.merge!(buckets) | |
86 | + end | |
87 | + end | |
88 | + end | |
89 | + end | |
90 | + | |
91 | + def took | |
92 | + response["took"] | |
93 | + end | |
94 | + | |
81 | 95 | def model_name |
82 | 96 | klass.model_name |
83 | 97 | end |
... | ... | @@ -141,19 +155,30 @@ module Searchkick |
141 | 155 | |
142 | 156 | private |
143 | 157 | |
144 | - def results_query(records, grouped_hits) | |
158 | + def results_query(records, hits) | |
159 | + ids = hits.map { |hit| hit["_id"] } | |
160 | + | |
161 | + if options[:includes] | |
162 | + records = | |
163 | + if defined?(NoBrainer::Document) && records < NoBrainer::Document | |
164 | + records.preload(options[:includes]) | |
165 | + else | |
166 | + records.includes(options[:includes]) | |
167 | + end | |
168 | + end | |
169 | + | |
145 | 170 | if records.respond_to?(:primary_key) && records.primary_key |
146 | 171 | # ActiveRecord |
147 | - records.where(records.primary_key => grouped_hits.map { |hit| hit["_id"] }).to_a | |
172 | + records.where(records.primary_key => ids) | |
148 | 173 | elsif records.respond_to?(:all) && records.all.respond_to?(:for_ids) |
149 | 174 | # Mongoid 2 |
150 | - records.all.for_ids(grouped_hits.map { |hit| hit["_id"] }).to_a | |
175 | + records.all.for_ids(ids) | |
151 | 176 | elsif records.respond_to?(:queryable) |
152 | 177 | # Mongoid 3+ |
153 | - records.queryable.for_ids(grouped_hits.map { |hit| hit["_id"] }).to_a | |
178 | + records.queryable.for_ids(ids) | |
154 | 179 | elsif records.respond_to?(:unscoped) && records.all.respond_to?(:preload) |
155 | 180 | # Nobrainer |
156 | - records.unscoped.where(:id.in => grouped_hits.map { |hit| hit["_id"] }).to_a | |
181 | + records.unscoped.where(:id.in => ids) | |
157 | 182 | else |
158 | 183 | raise "Not sure how to load records" |
159 | 184 | end | ... | ... |
lib/searchkick/tasks.rb
lib/searchkick/version.rb
... | ... | @@ -0,0 +1,81 @@ |
1 | +require_relative "test_helper" | |
2 | + | |
3 | +class AggsTest < Minitest::Test | |
4 | + def setup | |
5 | + super | |
6 | + store [ | |
7 | + {name: "Product Show", latitude: 37.7833, longitude: 12.4167, store_id: 1, in_stock: true, color: "blue", price: 21, created_at: 2.days.ago}, | |
8 | + {name: "Product Hide", latitude: 29.4167, longitude: -98.5000, store_id: 2, in_stock: false, color: "green", price: 25, created_at: 2.days.from_now}, | |
9 | + {name: "Product B", latitude: 43.9333, longitude: -122.4667, store_id: 2, in_stock: false, color: "red", price: 5}, | |
10 | + {name: "Foo", latitude: 43.9333, longitude: 12.4667, store_id: 3, in_stock: false, color: "yellow", price: 15} | |
11 | + ] | |
12 | + end | |
13 | + | |
14 | + def test_basic | |
15 | + assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: [:store_id]) | |
16 | + end | |
17 | + | |
18 | + def test_where | |
19 | + assert_equal ({1 => 1}), store_agg(aggs: {store_id: {where: {in_stock: true}}}) | |
20 | + end | |
21 | + | |
22 | + def test_field | |
23 | + assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {}}) | |
24 | + assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {field: "store_id"}}) | |
25 | + assert_equal ({1 => 1, 2 => 2}), store_agg({aggs: {store_id_new: {field: "store_id"}}}, "store_id_new") | |
26 | + end | |
27 | + | |
28 | + def test_limit | |
29 | + agg = Product.search("Product", aggs: {store_id: {limit: 1}}).aggs["store_id"] | |
30 | + assert_equal 1, agg["buckets"].size | |
31 | + # assert_equal 3, agg["doc_count"] | |
32 | + assert_equal(1, agg["sum_other_doc_count"]) if Gem::Version.new(Searchkick.server_version) >= Gem::Version.new("1.4.0") | |
33 | + end | |
34 | + | |
35 | + def test_where_no_smart_aggs | |
36 | + assert_equal ({2 => 2}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}}) | |
37 | + assert_equal ({2 => 2}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false}}}) | |
38 | + end | |
39 | + | |
40 | + def test_smart_aggs | |
41 | + assert_equal ({1 => 1}), store_agg(where: {in_stock: true}, aggs: [:store_id], smart_aggs: true) | |
42 | + end | |
43 | + | |
44 | + def test_smart_aggs_where | |
45 | + assert_equal ({2 => 1}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}}, smart_aggs: true) | |
46 | + end | |
47 | + | |
48 | + def test_smart_aggs_where_override | |
49 | + assert_equal ({2 => 1}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false, color: "blue"}}}, smart_aggs: true) | |
50 | + assert_equal ({}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false, color: "red"}}}, smart_aggs: true) | |
51 | + end | |
52 | + | |
53 | + def test_smart_aggs_skip_agg | |
54 | + assert_equal ({1 => 1, 2 => 2}), store_agg(where: {store_id: 2}, aggs: [:store_id], smart_aggs: true) | |
55 | + end | |
56 | + | |
57 | + def test_smart_aggs_skip_agg_complex | |
58 | + assert_equal ({1 => 1, 2 => 1}), store_agg(where: {store_id: 2, price: {gt: 5}}, aggs: [:store_id], smart_aggs: true) | |
59 | + end | |
60 | + | |
61 | + def test_multiple_aggs | |
62 | + assert_equal ({"store_id" => {1 => 1, 2 => 2}, "color" => {"blue" => 1, "green" => 1, "red" => 1}}), store_multiple_aggs(aggs: [:store_id, :color]) | |
63 | + end | |
64 | + | |
65 | + protected | |
66 | + | |
67 | + def buckets_as_hash(agg) | |
68 | + agg["buckets"].map { |v| [v["key"], v["doc_count"]] }.to_h | |
69 | + end | |
70 | + | |
71 | + def store_agg(options, agg_key = "store_id") | |
72 | + buckets = Product.search("Product", options).aggs[agg_key] | |
73 | + buckets_as_hash(buckets) | |
74 | + end | |
75 | + | |
76 | + def store_multiple_aggs(options) | |
77 | + Product.search("Product", options).aggs.map do |field, filtered_agg| | |
78 | + [field, buckets_as_hash(filtered_agg)] | |
79 | + end.to_h | |
80 | + end | |
81 | +end | ... | ... |
test/autocomplete_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestAutocomplete < Minitest::Test | |
4 | - | |
3 | +class AutocompleteTest < Minitest::Test | |
5 | 4 | def test_autocomplete |
6 | 5 | store_names ["Hummus"] |
7 | 6 | assert_search "hum", ["Hummus"], autocomplete: true |
... | ... | @@ -63,5 +62,4 @@ class TestAutocomplete < Minitest::Test |
63 | 62 | store_names ["hi@example.org"] |
64 | 63 | assert_search "hi@example.org", ["hi@example.org"], fields: [{name: :exact}] |
65 | 64 | end |
66 | - | |
67 | 65 | end | ... | ... |
test/boost_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestBoost < Minitest::Test | |
4 | - | |
3 | +class BoostTest < Minitest::Test | |
5 | 4 | # conversions |
6 | 5 | |
7 | 6 | def test_conversions |
... | ... | @@ -133,5 +132,4 @@ class TestBoost < Minitest::Test |
133 | 132 | ] |
134 | 133 | assert_order "san", ["San Francisco", "San Antonio", "San Marino"], boost_by_distance: {field: :location, origin: [37, -122], scale: "1000mi"} |
135 | 134 | end |
136 | - | |
137 | 135 | end | ... | ... |
... | ... | @@ -0,0 +1,15 @@ |
1 | +#!/usr/bin/env bash | |
2 | + | |
3 | +sudo apt-get purge elasticsearch | |
4 | +wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.7.3.deb | |
5 | +sudo dpkg -i elasticsearch-1.7.3.deb | |
6 | +sudo service elasticsearch start | |
7 | + | |
8 | +if [ -n "$NOBRAINER" ]; then | |
9 | + source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list | |
10 | + wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - | |
11 | + sudo apt-get update -q | |
12 | + sudo apt-get install rethinkdb | |
13 | + sudo cp /etc/rethinkdb/default.conf.sample /etc/rethinkdb/instances.d/instance1.conf | |
14 | + sudo service rethinkdb restart | |
15 | +fi | ... | ... |
test/facets_test.rb
1 | 1 | require_relative "test_helper" |
2 | -require "active_support/core_ext" | |
3 | - | |
4 | -class TestFacets < Minitest::Test | |
5 | 2 | |
3 | +class FacetsTest < Minitest::Test | |
6 | 4 | def setup |
5 | + skip if elasticsearch2? | |
7 | 6 | super |
8 | 7 | store [ |
9 | 8 | {name: "Product Show", latitude: 37.7833, longitude: 12.4167, store_id: 1, in_stock: true, color: "blue", price: 21, created_at: 2.days.ago}, |
... | ... | @@ -79,14 +78,13 @@ class TestFacets < Minitest::Test |
79 | 78 | skip if Gem::Version.new(Searchkick.server_version) >= Gem::Version.new("1.4.0") |
80 | 79 | options = {where: {store_id: 2}, facets: {store_id: {stats: true}}} |
81 | 80 | facets = Product.search("Product", options).facets["store_id"]["terms"] |
82 | - expected_facets_keys = %w[term count total_count min max total mean] | |
81 | + expected_facets_keys = %w(term count total_count min max total mean) | |
83 | 82 | assert_equal expected_facets_keys, facets.first.keys |
84 | 83 | end |
85 | 84 | |
86 | 85 | protected |
87 | 86 | |
88 | - def store_facet(options, facet_key="store_id") | |
87 | + def store_facet(options, facet_key = "store_id") | |
89 | 88 | Hash[Product.search("Product", options).facets[facet_key]["terms"].map { |v| [v["term"], v["count"]] }] |
90 | 89 | end |
91 | - | |
92 | 90 | end | ... | ... |
test/highlight_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestHighlight < Minitest::Test | |
4 | - | |
3 | +class HighlightTest < Minitest::Test | |
5 | 4 | def test_basic |
6 | 5 | store_names ["Two Door Cinema Club"] |
7 | 6 | assert_equal "Two Door <em>Cinema</em> Club", Product.search("cinema", fields: [:name], highlight: true).with_details.first[1][:highlight][:name] |
... | ... | @@ -41,7 +40,7 @@ class TestHighlight < Minitest::Test |
41 | 40 | json = { |
42 | 41 | query: { |
43 | 42 | match: { |
44 | - _all: "cinema" | |
43 | + "name.analyzed" => "cinema" | |
45 | 44 | } |
46 | 45 | }, |
47 | 46 | highlight: { |
... | ... | @@ -54,5 +53,4 @@ class TestHighlight < Minitest::Test |
54 | 53 | } |
55 | 54 | assert_equal "Two Door <strong>Cinema</strong> Club", Product.search(json: json).with_details.first[1][:highlight][:"name.analyzed"] |
56 | 55 | end |
57 | - | |
58 | 56 | end | ... | ... |
test/index_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestIndex < Minitest::Test | |
4 | - | |
3 | +class IndexTest < Minitest::Test | |
5 | 4 | def test_clean_indices |
6 | 5 | old_index = Searchkick::Index.new("products_test_20130801000000000") |
7 | 6 | different_index = Searchkick::Index.new("items_test_20130801000000000") |
... | ... | @@ -93,14 +92,14 @@ class TestIndex < Minitest::Test |
93 | 92 | end |
94 | 93 | |
95 | 94 | def test_unsupported_version |
96 | - raises_exception = ->(s) { raise Elasticsearch::Transport::Transport::Error.new("[500] No query registered for [multi_match]") } | |
95 | + raises_exception = ->(_) { raise Elasticsearch::Transport::Transport::Error.new("[500] No query registered for [multi_match]") } | |
97 | 96 | Searchkick.client.stub :search, raises_exception do |
98 | 97 | assert_raises(Searchkick::UnsupportedVersionError) { Product.search("test") } |
99 | 98 | end |
100 | 99 | end |
101 | 100 | |
102 | 101 | def test_invalid_query |
103 | - assert_raises(Searchkick::InvalidQueryError) { Product.search(query: {}) } | |
102 | + assert_raises(Searchkick::InvalidQueryError) { Product.search(query: {boom: true}) } | |
104 | 103 | end |
105 | 104 | |
106 | 105 | def test_reindex |
... | ... | @@ -119,5 +118,4 @@ class TestIndex < Minitest::Test |
119 | 118 | end |
120 | 119 | |
121 | 120 | end |
122 | - | |
123 | 121 | end | ... | ... |
test/inheritance_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestInheritance < Minitest::Test | |
4 | - | |
3 | +class InheritanceTest < Minitest::Test | |
5 | 4 | def test_child_reindex |
6 | 5 | store_names ["Max"], Cat |
7 | 6 | assert Dog.reindex |
... | ... | @@ -76,5 +75,4 @@ class TestInheritance < Minitest::Test |
76 | 75 | store_names ["Product B"], Animal |
77 | 76 | assert_search "product", ["Product A", "Product B"], index_name: [Product.searchkick_index.name, Animal.searchkick_index.name], conversions: false |
78 | 77 | end |
79 | - | |
80 | 78 | end | ... | ... |
test/match_test.rb
... | ... | @@ -2,8 +2,7 @@ |
2 | 2 | |
3 | 3 | require_relative "test_helper" |
4 | 4 | |
5 | -class TestMatch < Minitest::Test | |
6 | - | |
5 | +class MatchTest < Minitest::Test | |
7 | 6 | # exact |
8 | 7 | |
9 | 8 | def test_match |
... | ... | @@ -195,5 +194,4 @@ class TestMatch < Minitest::Test |
195 | 194 | ] |
196 | 195 | assert_search "almond", [] |
197 | 196 | end |
198 | - | |
199 | 197 | end | ... | ... |
test/model_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestModel < Minitest::Test | |
4 | - | |
3 | +class ModelTest < Minitest::Test | |
5 | 4 | def test_disable_callbacks_model |
6 | 5 | store_names ["product a"] |
7 | 6 | |
... | ... | @@ -20,7 +19,7 @@ class TestModel < Minitest::Test |
20 | 19 | def test_disable_callbacks_global |
21 | 20 | # make sure callbacks default to on |
22 | 21 | assert Searchkick.callbacks? |
23 | - | |
22 | + | |
24 | 23 | store_names ["product a"] |
25 | 24 | |
26 | 25 | Searchkick.disable_callbacks |
... | ... | @@ -34,5 +33,4 @@ class TestModel < Minitest::Test |
34 | 33 | |
35 | 34 | assert_search "product", ["product a", "product b"] |
36 | 35 | end |
37 | - | |
38 | 36 | end | ... | ... |
test/query_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestQuery < Minitest::Test | |
4 | - | |
3 | +class QueryTest < Minitest::Test | |
5 | 4 | def test_basic |
6 | 5 | store_names ["Milk", "Apple"] |
7 | 6 | query = Product.search("milk", execute: false) |
... | ... | @@ -10,5 +9,4 @@ class TestQuery < Minitest::Test |
10 | 9 | query.body[:query] = {match_all: {}} |
11 | 10 | assert_equal ["Apple", "Milk"], query.execute.map(&:name).sort |
12 | 11 | end |
13 | - | |
14 | 12 | end | ... | ... |
test/reindex_job_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestReindexJob < Minitest::Test | |
4 | - | |
3 | +class ReindexJobTest < Minitest::Test | |
5 | 4 | def setup |
6 | 5 | super |
7 | 6 | Searchkick.disable_callbacks |
... | ... | @@ -29,5 +28,4 @@ class TestReindexJob < Minitest::Test |
29 | 28 | Product.searchkick_index.refresh |
30 | 29 | assert_search "*", [] |
31 | 30 | end |
32 | - | |
33 | 31 | end | ... | ... |
test/reindex_v2_job_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestReindexV2Job < Minitest::Test | |
4 | - | |
3 | +class ReindexV2JobTest < Minitest::Test | |
5 | 4 | def setup |
6 | 5 | skip unless defined?(ActiveJob) |
7 | 6 | super |
... | ... | @@ -30,5 +29,4 @@ class TestReindexV2Job < Minitest::Test |
30 | 29 | Product.searchkick_index.refresh |
31 | 30 | assert_search "*", [] |
32 | 31 | end |
33 | - | |
34 | 32 | end | ... | ... |
test/routing_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestRouting < Minitest::Test | |
4 | - | |
3 | +class RoutingTest < Minitest::Test | |
5 | 4 | def test_routing_query |
5 | + skip if elasticsearch2? | |
6 | 6 | query = Store.search("Dollar Tree", routing: "Dollar Tree", execute: false) |
7 | 7 | assert_equal query.params[:routing], "Dollar Tree" |
8 | 8 | end |
9 | 9 | |
10 | 10 | def test_routing_mappings |
11 | + skip if elasticsearch2? | |
11 | 12 | index_options = Store.searchkick_index.index_options |
12 | - assert_equal index_options[:mappings][:_default_][:_routing], {required: true, path: "name"} | |
13 | + assert_equal index_options[:mappings][:_default_][:_routing], required: true, path: "name" | |
13 | 14 | end |
14 | 15 | end | ... | ... |
test/should_index_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestShouldIndex < Minitest::Test | |
4 | - | |
3 | +class ShouldIndexTest < Minitest::Test | |
5 | 4 | def test_basic |
6 | 5 | store_names ["INDEX", "DO NOT INDEX"] |
7 | 6 | assert_search "index", ["INDEX"] |
... | ... | @@ -30,5 +29,4 @@ class TestShouldIndex < Minitest::Test |
30 | 29 | Product.searchkick_index.refresh |
31 | 30 | assert_search "index", [] |
32 | 31 | end |
33 | - | |
34 | 32 | end | ... | ... |
test/similar_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestSimilar < Minitest::Test | |
4 | - | |
3 | +class SimilarTest < Minitest::Test | |
5 | 4 | def test_similar |
6 | 5 | store_names ["Annie's Naturals Organic Shiitake & Sesame Dressing"] |
7 | 6 | assert_search "Annie's Naturals Shiitake & Sesame Vinaigrette", ["Annie's Naturals Organic Shiitake & Sesame Dressing"], similar: true |
... | ... | @@ -16,5 +15,4 @@ class TestSimilar < Minitest::Test |
16 | 15 | store_names ["Lucerne Milk Chocolate Fat Free", "Clover Fat Free Milk"] |
17 | 16 | assert_order "Lucerne Fat Free Chocolate Milk", ["Lucerne Milk Chocolate Fat Free", "Clover Fat Free Milk"], similar: true |
18 | 17 | end |
19 | - | |
20 | 18 | end | ... | ... |
test/sql_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestSql < Minitest::Test | |
4 | - | |
3 | +class SqlTest < Minitest::Test | |
5 | 4 | def test_limit |
6 | 5 | store_names ["Product A", "Product B", "Product C", "Product D"] |
7 | 6 | assert_order "product", ["Product A", "Product B"], order: {name: :asc}, limit: 2 |
... | ... | @@ -139,11 +138,11 @@ class TestSql < Minitest::Test |
139 | 138 | # https://gist.github.com/jprante/7099463 |
140 | 139 | def test_where_range_array |
141 | 140 | store [ |
142 | - {name: "Product A", user_ids: [11, 23, 13, 16, 17, 23.6]}, | |
143 | - {name: "Product B", user_ids: [1, 2, 3, 4, 5, 6, 7, 8, 8.9, 9.1, 9.4]}, | |
141 | + {name: "Product A", user_ids: [11, 23, 13, 16, 17, 23]}, | |
142 | + {name: "Product B", user_ids: [1, 2, 3, 4, 5, 6, 7, 8, 9]}, | |
144 | 143 | {name: "Product C", user_ids: [101, 230, 150, 200]} |
145 | 144 | ] |
146 | - assert_search "product", ["Product A"], where: {user_ids: {gt: 10, lt: 23.9}} | |
145 | + assert_search "product", ["Product A"], where: {user_ids: {gt: 10, lt: 24}} | |
147 | 146 | end |
148 | 147 | |
149 | 148 | def test_where_range_array_again |
... | ... | @@ -244,6 +243,40 @@ class TestSql < Minitest::Test |
244 | 243 | assert_search "aaaa", ["aabb"], misspellings: {distance: 2} |
245 | 244 | end |
246 | 245 | |
246 | + def test_misspellings_prefix_length | |
247 | + store_names ["ap", "api", "apt", "any", "nap", "ah", "ahi"] | |
248 | + assert_search "ap", ["ap"], misspellings: {prefix_length: 2} | |
249 | + assert_search "api", ["ap", "api", "apt"], misspellings: {prefix_length: 2} | |
250 | + end | |
251 | + | |
252 | + def test_misspellings_prefix_length_operator | |
253 | + store_names ["ap", "api", "apt", "any", "nap", "ah", "aha"] | |
254 | + assert_search "ap ah", ["ap", "ah"], operator: "or", misspellings: {prefix_length: 2} | |
255 | + assert_search "api ahi", ["ap", "api", "apt", "ah", "aha"], operator: "or", misspellings: {prefix_length: 2} | |
256 | + end | |
257 | + | |
258 | + def test_misspellings_fields_operator | |
259 | + store [ | |
260 | + {name: "red", color: "red"}, | |
261 | + {name: "blue", color: "blue"}, | |
262 | + {name: "cyan", color: "blue green"}, | |
263 | + {name: "magenta", color: "red blue"}, | |
264 | + {name: "green", color: "green"} | |
265 | + ] | |
266 | + assert_search "red blue", ["red", "blue", "cyan", "magenta"], operator: "or", fields: ["color"], misspellings: false | |
267 | + end | |
268 | + | |
269 | + def test_fields_operator | |
270 | + store [ | |
271 | + {name: "red", color: "red"}, | |
272 | + {name: "blue", color: "blue"}, | |
273 | + {name: "cyan", color: "blue green"}, | |
274 | + {name: "magenta", color: "red blue"}, | |
275 | + {name: "green", color: "green"} | |
276 | + ] | |
277 | + assert_search "red blue", ["red", "blue", "cyan", "magenta"], operator: "or", fields: ["color"] | |
278 | + end | |
279 | + | |
247 | 280 | def test_fields |
248 | 281 | store [ |
249 | 282 | {name: "red", color: "light blue"}, |
... | ... | @@ -267,9 +300,9 @@ class TestSql < Minitest::Test |
267 | 300 | |
268 | 301 | def test_big_decimal |
269 | 302 | store [ |
270 | - {name: "Product", latitude: 100.0} | |
303 | + {name: "Product", latitude: 80.0} | |
271 | 304 | ] |
272 | - assert_search "product", ["Product"], where: {latitude: {gt: 99}} | |
305 | + assert_search "product", ["Product"], where: {latitude: {gt: 79}} | |
273 | 306 | end |
274 | 307 | |
275 | 308 | # load |
... | ... | @@ -299,7 +332,7 @@ class TestSql < Minitest::Test |
299 | 332 | def test_select |
300 | 333 | store [{name: "Product A", store_id: 1}] |
301 | 334 | result = Product.search("product", load: false, select: [:name, :store_id]).first |
302 | - assert_equal %w[id name store_id], result.keys.reject { |k| k.start_with?("_") }.sort | |
335 | + assert_equal %w(id name store_id), result.keys.reject { |k| k.start_with?("_") }.sort | |
303 | 336 | assert_equal ["Product A"], result.name # this is not great |
304 | 337 | end |
305 | 338 | |
... | ... | @@ -329,5 +362,4 @@ class TestSql < Minitest::Test |
329 | 362 | assert Product.search("product", include: [:store]).first.association(:store).loaded? |
330 | 363 | end |
331 | 364 | end |
332 | - | |
333 | 365 | end | ... | ... |
test/suggest_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestSuggest < Minitest::Test | |
4 | - | |
3 | +class SuggestTest < Minitest::Test | |
5 | 4 | def test_basic |
6 | 5 | store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"] |
7 | 6 | assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark", fields: [:name] |
... | ... | @@ -78,5 +77,4 @@ class TestSuggest < Minitest::Test |
78 | 77 | def assert_suggest_all(term, expected, options = {}) |
79 | 78 | assert_equal expected.sort, Product.search(term, options.merge(suggest: true)).suggestions.sort |
80 | 79 | end |
81 | - | |
82 | 80 | end | ... | ... |
test/synonyms_test.rb
1 | 1 | require_relative "test_helper" |
2 | 2 | |
3 | -class TestSynonyms < Minitest::Test | |
4 | - | |
3 | +class SynonymsTest < Minitest::Test | |
5 | 4 | def test_bleach |
6 | 5 | store_names ["Clorox Bleach", "Kroger Bleach"] |
7 | 6 | assert_search "clorox", ["Clorox Bleach", "Kroger Bleach"] |
... | ... | @@ -46,5 +45,4 @@ class TestSynonyms < Minitest::Test |
46 | 45 | # store_names ["Creature", "Beast", "Dragon"], Animal |
47 | 46 | # assert_search "animal", ["Creature", "Beast"], {}, Animal |
48 | 47 | # end |
49 | - | |
50 | 48 | end | ... | ... |
test/test_helper.rb
... | ... | @@ -3,6 +3,7 @@ Bundler.require(:default) |
3 | 3 | require "minitest/autorun" |
4 | 4 | require "minitest/pride" |
5 | 5 | require "logger" |
6 | +require "active_support/core_ext" if defined?(NoBrainer) | |
6 | 7 | |
7 | 8 | ENV["RACK_ENV"] = "test" |
8 | 9 | |
... | ... | @@ -17,6 +18,10 @@ I18n.config.enforce_available_locales = true |
17 | 18 | |
18 | 19 | ActiveJob::Base.logger = nil if defined?(ActiveJob) |
19 | 20 | |
21 | +def elasticsearch2? | |
22 | + Searchkick.server_version.starts_with?("2.") | |
23 | +end | |
24 | + | |
20 | 25 | if defined?(Mongoid) |
21 | 26 | |
22 | 27 | def mongoid2? |
... | ... | @@ -96,7 +101,7 @@ elsif defined?(NoBrainer) |
96 | 101 | field :color, type: String |
97 | 102 | field :latitude |
98 | 103 | field :longitude |
99 | - field :description, type: String | |
104 | + field :description, type: String | |
100 | 105 | |
101 | 106 | belongs_to :store, validates: false |
102 | 107 | end |
... | ... | @@ -220,15 +225,15 @@ end |
220 | 225 | |
221 | 226 | class Store |
222 | 227 | searchkick \ |
223 | - routing: :name, | |
228 | + routing: elasticsearch2? ? false : "name", | |
224 | 229 | merge_mappings: true, |
225 | 230 | mappings: { |
226 | 231 | store: { |
227 | 232 | properties: { |
228 | - name: {type: "string", analyzer: "keyword"}, | |
233 | + name: {type: "string", analyzer: "keyword"} | |
229 | 234 | } |
230 | 235 | } |
231 | - } | |
236 | + } | |
232 | 237 | end |
233 | 238 | |
234 | 239 | class Animal |
... | ... | @@ -247,7 +252,6 @@ Store.reindex |
247 | 252 | Animal.reindex |
248 | 253 | |
249 | 254 | class Minitest::Test |
250 | - | |
251 | 255 | def setup |
252 | 256 | Product.destroy_all |
253 | 257 | Store.destroy_all |
... | ... | @@ -279,5 +283,4 @@ class Minitest::Test |
279 | 283 | def assert_first(term, expected, options = {}, klass = Product) |
280 | 284 | assert_equal expected, klass.search(term, options).map(&:name).first |
281 | 285 | end |
282 | - | |
283 | 286 | end | ... | ... |