tire 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/README.markdown +3 -1
- data/lib/tire.rb +2 -1
- data/lib/tire/configuration.rb +1 -1
- data/lib/tire/http/client.rb +51 -0
- data/lib/tire/http/clients/curb.rb +55 -0
- data/lib/tire/http/response.rb +23 -0
- data/lib/tire/index.rb +11 -17
- data/lib/tire/model/naming.rb +45 -4
- data/lib/tire/model/persistence.rb +6 -0
- data/lib/tire/model/search.rb +15 -7
- data/lib/tire/results/pagination.rb +9 -1
- data/lib/tire/search.rb +9 -9
- data/lib/tire/version.rb +7 -1
- data/test/integration/active_record_searchable_test.rb +9 -3
- data/test/integration/index_store_test.rb +10 -4
- data/test/integration/percolator_test.rb +1 -3
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/test_helper.rb +2 -2
- data/test/unit/configuration_test.rb +2 -2
- data/test/unit/http_client_test.rb +27 -0
- data/test/unit/http_response_test.rb +45 -0
- data/test/unit/index_test.rb +13 -17
- data/test/unit/model_persistence_test.rb +19 -1
- data/test/unit/model_search_test.rb +70 -0
- data/test/unit/results_collection_test.rb +7 -0
- data/test/unit/search_test.rb +7 -6
- data/test/unit/tire_test.rb +2 -2
- metadata +182 -228
- data/lib/tire/client.rb +0 -25
- data/test/unit/client_test.rb +0 -25
data/.gitignore
CHANGED
data/README.markdown
CHANGED
@@ -356,7 +356,7 @@ When you now save a record:
|
|
356
356
|
:published_on => Time.now
|
357
357
|
```
|
358
358
|
|
359
|
-
it is automatically added into
|
359
|
+
it is automatically added into an index called 'articles', because of the included callbacks.
|
360
360
|
(You may want to skip them in special cases, like when your records are indexed via some external
|
361
361
|
mechanism, let's say a _CouchDB_ or _RabbitMQ_
|
362
362
|
[river](http://www.elasticsearch.org/blog/2010/09/28/the_river.html).
|
@@ -687,6 +687,8 @@ Of course, not all validations or `ActionPack` helpers will be available to your
|
|
687
687
|
but if you can live with that, you've just got a schema-free, highly-scalable storage
|
688
688
|
and retrieval engine for your data.
|
689
689
|
|
690
|
+
This will result in Article instances being stored in an index called 'test_articles' when used in tests but in the index 'development_articles' when used in the development environment.
|
691
|
+
|
690
692
|
Please be sure to peruse the [integration test suite](https://github.com/karmi/tire/tree/master/test/integration)
|
691
693
|
for examples of the API and _ActiveModel_ integration usage.
|
692
694
|
|
data/lib/tire.rb
CHANGED
@@ -6,7 +6,8 @@ require 'tire/rubyext/hash'
|
|
6
6
|
require 'tire/rubyext/symbol'
|
7
7
|
require 'tire/logger'
|
8
8
|
require 'tire/configuration'
|
9
|
-
require 'tire/
|
9
|
+
require 'tire/http/response'
|
10
|
+
require 'tire/http/client'
|
10
11
|
require 'tire/search'
|
11
12
|
require 'tire/search/query'
|
12
13
|
require 'tire/search/sort'
|
data/lib/tire/configuration.rb
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Tire
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
|
5
|
+
module Client
|
6
|
+
|
7
|
+
class RestClient
|
8
|
+
|
9
|
+
def self.get(url, data=nil)
|
10
|
+
perform ::RestClient::Request.new(:method => :get, :url => url, :payload => data).execute
|
11
|
+
rescue Exception => e
|
12
|
+
Response.new e.http_body, e.http_code
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.post(url, data)
|
16
|
+
perform ::RestClient.post(url, data)
|
17
|
+
rescue Exception => e
|
18
|
+
Response.new e.http_body, e.http_code
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.put(url, data)
|
22
|
+
perform ::RestClient.put(url, data)
|
23
|
+
rescue Exception => e
|
24
|
+
Response.new e.http_body, e.http_code
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.delete(url)
|
28
|
+
perform ::RestClient.delete(url)
|
29
|
+
rescue Exception => e
|
30
|
+
Response.new e.http_body, e.http_code
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.head(url)
|
34
|
+
perform ::RestClient.head(url)
|
35
|
+
rescue Exception => e
|
36
|
+
Response.new e.http_body, e.http_code
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def self.perform(response)
|
42
|
+
Response.new response.body, response.code, response.headers
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'curb'
|
2
|
+
|
3
|
+
module Tire
|
4
|
+
|
5
|
+
module HTTP
|
6
|
+
|
7
|
+
module Client
|
8
|
+
|
9
|
+
class Curb
|
10
|
+
@client = ::Curl::Easy.new
|
11
|
+
# @client.verbose = true
|
12
|
+
|
13
|
+
def self.get(url, data=nil)
|
14
|
+
@client.url = url
|
15
|
+
@client.post_body = data
|
16
|
+
# FIXME: Curb cannot post bodies with GET requests?
|
17
|
+
# Roy Fielding seems to approve:
|
18
|
+
# <http://tech.groups.yahoo.com/group/rest-discuss/message/9962>
|
19
|
+
@client.http_post
|
20
|
+
Response.new @client.body_str, @client.response_code
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.post(url, data)
|
24
|
+
@client.url = url
|
25
|
+
@client.post_body = data
|
26
|
+
@client.http_post
|
27
|
+
Response.new @client.body_str, @client.response_code
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.put(url, data)
|
31
|
+
@client.url = url
|
32
|
+
@client.put_data = data
|
33
|
+
@client.http_put
|
34
|
+
Response.new @client.body_str, @client.response_code
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.delete(url)
|
38
|
+
@client.url = url
|
39
|
+
@client.http_delete
|
40
|
+
Response.new @client.body_str, @client.response_code
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.head(url)
|
44
|
+
@client.url = url
|
45
|
+
@client.http_head
|
46
|
+
Response.new @client.body_str, @client.response_code
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Tire
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
|
5
|
+
class Response
|
6
|
+
attr_reader :body, :code, :headers
|
7
|
+
|
8
|
+
def initialize(body, code, headers={})
|
9
|
+
@body, @code, @headers = body, code.to_i, headers
|
10
|
+
end
|
11
|
+
|
12
|
+
def success?
|
13
|
+
code > 0 && code < 400
|
14
|
+
end
|
15
|
+
|
16
|
+
def failure?
|
17
|
+
! success?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/tire/index.rb
CHANGED
@@ -9,30 +9,24 @@ module Tire
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def exists?
|
12
|
-
|
13
|
-
rescue Exception => error
|
14
|
-
false
|
12
|
+
Configuration.client.head("#{Configuration.url}/#{@name}").success?
|
15
13
|
end
|
16
14
|
|
17
15
|
def delete
|
18
|
-
# FIXME: RestClient does not return response for DELETE requests?
|
19
16
|
@response = Configuration.client.delete "#{Configuration.url}/#{@name}"
|
20
|
-
return @response.
|
21
|
-
rescue Exception => error
|
22
|
-
false
|
17
|
+
return @response.success?
|
23
18
|
ensure
|
24
19
|
curl = %Q|curl -X DELETE "#{Configuration.url}/#{@name}"|
|
25
|
-
logged(
|
20
|
+
logged(@response.body, 'DELETE', curl)
|
26
21
|
end
|
27
22
|
|
28
23
|
def create(options={})
|
29
24
|
@options = options
|
30
25
|
@response = Configuration.client.post "#{Configuration.url}/#{@name}", MultiJson.encode(options)
|
31
|
-
|
32
|
-
false
|
26
|
+
@response.success? ? @response : false
|
33
27
|
ensure
|
34
28
|
curl = %Q|curl -X POST "#{Configuration.url}/#{@name}" -d '#{MultiJson.encode(options)}'|
|
35
|
-
logged(
|
29
|
+
logged(@response.body, 'CREATE', curl)
|
36
30
|
end
|
37
31
|
|
38
32
|
def mapping
|
@@ -83,16 +77,16 @@ module Tire
|
|
83
77
|
count = 0
|
84
78
|
|
85
79
|
begin
|
86
|
-
Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
|
80
|
+
response = Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
|
81
|
+
raise RuntimeError, "#{response.code} > #{response.body}" if response.failure?
|
82
|
+
response
|
87
83
|
rescue Exception => error
|
88
84
|
if count < tries
|
89
85
|
count += 1
|
90
|
-
STDERR.puts "[ERROR] #{error.message}
|
86
|
+
STDERR.puts "[ERROR] #{error.message}, retrying (#{count})..."
|
91
87
|
retry
|
92
88
|
else
|
93
|
-
STDERR.puts "[ERROR] Too many exceptions occured, giving up
|
94
|
-
STDERR.puts "Response: #{error.http_body rescue nil}"
|
95
|
-
raise
|
89
|
+
STDERR.puts "[ERROR] Too many exceptions occured, giving up. The HTTP response was: #{error.message}"
|
96
90
|
end
|
97
91
|
ensure
|
98
92
|
curl = %Q|curl -X POST "#{Configuration.url}/_bulk" -d '{... data omitted ...}'|
|
@@ -135,7 +129,7 @@ module Tire
|
|
135
129
|
raise ArgumentError, "Please pass a document ID" unless id
|
136
130
|
|
137
131
|
result = Configuration.client.delete "#{Configuration.url}/#{@name}/#{type}/#{id}"
|
138
|
-
MultiJson.decode(result) if result
|
132
|
+
MultiJson.decode(result.body) if result.success?
|
139
133
|
end
|
140
134
|
|
141
135
|
def retrieve(type, id)
|
data/lib/tire/model/naming.rb
CHANGED
@@ -9,6 +9,9 @@ module Tire
|
|
9
9
|
|
10
10
|
# Get or set the index name for this model, based on arguments.
|
11
11
|
#
|
12
|
+
# By default, uses ActiveSupport inflection, so a class named `Article`
|
13
|
+
# will be stored in the `articles` index.
|
14
|
+
#
|
12
15
|
# To get the index name:
|
13
16
|
#
|
14
17
|
# Article.index_name
|
@@ -19,13 +22,51 @@ module Tire
|
|
19
22
|
#
|
20
23
|
def index_name name=nil
|
21
24
|
@index_name = name if name
|
22
|
-
@index_name || klass.model_name.plural
|
25
|
+
@index_name || [index_prefix, klass.model_name.plural].compact.join('_')
|
23
26
|
end
|
24
27
|
|
25
|
-
#
|
28
|
+
# Set or get index prefix for all models or for a specific model.
|
26
29
|
#
|
27
|
-
|
28
|
-
|
30
|
+
# To set the prefix for all models (preferably in an initializer inside Rails):
|
31
|
+
#
|
32
|
+
# Tire::Model::Search.index_prefix Rails.env
|
33
|
+
#
|
34
|
+
# To set the prefix for specific model:
|
35
|
+
#
|
36
|
+
# class Article
|
37
|
+
# # ...
|
38
|
+
# index_prefix 'my_prefix'
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# TODO: Maybe this would be more sane with ActiveSupport extensions such as `class_attribute`?
|
42
|
+
#
|
43
|
+
@@__index_prefix__ = nil
|
44
|
+
def index_prefix(*args)
|
45
|
+
# Uses class or instance variable depending on the context
|
46
|
+
if args.size > 0
|
47
|
+
value = args.pop
|
48
|
+
self.is_a?(Module) ? ( @@__index_prefix__ = value ) : ( @__index_prefix__ = value )
|
49
|
+
end
|
50
|
+
self.is_a?(Module) ? ( @@__index_prefix__ || nil ) : ( @__index_prefix__ || @@__index_prefix__ || nil )
|
51
|
+
end
|
52
|
+
extend self
|
53
|
+
|
54
|
+
# Get or set the document type for this model, based on arguments.
|
55
|
+
#
|
56
|
+
# By default, uses ActiveSupport inflection, so a class named `Article`
|
57
|
+
# will be stored as the `article` type.
|
58
|
+
#
|
59
|
+
# To get the document type:
|
60
|
+
#
|
61
|
+
# Article.document_type
|
62
|
+
#
|
63
|
+
# To set the document type:
|
64
|
+
#
|
65
|
+
# Article.document_type 'my-custom-type'
|
66
|
+
#
|
67
|
+
def document_type name=nil
|
68
|
+
@document_type = name if name
|
69
|
+
@document_type || klass.model_name.singular
|
29
70
|
end
|
30
71
|
end
|
31
72
|
|
@@ -44,6 +44,12 @@ module Tire
|
|
44
44
|
include Persistence::Attributes::InstanceMethods
|
45
45
|
|
46
46
|
include Persistence::Storage
|
47
|
+
|
48
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches'].each do |attr|
|
49
|
+
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
|
50
|
+
define_method("#{attr}") { @attributes[attr] }
|
51
|
+
end
|
52
|
+
|
47
53
|
end
|
48
54
|
|
49
55
|
end
|
data/lib/tire/model/search.rb
CHANGED
@@ -19,6 +19,12 @@ module Tire
|
|
19
19
|
#
|
20
20
|
module Search
|
21
21
|
|
22
|
+
# Alias for Tire::Model::Naming::ClassMethods.index_prefix
|
23
|
+
#
|
24
|
+
def self.index_prefix(*args)
|
25
|
+
Naming::ClassMethods.index_prefix(*args)
|
26
|
+
end
|
27
|
+
|
22
28
|
module ClassMethods
|
23
29
|
|
24
30
|
# Returns search results for a given query.
|
@@ -152,6 +158,14 @@ module Tire
|
|
152
158
|
end
|
153
159
|
end
|
154
160
|
|
161
|
+
def matches
|
162
|
+
@attributes['matches']
|
163
|
+
end
|
164
|
+
|
165
|
+
def matches=(value)
|
166
|
+
@attributes ||= {}; @attributes['matches'] = value
|
167
|
+
end
|
168
|
+
|
155
169
|
end
|
156
170
|
|
157
171
|
module Loader
|
@@ -192,13 +206,6 @@ module Tire
|
|
192
206
|
include Tire::Model::Percolate::InstanceMethods
|
193
207
|
include InstanceMethods
|
194
208
|
|
195
|
-
['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches'].each do |attr|
|
196
|
-
# TODO: Find a sane way to add attributes like _score for ActiveRecord -
|
197
|
-
# `define_attribute_methods [attr]` does not work in AR.
|
198
|
-
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
|
199
|
-
define_method("#{attr}") { @attributes[attr] }
|
200
|
-
end
|
201
|
-
|
202
209
|
INTERFACE = public_instance_methods.map(&:to_sym) - Object.public_instance_methods.map(&:to_sym)
|
203
210
|
|
204
211
|
attr_reader :instance
|
@@ -267,6 +274,7 @@ module Tire
|
|
267
274
|
Results::Item.send :include, Loader
|
268
275
|
end
|
269
276
|
|
277
|
+
|
270
278
|
end
|
271
279
|
|
272
280
|
end
|
@@ -7,8 +7,12 @@ module Tire
|
|
7
7
|
@total
|
8
8
|
end
|
9
9
|
|
10
|
+
def per_page
|
11
|
+
(@options[:per_page] || @options[:size] || 10 ).to_i
|
12
|
+
end
|
13
|
+
|
10
14
|
def total_pages
|
11
|
-
( @total.to_f /
|
15
|
+
( @total.to_f / per_page ).ceil
|
12
16
|
end
|
13
17
|
|
14
18
|
def current_page
|
@@ -27,6 +31,10 @@ module Tire
|
|
27
31
|
current_page < total_pages ? (current_page + 1) : nil
|
28
32
|
end
|
29
33
|
|
34
|
+
def offset
|
35
|
+
per_page * (current_page - 1)
|
36
|
+
end
|
37
|
+
|
30
38
|
def out_of_bounds?
|
31
39
|
current_page > total_pages
|
32
40
|
end
|
data/lib/tire/search.rb
CHANGED
@@ -68,14 +68,15 @@ module Tire
|
|
68
68
|
|
69
69
|
def perform
|
70
70
|
@response = Configuration.client.get(@url, self.to_json)
|
71
|
+
if @response.failure?
|
72
|
+
STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
|
73
|
+
return false
|
74
|
+
end
|
71
75
|
@json = MultiJson.decode(@response.body)
|
72
76
|
@results = Results::Collection.new(@json, @options)
|
73
|
-
self
|
74
|
-
rescue Exception => error
|
75
|
-
STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
|
76
|
-
raise
|
77
|
+
return self
|
77
78
|
ensure
|
78
|
-
logged
|
79
|
+
logged
|
79
80
|
end
|
80
81
|
|
81
82
|
def to_curl
|
@@ -104,21 +105,20 @@ module Tire
|
|
104
105
|
|
105
106
|
Configuration.logger.log_request '_search', indices, to_curl
|
106
107
|
|
107
|
-
code = @response ? @response.code : error.message
|
108
108
|
took = @json['took'] rescue nil
|
109
109
|
|
110
110
|
if Configuration.logger.level.to_s == 'debug'
|
111
111
|
# FIXME: Depends on RestClient implementation
|
112
|
-
body = if @
|
112
|
+
body = if @json
|
113
113
|
defined?(Yajl) ? Yajl::Encoder.encode(@json, :pretty => true) : MultiJson.encode(@json)
|
114
114
|
else
|
115
|
-
|
115
|
+
@response.body
|
116
116
|
end
|
117
117
|
else
|
118
118
|
body = ''
|
119
119
|
end
|
120
120
|
|
121
|
-
Configuration.logger.log_response code, took, body
|
121
|
+
Configuration.logger.log_response @response.code, took, body
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|