load_balanced_tire 0.1

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.
Files changed (121) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +29 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.markdown +760 -0
  6. data/Rakefile +78 -0
  7. data/examples/rails-application-template.rb +249 -0
  8. data/examples/tire-dsl.rb +876 -0
  9. data/lib/tire.rb +55 -0
  10. data/lib/tire/alias.rb +296 -0
  11. data/lib/tire/configuration.rb +30 -0
  12. data/lib/tire/dsl.rb +43 -0
  13. data/lib/tire/http/client.rb +62 -0
  14. data/lib/tire/http/clients/curb.rb +61 -0
  15. data/lib/tire/http/clients/faraday.rb +71 -0
  16. data/lib/tire/http/response.rb +27 -0
  17. data/lib/tire/index.rb +361 -0
  18. data/lib/tire/logger.rb +60 -0
  19. data/lib/tire/model/callbacks.rb +40 -0
  20. data/lib/tire/model/import.rb +26 -0
  21. data/lib/tire/model/indexing.rb +128 -0
  22. data/lib/tire/model/naming.rb +100 -0
  23. data/lib/tire/model/percolate.rb +99 -0
  24. data/lib/tire/model/persistence.rb +71 -0
  25. data/lib/tire/model/persistence/attributes.rb +143 -0
  26. data/lib/tire/model/persistence/finders.rb +66 -0
  27. data/lib/tire/model/persistence/storage.rb +69 -0
  28. data/lib/tire/model/search.rb +307 -0
  29. data/lib/tire/results/collection.rb +114 -0
  30. data/lib/tire/results/item.rb +86 -0
  31. data/lib/tire/results/pagination.rb +54 -0
  32. data/lib/tire/rubyext/hash.rb +8 -0
  33. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  34. data/lib/tire/rubyext/symbol.rb +11 -0
  35. data/lib/tire/search.rb +188 -0
  36. data/lib/tire/search/facet.rb +74 -0
  37. data/lib/tire/search/filter.rb +28 -0
  38. data/lib/tire/search/highlight.rb +37 -0
  39. data/lib/tire/search/query.rb +186 -0
  40. data/lib/tire/search/scan.rb +114 -0
  41. data/lib/tire/search/script_field.rb +23 -0
  42. data/lib/tire/search/sort.rb +25 -0
  43. data/lib/tire/tasks.rb +135 -0
  44. data/lib/tire/utils.rb +17 -0
  45. data/lib/tire/version.rb +22 -0
  46. data/test/fixtures/articles/1.json +1 -0
  47. data/test/fixtures/articles/2.json +1 -0
  48. data/test/fixtures/articles/3.json +1 -0
  49. data/test/fixtures/articles/4.json +1 -0
  50. data/test/fixtures/articles/5.json +1 -0
  51. data/test/integration/active_model_indexing_test.rb +51 -0
  52. data/test/integration/active_model_searchable_test.rb +114 -0
  53. data/test/integration/active_record_searchable_test.rb +446 -0
  54. data/test/integration/boolean_queries_test.rb +43 -0
  55. data/test/integration/count_test.rb +34 -0
  56. data/test/integration/custom_score_queries_test.rb +88 -0
  57. data/test/integration/dis_max_queries_test.rb +68 -0
  58. data/test/integration/dsl_search_test.rb +22 -0
  59. data/test/integration/explanation_test.rb +44 -0
  60. data/test/integration/facets_test.rb +259 -0
  61. data/test/integration/filtered_queries_test.rb +66 -0
  62. data/test/integration/filters_test.rb +63 -0
  63. data/test/integration/fuzzy_queries_test.rb +20 -0
  64. data/test/integration/highlight_test.rb +64 -0
  65. data/test/integration/index_aliases_test.rb +122 -0
  66. data/test/integration/index_mapping_test.rb +43 -0
  67. data/test/integration/index_store_test.rb +96 -0
  68. data/test/integration/index_update_document_test.rb +111 -0
  69. data/test/integration/mongoid_searchable_test.rb +309 -0
  70. data/test/integration/percolator_test.rb +111 -0
  71. data/test/integration/persistent_model_test.rb +130 -0
  72. data/test/integration/prefix_query_test.rb +43 -0
  73. data/test/integration/query_return_version_test.rb +70 -0
  74. data/test/integration/query_string_test.rb +52 -0
  75. data/test/integration/range_queries_test.rb +36 -0
  76. data/test/integration/reindex_test.rb +46 -0
  77. data/test/integration/results_test.rb +39 -0
  78. data/test/integration/scan_test.rb +56 -0
  79. data/test/integration/script_fields_test.rb +38 -0
  80. data/test/integration/sort_test.rb +36 -0
  81. data/test/integration/text_query_test.rb +39 -0
  82. data/test/models/active_model_article.rb +31 -0
  83. data/test/models/active_model_article_with_callbacks.rb +49 -0
  84. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  85. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  86. data/test/models/active_record_models.rb +122 -0
  87. data/test/models/article.rb +15 -0
  88. data/test/models/mongoid_models.rb +97 -0
  89. data/test/models/persistent_article.rb +11 -0
  90. data/test/models/persistent_article_in_namespace.rb +12 -0
  91. data/test/models/persistent_article_with_casting.rb +28 -0
  92. data/test/models/persistent_article_with_defaults.rb +11 -0
  93. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  94. data/test/models/supermodel_article.rb +17 -0
  95. data/test/models/validated_model.rb +11 -0
  96. data/test/test_helper.rb +93 -0
  97. data/test/unit/active_model_lint_test.rb +17 -0
  98. data/test/unit/configuration_test.rb +74 -0
  99. data/test/unit/http_client_test.rb +76 -0
  100. data/test/unit/http_response_test.rb +49 -0
  101. data/test/unit/index_alias_test.rb +275 -0
  102. data/test/unit/index_test.rb +894 -0
  103. data/test/unit/logger_test.rb +125 -0
  104. data/test/unit/model_callbacks_test.rb +116 -0
  105. data/test/unit/model_import_test.rb +71 -0
  106. data/test/unit/model_persistence_test.rb +528 -0
  107. data/test/unit/model_search_test.rb +913 -0
  108. data/test/unit/results_collection_test.rb +281 -0
  109. data/test/unit/results_item_test.rb +162 -0
  110. data/test/unit/rubyext_test.rb +66 -0
  111. data/test/unit/search_facet_test.rb +153 -0
  112. data/test/unit/search_filter_test.rb +42 -0
  113. data/test/unit/search_highlight_test.rb +46 -0
  114. data/test/unit/search_query_test.rb +301 -0
  115. data/test/unit/search_scan_test.rb +113 -0
  116. data/test/unit/search_script_field_test.rb +26 -0
  117. data/test/unit/search_sort_test.rb +50 -0
  118. data/test/unit/search_test.rb +499 -0
  119. data/test/unit/tire_test.rb +126 -0
  120. data/tire.gemspec +90 -0
  121. metadata +549 -0
