tire-erez 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +32 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +10 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +775 -0
  7. data/Rakefile +51 -0
  8. data/examples/rails-application-template.rb +263 -0
  9. data/examples/tire-dsl.rb +932 -0
  10. data/lib/tire.rb +59 -0
  11. data/lib/tire/alias.rb +296 -0
  12. data/lib/tire/configuration.rb +38 -0
  13. data/lib/tire/count.rb +85 -0
  14. data/lib/tire/dsl.rb +114 -0
  15. data/lib/tire/http/client.rb +62 -0
  16. data/lib/tire/http/clients/curb.rb +61 -0
  17. data/lib/tire/http/clients/faraday.rb +71 -0
  18. data/lib/tire/http/response.rb +27 -0
  19. data/lib/tire/index.rb +443 -0
  20. data/lib/tire/logger.rb +60 -0
  21. data/lib/tire/model/callbacks.rb +40 -0
  22. data/lib/tire/model/import.rb +27 -0
  23. data/lib/tire/model/indexing.rb +134 -0
  24. data/lib/tire/model/naming.rb +100 -0
  25. data/lib/tire/model/percolate.rb +99 -0
  26. data/lib/tire/model/persistence.rb +72 -0
  27. data/lib/tire/model/persistence/attributes.rb +148 -0
  28. data/lib/tire/model/persistence/finders.rb +54 -0
  29. data/lib/tire/model/persistence/storage.rb +77 -0
  30. data/lib/tire/model/search.rb +322 -0
  31. data/lib/tire/multi_search.rb +263 -0
  32. data/lib/tire/results/collection.rb +156 -0
  33. data/lib/tire/results/item.rb +94 -0
  34. data/lib/tire/results/pagination.rb +68 -0
  35. data/lib/tire/rubyext/hash.rb +8 -0
  36. data/lib/tire/rubyext/ruby_1_8.rb +1 -0
  37. data/lib/tire/rubyext/symbol.rb +11 -0
  38. data/lib/tire/rubyext/uri_escape.rb +74 -0
  39. data/lib/tire/search.rb +211 -0
  40. data/lib/tire/search/facet.rb +81 -0
  41. data/lib/tire/search/filter.rb +28 -0
  42. data/lib/tire/search/highlight.rb +37 -0
  43. data/lib/tire/search/queries/match.rb +40 -0
  44. data/lib/tire/search/query.rb +250 -0
  45. data/lib/tire/search/scan.rb +114 -0
  46. data/lib/tire/search/script_field.rb +23 -0
  47. data/lib/tire/search/sort.rb +25 -0
  48. data/lib/tire/tasks.rb +138 -0
  49. data/lib/tire/utils.rb +17 -0
  50. data/lib/tire/version.rb +18 -0
  51. data/test/fixtures/articles/1.json +1 -0
  52. data/test/fixtures/articles/2.json +1 -0
  53. data/test/fixtures/articles/3.json +1 -0
  54. data/test/fixtures/articles/4.json +1 -0
  55. data/test/fixtures/articles/5.json +1 -0
  56. data/test/integration/active_model_indexing_test.rb +51 -0
  57. data/test/integration/active_model_searchable_test.rb +114 -0
  58. data/test/integration/active_record_searchable_test.rb +620 -0
  59. data/test/integration/boolean_queries_test.rb +43 -0
  60. data/test/integration/boosting_queries_test.rb +32 -0
  61. data/test/integration/bulk_test.rb +86 -0
  62. data/test/integration/count_test.rb +64 -0
  63. data/test/integration/custom_score_queries_test.rb +89 -0
  64. data/test/integration/dis_max_queries_test.rb +68 -0
  65. data/test/integration/dsl_search_test.rb +30 -0
  66. data/test/integration/explanation_test.rb +44 -0
  67. data/test/integration/facets_test.rb +311 -0
  68. data/test/integration/filtered_queries_test.rb +66 -0
  69. data/test/integration/filters_test.rb +75 -0
  70. data/test/integration/fuzzy_queries_test.rb +20 -0
  71. data/test/integration/highlight_test.rb +64 -0
  72. data/test/integration/index_aliases_test.rb +122 -0
  73. data/test/integration/index_mapping_test.rb +43 -0
  74. data/test/integration/index_store_test.rb +112 -0
  75. data/test/integration/index_update_document_test.rb +121 -0
  76. data/test/integration/match_query_test.rb +79 -0
  77. data/test/integration/mongoid_searchable_test.rb +309 -0
  78. data/test/integration/multi_search_test.rb +114 -0
  79. data/test/integration/nested_query_test.rb +135 -0
  80. data/test/integration/percolator_test.rb +111 -0
  81. data/test/integration/persistent_model_test.rb +205 -0
  82. data/test/integration/prefix_query_test.rb +43 -0
  83. data/test/integration/query_return_version_test.rb +70 -0
  84. data/test/integration/query_string_test.rb +52 -0
  85. data/test/integration/range_queries_test.rb +36 -0
  86. data/test/integration/reindex_test.rb +56 -0
  87. data/test/integration/results_test.rb +58 -0
  88. data/test/integration/scan_test.rb +56 -0
  89. data/test/integration/script_fields_test.rb +38 -0
  90. data/test/integration/sort_test.rb +52 -0
  91. data/test/integration/text_query_test.rb +39 -0
  92. data/test/models/active_model_article.rb +31 -0
  93. data/test/models/active_model_article_with_callbacks.rb +49 -0
  94. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  95. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  96. data/test/models/active_record_models.rb +131 -0
  97. data/test/models/article.rb +15 -0
  98. data/test/models/mongoid_models.rb +85 -0
  99. data/test/models/persistent_article.rb +11 -0
  100. data/test/models/persistent_article_in_index.rb +16 -0
  101. data/test/models/persistent_article_in_namespace.rb +12 -0
  102. data/test/models/persistent_article_with_casting.rb +28 -0
  103. data/test/models/persistent_article_with_defaults.rb +12 -0
  104. data/test/models/persistent_article_with_percolation.rb +5 -0
  105. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  106. data/test/models/supermodel_article.rb +17 -0
  107. data/test/models/validated_model.rb +11 -0
  108. data/test/test_helper.rb +118 -0
  109. data/test/unit/active_model_lint_test.rb +17 -0
  110. data/test/unit/configuration_test.rb +84 -0
  111. data/test/unit/count_test.rb +67 -0
  112. data/test/unit/http_client_test.rb +79 -0
  113. data/test/unit/http_response_test.rb +49 -0
  114. data/test/unit/index_alias_test.rb +335 -0
  115. data/test/unit/index_test.rb +1098 -0
  116. data/test/unit/logger_test.rb +125 -0
  117. data/test/unit/model_callbacks_test.rb +116 -0
  118. data/test/unit/model_import_test.rb +75 -0
  119. data/test/unit/model_initialization_test.rb +31 -0
  120. data/test/unit/model_persistence_test.rb +548 -0
  121. data/test/unit/model_search_test.rb +964 -0
  122. data/test/unit/multi_search_test.rb +304 -0
  123. data/test/unit/results_collection_test.rb +372 -0
  124. data/test/unit/results_item_test.rb +173 -0
  125. data/test/unit/rubyext_test.rb +66 -0
  126. data/test/unit/search_facet_test.rb +186 -0
  127. data/test/unit/search_filter_test.rb +42 -0
  128. data/test/unit/search_highlight_test.rb +46 -0
  129. data/test/unit/search_query_test.rb +419 -0
  130. data/test/unit/search_scan_test.rb +113 -0
  131. data/test/unit/search_script_field_test.rb +26 -0
  132. data/test/unit/search_sort_test.rb +50 -0
  133. data/test/unit/search_test.rb +556 -0
  134. data/test/unit/tire_test.rb +144 -0
  135. data/tire.gemspec +83 -0
  136. metadata +586 -0
