tire 0.1.0

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 (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,35 @@
1
+ require 'rest_client'
2
+ require 'yajl/json_gem'
3
+ require 'active_model'
4
+
5
+ require 'tire/rubyext/hash'
6
+ require 'tire/rubyext/symbol'
7
+ require 'tire/logger'
8
+ require 'tire/configuration'
9
+ require 'tire/client'
10
+ require 'tire/client'
11
+ require 'tire/search'
12
+ require 'tire/search/query'
13
+ require 'tire/search/sort'
14
+ require 'tire/search/facet'
15
+ require 'tire/search/filter'
16
+ require 'tire/search/highlight'
17
+ require 'tire/results/pagination'
18
+ require 'tire/results/collection'
19
+ require 'tire/results/item'
20
+ require 'tire/index'
21
+ require 'tire/dsl'
22
+ require 'tire/model/naming'
23
+ require 'tire/model/callbacks'
24
+ require 'tire/model/search'
25
+ require 'tire/model/indexing'
26
+ require 'tire/model/import'
27
+ require 'tire/model/persistence/finders'
28
+ require 'tire/model/persistence/attributes'
29
+ require 'tire/model/persistence/storage'
30
+ require 'tire/model/persistence'
31
+ require 'tire/tasks'
32
+
33
+ module Tire
34
+ extend DSL
35
+ end
@@ -0,0 +1,40 @@
1
+ module Tire
2
+
3
+ module Client
4
+
5
+ class Base
6
+ def get(url)
7
+ raise_no_method_error
8
+ end
9
+ def post(url, data)
10
+ raise_no_method_error
11
+ end
12
+ def put(url, data)
13
+ raise NoMethodError, "Implement this method in your client class"
14
+ end
15
+ def delete(url)
16
+ raise_no_method_error
17
+ end
18
+ def raise_no_method_error
19
+ raise NoMethodError, "Implement this method in your client class"
20
+ end
21
+ end
22
+
23
+ class RestClient < Base
24
+ def self.get(url)
25
+ ::RestClient.get url
26
+ end
27
+ def self.post(url, data)
28
+ ::RestClient.post url, data
29
+ end
30
+ def self.put(url, data)
31
+ ::RestClient.put url, data
32
+ end
33
+ def self.delete(url)
34
+ ::RestClient.delete url rescue nil
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,29 @@
1
+ module Tire
2
+
3
+ class Configuration
4
+
5
+ def self.url(value=nil)
6
+ @url = value || @url || "http://localhost:9200"
7
+ end
8
+
9
+ def self.client(klass=nil)
10
+ @client = klass || @client || Client::RestClient
11
+ end
12
+
13
+ def self.wrapper(klass=nil)
14
+ @wrapper = klass || @wrapper || Results::Item
15
+ end
16
+
17
+ def self.logger(device=nil, options={})
18
+ return @logger = Logger.new(device, options) if device
19
+ @logger || nil
20
+ end
21
+
22
+ def self.reset(*properties)
23
+ reset_variables = properties.empty? ? instance_variables : instance_variables & properties.map { |p| "@#{p}" }
24
+ reset_variables.each { |v| instance_variable_set(v, nil) }
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,33 @@
1
+ module Tire
2
+ module DSL
3
+
4
+ def configure(&block)
5
+ Configuration.class_eval(&block)
6
+ end
7
+
8
+ def search(indices, options={}, &block)
9
+ if block_given?
10
+ Search::Search.new(indices, options, &block).perform
11
+ else
12
+ payload = case options
13
+ when Hash then options.to_json
14
+ when String then options
15
+ else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
16
+ end
17
+
18
+ response = Configuration.client.post( "#{Configuration.url}/#{indices}/_search", payload)
19
+ json = Yajl::Parser.parse(response.body)
20
+ results = Results::Collection.new(json, options)
21
+ end
22
+ rescue Exception => error
23
+ STDERR.puts "[REQUEST FAILED] #{error.class} #{error.http_body rescue nil}\n"
24
+ raise
25
+ ensure
26
+ end
27
+
28
+ def index(name, &block)
29
+ Index.new(name, &block)
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,209 @@
1
+ module Tire
2
+ class Index
3
+
4
+ attr_reader :name
5
+
6
+ def initialize(name, &block)
7
+ @name = name
8
+ instance_eval(&block) if block_given?
9
+ end
10
+
11
+ def exists?
12
+ !!Configuration.client.get("#{Configuration.url}/#{@name}/_status")
13
+ rescue Exception => error
14
+ false
15
+ end
16
+
17
+ def delete
18
+ # FIXME: RestClient does not return response for DELETE requests?
19
+ @response = Configuration.client.delete "#{Configuration.url}/#{@name}"
20
+ return @response.body =~ /error/ ? false : true
21
+ rescue Exception => error
22
+ false
23
+ ensure
24
+ curl = %Q|curl -X DELETE "#{Configuration.url}/#{@name}"|
25
+ logged(error, 'DELETE', curl)
26
+ end
27
+
28
+ def create(options={})
29
+ @options = options
30
+ @response = Configuration.client.post "#{Configuration.url}/#{@name}", Yajl::Encoder.encode(options)
31
+ rescue Exception => error
32
+ false
33
+ ensure
34
+ curl = %Q|curl -X POST "#{Configuration.url}/#{@name}" -d '#{Yajl::Encoder.encode(options, :pretty => true)}'|
35
+ logged(error, 'CREATE', curl)
36
+ end
37
+
38
+ def mapping
39
+ @response = Configuration.client.get("#{Configuration.url}/#{@name}/_mapping")
40
+ JSON.parse(@response.body)[@name]
41
+ end
42
+
43
+ def store(*args)
44
+ # TODO: Infer type from the document (hash property, method)
45
+
46
+ if args.size > 1
47
+ (type, document = args)
48
+ else
49
+ (document = args.pop; type = :document)
50
+ end
51
+
52
+ old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
53
+ id = case true
54
+ when document.is_a?(Hash) then document[:id] || document['id']
55
+ when document.respond_to?(:id) && document.id != document.object_id then document.id
56
+ end
57
+ $VERBOSE = old_verbose
58
+
59
+ document = case true
60
+ when document.is_a?(String) then document
61
+ when document.respond_to?(:to_indexed_json) then document.to_indexed_json
62
+ else raise ArgumentError, "Please pass a JSON string or object with a 'to_indexed_json' method"
63
+ end
64
+
65
+ url = id ? "#{Configuration.url}/#{@name}/#{type}/#{id}" : "#{Configuration.url}/#{@name}/#{type}/"
66
+
67
+ @response = Configuration.client.post url, document
68
+ JSON.parse(@response.body)
69
+
70
+ rescue Exception => error
71
+ raise
72
+ ensure
73
+ curl = %Q|curl -X POST "#{url}" -d '#{document}'|
74
+ logged(error, "/#{@name}/#{type}/", curl)
75
+ end
76
+
77
+ def bulk_store documents
78
+ create unless exists?
79
+
80
+ payload = documents.map do |document|
81
+ old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
82
+ id = case
83
+ when document.is_a?(Hash) then document[:id] || document['id']
84
+ when document.respond_to?(:id) && document.id != document.object_id then document.id
85
+ # TODO: Raise error when no id present
86
+ end
87
+ $VERBOSE = old_verbose
88
+
89
+ type = case
90
+ when document.is_a?(Hash) then document[:type] || document['type']
91
+ when document.respond_to?(:document_type) then document.document_type
92
+ end || 'document'
93
+
94
+ output = []
95
+ output << %Q|{"index":{"_index":"#{@name}","_type":"#{type}","_id":"#{id}"}}|
96
+ output << document.to_indexed_json
97
+ output.join("\n")
98
+ end
99
+ payload << ""
100
+
101
+ tries = 5
102
+ count = 0
103
+
104
+ begin
105
+ # STDERR.puts "Posting payload..."
106
+ # STDERR.puts payload.join("\n")
107
+ Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
108
+ rescue Exception => error
109
+ if count < tries
110
+ count += 1
111
+ STDERR.puts "[ERROR] #{error.message}:#{error.http_body rescue nil}, retrying (#{count})..."
112
+ retry
113
+ else
114
+ STDERR.puts "[ERROR] Too many exceptions occured, giving up..."
115
+ STDERR.puts "Response: #{error.http_body rescue nil}"
116
+ raise
117
+ end
118
+ ensure
119
+ curl = %Q|curl -X POST "#{Configuration.url}/_bulk" -d '{... data omitted ...}'|
120
+ logged(error, 'BULK', curl)
121
+ end
122
+ end
123
+
124
+ def import(klass_or_collection, method=nil, options={})
125
+ # p [klass_or_collection, method, options]
126
+
127
+ case
128
+
129
+ when method
130
+ options = {:page => 1, :per_page => 1000}.merge options
131
+ while documents = klass_or_collection.send(method.to_sym, options.merge(:page => options[:page])) \
132
+ and not documents.empty?
133
+ documents = yield documents if block_given?
134
+
135
+ bulk_store documents
136
+ options[:page] += 1
137
+ end
138
+
139
+ when klass_or_collection.respond_to?(:map)
140
+ documents = block_given? ? yield(klass_or_collection) : klass_or_collection
141
+ bulk_store documents
142
+ else
143
+ raise ArgumentError, "Please pass either a collection of objects, "+
144
+ "or method for fetching records, or Enumerable compatible class"
145
+ end
146
+ end
147
+
148
+ def remove(*args)
149
+ # TODO: Infer type from the document (hash property, method)
150
+
151
+ if args.size > 1
152
+ (type, document = args)
153
+ else
154
+ (document = args.pop; type = :document)
155
+ end
156
+
157
+ old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
158
+ id = case true
159
+ when document.is_a?(Hash) then document[:id] || document['id']
160
+ when document.respond_to?(:id) && document.id != document.object_id then document.id
161
+ else document
162
+ end
163
+ $VERBOSE = old_verbose
164
+
165
+ result = Configuration.client.delete "#{Configuration.url}/#{@name}/#{type}/#{id}"
166
+ JSON.parse(result) if result
167
+ end
168
+
169
+ def retrieve(type, id)
170
+ @response = Configuration.client.get "#{Configuration.url}/#{@name}/#{type}/#{id}"
171
+ h = JSON.parse(@response.body)
172
+ if Configuration.wrapper == Hash then h
173
+ else
174
+ document = {}
175
+ document = h['_source'] ? document.update( h['_source'] ) : document.update( h['fields'] )
176
+ document.update('id' => h['_id'], '_version' => h['_version'])
177
+ Configuration.wrapper.new(document)
178
+ end
179
+ end
180
+
181
+ def refresh
182
+ @response = Configuration.client.post "#{Configuration.url}/#{@name}/_refresh", ''
183
+ rescue Exception => error
184
+ raise
185
+ ensure
186
+ curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_refresh"|
187
+ logged(error, '_refresh', curl)
188
+ end
189
+
190
+ def logged(error=nil, endpoint='/', curl='')
191
+ if Configuration.logger
192
+
193
+ Configuration.logger.log_request endpoint, @name, curl
194
+
195
+ code = @response ? @response.code : error.message rescue 200
196
+
197
+ if Configuration.logger.level.to_s == 'debug'
198
+ # FIXME: Depends on RestClient implementation
199
+ body = @response ? Yajl::Encoder.encode(@response.body, :pretty => true) : error.http_body rescue ''
200
+ else
201
+ body = ''
202
+ end
203
+
204
+ Configuration.logger.log_response code, nil, body
205
+ end
206
+ end
207
+
208
+ end
209
+ end
@@ -0,0 +1,60 @@
1
+ module Tire
2
+ class Logger
3
+
4
+ def initialize(device, options={})
5
+ @device = if device.respond_to?(:write)
6
+ device
7
+ else
8
+ File.open(device, 'a')
9
+ end
10
+ @device.sync = true
11
+ @options = options
12
+ at_exit { @device.close unless @device.closed? }
13
+ end
14
+
15
+ def level
16
+ @options[:level] || 'info'
17
+ end
18
+
19
+ def write(message)
20
+ @device.write message
21
+ end
22
+
23
+ def log_request(endpoint, params=nil, curl='')
24
+ # 2001-02-12 18:20:42:32 [_search] (articles,users)
25
+ #
26
+ # curl -X POST ....
27
+ #
28
+ content = "# #{time}"
29
+ content += " [#{endpoint}]"
30
+ content += " (#{params.inspect})" if params
31
+ content += "\n#\n"
32
+ content += curl
33
+ content += "\n\n"
34
+ write content
35
+ end
36
+
37
+ def log_response(status, took=nil, json='')
38
+ # 2001-02-12 18:20:42:32 [200] (4 msec)
39
+ #
40
+ # {
41
+ # "took" : 4,
42
+ # "hits" : [...]
43
+ # ...
44
+ # }
45
+ #
46
+ content = "# #{time}"
47
+ content += " [#{status}]"
48
+ content += " (#{took} msec)" if took
49
+ content += "\n#\n" unless json == ''
50
+ json.each_line { |line| content += "# #{line}" } unless json == ''
51
+ content += "\n\n"
52
+ write content
53
+ end
54
+
55
+ def time
56
+ Time.now.strftime('%Y-%m-%d %H:%M:%S:%L')
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,23 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Callbacks
5
+
6
+ def self.included(base)
7
+ if base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
8
+ base.send :after_save, :update_elastic_search_index
9
+ base.send :after_destroy, :update_elastic_search_index
10
+ end
11
+
12
+ if base.respond_to?(:before_destroy) && !base.respond_to?(:destroyed?)
13
+ base.class_eval do
14
+ before_destroy { @destroyed = true }
15
+ def destroyed?; !!@destroyed; end
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module Tire
2
+ module Model
3
+
4
+ module Import
5
+
6
+ module ClassMethods
7
+
8
+ def import options={}, &block
9
+ method = options.delete(:method) || 'paginate'
10
+ self.index.import self, method, options, &block
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end