tire 0.3.12 → 0.4.0.pre

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 (41) hide show
  1. data/README.markdown +41 -45
  2. data/examples/tire-dsl.rb +7 -2
  3. data/lib/tire/configuration.rb +1 -1
  4. data/lib/tire/dsl.rb +1 -1
  5. data/lib/tire/http/client.rb +11 -0
  6. data/lib/tire/http/clients/curb.rb +9 -2
  7. data/lib/tire/index.rb +4 -4
  8. data/lib/tire/model/indexing.rb +7 -2
  9. data/lib/tire/model/persistence/attributes.rb +84 -2
  10. data/lib/tire/model/persistence/finders.rb +8 -5
  11. data/lib/tire/model/persistence/storage.rb +2 -4
  12. data/lib/tire/model/persistence.rb +12 -0
  13. data/lib/tire/model/search.rb +20 -4
  14. data/lib/tire/search/facet.rb +11 -1
  15. data/lib/tire/search/query.rb +9 -2
  16. data/lib/tire/search.rb +21 -4
  17. data/lib/tire/tasks.rb +14 -1
  18. data/lib/tire/version.rb +9 -4
  19. data/lib/tire.rb +1 -0
  20. data/test/integration/active_model_indexing_test.rb +50 -0
  21. data/test/integration/facets_test.rb +53 -2
  22. data/test/integration/filtered_queries_test.rb +23 -4
  23. data/test/integration/index_store_test.rb +23 -1
  24. data/test/integration/persistent_model_test.rb +34 -0
  25. data/test/integration/query_return_version_test.rb +70 -0
  26. data/test/integration/query_string_test.rb +8 -8
  27. data/test/integration/text_query_test.rb +25 -0
  28. data/test/models/persistent_article_with_casting.rb +28 -0
  29. data/test/models/persistent_article_with_defaults.rb +11 -0
  30. data/test/test_helper.rb +3 -1
  31. data/test/unit/configuration_test.rb +6 -0
  32. data/test/unit/http_client_test.rb +42 -0
  33. data/test/unit/index_test.rb +22 -2
  34. data/test/unit/model_persistence_test.rb +55 -9
  35. data/test/unit/model_search_test.rb +37 -1
  36. data/test/unit/search_facet_test.rb +28 -0
  37. data/test/unit/search_query_test.rb +22 -7
  38. data/test/unit/search_test.rb +38 -3
  39. data/test/unit/tire_test.rb +15 -0
  40. data/tire.gemspec +5 -4
  41. metadata +171 -225
data/README.markdown CHANGED
@@ -161,8 +161,8 @@ from the database:
161
161
 
162
162
  sort { by :title, 'desc' }
163
163
 
164
- facet 'global-tags' do
165
- terms :tags, :global => true
164
+ facet 'global-tags', :global => true do
165
+ terms :tags
166
166
  end
167
167
 
168
168
  facet 'current-tags' do
@@ -282,6 +282,15 @@ Note, that you can pass options for configuring queries, facets, etc. by passing
282
282
  end
283
283
  ```
284
284
 
285
+ You don't have to define the search criteria in one monolithic _Ruby_ block -- you can build the search step by step,
286
+ until you call the `results` method:
287
+
288
+ ```ruby
289
+ s = Tire.search('articles') { query { string 'title:T*' } }
290
+ s.filter :terms, :tags => ['ruby']
291
+ p s.results
292
+ ```
293
+
285
294
  If configuring the search payload with blocks feels somehow too weak for you, you can pass
286
295
  a plain old Ruby `Hash` (or JSON string) with the query declaration to the `search` method:
287
296
 
@@ -293,7 +302,7 @@ If this sounds like a great idea to you, you are probably able to write your app
293
302
  using just `curl`, `sed` and `awk`.
294
303
 
295
304
  Do note again, however, that you're not tied to the declarative block-style DSL _Tire_ offers to you.
296
- If it makes more sense in your context, you can use its classes directly, in a more imperative style:
305
+ If it makes more sense in your context, you can use the API directly, in a more imperative style:
297
306
 
298
307
  ```ruby
299
308
  search = Tire::Search::Search.new('articles')
