tire 0.3.12 → 0.4.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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