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.
- data/README.markdown +41 -45
- data/examples/tire-dsl.rb +7 -2
- data/lib/tire/configuration.rb +1 -1
- data/lib/tire/dsl.rb +1 -1
- data/lib/tire/http/client.rb +11 -0
- data/lib/tire/http/clients/curb.rb +9 -2
- data/lib/tire/index.rb +4 -4
- data/lib/tire/model/indexing.rb +7 -2
- data/lib/tire/model/persistence/attributes.rb +84 -2
- data/lib/tire/model/persistence/finders.rb +8 -5
- data/lib/tire/model/persistence/storage.rb +2 -4
- data/lib/tire/model/persistence.rb +12 -0
- data/lib/tire/model/search.rb +20 -4
- data/lib/tire/search/facet.rb +11 -1
- data/lib/tire/search/query.rb +9 -2
- data/lib/tire/search.rb +21 -4
- data/lib/tire/tasks.rb +14 -1
- data/lib/tire/version.rb +9 -4
- data/lib/tire.rb +1 -0
- data/test/integration/active_model_indexing_test.rb +50 -0
- data/test/integration/facets_test.rb +53 -2
- data/test/integration/filtered_queries_test.rb +23 -4
- data/test/integration/index_store_test.rb +23 -1
- data/test/integration/persistent_model_test.rb +34 -0
- data/test/integration/query_return_version_test.rb +70 -0
- data/test/integration/query_string_test.rb +8 -8
- data/test/integration/text_query_test.rb +25 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/test_helper.rb +3 -1
- data/test/unit/configuration_test.rb +6 -0
- data/test/unit/http_client_test.rb +42 -0
- data/test/unit/index_test.rb +22 -2
- data/test/unit/model_persistence_test.rb +55 -9
- data/test/unit/model_search_test.rb +37 -1
- data/test/unit/search_facet_test.rb +28 -0
- data/test/unit/search_query_test.rb +22 -7
- data/test/unit/search_test.rb +38 -3
- data/test/unit/tire_test.rb +15 -0
- data/tire.gemspec +5 -4
- 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
|
|
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
|
|
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.
|
|
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, :
|
|
417
|
-
indexes :title, :
|
|
418
|
-
indexes :content, :
|
|
419
|
-
indexes :
|
|
420
|
-
indexes :
|
|
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
|
|
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
|
|
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
|
-
|
|
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 :
|
|
718
|
-
property :
|
|
719
|
-
property :
|
|
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
|
|
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.
|
data/lib/tire/configuration.rb
CHANGED
|
@@ -3,7 +3,7 @@ module Tire
|
|
|
3
3
|
class Configuration
|
|
4
4
|
|
|
5
5
|
def self.url(value=nil)
|
|
6
|
-
@url
|
|
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
data/lib/tire/http/client.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
document = h['_source']
|
|
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}/
|
|
180
|
+
curl = %Q|curl -X POST "#{Configuration.url}/#{@name}/_open"|
|
|
181
181
|
logged('_open', curl)
|
|
182
182
|
end
|
|
183
183
|
|
data/lib/tire/model/indexing.rb
CHANGED
|
@@ -38,8 +38,9 @@ module Tire
|
|
|
38
38
|
# class Article
|
|
39
39
|
# # ...
|
|
40
40
|
# mapping :_source => { :compress => true } do
|
|
41
|
-
# indexes :id, :
|
|
42
|
-
# indexes :title, :
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
20
|
-
query
|
|
21
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
33
|
+
__update_attributes name => value
|
|
34
34
|
save
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def update_attributes(attributes={})
|
|
38
|
-
attributes
|
|
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
|
data/lib/tire/model/search.rb
CHANGED
|
@@ -94,7 +94,7 @@ module Tire
|
|
|
94
94
|
s.fields Array(options[:fields]) if options[:fields]
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
s.
|
|
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.
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
data/lib/tire/search/facet.rb
CHANGED
|
@@ -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
|
data/lib/tire/search/query.rb
CHANGED
|
@@ -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]
|
|
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
|
|