@@ -302,7 +311,7 @@ If it makes more sense in your context, you can use its classes directly, in a m
302
311
  search.sort { by :title, 'desc' }
303
312
  search.facet('global-tags') { terms :tags, :global => true }
304
313
  # ...
305
- p search.perform.results
314
+ p search.results
306
315
  ```
307
316
 
308
317
  To debug the query we have laboriously set up like this,
@@ -413,11 +422,12 @@ For serious usage, though, you'll definitely want to define a custom _mapping_ f
413
422
  include Tire::Model::Callbacks
414
423
 
415
424
  mapping do
416
- indexes :id, :type => 'string', :index => :not_analyzed
417
- indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 100
418
- indexes :content, :type => 'string', :analyzer => 'snowball'
419
- indexes :author, :type => 'string', :analyzer => 'keyword'
420
- indexes :published_on, :type => 'date', :include_in_all => false
425
+ indexes :id, :index => :not_analyzed
426
+ indexes :title, :analyzer => 'snowball', :boost => 100
427
+ indexes :content, :analyzer => 'snowball'
428
+ indexes :content_size, :as => 'content.size'
429
+ indexes :author, :analyzer => 'keyword'
430
+ indexes :published_on, :type => 'date', :include_in_all => false
421
431
  end
422
432
  end
423
433
  ```
@@ -425,6 +435,13 @@ For serious usage, though, you'll definitely want to define a custom _mapping_ f
425
435
  In this case, _only_ the defined model attributes are indexed. The `mapping` declaration creates the
426
436
  index when the class is loaded or when the importing features are used, and _only_ when it does not yet exist.
427
437
 