@@ -0,0 +1,114 @@
1
+ module Tire
2
+ module Results
3
+
4
+ class Collection
5
+ include Enumerable
6
+ include Pagination
7
+
8
+ attr_reader :time, :total, :options, :facets
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
15
+ @facets = response['facets']
16
+ @wrapper = options[:wrapper] || Configuration.wrapper
17
+ end
18
+
19
+ def results
20
+ @results ||= begin
21
+ hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
22
+
23
+ unless @options[:load]
24
+ if @wrapper == Hash
25
+ hits
26
+ else
27
+ hits.map do |h|
28
+ document = {}
29
+
30
+ # Update the document with content and ID
31
+ document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( __parse_fields__(h['fields']) )
32
+ document.update( {'id' => h['_id']} )
33
+
34
+ # Update the document with meta information
35
+ ['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
36
+
37
+ # Return an instance of the "wrapper" class
38
+ @wrapper.new(document)
39
+ end
40
+ end
41
+
42
+ else
43
+ return [] if hits.empty?
44
+
45
+ records = {}
46
+ @response['hits']['hits'].group_by { |item| item['_type'] }.each do |type, items|
47
+ raise NoMethodError, "You have tried to eager load the model instances, " +
48
+ "but Tire cannot find the model class because " +
49
+ "document has no _type property." unless type
50
+
51
+ begin
52
+ klass = type.camelize.constantize
53
+ rescue NameError => e
54
+ raise NameError, "You have tried to eager load the model instances, but " +
55
+ "Tire cannot find the model class '#{type.camelize}' " +
56
+ "based on _type '#{type}'.", e.backtrace
57
+ end
58
+ ids = items.map { |h| h['_id'] }
59
+ records[type] = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
60
+ end
61
+
62
+ # Reorder records to preserve order from search results
63
+ @response['hits']['hits'].map { |item| records[item['_type']].detect { |record| record.id.to_s == item['_id'].to_s } }
64
+ end
65
+ end
66
+ end
67
+
68
+ def each(&block)
69
+ results.each(&block)
70
+ end
71
+
72
+ def empty?
73
+ results.empty?
74
+ end
75
+
76
+ def size
77
+ results.size
78
+ end
79
+ alias :length :size
80
+
81
+ def [](index)
82
+ results[index]
83
+ end
84
+
85
+ def to_ary
86
+ self
87
+ end
88
+
89
+ # Handles _source prefixed fields properly: strips the prefix and converts fields to nested Hashes
90
+ #
91
+ def __parse_fields__(fields={})
92
+ ( fields ||= {} ).clone.each_pair do |key,value|
93
+ next unless key.to_s =~ /_source/ # Skip regular JSON immediately
94
+
95
+ keys = key.to_s.split('.').reject { |n| n == '_source' }
96
+ fields.delete(key)
97
+
98
+ result = {}
99
+ path = []
100
+
101
+ keys.each do |name|
102
+ path << name
103
+ eval "result[:#{path.join('][:')}] ||= {}"
104
+ eval "result[:#{path.join('][:')}] = #{value.inspect}" if keys.last == name
105
+ end
106
+ fields.update result
107
+ end
108
+ fields
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,86 @@
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,
24
+ # otherwise return +nil+.
25
+ #
26
+ def method_missing(method_name, *arguments)
27
+ @attributes.has_key?(method_name.to_sym) ? @attributes[method_name.to_sym] : nil
28
+ end
29
+
30
+ def [](key)
31
+ @attributes[key.to_sym]
32
+ end
33
+
34
+ def id
35
+ @attributes[:_id] || @attributes[:id]
36
+ end
37
+
38
+ def type
39
+ @attributes[:_type] || @attributes[:type]
40
+ end
41
+
42
+ def persisted?
43
+ !!id
44
+ end
45
+
46
+ def errors
47
+ ActiveModel::Errors.new(self)
48
+ end
49
+
50
+ def valid?
51
+ true
52
+ end
53
+
54
+ def to_key
55
+ persisted? ? [id] : nil
56
+ end
57
+
58
+ def to_hash
59
+ @attributes.reduce({}) do |sum, item|
60
+ sum[ item.first ] = item.last.respond_to?(:to_hash) ? item.last.to_hash : item.last
61
+ sum
62
+ end
63
+ end
64
+
65
+ # Let's pretend we're someone else in Rails
66
+ #
67
+ def class
68
+ defined?(::Rails) && @attributes[:_type] ? @attributes[:_type].camelize.constantize : super
69
+ rescue NameError
70
+ super
71
+ end
72
+
73
+ def inspect
74
+ s = []; @attributes.each { |k,v| s << "#{k}: #{v.inspect}" }
75
+ %Q|<Item#{self.class.to_s == 'Tire::Results::Item' ? '' : " (#{self.class})"} #{s.join(', ')}>|
76
+ end
77
+
78
+ def to_json(options=nil)
79
+ @attributes.to_json(options)
80
+ end
81
+ alias_method :to_indexed_json, :to_json
82
+
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,54 @@
1
+ module Tire
2
+ module Results
3
+
4
+ # Adds support for WillPaginate and Kaminari
5
+ #
6
+ module Pagination
7
+
8
+ def total_entries
9
+ @total
10
+ end
11
+
12
+ def per_page
13
+ (@options[:per_page] || @options[:size] || 10 ).to_i
14
+ end
15
+
16
+ def total_pages
17
+ ( @total.to_f / per_page ).ceil
18
+ end
19
+
20
+ def current_page
21
+ if @options[:page]
22
+ @options[:page].to_i
23
+ else
24
+ (per_page + @options[:from].to_i) / per_page
25
+ end
26
+ end
27
+
28
+ def previous_page
29
+ current_page > 1 ? (current_page - 1) : nil
30
+ end
31
+
32
+ def next_page
33
+ current_page < total_pages ? (current_page + 1) : nil
34
+ end
35
+
36
+ def offset
37
+ per_page * (current_page - 1)
38
+ end
39
+
40
+ def out_of_bounds?
41
+ current_page > total_pages
42
+ end
43
+
44
+ # Kaminari support
45
+ #
46
+ alias :limit_value :per_page
47
+ alias :total_count :total_entries
48
+ alias :num_pages :total_pages
49
+ alias :offset_value :offset
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,8 @@
1
+ class Hash
2
+
3
+ def to_json(options=nil)
4
+ MultiJson.encode(self)
5
+ end unless respond_to?(:to_json)
6
+
7
+ alias_method :to_indexed_json, :to_json
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+
3
+ # Require URI escape/unescape compatibility layer from Rack
4
+ #
5
+ # See <http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html#method-c-encode_www_form_component>
6
+ #
7
+ require 'rack/backports/uri/common_18'
@@ -0,0 +1,11 @@
1
+ # ActiveModel::Serialization Ruby < 1.9.x compatibility
2
+
3
+ class Symbol
4
+ def <=> other
5
+ self.to_s <=> other.to_s
6
+ end unless method_defined?(:'<=>')
7
+
8
+ def capitalize
9
+ to_s.capitalize
10
+ end unless method_defined?(:capitalize)
11
+ end
@@ -0,0 +1,188 @@
1
+ module Tire
2
+ module Search
3
+ class SearchRequestFailed < StandardError; end
4
+
5
+ class Search
6
+
7
+ attr_reader :indices, :query, :facets, :filters, :options, :explain, :script_fields
8
+
9
+ def initialize(indices=nil, options={}, &block)
10
+ if indices.is_a?(Hash)
11
+ set_indices_options(indices)
12
+ @indices = indices.keys
13
+ else
14
+ @indices = Array(indices)
15
+ end
16
+ @types = Array(options.delete(:type)).map { |type| Utils.escape(type) }
17
+ @options = options
18
+
19
+ @path = ['/', @indices.join(','), @types.join(','), '_search'].compact.join('/').squeeze('/')
20
+
21
+ block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
22
+ end
23
+
24
+
25
+ def set_indices_options(indices)
26
+ indices.each do |index, index_options|
27
+ if index_options[:boost]
28
+ @indices_boost ||= {}
29
+ @indices_boost[index] = index_options[:boost]
30
+ end
31
+ end
32
+ end
33
+
34
+ def results
35
+ @results || (perform; @results)
36
+ end
37
+
38
+ def response
39
+ @response || (perform; @response)
40
+ end
41
+
42
+ def json
43
+ @json || (perform; @json)
44
+ end
45
+
46
+ def url
47
+ Configuration.url + @path
48
+ end
49
+
50
+ def params
51
+ @options.empty? ? '' : '?' + @options.to_param
52
+ end
53
+
54
+ def query(&block)
55
+ @query = Query.new
56
+ block.arity < 1 ? @query.instance_eval(&block) : block.call(@query)
57
+ self
58
+ end
59
+
60
+ def sort(&block)
61
+ @sort = Sort.new(&block).to_ary
62
+ self
63
+ end
64
+
65
+ def facet(name, options={}, &block)
66
+ @facets ||= {}
67
+ @facets.update Facet.new(name, options, &block).to_hash
68
+ self
69
+ end
70
+
71
+ def filter(type, *options)
72
+ @filters ||= []
73
+ @filters << Filter.new(type, *options).to_hash
74
+ self
75
+ end
76
+
77
+ def script_field(name, options={})
78
+ @script_fields ||= {}
79
+ @script_fields.merge! ScriptField.new(name, options).to_hash
80
+ self
81
+ end
82
+
83
+ def highlight(*args)
84
+ unless args.empty?
85
+ @highlight = Highlight.new(*args)
86
+ self
87
+ else
88
+ @highlight
89
+ end
90
+ end
91
+
92
+ def from(value)
93
+ @from = value
94
+ @options[:from] = value
95
+ self
96
+ end
97
+
98
+ def size(value)
99
+ @size = value
100
+ @options[:size] = value
101
+ self
102
+ end
103
+
104
+ def fields(*fields)
105
+ @fields = Array(fields.flatten)
106
+ self
107
+ end
108
+
109
+ def explain(value)
110
+ @explain = value
111
+ self
112
+ end
113
+
114
+ def version(value)
115
+ @version = value
116
+ self
117
+ end
118
+
119
+ def perform
120
+ @response = Configuration.client.get(self.url + self.params, self.to_json)
121
+ if @response.failure?
122
+ STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
123
+ raise SearchRequestFailed, @response.to_s
124
+ end
125
+ @json = MultiJson.decode(@response.body)
126
+ @results = Results::Collection.new(@json, @options)
127
+ return self
128
+ ensure
129
+ logged
130
+ end
131
+
132
+ def to_curl
133
+ %Q|curl -X GET "#{url}#{params.empty? ? '?' : params.to_s + '&'}pretty=true" -d '#{to_json}'|
134
+ end
135
+
136
+ def to_hash
137
+ @options.delete(:payload) || begin
138
+ request = {}
139
+ request.update( { :indices_boost => @indices_boost } ) if @indices_boost
140
+ request.update( { :query => @query.to_hash } ) if @query
141
+ request.update( { :sort => @sort.to_ary } ) if @sort
142
+ request.update( { :facets => @facets.to_hash } ) if @facets
143
+ request.update( { :filter => @filters.first.to_hash } ) if @filters && @filters.size == 1
144
+ request.update( { :filter => { :and => @filters.map {|filter| filter.to_hash} } } ) if @filters && @filters.size > 1
145
+ request.update( { :highlight => @highlight.to_hash } ) if @highlight
146
+ request.update( { :size => @size } ) if @size
147
+ request.update( { :from => @from } ) if @from
148
+ request.update( { :fields => @fields } ) if @fields
149
+ request.update( { :script_fields => @script_fields } ) if @script_fields
150
+ request.update( { :version => @version } ) if @version
151
+ request.update( { :explain => @explain } ) if @explain
152
+ request
153
+ end
154
+ end
155
+
156
+ def to_json
157
+ payload = to_hash
158
+ # TODO: Remove when deprecated interface is removed
159
+ payload.is_a?(String) ? payload : payload.to_json
160
+ end
161
+
162
+ def logged(error=nil)
163
+ if Configuration.logger
164
+
165
+ Configuration.logger.log_request '_search', indices, to_curl
166
+
167
+ took = @json['took'] rescue nil
168
+ code = @response.code rescue nil
169
+
170
+ if Configuration.logger.level.to_s == 'debug'
171
+ # FIXME: Depends on RestClient implementation
172
+ body = if @json
173
+ defined?(Yajl) ? Yajl::Encoder.encode(@json, :pretty => true) : MultiJson.encode(@json)
174
+ else
175
+ @response.body rescue nil
176
+ end
177
+ else
178
+ body = ''
179
+ end
180
+
181
+ Configuration.logger.log_response code || 'N/A', took || 'N/A', body || 'N/A'
182
+ end
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+ end