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.
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