@@ -0,0 +1,263 @@
1
+ module Tire
2
+ module Search
3
+
4
+ module Multi
5
+
6
+ # Wraps the search definitions for Tire::Multi::Search
7
+ #
8
+ class SearchDefinitions
9
+ include Enumerable
10
+
11
+ attr_reader :names
12
+
13
+ def initialize
14
+ @names = []
15
+ @searches = []
16
+ end
17
+
18
+ def << value
19
+ @names << value[:name]
20
+ @searches << value[:search]
21
+ end
22
+
23
+ def [] name
24
+ @searches[ @names.index(name) ]
25
+ end
26
+
27
+ def each(&block)
28
+ @searches.each(&block)
29
+ end
30
+
31
+ def size
32
+ @searches.size
33
+ end
34
+
35
+ def to_a
36
+ @searches
37
+ end
38
+ end
39
+
40
+ # Wraps the search result sets for Tire::Multi::Search
41
+ #
42
+ class Results
43
+ include Enumerable
44
+
45
+ def initialize(searches, results)
46
+ @searches = searches
47
+ @results = results
48
+ @collection = @results.zip(@searches.to_a).map do |results, search|
49
+ Tire::Results::Collection.new(results, search.options)
50
+ end
51
+ end
52
+
53
+ # Return a specific result sets
54
+ def [] name
55
+ if index = @searches.names.index(name)
56
+ @collection[ index ]
57
+ end
58
+ end
59
+
60
+ def each(&block)
61
+ @collection.each(&block)
62
+ end
63
+
64
+ def each_pair(&block)
65
+ @searches.names.zip(@collection).each(&block)
66
+ end
67
+
68
+ def size
69
+ @results.size
70
+ end
71
+
72
+ # Returns the multi-search result sets as a Hash with the search name
73
+ # as key and the results as value.
74
+ #
75
+ def to_hash
76
+ result = {}
77
+ each_pair { |name,results| result[name] = results }
78
+ result
79
+ end
80
+ end
81
+
82
+ # Build and perform a [multi-search](http://elasticsearch.org/guide/reference/api/multi-search.html)
83
+ # request.
84
+ #
85
+ # s = Tire::Search::Multi::Search.new 'my-index' do
86
+ # search :names do
87
+ # query { match :name, 'john' }
88
+ # end
89
+ # search :counts, search_type: 'count' do
90
+ # query { match :_all, 'john' }
91
+ # end
92
+ # search :other, index: 'other-index' do
93
+ # query { string "first_name:john" }
94
+ # end
95
+ # end
96
+ #
97
+ # You can optionally pass an index and type to the constructor, using them as defaults
98
+ # for searches which don't define them.
99
+ #
100
+ # Use the {#search} method to add a search definition to the request, passing it search options
101
+ # as a Hash and the search definition itself using Tire's DSL.
102
+ #
103
+ class Search
104
+
105
+ attr_reader :indices, :types, :path
106
+
107
+ def initialize(indices=nil, options={}, &block)
108
+ @indices = Array(indices)
109
+ @types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
110
+ @options = options
111
+ @path = ['/', @indices.join(','), @types.join(','), '_msearch'].compact.join('/').squeeze('/')
112
+ @searches = Tire::Search::Multi::SearchDefinitions.new
113
+
114
+ block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
115
+ end
116
+
117
+ # Add a search definition to the multi-search request.
118
+ #
119
+ # The usual search options such as `search_type`, `routing`, etc. can be passed as a Hash,
120
+ # and the search definition itself should be passed as a block using the Tire DSL.
121
+ #
122
+ def search(*args, &block)
123
+ name_or_options = args.pop
124
+
125
+ if name_or_options.is_a?(Hash)
126
+ options = name_or_options
127
+ name = args.pop
128
+ else
129
+ name = name_or_options
130
+ end
131
+
132
+ name ||= @searches.size
133
+ options ||= {}
134
+ indices = options.delete(:index) || options.delete(:indices)
135
+
136
+ @searches << { :name => name, :search => Tire::Search::Search.new(indices, options, &block) }
137
+ end
138
+
139
+ # Without argument, returns the collection of search definitions.
140
+ # With argument, returns a search definition by name or order.
141
+ #
142
+ def searches(name=nil)
143
+ name ? @searches[ name ] : @searches
144
+ end
145
+
146
+ # Returns and array of search definition names
147
+ #
148
+ def names
149
+ @searches.names
150
+ end
151
+
152
+ # Serializes the search definitions as an array of JSON definitions
153
+ #
154
+ def to_array
155
+ @searches.map do |search|
156
+ header = {}
157
+ header.update(:index => search.indices.join(',')) unless search.indices.empty?
158
+ header.update(:type => search.types.join(',')) unless search.types.empty?
159
+ header.update(:search_type => search.options[:search_type]) if search.options[:search_type]
160
+ header.update(:routing => search.options[:routing]) if search.options[:routing]
161
+ header.update(:preference => search.options[:preference]) if search.options[:preference]
162
+ body = search.to_hash
163
+ [ header, body ]
164
+ end.flatten
165
+ end
166
+
167
+ # Serializes the search definitions as a multi-line string payload
168
+ #
169
+ def to_payload
170
+ to_array.map { |line| MultiJson.encode(line) }.join("\n") + "\n"
171
+ end
172
+
173
+ # Returns the request URL
174
+ #
175
+ def url
176
+ [ Configuration.url, @path ].join
177
+ end
178
+
179
+ # Serializes the request URL parameters
180
+ #
181
+ def params
182
+ options = @options.dup
183
+ options.empty? ? '' : '?' + options.to_param
184
+ end
185
+
186
+ # Returns an enumerable collection of result sets.
187
+ #
188
+ # You can simply iterate over them:
189
+ #
190
+ # search.results.each do |results|
191
+ # puts results.each.map(&:name)
192
+ # end
193
+ #
194
+ # To iterate over named result sets, use the `each_pair` method:
195
+ #
196
+ # search.results.each_pair do |name,results|
197
+ # puts "Search #{name} got #{results.size} results"
198
+ # end
199
+ #
200
+ # To get a specific result set:
201
+ #
202
+ # search.results[:myname]
203
+ #
204
+ def results
205
+ @results || perform and @results
206
+ end
207
+
208
+ # Returns the HTTP response
209
+ #
210
+ def response
211
+ @response || perform and @response
212
+ end
213
+
214
+ # Returns the raw JSON as a Hash
215
+ #
216
+ def json
217
+ @json || perform and @json
218
+ end
219
+
220
+ def perform
221
+ @responses = Configuration.client.get(url + params, to_payload)
222
+ if @responses.failure?
223
+ STDERR.puts "[REQUEST FAILED] #{to_curl}\n"
224
+ raise SearchRequestFailed, @responses.to_s
225
+ end
226
+ @json = MultiJson.decode(@responses.body)
227
+ @results = Tire::Search::Multi::Results.new @searches, @json['responses']
228
+ return self
229
+ ensure
230
+ logged
231
+ end
232
+
233
+ def to_curl
234
+ %Q|curl -X GET '#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty' -d '\n#{to_payload}'|
235
+ end
236
+
237
+ def logged(endpoint='_msearch')
238
+ if Configuration.logger
239
+
240
+ Configuration.logger.log_request endpoint, indices, to_curl
241
+
242
+ took = @json['took'] rescue nil
243
+ code = @response.code rescue nil
244
+
245
+ if Configuration.logger.level.to_s == 'debug'
246
+ body = if @json
247
+ MultiJson.encode( @json, :pretty => Configuration.pretty)
248
+ else
249
+ MultiJson.encode( MultiJson.load(@response.body), :pretty => Configuration.pretty) rescue ''
250
+ end
251
+ else
252
+ body = ''
253
+ end
254
+
255
+ Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
256
+ end
257
+ end
258
+ end
259
+
260
+ end
261
+
262
+ end
263
+ end
@@ -0,0 +1,156 @@
1
+ module Tire
2
+ module Results
3
+
4
+ class Collection
5
+ include Enumerable
6
+ include Pagination
7
+
8
+ attr_reader :time, :total, :options, :facets, :max_score
9
+
10
+ def initialize(response, options={})
11
+ @response = response
12
+ @options = options
13
+ @time = response['took'].to_i
14
+ @total = response['hits']['total'].to_i rescue nil
15
+ @facets = response['facets']
16
+ @max_score = response['hits']['max_score'].to_f rescue nil
17
+ @wrapper = options[:wrapper] || Configuration.wrapper
18
+ end
19
+
20
+ def results
21
+ return [] if failure?
22
+ @results ||= begin
23
+ hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
24
+ unless @options[:load]
25
+ __get_results_without_load(hits)
26
+ else
27
+ __get_results_with_load(hits)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Iterates over the `results` collection
33
+ #
34
+ def each(&block)
35
+ results.each(&block)
36
+ end
37
+
38
+ # Iterates over the `results` collection and yields
39
+ # the `result` object (Item or model instance) and the
40
+ # `hit` -- raw Elasticsearch response parsed as a Hash
41
+ #
42
+ def each_with_hit(&block)
43
+ results.zip(@response['hits']['hits']).each(&block)
44
+ end
45
+
46
+ def empty?
47
+ results.empty?
48
+ end
49
+
50
+ def size
51
+ results.size
52
+ end
53
+ alias :length :size
54
+
55
+ def slice(*args)
56
+ results.slice(*args)
57
+ end
58
+ alias :[] :slice
59
+
60
+ def to_ary
61
+ self
62
+ end
63
+
64
+ def as_json(options=nil)
65
+ to_a.map { |item| item.as_json(options) }
66
+ end
67
+
68
+ def error
69
+ @response['error']
70
+ end
71
+
72
+ def success?
73
+ error.to_s.empty?
74
+ end
75
+
76
+ def failure?
77
+ ! success?
78
+ end
79
+
80
+ # Handles _source prefixed fields properly: strips the prefix and converts fields to nested Hashes
81
+ #
82
+ def __parse_fields__(fields={})
83
+ ( fields ||= {} ).clone.each_pair do |key,value|
84
+ next unless key.to_s =~ /_source/ # Skip regular JSON immediately
85
+
86
+ keys = key.to_s.split('.').reject { |n| n == '_source' }
87
+ fields.delete(key)
88
+
89
+ result = {}
90
+ path = []
91
+
92
+ keys.each do |name|
93
+ path << name
94
+ eval "result[:#{path.join('][:')}] ||= {}"
95
+ eval "result[:#{path.join('][:')}] = #{value.inspect}" if keys.last == name
96
+ end
97
+ fields.update result
98
+ end
99
+ fields
100
+ end
101
+
102
+ def __get_results_without_load(hits)
103
+ if @wrapper == Hash
104
+ hits
105
+ else
106
+ hits.map do |h|
107
+ document = {}
108
+
109
+ # Update the document with content and ID
110
+ document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
111
+ document.update( {'id' => h['_id']} )
112
+
113
+ # Update the document with meta information
114
+ ['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
115
+
116
+ # Return an instance of the "wrapper" class
117
+ @wrapper.new(document)
118
+ end
119
+ end
120
+ end
121
+
122
+ def __get_results_with_load(hits)
123
+ return [] if hits.empty?
124
+
125
+ records = {}
126
+ @response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
127
+ raise NoMethodError, "You have tried to eager load the model instances, " +
128
+ "but Tire cannot find the model class because " +
129
+ "document has no _type property." unless type
130
+
131
+ begin
132
+ klass = type.camelize.constantize
133
+ rescue NameError => e
134
+ raise NameError, "You have tried to eager load the model instances, but " +
135
+ "Tire cannot find the model class '#{type.camelize}' " +
136
+ "based on _type '#{type}'.", e.backtrace
137
+ end
138
+
139
+ records[type] = __find_records_by_ids klass, items.map { |h| h['_id'] }
140
+ end
141
+
142
+ # Reorder records to preserve the order from search results
143
+ @response['hits']['hits'].map do |item|
144
+ records[item['_type']].detect do |record|
145
+ record.id.to_s == item['_id'].to_s
146
+ end
147
+ end
148
+ end
149
+
150
+ def __find_records_by_ids(klass, ids)
151
+ @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
152
+ end
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,94 @@
1
+ module Tire
2
+ module Results
3
+
4
+ class Item
5
+ extend ActiveModel::Naming
6
+ include ActiveModel::Conversion
7
+
8
+ # Create new instance, recursively converting all Hashes to Item
9
+ # and leaving everything else alone.
10
+ #
11
+ def initialize(args={})
12
+ raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair)
13
+ @attributes = {}
14
+ args.each_pair do |key, value|
15
+ if value.is_a?(Array)
16
+ @attributes[key.to_sym] = value.map { |item| @attributes[key.to_sym] = item.is_a?(Hash) ? Item.new(item.to_hash) : item }
17
+ else
18
+ @attributes[key.to_sym] = value.is_a?(Hash) ? Item.new(value.to_hash) : value
19
+ end
20
+ end
21
+ end
22
+
23
+ # Delegate method to a key in underlying hash, if present, otherwise return +nil+.
24
+ #
25
+ def method_missing(method_name, *arguments)
26
+ @attributes[method_name.to_sym]
27
+ end
28
+
29
+ def respond_to?(method_name, include_private = false)
30
+ @attributes.has_key?(method_name.to_sym) || super
31
+ end
32
+
33
+ def [](key)
34
+ @attributes[key.to_sym]
35
+ end
36
+
37
+ def id
38
+ @attributes[:_id] || @attributes[:id]
39
+ end
40
+
41
+ def type
42
+ @attributes[:_type] || @attributes[:type]
43
+ end
44
+
45
+ def persisted?
46
+ !!id
47
+ end
48
+
49
+ def errors
50
+ ActiveModel::Errors.new(self)
51
+ end
52
+
53
+ def valid?
54
+ true
55
+ end
56
+
57
+ def to_key
58
+ persisted? ? [id] : nil
59
+ end
60
+
61
+ def to_hash
62
+ @attributes.reduce({}) do |sum, item|
63
+ sum[ item.first ] = item.last.respond_to?(:to_hash) ? item.last.to_hash : item.last
64
+ sum
65
+ end
66
+ end
67
+
68
+ def as_json(options=nil)
69
+ hash = to_hash
70
+ hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access.as_json(options) : hash.as_json(options)
71
+ end
72
+
73
+ def to_json(options=nil)
74
+ as_json.to_json(options)
75
+ end
76
+ alias_method :to_indexed_json, :to_json
77
+
78
+ # Let's pretend we're someone else in Rails
79
+ #
80
+ def class
81
+ defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super
82
+ rescue NameError
83
+ super
84
+ end
85
+
86
+ def inspect
87
+ s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" }
88
+ %Q|<Item#{self.class.to_s == 'Tire::Results::Item' ? '' : " (#{self.class})"} #{s.join(', ')}>|
89
+ end
90
+
91
+ end
92
+
93
+ end
94
+ end