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.
Files changed (59) hide show
  1. data/lib/tire.rb +18 -3
  2. data/lib/tire/alias.rb +11 -35
  3. data/lib/tire/index.rb +34 -76
  4. data/lib/tire/model/callbacks.rb +40 -0
  5. data/lib/tire/model/import.rb +26 -0
  6. data/lib/tire/model/indexing.rb +128 -0
  7. data/lib/tire/model/naming.rb +100 -0
  8. data/lib/tire/model/percolate.rb +99 -0
  9. data/lib/tire/model/persistence.rb +72 -0
  10. data/lib/tire/model/persistence/attributes.rb +143 -0
  11. data/lib/tire/model/persistence/finders.rb +66 -0
  12. data/lib/tire/model/persistence/storage.rb +71 -0
  13. data/lib/tire/model/search.rb +305 -0
  14. data/lib/tire/results/collection.rb +38 -13
  15. data/lib/tire/results/item.rb +19 -0
  16. data/lib/tire/rubyext/hash.rb +8 -0
  17. data/lib/tire/rubyext/ruby_1_8.rb +54 -0
  18. data/lib/tire/rubyext/symbol.rb +11 -0
  19. data/lib/tire/search.rb +7 -8
  20. data/lib/tire/search/scan.rb +8 -8
  21. data/lib/tire/search/sort.rb +1 -1
  22. data/lib/tire/utils.rb +17 -0
  23. data/lib/tire/version.rb +7 -38
  24. data/test/integration/active_model_indexing_test.rb +51 -0
  25. data/test/integration/active_model_searchable_test.rb +114 -0
  26. data/test/integration/active_record_searchable_test.rb +446 -0
  27. data/test/integration/mongoid_searchable_test.rb +309 -0
  28. data/test/integration/persistent_model_test.rb +117 -0
  29. data/test/integration/reindex_test.rb +2 -2
  30. data/test/integration/scan_test.rb +1 -1
  31. data/test/models/active_model_article.rb +31 -0
  32. data/test/models/active_model_article_with_callbacks.rb +49 -0
  33. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  34. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  35. data/test/models/active_record_models.rb +122 -0
  36. data/test/models/mongoid_models.rb +97 -0
  37. data/test/models/persistent_article.rb +11 -0
  38. data/test/models/persistent_article_in_namespace.rb +12 -0
  39. data/test/models/persistent_article_with_casting.rb +28 -0
  40. data/test/models/persistent_article_with_defaults.rb +11 -0
  41. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  42. data/test/models/supermodel_article.rb +17 -0
  43. data/test/models/validated_model.rb +11 -0
  44. data/test/test_helper.rb +27 -3
  45. data/test/unit/active_model_lint_test.rb +17 -0
  46. data/test/unit/index_alias_test.rb +3 -17
  47. data/test/unit/index_test.rb +30 -18
  48. data/test/unit/model_callbacks_test.rb +116 -0
  49. data/test/unit/model_import_test.rb +71 -0
  50. data/test/unit/model_persistence_test.rb +516 -0
  51. data/test/unit/model_search_test.rb +899 -0
  52. data/test/unit/results_collection_test.rb +60 -0
  53. data/test/unit/results_item_test.rb +37 -0
  54. data/test/unit/rubyext_test.rb +3 -3
  55. data/test/unit/search_test.rb +1 -6
  56. data/test/unit/tire_test.rb +15 -0
  57. data/tire.gemspec +30 -13
  58. metadata +153 -41
  59. 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