tire 0.4.0.pre → 0.4.0.rc
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/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
|