tire 0.3.2 → 0.3.3
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.
- 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
|
|