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.
Files changed (46) hide show
  1. data/README.markdown +6 -10
  2. data/examples/rails-application-template.rb +6 -6
  3. data/examples/tire-dsl.rb +10 -9
  4. data/lib/tire.rb +8 -0
  5. data/lib/tire/dsl.rb +7 -6
  6. data/lib/tire/index.rb +22 -12
  7. data/lib/tire/model/import.rb +3 -2
  8. data/lib/tire/model/indexing.rb +21 -13
  9. data/lib/tire/model/naming.rb +2 -1
  10. data/lib/tire/model/persistence.rb +1 -1
  11. data/lib/tire/results/collection.rb +20 -17
  12. data/lib/tire/results/item.rb +1 -1
  13. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  14. data/lib/tire/search.rb +33 -19
  15. data/lib/tire/search/facet.rb +5 -0
  16. data/lib/tire/search/query.rb +8 -3
  17. data/lib/tire/tasks.rb +47 -14
  18. data/lib/tire/utils.rb +17 -0
  19. data/lib/tire/version.rb +13 -2
  20. data/test/integration/active_model_indexing_test.rb +1 -0
  21. data/test/integration/active_model_searchable_test.rb +7 -5
  22. data/test/integration/active_record_searchable_test.rb +159 -72
  23. data/test/integration/count_test.rb +34 -0
  24. data/test/integration/dsl_search_test.rb +22 -0
  25. data/test/integration/explanation_test.rb +44 -0
  26. data/test/integration/facets_test.rb +15 -0
  27. data/test/integration/fuzzy_queries_test.rb +20 -0
  28. data/test/integration/mongoid_searchable_test.rb +1 -0
  29. data/test/integration/persistent_model_test.rb +22 -1
  30. data/test/integration/text_query_test.rb +17 -3
  31. data/test/models/active_record_models.rb +43 -1
  32. data/test/models/mongoid_models.rb +0 -1
  33. data/test/models/persistent_article_in_namespace.rb +12 -0
  34. data/test/models/supermodel_article.rb +5 -10
  35. data/test/test_helper.rb +16 -2
  36. data/test/unit/index_test.rb +90 -16
  37. data/test/unit/model_import_test.rb +4 -4
  38. data/test/unit/model_search_test.rb +13 -10
  39. data/test/unit/results_collection_test.rb +6 -0
  40. data/test/unit/results_item_test.rb +8 -0
  41. data/test/unit/search_facet_test.rb +9 -0
  42. data/test/unit/search_query_test.rb +36 -7
  43. data/test/unit/search_test.rb +70 -1
  44. data/test/unit/tire_test.rb +23 -12
  45. data/tire.gemspec +11 -8
  46. 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.17.6.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz
23
- $ tar -zxvf elasticsearch-0.17.6.tar.gz
24
- $ ./elasticsearch-0.17.6/bin/elasticsearch -f
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 => { :fuzzy => { :title => 'Sour' } }
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.to_json
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.17.6/
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.17.6.tar.gz \
75
- "http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz"
76
- tar -zxf elasticsearch-0.17.6.tar.gz
77
- rm -f elasticsearch-0.17.6.tar.gz
78
- ./elasticsearch-0.17.6/bin/elasticsearch -p #{destination_root}/tmp/pids/elasticsearch.pid
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.17.6.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz
49
- tar -zxvf elasticsearch-0.17.6.tar.gz
50
- ./elasticsearch-0.17.6/bin/elasticsearch -f
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 options.to_json
14
- when String then options
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
- response = Configuration.client.post( "#{Configuration.url}/#{indices}/_search", payload)
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 documents
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, method=nil, options={})
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 a collection of objects, " +
122
- "or method for fetching records, or Enumerable compatible class"
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
- type || :document
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) then document
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
@@ -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
- method = options.delete(:method) || 'paginate'
17
- index.import klass, method, options, &block
17
+ options = { :method => 'paginate' }.update options
18
+ index.import klass, options, &block
18
19
  end
19
20
 
20
21
  end
@@ -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] ||= { :type => 'object', :properties => {} }.update(options)
81
- @_nested_mapping = name
82
- nested = yield
83
- @_nested_mapping = nil
84
- self
85
- else
86
- options[:type] ||= 'string'
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.
@@ -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.singular
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
- type = @response['hits']['hits'].first['_type']
44
- raise NoMethodError, "You have tried to eager load the model instances, " +
45
- "but Tire cannot find the model class because " +
46
- "document has no _type property." unless type
47
-
48
- begin
49
- klass = type.camelize.constantize
50
- rescue NameError => e
51
- raise NameError, "You have tried to eager load the model instances, but " +
52
- "Tire cannot find the model class '#{type.camelize}' " +
53
- "based on _type '#{type}'.", e.backtrace
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
- ids.map { |id| records.detect { |record| record.id.to_s == id.to_s } }
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