tire 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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