tire 0.4.0.pre → 0.4.0.rc
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +6 -10
- data/examples/rails-application-template.rb +6 -6
- data/examples/tire-dsl.rb +10 -9
- data/lib/tire.rb +8 -0
- data/lib/tire/dsl.rb +7 -6
- data/lib/tire/index.rb +22 -12
- data/lib/tire/model/import.rb +3 -2
- data/lib/tire/model/indexing.rb +21 -13
- data/lib/tire/model/naming.rb +2 -1
- data/lib/tire/model/persistence.rb +1 -1
- data/lib/tire/results/collection.rb +20 -17
- data/lib/tire/results/item.rb +1 -1
- data/lib/tire/rubyext/ruby_1_8.rb +7 -0
- data/lib/tire/search.rb +33 -19
- data/lib/tire/search/facet.rb +5 -0
- data/lib/tire/search/query.rb +8 -3
- data/lib/tire/tasks.rb +47 -14
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +13 -2
- data/test/integration/active_model_indexing_test.rb +1 -0
- data/test/integration/active_model_searchable_test.rb +7 -5
- data/test/integration/active_record_searchable_test.rb +159 -72
- data/test/integration/count_test.rb +34 -0
- data/test/integration/dsl_search_test.rb +22 -0
- data/test/integration/explanation_test.rb +44 -0
- data/test/integration/facets_test.rb +15 -0
- data/test/integration/fuzzy_queries_test.rb +20 -0
- data/test/integration/mongoid_searchable_test.rb +1 -0
- data/test/integration/persistent_model_test.rb +22 -1
- data/test/integration/text_query_test.rb +17 -3
- data/test/models/active_record_models.rb +43 -1
- data/test/models/mongoid_models.rb +0 -1
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/supermodel_article.rb +5 -10
- data/test/test_helper.rb +16 -2
- data/test/unit/index_test.rb +90 -16
- data/test/unit/model_import_test.rb +4 -4
- data/test/unit/model_search_test.rb +13 -10
- data/test/unit/results_collection_test.rb +6 -0
- data/test/unit/results_item_test.rb +8 -0
- data/test/unit/search_facet_test.rb +9 -0
- data/test/unit/search_query_test.rb +36 -7
- data/test/unit/search_test.rb +70 -1
- data/test/unit/tire_test.rb +23 -12
- data/tire.gemspec +11 -8
- metadata +90 -48
data/README.markdown
CHANGED
@@ -19,9 +19,9 @@ Installation
|
|
19
19
|
|
20
20
|
OK. First, you need a running _ElasticSearch_ server. Thankfully, it's easy. Let's define easy:
|
21
21
|
|
22
|
-
$ curl -k -L -o elasticsearch-0.
|
23
|
-
$ tar -zxvf elasticsearch-0.
|
24
|
-
$ ./elasticsearch-0.
|
22
|
+
$ curl -k -L -o elasticsearch-0.19.0.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.19.0.tar.gz
|
23
|
+
$ tar -zxvf elasticsearch-0.19.0.tar.gz
|
24
|
+
$ ./elasticsearch-0.19.0/bin/elasticsearch -f
|
25
25
|
|
26
26
|
See, easy. On a Mac, you can also use _Homebrew_:
|
27
27
|
|
@@ -295,7 +295,7 @@ If configuring the search payload with blocks feels somehow too weak for you, yo
|
|
295
295
|
a plain old Ruby `Hash` (or JSON string) with the query declaration to the `search` method:
|
296
296
|
|
297
297
|
```ruby
|
298
|
-
Tire.search 'articles', :query => { :
|
298
|
+
Tire.search 'articles', :query => { :prefix => { :title => 'fou' } }
|
299
299
|
```
|
300
300
|
|
301
301
|
If this sounds like a great idea to you, you are probably able to write your application
|
@@ -688,21 +688,17 @@ Well, things stay mostly the same:
|
|
688
688
|
include Tire::Model::Search
|
689
689
|
include Tire::Model::Callbacks
|
690
690
|
|
691
|
-
# Let's use a different index name so stuff doesn't get mixed up.
|
692
|
-
#
|
693
|
-
index_name 'mongo-articles'
|
694
|
-
|
695
691
|
# These Mongo guys sure do get funky with their IDs in +serializable_hash+, let's fix it.
|
696
692
|
#
|
697
693
|
def to_indexed_json
|
698
|
-
self.
|
694
|
+
self.as_json
|
699
695
|
end
|
700
696
|
|
701
697
|
end
|
702
698
|
|
703
699
|
Article.create :title => 'I Love ElasticSearch'
|
704
700
|
|
705
|
-
Article.search 'love'
|
701
|
+
Article.tire.search 'love'
|
706
702
|
```
|
707
703
|
|
708
704
|
_Tire_ does not care what's your primary data storage solution, if it has an _ActiveModel_-compatible
|
@@ -62,7 +62,7 @@ file ".gitignore", <<-END.gsub(/ /, '')
|
|
62
62
|
tmp/**/*
|
63
63
|
config/database.yml
|
64
64
|
db/*.sqlite3
|
65
|
-
vendor/elasticsearch-0.
|
65
|
+
vendor/elasticsearch-0.19.0/
|
66
66
|
END
|
67
67
|
|
68
68
|
git :init
|
@@ -71,11 +71,11 @@ git :commit => "-m 'Initial commit: Clean application'"
|
|
71
71
|
|
72
72
|
unless (RestClient.get('http://localhost:9200') rescue false)
|
73
73
|
COMMAND = <<-COMMAND.gsub(/^ /, '')
|
74
|
-
curl -k -L -# -o elasticsearch-0.
|
75
|
-
"http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.
|
76
|
-
tar -zxf elasticsearch-0.
|
77
|
-
rm -f
|
78
|
-
./elasticsearch-0.
|
74
|
+
curl -k -L -# -o elasticsearch-0.19.0.tar.gz \
|
75
|
+
"http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.19.0.tar.gz"
|
76
|
+
tar -zxf elasticsearch-0.19.0.tar.gz
|
77
|
+
rm -f elasticsearch-0.19.0.tar.gz
|
78
|
+
./elasticsearch-0.19.0/bin/elasticsearch -p #{destination_root}/tmp/pids/elasticsearch.pid
|
79
79
|
COMMAND
|
80
80
|
|
81
81
|
puts "\n"
|
data/examples/tire-dsl.rb
CHANGED
@@ -45,9 +45,9 @@ require 'tire'
|
|
45
45
|
|
46
46
|
[ERROR] You don’t appear to have ElasticSearch installed. Please install and launch it with the following commands:
|
47
47
|
|
48
|
-
curl -k -L -o elasticsearch-0.
|
49
|
-
tar -zxvf elasticsearch-0.
|
50
|
-
./elasticsearch-0.
|
48
|
+
curl -k -L -o elasticsearch-0.19.0.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.19.0.tar.gz
|
49
|
+
tar -zxvf elasticsearch-0.19.0.tar.gz
|
50
|
+
./elasticsearch-0.19.0/bin/elasticsearch -f
|
51
51
|
INSTALL
|
52
52
|
|
53
53
|
### Storing and indexing documents
|
@@ -319,7 +319,7 @@ Tire.configure do
|
|
319
319
|
# # 2011-04-24 11:34:01:150 [CREATE] ("articles")
|
320
320
|
# #
|
321
321
|
# curl -X POST "http://localhost:9200/articles"
|
322
|
-
#
|
322
|
+
#
|
323
323
|
# # 2011-04-24 11:34:01:152 [200]
|
324
324
|
#
|
325
325
|
logger 'elasticsearch.log'
|
@@ -481,6 +481,7 @@ end
|
|
481
481
|
# * [terms](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
|
482
482
|
# * [bool](http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html)
|
483
483
|
# * [custom_score](http://www.elasticsearch.org/guide/reference/query-dsl/custom-score-query.html)
|
484
|
+
# * [fuzzy](http://www.elasticsearch.org/guide/reference/query-dsl/fuzzy-query.html)
|
484
485
|
# * [all](http://www.elasticsearch.org/guide/reference/query-dsl/match-all-query.html)
|
485
486
|
# * [ids](http://www.elasticsearch.org/guide/reference/query-dsl/ids-query.html)
|
486
487
|
|
@@ -606,10 +607,10 @@ end
|
|
606
607
|
# are returned.
|
607
608
|
#
|
608
609
|
s = Tire.search 'articles' do
|
609
|
-
|
610
|
+
|
610
611
|
# We will use just the same **query** as before.
|
611
612
|
#
|
612
|
-
query { string 'title:T*' }
|
613
|
+
query { string 'title:T*' }
|
613
614
|
|
614
615
|
# But we will add a _terms_ **filter** based on tags.
|
615
616
|
#
|
@@ -671,7 +672,7 @@ s = Tire.search 'articles' do
|
|
671
672
|
|
672
673
|
# We will search for articles tagged “ruby”, again, ...
|
673
674
|
#
|
674
|
-
query { string 'tags:ruby' }
|
675
|
+
query { string 'tags:ruby' }
|
675
676
|
|
676
677
|
# ... but will sort them by their `title`, in descending order.
|
677
678
|
#
|
@@ -694,7 +695,7 @@ s = Tire.search 'articles' do
|
|
694
695
|
|
695
696
|
# We will just get all articles in this case.
|
696
697
|
#
|
697
|
-
query { all }
|
698
|
+
query { all }
|
698
699
|
|
699
700
|
sort do
|
700
701
|
|
@@ -728,7 +729,7 @@ end
|
|
728
729
|
s = Tire.search 'articles' do
|
729
730
|
|
730
731
|
# Let's search for documents containing word “Two” in their titles,
|
731
|
-
query { string 'title:Two' }
|
732
|
+
query { string 'title:Two' }
|
732
733
|
|
733
734
|
# and instruct _ElasticSearch_ to highlight relevant snippets.
|
734
735
|
#
|
data/lib/tire.rb
CHANGED
@@ -2,9 +2,17 @@ require 'rest_client'
|
|
2
2
|
require 'multi_json'
|
3
3
|
require 'active_model'
|
4
4
|
require 'hashr'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
require 'active_support/core_ext/object/to_param'
|
8
|
+
require 'active_support/core_ext/object/to_query'
|
9
|
+
|
10
|
+
# Ruby 1.8 compatibility
|
11
|
+
require 'tire/rubyext/ruby_1_8' if defined?(RUBY_VERSION) && RUBY_VERSION < '1.9'
|
5
12
|
|
6
13
|
require 'tire/rubyext/hash'
|
7
14
|
require 'tire/rubyext/symbol'
|
15
|
+
require 'tire/utils'
|
8
16
|
require 'tire/logger'
|
9
17
|
require 'tire/configuration'
|
10
18
|
require 'tire/http/response'
|
data/lib/tire/dsl.rb
CHANGED
@@ -10,15 +10,16 @@ module Tire
|
|
10
10
|
Search::Search.new(indices, options, &block)
|
11
11
|
else
|
12
12
|
payload = case options
|
13
|
-
when Hash then
|
14
|
-
|
13
|
+
when Hash then
|
14
|
+
options
|
15
|
+
when String then
|
16
|
+
Tire.warn "Passing the payload as a JSON string in Tire.search has been deprecated, " +
|
17
|
+
"please use the block syntax or pass a plain Hash."
|
18
|
+
options
|
15
19
|
else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
|
16
20
|
end
|
17
21
|
|
18
|
-
|
19
|
-
raise Tire::Search::SearchRequestFailed, response.to_s if response.failure?
|
20
|
-
json = MultiJson.decode(response.body)
|
21
|
-
results = Results::Collection.new(json, options)
|
22
|
+
Search::Search.new(indices, :payload => payload)
|
22
23
|
end
|
23
24
|
rescue Exception => error
|
24
25
|
STDERR.puts "[REQUEST FAILED] #{error.class} #{error.message rescue nil}\n"
|
data/lib/tire/index.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Tire
|
2
2
|
class Index
|
3
3
|
|
4
|
-
attr_reader :name
|
4
|
+
attr_reader :name, :response
|
5
5
|
|
6
6
|
def initialize(name, &block)
|
7
7
|
@name = name
|
@@ -64,10 +64,10 @@ module Tire
|
|
64
64
|
logged([type, id].join('/'), curl)
|
65
65
|
end
|
66
66
|
|
67
|
-
def bulk_store
|
67
|
+
def bulk_store(documents, options={})
|
68
68
|
payload = documents.map do |document|
|
69
|
+
type = get_type_from_document(document, :escape => false) # Do not URL-escape the _type
|
69
70
|
id = get_id_from_document(document)
|
70
|
-
type = get_type_from_document(document)
|
71
71
|
|
72
72
|
STDERR.puts "[ERROR] Document #{document.inspect} does not have ID" unless id
|
73
73
|
|
@@ -92,6 +92,7 @@ module Tire
|
|
92
92
|
retry
|
93
93
|
else
|
94
94
|
STDERR.puts "[ERROR] Too many exceptions occured, giving up. The HTTP response was: #{error.message}"
|
95
|
+
raise if options[:raise]
|
95
96
|
end
|
96
97
|
|
97
98
|
ensure
|
@@ -100,32 +101,33 @@ module Tire
|
|
100
101
|
end
|
101
102
|
end
|
102
103
|
|
103
|
-
def import(klass_or_collection,
|
104
|
+
def import(klass_or_collection, options={})
|
104
105
|
case
|
105
|
-
when method
|
106
|
+
when method = options.delete(:method)
|
106
107
|
options = {:page => 1, :per_page => 1000}.merge options
|
107
108
|
while documents = klass_or_collection.send(method.to_sym, options.merge(:page => options[:page])) \
|
108
109
|
and documents.to_a.length > 0
|
109
110
|
|
110
111
|
documents = yield documents if block_given?
|
111
112
|
|
112
|
-
bulk_store documents
|
113
|
+
bulk_store documents, options
|
113
114
|
options[:page] += 1
|
114
115
|
end
|
115
116
|
|
116
117
|
when klass_or_collection.respond_to?(:map)
|
117
118
|
documents = block_given? ? yield(klass_or_collection) : klass_or_collection
|
118
|
-
bulk_store documents
|
119
|
+
bulk_store documents, options
|
119
120
|
|
120
121
|
else
|
121
|
-
raise ArgumentError, "Please pass either
|
122
|
-
"
|
122
|
+
raise ArgumentError, "Please pass either an Enumerable compatible class, or a collection object" +
|
123
|
+
"with a method for fetching records in batches (such as 'paginate')"
|
123
124
|
end
|
124
125
|
end
|
125
126
|
|
126
127
|
def remove(*args)
|
127
128
|
if args.size > 1
|
128
129
|
type, document = args
|
130
|
+
type = Utils.escape(type)
|
129
131
|
id = get_id_from_document(document) || document
|
130
132
|
else
|
131
133
|
document = args.pop
|
@@ -146,6 +148,7 @@ module Tire
|
|
146
148
|
def retrieve(type, id)
|
147
149
|
raise ArgumentError, "Please pass a document ID" unless id
|
148
150
|
|
151
|
+
type = Utils.escape(type)
|
149
152
|
url = "#{Configuration.url}/#{@name}/#{type}/#{id}"
|
150
153
|
@response = Configuration.client.get url
|
151
154
|
|
@@ -262,7 +265,9 @@ module Tire
|
|
262
265
|
end
|
263
266
|
end
|
264
267
|
|
265
|
-
def get_type_from_document(document)
|
268
|
+
def get_type_from_document(document, options={})
|
269
|
+
options = {:escape => true}.merge(options)
|
270
|
+
|
266
271
|
old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#type deprecation warnings
|
267
272
|
type = case
|
268
273
|
when document.respond_to?(:document_type)
|
@@ -275,7 +280,9 @@ module Tire
|
|
275
280
|
document.type
|
276
281
|
end
|
277
282
|
$VERBOSE = old_verbose
|
278
|
-
|
283
|
+
|
284
|
+
type ||= 'document'
|
285
|
+
options[:escape] ? Utils.escape(type) : type
|
279
286
|
end
|
280
287
|
|
281
288
|
def get_id_from_document(document)
|
@@ -292,7 +299,10 @@ module Tire
|
|
292
299
|
|
293
300
|
def convert_document_to_json(document)
|
294
301
|
document = case
|
295
|
-
when document.is_a?(String)
|
302
|
+
when document.is_a?(String)
|
303
|
+
Tire.warn "Passing the document as JSON string in Index#store has been deprecated, " +
|
304
|
+
"please pass an object which responds to `to_indexed_json` or a plain Hash."
|
305
|
+
document
|
296
306
|
when document.respond_to?(:to_indexed_json) then document.to_indexed_json
|
297
307
|
else raise ArgumentError, "Please pass a JSON string or object with a 'to_indexed_json' method"
|
298
308
|
end
|
data/lib/tire/model/import.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
|
1
2
|
module Tire
|
2
3
|
module Model
|
3
4
|
|
@@ -13,8 +14,8 @@ module Tire
|
|
13
14
|
module ClassMethods
|
14
15
|
|
15
16
|
def import options={}, &block
|
16
|
-
|
17
|
-
index.import klass,
|
17
|
+
options = { :method => 'paginate' }.update options
|
18
|
+
index.import klass, options, &block
|
18
19
|
end
|
19
20
|
|
20
21
|
end
|
data/lib/tire/model/indexing.rb
CHANGED
@@ -41,6 +41,14 @@ module Tire
|
|
41
41
|
# indexes :id, :index => :not_analyzed
|
42
42
|
# indexes :title, :analyzer => 'snowball', :boost => 100
|
43
43
|
# indexes :words, :as => 'content.split(/\W/).length'
|
44
|
+
#
|
45
|
+
# indexes :comments do
|
46
|
+
# indexes :body
|
47
|
+
# indexes :author do
|
48
|
+
# indexes :name
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
#
|
44
52
|
# # ...
|
45
53
|
# end
|
46
54
|
# end
|
@@ -76,21 +84,21 @@ module Tire
|
|
76
84
|
# for more information.
|
77
85
|
#
|
78
86
|
def indexes(name, options = {}, &block)
|
87
|
+
mapping[name] = options
|
88
|
+
|
79
89
|
if block_given?
|
80
|
-
mapping[name]
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
if @_nested_mapping
|
88
|
-
mapping[@_nested_mapping][:properties][name] = options
|
89
|
-
else
|
90
|
-
mapping[name] = options
|
91
|
-
end
|
92
|
-
self
|
90
|
+
mapping[name][:type] ||= 'object'
|
91
|
+
mapping[name][:properties] ||= {}
|
92
|
+
|
93
|
+
previous = @mapping
|
94
|
+
@mapping = mapping[name][:properties]
|
95
|
+
yield
|
96
|
+
@mapping = previous
|
93
97
|
end
|
98
|
+
|
99
|
+
mapping[name][:type] ||= 'string'
|
100
|
+
|
101
|
+
self
|
94
102
|
end
|
95
103
|
|
96
104
|
# Creates the corresponding index with desired settings and mappings, when it does not exists yet.
|
data/lib/tire/model/naming.rb
CHANGED
@@ -30,6 +30,7 @@ module Tire
|
|
30
30
|
def index_name name=nil, &block
|
31
31
|
@index_name = name if name
|
32
32
|
@index_name = block if block_given?
|
33
|
+
# TODO: Try to get index_name from ancestor classes
|
33
34
|
@index_name || [index_prefix, klass.model_name.plural].compact.join('_')
|
34
35
|
end
|
35
36
|
|
@@ -74,7 +75,7 @@ module Tire
|
|
74
75
|
#
|
75
76
|
def document_type name=nil
|
76
77
|
@document_type = name if name
|
77
|
-
@document_type || klass.model_name.
|
78
|
+
@document_type || klass.model_name.underscore
|
78
79
|
end
|
79
80
|
end
|
80
81
|
|
@@ -45,7 +45,7 @@ module Tire
|
|
45
45
|
|
46
46
|
include Persistence::Storage
|
47
47
|
|
48
|
-
['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches'].each do |attr|
|
48
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches', '_explanation'].each do |attr|
|
49
49
|
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
|
50
50
|
define_method("#{attr}") { @attributes[attr] }
|
51
51
|
end
|
@@ -18,7 +18,8 @@ module Tire
|
|
18
18
|
|
19
19
|
def results
|
20
20
|
@results ||= begin
|
21
|
-
hits = @response['hits']['hits']
|
21
|
+
hits = @response['hits']['hits'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
|
22
|
+
|
22
23
|
unless @options[:load]
|
23
24
|
if @wrapper == Hash
|
24
25
|
hits
|
@@ -31,33 +32,35 @@ module Tire
|
|
31
32
|
document.update( {'id' => h['_id']} )
|
32
33
|
|
33
34
|
# Update the document with meta information
|
34
|
-
['_score', '_type', '_index', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }
|
35
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', '_explanation'].each { |key| document.update( {key => h[key]} || {} ) }
|
35
36
|
|
36
37
|
# Return an instance of the "wrapper" class
|
37
38
|
@wrapper.new(document)
|
38
39
|
end
|
39
40
|
end
|
41
|
+
|
40
42
|
else
|
41
43
|
return [] if hits.empty?
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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])
|
54
60
|
end
|
55
61
|
|
56
|
-
ids = @response['hits']['hits'].map { |h| h['_id'] }
|
57
|
-
records = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
|
58
|
-
|
59
62
|
# Reorder records to preserve order from search results
|
60
|
-
|
63
|
+
@response['hits']['hits'].map { |item| records[item['_type']].detect { |record| record.id.to_s == item['_id'].to_s } }
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|