ssickles-tire 0.4.2.7 → 0.4.3
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/lib/tire.rb +18 -3
- data/lib/tire/alias.rb +11 -35
- data/lib/tire/index.rb +34 -76
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +26 -0
- data/lib/tire/model/indexing.rb +128 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +72 -0
- data/lib/tire/model/persistence/attributes.rb +143 -0
- data/lib/tire/model/persistence/finders.rb +66 -0
- data/lib/tire/model/persistence/storage.rb +71 -0
- data/lib/tire/model/search.rb +305 -0
- data/lib/tire/results/collection.rb +38 -13
- data/lib/tire/results/item.rb +19 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +54 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +7 -8
- data/lib/tire/search/scan.rb +8 -8
- data/lib/tire/search/sort.rb +1 -1
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +7 -38
- data/test/integration/active_model_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +446 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/persistent_model_test.rb +117 -0
- data/test/integration/reindex_test.rb +2 -2
- data/test/integration/scan_test.rb +1 -1
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +122 -0
- data/test/models/mongoid_models.rb +97 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +27 -3
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/index_alias_test.rb +3 -17
- data/test/unit/index_test.rb +30 -18
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +516 -0
- data/test/unit/model_search_test.rb +899 -0
- data/test/unit/results_collection_test.rb +60 -0
- data/test/unit/results_item_test.rb +37 -0
- data/test/unit/rubyext_test.rb +3 -3
- data/test/unit/search_test.rb +1 -6
- data/test/unit/tire_test.rb +15 -0
- data/tire.gemspec +30 -13
- metadata +153 -41
- data/lib/tire/rubyext/to_json.rb +0 -21
@@ -0,0 +1,100 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Contains logic for getting and setting the index name and document type for this model.
|
5
|
+
#
|
6
|
+
module Naming
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Get or set the index name for this model, based on arguments.
|
11
|
+
#
|
12
|
+
# By default, uses ActiveSupport inflection, so a class named `Article`
|
13
|
+
# will be stored in the `articles` index.
|
14
|
+
#
|
15
|
+
# To get the index name:
|
16
|
+
#
|
17
|
+
# Article.index_name
|
18
|
+
#
|
19
|
+
# To set the index name:
|
20
|
+
#
|
21
|
+
# Article.index_name 'my-custom-name'
|
22
|
+
#
|
23
|
+
# You can also use a block for defining the index name,
|
24
|
+
# which is evaluated in the class context:
|
25
|
+
#
|
26
|
+
# Article.index_name { "articles-#{Time.now.year}" }
|
27
|
+
#
|
28
|
+
# Article.index_name { "articles-#{Rails.env}" }
|
29
|
+
#
|
30
|
+
def index_name name=nil, &block
|
31
|
+
@index_name = name if name
|
32
|
+
@index_name = block if block_given?
|
33
|
+
# TODO: Try to get index_name from ancestor classes
|
34
|
+
@index_name || [index_prefix, klass.model_name.plural].compact.join('_')
|
35
|
+
end
|
36
|
+
|
37
|
+
# Set or get index prefix for all models or for a specific model.
|
38
|
+
#
|
39
|
+
# To set the prefix for all models (preferably in an initializer inside Rails):
|
40
|
+
#
|
41
|
+
# Tire::Model::Search.index_prefix Rails.env
|
42
|
+
#
|
43
|
+
# To set the prefix for specific model:
|
44
|
+
#
|
45
|
+
# class Article
|
46
|
+
# # ...
|
47
|
+
# index_prefix 'my_prefix'
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# TODO: Maybe this would be more sane with ActiveSupport extensions such as `class_attribute`?
|
51
|
+
#
|
52
|
+
@@__index_prefix__ = nil
|
53
|
+
def index_prefix(*args)
|
54
|
+
# Uses class or instance variable depending on the context
|
55
|
+
if args.size > 0
|
56
|
+
value = args.pop
|
57
|
+
self.is_a?(Module) ? ( @@__index_prefix__ = value ) : ( @__index_prefix__ = value )
|
58
|
+
end
|
59
|
+
self.is_a?(Module) ? ( @@__index_prefix__ || nil ) : ( @__index_prefix__ || @@__index_prefix__ || nil )
|
60
|
+
end
|
61
|
+
extend self
|
62
|
+
|
63
|
+
# Get or set the document type for this model, based on arguments.
|
64
|
+
#
|
65
|
+
# By default, uses ActiveSupport inflection, so a class named `Article`
|
66
|
+
# will be stored as the `article` type.
|
67
|
+
#
|
68
|
+
# To get the document type:
|
69
|
+
#
|
70
|
+
# Article.document_type
|
71
|
+
#
|
72
|
+
# To set the document type:
|
73
|
+
#
|
74
|
+
# Article.document_type 'my-custom-type'
|
75
|
+
#
|
76
|
+
def document_type name=nil
|
77
|
+
@document_type = name if name
|
78
|
+
@document_type || klass.model_name.underscore
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module InstanceMethods
|
83
|
+
|
84
|
+
# Proxy to class method `index_name`.
|
85
|
+
#
|
86
|
+
def index_name
|
87
|
+
instance.class.tire.index_name
|
88
|
+
end
|
89
|
+
|
90
|
+
# Proxy to instance method `document_type`.
|
91
|
+
#
|
92
|
+
def document_type
|
93
|
+
instance.class.tire.document_type
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Contains support for the [percolation](http://www.elasticsearch.org/guide/reference/api/percolate.html)
|
5
|
+
# feature of _ElasticSearch_.
|
6
|
+
#
|
7
|
+
module Percolate
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
# Set up the percolation when documents are being added to the index.
|
12
|
+
#
|
13
|
+
# Usage:
|
14
|
+
#
|
15
|
+
# class Article
|
16
|
+
# # ...
|
17
|
+
# percolate!
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# First, you have to register a percolator query:
|
21
|
+
#
|
22
|
+
# Article.index.register_percolator_query('fail') { |query| query.string 'fail' }
|
23
|
+
#
|
24
|
+
# Then, when you update the index, matching queries are returned in the `matches` property:
|
25
|
+
#
|
26
|
+
# p Article.create(:title => 'This is a FAIL!').matches
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# You may pass a pattern to filter which percolator queries will be executed.
|
30
|
+
#
|
31
|
+
# See <http://www.elasticsearch.org/guide/reference/api/index_.html> for more information.
|
32
|
+
#
|
33
|
+
def percolate!(pattern=true)
|
34
|
+
@@_percolator = pattern
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# A callback method for intercepting percolator matches.
|
39
|
+
#
|
40
|
+
# Usage:
|
41
|
+
#
|
42
|
+
# class Article
|
43
|
+
# # ...
|
44
|
+
# on_percolate do
|
45
|
+
# puts "Article title “#{title}” matches queries: #{matches.inspect}" unless matches.empty?
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# Based on the response received in `matches`, you may choose to fire notifications,
|
50
|
+
# increment counters, send out e-mail alerts, etc.
|
51
|
+
#
|
52
|
+
def on_percolate(pattern=true,&block)
|
53
|
+
percolate!(pattern)
|
54
|
+
klass.after_update_elasticsearch_index(block)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the status or pattern of percolator for this class.
|
58
|
+
#
|
59
|
+
def percolator
|
60
|
+
defined?(@@_percolator) ? @@_percolator : nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module InstanceMethods
|
65
|
+
|
66
|
+
# Run this document against registered percolator queries, without indexing it.
|
67
|
+
#
|
68
|
+
# First, register a percolator query:
|
69
|
+
#
|
70
|
+
# Article.index.register_percolator_query('fail') { |query| query.string 'fail' }
|
71
|
+
#
|
72
|
+
# Then, you may query the percolator endpoint with:
|
73
|
+
#
|
74
|
+
# p Article.new(:title => 'This is a FAIL!').percolate
|
75
|
+
#
|
76
|
+
# Optionally, you may pass a block to filter which percolator queries will be executed.
|
77
|
+
#
|
78
|
+
# See <http://www.elasticsearch.org/guide/reference/api/percolate.html> for more information.
|
79
|
+
def percolate(&block)
|
80
|
+
index.percolate instance, block
|
81
|
+
end
|
82
|
+
|
83
|
+
# Mark this instance for percolation when adding it to the index.
|
84
|
+
#
|
85
|
+
def percolate=(pattern)
|
86
|
+
@_percolator = pattern
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the status or pattern of percolator for this instance.
|
90
|
+
#
|
91
|
+
def percolator
|
92
|
+
@_percolator || instance.class.tire.percolator || nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Allows to use _ElasticSearch_ as a primary database (storage).
|
5
|
+
#
|
6
|
+
# Contains all the `Tire::Model::Search` features and provides
|
7
|
+
# an [_ActiveModel_](http://rubygems.org/gems/activemodel)-compatible
|
8
|
+
# interface for persistance.
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
#
|
12
|
+
# class Article
|
13
|
+
# include Tire::Model::Persistence
|
14
|
+
#
|
15
|
+
# property :title
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# Article.create :id => 1, :title => 'One'
|
19
|
+
#
|
20
|
+
# article = Article.find
|
21
|
+
#
|
22
|
+
# article.destroy
|
23
|
+
#
|
24
|
+
module Persistence
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
|
28
|
+
base.class_eval do
|
29
|
+
include ActiveModel::AttributeMethods
|
30
|
+
include ActiveModel::Validations
|
31
|
+
include ActiveModel::Serialization
|
32
|
+
include ActiveModel::Serializers::JSON
|
33
|
+
include ActiveModel::Naming
|
34
|
+
include ActiveModel::Conversion
|
35
|
+
|
36
|
+
extend ActiveModel::Callbacks
|
37
|
+
define_model_callbacks :save, :destroy
|
38
|
+
|
39
|
+
include Tire::Model::Search
|
40
|
+
include Tire::Model::Callbacks
|
41
|
+
|
42
|
+
extend Persistence::Finders::ClassMethods
|
43
|
+
extend Persistence::Attributes::ClassMethods
|
44
|
+
include Persistence::Attributes::InstanceMethods
|
45
|
+
|
46
|
+
include Persistence::Storage
|
47
|
+
|
48
|
+
['_score', '_type', '_index', '_version', 'sort', 'highlight', 'matches', '_explanation'].each do |attr|
|
49
|
+
define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
|
50
|
+
define_method("#{attr}") { @attributes[attr] }
|
51
|
+
end
|
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
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
# Provides infrastructure for declaring the model properties and accessing them.
|
7
|
+
#
|
8
|
+
module Attributes
|
9
|
+
|
10
|
+
module ClassMethods
|
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
|
+
#
|
26
|
+
def property(name, options = {})
|
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:
|
39
|
+
properties << name.to_s unless properties.include?(name.to_s)
|
40
|
+
|
41
|
+
# Define convenience <NAME>? method:
|
42
|
+
define_query_method name.to_sym
|
43
|
+
|
44
|
+
# ActiveModel compatibility. NEEDED?
|
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:
|
56
|
+
mapping[name] = options
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def properties
|
61
|
+
@properties ||= []
|
62
|
+
end
|
63
|
+
|
64
|
+
def property_defaults
|
65
|
+
@property_defaults ||= {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def property_types
|
69
|
+
@property_types ||= {}
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def define_query_method name
|
75
|
+
define_method("#{name}?") { !! send(name) }
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
module InstanceMethods
|
81
|
+
|
82
|
+
attr_accessor :id
|
83
|
+
|
84
|
+
def initialize(attributes={})
|
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))
|
93
|
+
end
|
94
|
+
|
95
|
+
def attributes
|
96
|
+
self.class.properties.
|
97
|
+
inject( self.id ? {'id' => self.id} : {} ) {|attributes, key| attributes[key] = send(key); attributes}
|
98
|
+
end
|
99
|
+
|
100
|
+
def attribute_names
|
101
|
+
self.class.properties.sort
|
102
|
+
end
|
103
|
+
|
104
|
+
def has_attribute?(name)
|
105
|
+
properties.include?(name.to_s)
|
106
|
+
end
|
107
|
+
alias :has_property? :has_attribute?
|
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
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Tire
|
2
|
+
module Model
|
3
|
+
|
4
|
+
module Persistence
|
5
|
+
|
6
|
+
# Provides infrastructure for an _ActiveRecord_-like interface for finding records.
|
7
|
+
#
|
8
|
+
module Finders
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def find *args
|
13
|
+
# TODO: Options like `sort`
|
14
|
+
old_wrapper = Tire::Configuration.wrapper
|
15
|
+
Tire::Configuration.wrapper self
|
16
|
+
options = args.pop if args.last.is_a?(Hash)
|
17
|
+
args.flatten!
|
18
|
+
if args.size > 1
|
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
|
25
|
+
else
|
26
|
+
case args = args.pop
|
27
|
+
when Fixnum, String
|
28
|
+
index.retrieve document_type, args
|
29
|
+
when :all, :first
|
30
|
+
send(args)
|
31
|
+
else
|
32
|
+
raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
ensure
|
36
|
+
Tire::Configuration.wrapper old_wrapper
|
37
|
+
end
|
38
|
+
|
39
|
+
def all
|
40
|
+
# TODO: Options like `sort`; Possibly `filters`
|
41
|
+
old_wrapper = Tire::Configuration.wrapper
|
42
|
+
Tire::Configuration.wrapper self
|
43
|
+
s = Tire::Search::Search.new(index.name).query { all }
|
44
|
+
s.results
|
45
|
+
ensure
|
46
|
+
Tire::Configuration.wrapper old_wrapper
|
47
|
+
end
|
48
|
+
|
49
|
+
def first
|
50
|
+
# TODO: Options like `sort`; Possibly `filters`
|
51
|
+
old_wrapper = Tire::Configuration.wrapper
|
52
|
+
Tire::Configuration.wrapper self
|
53
|
+
s = Tire::Search::Search.new(index.name).query { all }.size(1)
|
54
|
+
s.results.first
|
55
|
+
ensure
|
56
|
+
Tire::Configuration.wrapper old_wrapper
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|