438
+ You can define different [_analyzers_](http://www.elasticsearch.org/guide/reference/index-modules/analysis/index.html),
439
+ [_boost_](http://www.elasticsearch.org/guide/reference/mapping/boost-field.html) levels for different properties,
440
+ or any other configuration for _elasticsearch_.
441
+
442
+ You're not limited to 1:1 mapping between your model properties and the serialized document. With the `:as` option,
443
+ you can pass a string or a _Proc_ object which is evaluated in the instance context (see the `content_size` property).
444
+
428
445
  Chances are, you want to declare also a custom _settings_ for the index, such as set the number of shards,
429
446
  replicas, or create elaborate analyzer chains, such as the hipster's choice: [_ngrams_](https://gist.github.com/1160430).
430
447
  In this case, just wrap the `mapping` method in a `settings` one, passing it the settings as a Hash:
@@ -671,12 +688,11 @@ Well, things stay mostly the same:
671
688
  include Tire::Model::Search
672
689
  include Tire::Model::Callbacks
673
690
 
674
- # Let's use a different index name so stuff doesn't get mixed up
691
+ # Let's use a different index name so stuff doesn't get mixed up.
675
692
  #
676
693
  index_name 'mongo-articles'
677
694
 
678
- # These Mongo guys sure do some funky stuff with their IDs
679
- # in +serializable_hash+, let's fix it.
695
+ # These Mongo guys sure do get funky with their IDs in +serializable_hash+, let's fix it.
680
696
  #
681
697
  def to_indexed_json
682
698
  self.to_json
@@ -696,36 +712,31 @@ _Tire_ implements not only _searchable_ features, but also _persistence_ feature
696
712
 
697
713
  Well, because you're tired of database migrations and lots of hand-holding with your
698
714
  database to store stuff like `{ :name => 'Tire', :tags => [ 'ruby', 'search' ] }`.
699
- Because what you need is to just dump a JSON-representation of your data into a database and
700
- load it back when needed.
715
+ Because all you need, really, is to just dump a JSON-representation of your data into a database and load it back again.
701
716
  Because you've noticed that _searching_ your data is a much more effective way of retrieval
702
717
  then constructing elaborate database query conditions.
703
- Because you have _lots_ of data and want to use _ElasticSearch's_
704
- advanced distributed features.
718
+ Because you have _lots_ of data and want to use _ElasticSearch's_ advanced distributed features.
705
719
 
706
- To use the persistence features, just include the `Tire::Persistence` module in your class and define the properties (like with _CouchDB_- or _MongoDB_-based models):
720
+ All good reasons to use _ElasticSearch_ as a schema-free and highly-scalable storage and retrieval/aggregation engine for your data.
721
+
722
+ To use the persistence mode, we'll include the `Tire::Persistence` module in our class and define its properties;
723
+ we can add the standard mapping declarations, set default values, or define casting for the property to create
724
+ lightweight associations between the models.
707
725
 
708
726
  ```ruby
709
727
  class Article
710
728
  include Tire::Model::Persistence
711
- include Tire::Model::Search
712
- include Tire::Model::Callbacks
713
729
 
714
730
  validates_presence_of :title, :author
715
731
 
716
- property :title
717
- property :author
718
- property :content
719
- property :published_on
732
+ property :title, :analyzer => 'snowball'
733
+ property :published_on, :type => 'date'
734
+ property :tags, :default => [], :analyzer => 'keyword'
735
+ property :author, :class => Author
736
+ property :comments, :class => [Comment]
720
737
  end
721
738
  ```
722
739
 
723
- Of course, not all validations or `ActionPack` helpers will be available to your models,
724
- but if you can live with that, you've just got a schema-free, highly-scalable storage
725
- and retrieval engine for your data.
726
-
727
- 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.
728
-
729
740
  Please be sure to peruse the [integration test suite](https://github.com/karmi/tire/tree/master/test/integration)
730
741
  for examples of the API and _ActiveModel_ integration usage.
731
742
 
@@ -734,22 +745,7 @@ Extensions and Additions
734
745
  ------------------------
735
746
 
736
747
  The [_tire-contrib_](http://github.com/karmi/tire-contrib/) project contains additions
737
- and extensions to the _Tire_ functionality.
738
-
739
-
740
- Todo, Plans & Ideas
741
- -------------------
742
-
743
- _Tire_ is already used in production by its authors. Nevertheless, it's not considered finished yet.
744
-
745
- There are todos, plans and ideas, some of which are listed below, in the order of importance:
746
-
747
- * Proper RDoc annotations for the source code
748
- * [Statistical](http://www.elasticsearch.org/guide/reference/api/search/facets/statistical-facet.html) facets
749
- * [Geo Distance](http://www.elasticsearch.org/guide/reference/api/search/facets/geo-distance-facet.html) facets
750
- * [Index aliases](http://www.elasticsearch.org/guide/reference/api/admin-indices-aliases.html) management
751
- * [Analyze](http://www.elasticsearch.org/guide/reference/api/admin-indices-analyze.html) API support
752
- * Embedded webserver to display statistics and to allow easy searches
748
+ and extensions to the core _Tire_ functionality — be sure to check them out.
753
749
 
754
750
 
755
751
  Other Clients
data/examples/tire-dsl.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+ #
1
3
  # **Tire** provides rich and comfortable Ruby API for the
2
4
  # [_ElasticSearch_](http://www.elasticsearch.org/) search engine/database.
3
5
  #
@@ -474,6 +476,7 @@ end
474
476
  # Eventually, _Tire_ will support all of them. So far, only these are supported:
475
477
  #
476
478
  # * [string](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
479
+ # * [text](http://www.elasticsearch.org/guide/reference/query-dsl/text-query.html)
477
480
  # * [term](http://elasticsearch.org/guide/reference/query-dsl/term-query.html)
478
481
  # * [terms](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
479
482
  # * [bool](http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html)
@@ -532,11 +535,11 @@ s = Tire.search 'articles' do
532
535
  #
533
536
  query { string 'title:T*' }
534
537
 
535
- facet 'global-tags' do
538
+ facet 'global-tags', :global => true do
536
539
 
537
540
  # ...but set the `global` scope for the facet in this case.
538
541
  #
539
- terms :tags, :global => true
542
+ terms :tags
540
543
  end
541
544
 
542
545
  # We can even _combine_ facets scoped to the current query
@@ -583,6 +586,8 @@ end
583
586
  # * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
584
587
  # * [range](http://www.elasticsearch.org/guide/reference/api/search/facets/range-facet.html)
585
588
  # * [histogram](http://www.elasticsearch.org/guide/reference/api/search/facets/histogram-facet.html)
589
+ # * [statistical](http://www.elasticsearch.org/guide/reference/api/search/facets/statistical-facet.html)
590
+ # * [terms_stats](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-stats-facet.html)
586
591
  # * [query](http://www.elasticsearch.org/guide/reference/api/search/facets/query-facet.html)
587
592
 
588
593
  # We have seen that _ElasticSearch_ facets enable us to fetch complex aggregations from our data.
@@ -3,7 +3,7 @@ module Tire
3
3
  class Configuration
4
4
 
5
5
  def self.url(value=nil)
6
- @url = (value ? value.to_s.gsub(%r|/*$|, '') : nil) || @url || "http://localhost:9200"
6
+ @url = (value ? value.to_s.gsub(%r|/*$|, '') : nil) || @url || ENV['ELASTICSEARCH_URL'] || "http://localhost:9200"
7
7
  end
8
8
 
9
9
  def self.client(klass=nil)
data/lib/tire/dsl.rb CHANGED
@@ -7,7 +7,7 @@ module Tire
7
7
 
8
8
  def search(indices=nil, options={}, &block)
9
9
  if block_given?
10
- Search::Search.new(indices, options, &block).perform
10
+ Search::Search.new(indices, options, &block)
11
11
  else
12
12
  payload = case options
13
13
  when Hash then options.to_json
@@ -5,33 +5,44 @@ module Tire
5
5
  module Client
6
6
 
7
7
  class RestClient
8
+ ConnectionExceptions = [::RestClient::ServerBrokeConnection, ::RestClient::RequestTimeout]
8
9
 
9
10
  def self.get(url, data=nil)
10
11
  perform ::RestClient::Request.new(:method => :get, :url => url, :payload => data).execute
12
+ rescue *ConnectionExceptions
13
+ raise
11
14
  rescue ::RestClient::Exception => e
12
15
  Response.new e.http_body, e.http_code
13
16
  end
14
17
 
15
18
  def self.post(url, data)
16
19
  perform ::RestClient.post(url, data)
20
+ rescue *ConnectionExceptions
21
+ raise
17
22
  rescue ::RestClient::Exception => e
18
23
  Response.new e.http_body, e.http_code
19
24
  end
20
25
 
21
26
  def self.put(url, data)
22
27
  perform ::RestClient.put(url, data)
28
+ rescue *ConnectionExceptions
29
+ raise
23
30
  rescue ::RestClient::Exception => e
24
31
  Response.new e.http_body, e.http_code
25
32
  end
26
33
 
27
34
  def self.delete(url)
28
35
  perform ::RestClient.delete(url)
36
+ rescue *ConnectionExceptions
37
+ raise
29
38
  rescue ::RestClient::Exception => e
30
39
  Response.new e.http_body, e.http_code
31
40
  end
32
41
 
33
42
  def self.head(url)
34
43
  perform ::RestClient.head(url)
44
+ rescue *ConnectionExceptions
45
+ raise
35
46
  rescue ::RestClient::Exception => e
36
47
  Response.new e.http_body, e.http_code
37
48
  end
@@ -8,15 +8,22 @@ module Tire
8
8
 
9
9
  class Curb
10
10
  @client = ::Curl::Easy.new
11
+ @client.resolve_mode = :ipv4
12
+
11
13
  # @client.verbose = true
12
14
 
13
15
  def self.get(url, data=nil)
14
16
  @client.url = url
15
- @client.post_body = data
17
+
16
18
  # FIXME: Curb cannot post bodies with GET requests?
17
19
  # Roy Fielding seems to approve:
18
20
  # <http://tech.groups.yahoo.com/group/rest-discuss/message/9962>
19
- @client.http_post
21
+ if data
22
+ @client.post_body = data
23
+ @client.http_post
24
+ else
25
+ @client.http_get
26
+ end
20
27
  Response.new @client.body_str, @client.response_code
21
28
  end
22
29
 
data/lib/tire/index.rb CHANGED
@@ -85,7 +85,7 @@ module Tire
85
85
  response = Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
86
86
  raise RuntimeError, "#{response.code} > #{response.body}" if response.failure?
87
87
  response
88
- rescue Exception => error
88
+ rescue StandardError => error
89
89
  if count < tries
90
90
  count += 1
91
91
  STDERR.puts "[ERROR] #{error.message}, retrying (#{count})..."
@@ -152,8 +152,8 @@ module Tire
152
152
  h = MultiJson.decode(@response.body)
153
153
  if Configuration.wrapper == Hash then h
154
154
  else
155
- document = {}
156
- document = h['_source'] ? document.update( h['_source'] ) : document.update( h['fields'] )
155
+ return nil if h['exists'] == false
156
+ document = h['_source'] || h['fields'] || {}
157
157
  document.update('id' => h['_id'], '_type' => h['_type'], '_index' => h['_index'], '_version' => h['_version'])
158
158
  Configuration.wrapper.new(document)
159
159
  end
@@ -177,7 +177,7 @@ module Tire
177
177
  MultiJson.decode(@response.body)['ok']
178
178
 
179
179
  ensure
180
- curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/open"|
180
+ curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_open"|
181
181
  logged('_open', curl)
182
182
  end
183
183
 
@@ -38,8 +38,9 @@ module Tire
38
38
  # class Article
39
39
  # # ...
40
40
  # mapping :_source => { :compress => true } do
41
- # indexes :id, :type => 'string', :index => :not_analyzed
42
- # indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 100
41
+ # indexes :id, :index => :not_analyzed
42
+ # indexes :title, :analyzer => 'snowball', :boost => 100
43
+ # indexes :words, :as => 'content.split(/\W/).length'
43
44
  # # ...
44
45
  # end
45
46
  # end
@@ -66,6 +67,10 @@ module Tire
66
67
  #
67
68
  # * Use different analyzer for indexing a property: `indexes :title, :analyzer => 'snowball'`
68
69
  #
70
+ # * Use the `:as` option to dynamically define the serialized property value, eg:
71
+ #
72
+ # :as => 'content.split(/\W/).length'
73
+ #
69
74
  # Please refer to the
70
75
  # [_mapping_ documentation](http://www.elasticsearch.org/guide/reference/mapping/index.html)
71
76
  # for more information.
@@ -9,11 +9,50 @@ module Tire
9
9
 
10
10
  module ClassMethods
11
11
 
12
+ # Define property of the model:
13
+ #
14
+ # class Article
15
+ # include Tire::Model::Persistence
16
+ #
17
+ # property :title, :analyzer => 'snowball'
18
+ # property :published, :type => 'date'
19
+ # property :tags, :analyzer => 'keywords', :default => []
20
+ # end
21
+ #
22
+ # You can pass mapping definition for ElasticSearch in the options Hash.
23
+ #
24
+ # You can define default property values.
25
+ #
12
26
  def property(name, options = {})
13
- attr_accessor name.to_sym
27
+
28
+ # Define attribute reader:
29
+ define_method("#{name}") do
30
+ instance_variable_get(:"@#{name}")
31
+ end
32
+
33
+ # Define attribute writer:
34
+ define_method("#{name}=") do |value|
35
+ instance_variable_set(:"@#{name}", value)
36
+ end
37
+
38
+ # Save the property in properties array:
14
39
  properties << name.to_s unless properties.include?(name.to_s)
40
+
41
+ # Define convenience <NAME>? method:
15
42
  define_query_method name.to_sym
43
+
44
+ # ActiveModel compatibility. NEEDED?
16
45
  define_attribute_methods [name.to_sym]
46
+
47
+ # Save property default value (when relevant):
48
+ unless (default_value = options.delete(:default)).nil?
49
+ property_defaults[name.to_sym] = default_value
50
+ end
51
+
52
+ # Save property casting (when relevant):
53
+ property_types[name.to_sym] = options[:class] if options[:class]
54
+
55
+ # Store mapping for the property:
17
56
  mapping[name] = options
18
57
  self
19
58
  end
@@ -22,6 +61,14 @@ module Tire
22
61
  @properties ||= []
23
62
  end
24
63
 
64
+ def property_defaults
65
+ @property_defaults ||= {}
66
+ end
67
+
68
+ def property_types
69
+ @property_types ||= {}
70
+ end
71
+
25
72
  private
26
73
 
27
74
  def define_query_method name
@@ -35,7 +82,14 @@ module Tire
35
82
  attr_accessor :id
36
83
 
37
84
  def initialize(attributes={})
38
- attributes.each { |name, value| send("#{name}=", value) }
85
+ # Make a copy of objects in the property defaults hash, so default values such as `[]` or `{ foo: [] }` are left intact
86
+ property_defaults = self.class.property_defaults.inject({}) do |hash, item|
87
+ key, value = item
88
+ hash[key.to_s] = value.class.respond_to?(:new) ? value.clone : value
89
+ hash
90
+ end
91
+
92
+ __update_attributes(property_defaults.merge(attributes))
39
93
  end
40
94
 
41
95
  def attributes
@@ -52,6 +106,34 @@ module Tire
52
106
  end
53
107
  alias :has_property? :has_attribute?
54
108
 
109
+ def __update_attributes(attributes)
110
+ attributes.each { |name, value| send "#{name}=", __cast_value(name, value) }
111
+ end
112
+
113
+ # Casts the values according to the <tt>:class</tt> option set when
114
+ # defining the property, cast Hashes as Hashr[http://rubygems.org/gems/hashr]
115
+ # instances and automatically convert UTC formatted strings to Time.
116
+ #
117
+ def __cast_value(name, value)
118
+ case
119
+
120
+ when klass = self.class.property_types[name.to_sym]
121
+ if klass.is_a?(Array) && value.is_a?(Array)
122
+ value.map { |v| klass.first.new(v) }
123
+ else
124
+ klass.new(value)
125
+ end
126
+
127
+ when value.is_a?(Hash)
128
+ Hashr.new(value)
129
+
130
+ else
131
+ # Strings formatted as <http://en.wikipedia.org/wiki/ISO8601> are automatically converted to Time
132
+ value = Time.parse(value) if value.is_a?(String) && value =~ /^\d{4}[\/\-]\d{2}[\/\-]\d{2}T\d{2}\:\d{2}\:\d{2}Z$/
133
+ value
134
+ end
135
+ end
136
+
55
137
  end
56
138
 
57
139
  end
@@ -16,9 +16,12 @@ module Tire
16
16
  options = args.pop if args.last.is_a?(Hash)
17
17
  args.flatten!
18
18
  if args.size > 1
19
- Tire::Search::Search.new(index.name).query do |query|
20
- query.ids(args, document_type)
21
- end.perform.results
19
+ Tire::Search::Search.new(index.name) do |search|
20
+ search.query do |query|
21
+ query.ids(args, document_type)
22
+ end
23
+ search.size args.size
24
+ end.results
22
25
  else
23
26
  case args = args.pop
24
27
  when Fixnum, String
@@ -38,7 +41,7 @@ module Tire
38
41
  old_wrapper = Tire::Configuration.wrapper
39
42
  Tire::Configuration.wrapper self
40
43
  s = Tire::Search::Search.new(index.name).query { all }
41
- s.perform.results
44
+ s.results
42
45
  ensure
43
46
  Tire::Configuration.wrapper old_wrapper
44
47
  end
@@ -48,7 +51,7 @@ module Tire
48
51
  old_wrapper = Tire::Configuration.wrapper
49
52
  Tire::Configuration.wrapper self
50
53
  s = Tire::Search::Search.new(index.name).query { all }.size(1)
51
- s.perform.results.first
54
+ s.results.first
52
55
  ensure
53
56
  Tire::Configuration.wrapper old_wrapper
54
57
  end
@@ -30,14 +30,12 @@ module Tire
30
30
  module InstanceMethods
31
31
 
32
32
  def update_attribute(name, value)
33
- send("#{name}=", value)
33
+ __update_attributes name => value
34
34
  save
35
35
  end
36
36
 
37
37
  def update_attributes(attributes={})
38
- attributes.each do |name, value|
39
- send("#{name}=", value)
40
- end
38
+ __update_attributes attributes
41
39
  save
42
40
  end
43
41
 
@@ -50,6 +50,18 @@ module Tire
50
50
  define_method("#{attr}") { @attributes[attr] }
51
51
  end
52
52
 
53
+ def self.search(*args, &block)
54
+ # Update options Hash with the wrapper definition
55
+ args.last.update(:wrapper => self) if args.last.is_a? Hash
56
+ args << { :wrapper => self } unless args.any? { |a| a.is_a? Hash }
57
+
58
+ self.__search_without_persistence(*args, &block)
59
+ end
60
+
61
+ def self.__search_without_persistence(*args, &block)
62
+ self.tire.search(*args, &block)
63
+ end
64
+
53
65
  end
54
66
 
55
67
  end
@@ -94,7 +94,7 @@ module Tire
94
94
  s.fields Array(options[:fields]) if options[:fields]
95
95
  end
96
96
 
97
- s.perform.results
97
+ s.results
98
98
  end
99
99
 
100
100
  # Returns a Tire::Index instance for this model.
@@ -151,13 +151,29 @@ module Tire
151
151
  # If you do define the mapping for _ElasticSearch_, only attributes
152
152
  # declared in the mapping are serialized.
153
153
  #
154
+ # For properties declared with the `:as` option, the passed String or Proc
155
+ # is evaluated in the instance context.
156
+ #
154
157
  def to_indexed_json
155
158
  if instance.class.tire.mapping.empty?
159
+ # Reject the id and type keys
156
160
  instance.to_hash.reject {|key,_| key.to_s == 'id' || key.to_s == 'type' }.to_json
157
161
  else
158
- instance.to_hash.
159
- reject { |key, value| ! instance.class.tire.mapping.keys.map(&:to_s).include?(key.to_s) }.
160
- to_json
162
+ mapping = instance.class.tire.mapping
163
+ # Reject keys not declared in mapping
164
+ hash = instance.to_hash.reject { |key, value| ! mapping.keys.map(&:to_s).include?(key.to_s) }
165
+
166
+ # Evalute the `:as` options
167
+ mapping.each do |key, options|
168
+ case options[:as]
169
+ when String
170
+ hash[key] = instance.instance_eval(options[:as])
171
+ when Proc
172
+ hash[key] = instance.instance_eval(&options[:as])
173
+ end
174
+ end
175
+
176
+ hash.to_json
161
177
  end
162
178
  end
163
179
 
@@ -11,7 +11,7 @@ module Tire
11
11
  def initialize(name, options={}, &block)
12
12
  @name = name
13
13
  @options = options
14
- self.instance_eval(&block) if block_given?
14
+ block.arity < 1 ? self.instance_eval(&block) : block.call(self) if block_given?
15
15
  end
16
16
 
17
17
  def terms(field, options={})
@@ -37,6 +37,16 @@ module Tire
37
37
  self
38
38
  end
39
39
 
40
+ def statistical(field, options={})
41
+ @value = { :statistical => (options.delete(:statistical) || {:field => field}.update(options)) }
42
+ self
43
+ end
44
+
45
+ def terms_stats(key_field, value_field, options={})
46
+ @value = { :terms_stats => {:key_field => key_field, :value_field => value_field}.update(options) }
47
+ self
48
+ end
49
+
40
50
  def query(&block)
41
51
  @value = { :query => Query.new(&block).to_hash }
42
52
  end
@@ -21,6 +21,12 @@ module Tire
21
21
  @value = { :range => { field => value } }
22
22
  end
23
23
 
24
+ def text(field, value, options={})
25
+ query_options = { :query => value }.update(options)
26
+ @value = { :text => { field => query_options } }
27
+ @value
28
+ end
29
+
24
30
  def string(value, options={})
25
31
  @value = { :query_string => { :query => value } }
26
32
  @value[:query_string].update(options)
@@ -121,8 +127,9 @@ module Tire
121
127
  end
122
128
 
123
129
  def filter(type, *options)
124
- @value[:filter] ||= []
125
- @value[:filter] << Filter.new(type, *options).to_hash
130
+ @value[:filter] ||= {}
131
+ @value[:filter][:and] ||= []
132
+ @value[:filter][:and] << Filter.new(type, *options).to_hash
126
133
  @value
127
134
  end
128
135