elasticsearch-model 0.0.1 → 0.1.0.rc1

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 (64) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE.txt +1 -1
  3. data/README.md +669 -8
  4. data/Rakefile +52 -0
  5. data/elasticsearch-model.gemspec +48 -17
  6. data/examples/activerecord_article.rb +77 -0
  7. data/examples/activerecord_associations.rb +153 -0
  8. data/examples/couchbase_article.rb +66 -0
  9. data/examples/datamapper_article.rb +71 -0
  10. data/examples/mongoid_article.rb +68 -0
  11. data/examples/ohm_article.rb +70 -0
  12. data/examples/riak_article.rb +52 -0
  13. data/gemfiles/3.gemfile +11 -0
  14. data/gemfiles/4.gemfile +11 -0
  15. data/lib/elasticsearch/model.rb +151 -1
  16. data/lib/elasticsearch/model/adapter.rb +145 -0
  17. data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
  18. data/lib/elasticsearch/model/adapters/default.rb +44 -0
  19. data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
  20. data/lib/elasticsearch/model/callbacks.rb +35 -0
  21. data/lib/elasticsearch/model/client.rb +61 -0
  22. data/lib/elasticsearch/model/importing.rb +94 -0
  23. data/lib/elasticsearch/model/indexing.rb +332 -0
  24. data/lib/elasticsearch/model/naming.rb +101 -0
  25. data/lib/elasticsearch/model/proxy.rb +127 -0
  26. data/lib/elasticsearch/model/response.rb +70 -0
  27. data/lib/elasticsearch/model/response/base.rb +44 -0
  28. data/lib/elasticsearch/model/response/pagination.rb +96 -0
  29. data/lib/elasticsearch/model/response/records.rb +71 -0
  30. data/lib/elasticsearch/model/response/result.rb +50 -0
  31. data/lib/elasticsearch/model/response/results.rb +32 -0
  32. data/lib/elasticsearch/model/searching.rb +107 -0
  33. data/lib/elasticsearch/model/serializing.rb +35 -0
  34. data/lib/elasticsearch/model/support/forwardable.rb +44 -0
  35. data/lib/elasticsearch/model/version.rb +1 -1
  36. data/test/integration/active_record_associations_parent_child.rb +138 -0
  37. data/test/integration/active_record_associations_test.rb +306 -0
  38. data/test/integration/active_record_basic_test.rb +139 -0
  39. data/test/integration/active_record_import_test.rb +74 -0
  40. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  41. data/test/integration/active_record_pagination_test.rb +109 -0
  42. data/test/integration/mongoid_basic_test.rb +178 -0
  43. data/test/test_helper.rb +57 -0
  44. data/test/unit/adapter_active_record_test.rb +93 -0
  45. data/test/unit/adapter_default_test.rb +31 -0
  46. data/test/unit/adapter_mongoid_test.rb +87 -0
  47. data/test/unit/adapter_test.rb +69 -0
  48. data/test/unit/callbacks_test.rb +30 -0
  49. data/test/unit/client_test.rb +27 -0
  50. data/test/unit/importing_test.rb +97 -0
  51. data/test/unit/indexing_test.rb +364 -0
  52. data/test/unit/module_test.rb +46 -0
  53. data/test/unit/naming_test.rb +76 -0
  54. data/test/unit/proxy_test.rb +88 -0
  55. data/test/unit/response_base_test.rb +40 -0
  56. data/test/unit/response_pagination_test.rb +159 -0
  57. data/test/unit/response_records_test.rb +87 -0
  58. data/test/unit/response_result_test.rb +52 -0
  59. data/test/unit/response_results_test.rb +31 -0
  60. data/test/unit/response_test.rb +57 -0
  61. data/test/unit/searching_search_request_test.rb +73 -0
  62. data/test/unit/searching_test.rb +39 -0
  63. data/test/unit/serializing_test.rb +17 -0
  64. metadata +418 -11
@@ -0,0 +1,101 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Provides methods for getting and setting index name and document type for the model
5
+ #
6
+ module Naming
7
+
8
+ module ClassMethods
9
+
10
+ # Get or set the name of the index
11
+ #
12
+ # @example Set the index name for the `Article` model
13
+ #
14
+ # class Article
15
+ # index_name "articles-#{Rails.env}"
16
+ # end
17
+ #
18
+ # @example Directly set the index name for the `Article` model
19
+ #
20
+ # Article.index_name "articles-#{Rails.env}"
21
+ #
22
+ # TODO: Dynamic names a la Tire -- `Article.index_name { "articles-#{Time.now.year}" }`
23
+ #
24
+ def index_name name=nil
25
+ @index_name = name || @index_name || self.model_name.collection.gsub(/\//, '-')
26
+ end
27
+
28
+ # Set the index name
29
+ #
30
+ # @see index_name
31
+ def index_name=(name)
32
+ @index_name = name
33
+ end
34
+
35
+ # Get or set the document type
36
+ #
37
+ # @example Set the document type for the `Article` model
38
+ #
39
+ # class Article
40
+ # document_type "my-article"
41
+ # end
42
+ #
43
+ # @example Directly set the document type for the `Article` model
44
+ #
45
+ # Article.document_type "my-article"
46
+ #
47
+ def document_type name=nil
48
+ @document_type = name || @document_type || self.model_name.element
49
+ end
50
+
51
+
52
+ # Set the document type
53
+ #
54
+ # @see document_type
55
+ #
56
+ def document_type=(name)
57
+ @document_type = name
58
+ end
59
+ end
60
+
61
+ module InstanceMethods
62
+
63
+ # Get or set the index name for the model instance
64
+ #
65
+ # @example Set the index name for an instance of the `Article` model
66
+ #
67
+ # @article.index_name "articles-#{@article.user_id}"
68
+ # @article.__elasticsearch__.update_document
69
+ #
70
+ def index_name name=nil
71
+ @index_name = name || @index_name || self.class.index_name
72
+ end
73
+
74
+ # Set the index name
75
+ #
76
+ # @see index_name
77
+ def index_name=(name)
78
+ @index_name = name
79
+ end
80
+
81
+ # @example Set the document type for an instance of the `Article` model
82
+ #
83
+ # @article.document_type "my-article"
84
+ # @article.__elasticsearch__.update_document
85
+ #
86
+ def document_type name=nil
87
+ @document_type = name || @document_type || self.class.document_type
88
+ end
89
+
90
+ # Set the document type
91
+ #
92
+ # @see document_type
93
+ #
94
+ def document_type=(name)
95
+ @document_type = name
96
+ end
97
+ end
98
+
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,127 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # This module provides a proxy interfacing between the including class and
5
+ # {Elasticsearch::Model}, preventing the pollution of the including class namespace.
6
+ #
7
+ # The only "gateway" between the model and Elasticsearch::Model is the
8
+ # `__elasticsearch__` class and instance method.
9
+ #
10
+ # The including class must be compatible with
11
+ # [ActiveModel](https://github.com/rails/rails/tree/master/activemodel).
12
+ #
13
+ # @example Include the {Elasticsearch::Model} module into an `Article` model
14
+ #
15
+ # class Article < ActiveRecord::Base
16
+ # include Elasticsearch::Model
17
+ # end
18
+ #
19
+ # Article.__elasticsearch__.respond_to?(:search)
20
+ # # => true
21
+ #
22
+ # article = Article.first
23
+ #
24
+ # article.respond_to? :index_document
25
+ # # => false
26
+ #
27
+ # article.__elasticsearch__.respond_to?(:index_document)
28
+ # # => true
29
+ #
30
+ module Proxy
31
+
32
+ # Define the `__elasticsearch__` class and instance methods in the including class
33
+ # and register a callback for intercepting changes in the model.
34
+ #
35
+ # @note The callback is triggered only when `Elasticsearch::Model` is included in the
36
+ # module and the functionality is accessible via the proxy.
37
+ #
38
+ def self.included(base)
39
+ base.class_eval do
40
+ # {ClassMethodsProxy} instance, accessed as `MyModel.__elasticsearch__`
41
+ #
42
+ def self.__elasticsearch__ &block
43
+ @__elasticsearch__ ||= ClassMethodsProxy.new(self)
44
+ @__elasticsearch__.instance_eval(&block) if block_given?
45
+ @__elasticsearch__
46
+ end
47
+
48
+ # {InstanceMethodsProxy}, accessed as `@mymodel.__elasticsearch__`
49
+ #
50
+ def __elasticsearch__ &block
51
+ @__elasticsearch__ ||= InstanceMethodsProxy.new(self)
52
+ @__elasticsearch__.instance_eval(&block) if block_given?
53
+ @__elasticsearch__
54
+ end
55
+
56
+ # Register a callback for storing changed attributes for models which implement
57
+ # `before_save` and `changed_attributes` methods (when `Elasticsearch::Model` is included)
58
+ #
59
+ # @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html
60
+ #
61
+ before_save do |i|
62
+ i.__elasticsearch__.instance_variable_set(:@__changed_attributes,
63
+ Hash[ i.changes.map { |key, value| [key, value.last] } ])
64
+ end if respond_to?(:before_save) && instance_methods.include?(:changed_attributes)
65
+ end
66
+ end
67
+
68
+ # Common module for the proxy classes
69
+ #
70
+ module Base
71
+ attr_reader :target
72
+
73
+ def initialize(target)
74
+ @target = target
75
+ end
76
+
77
+ # Delegate methods to `@target`
78
+ #
79
+ def method_missing(method_name, *arguments, &block)
80
+ target.respond_to?(method_name) ? target.__send__(method_name, *arguments, &block) : super
81
+ end
82
+
83
+ # Respond to methods from `@target`
84
+ #
85
+ def respond_to?(method_name, include_private = false)
86
+ target.respond_to?(method_name) || super
87
+ end
88
+
89
+ def inspect
90
+ "[PROXY] #{target.inspect}"
91
+ end
92
+ end
93
+
94
+ # A proxy interfacing between Elasticsearch::Model class methods and model class methods
95
+ #
96
+ # TODO: Inherit from BasicObject and make Pry's `ls` command behave?
97
+ #
98
+ class ClassMethodsProxy
99
+ include Base
100
+ end
101
+
102
+ # A proxy interfacing between Elasticsearch::Model instance methods and model instance methods
103
+ #
104
+ # TODO: Inherit from BasicObject and make Pry's `ls` command behave?
105
+ #
106
+ class InstanceMethodsProxy
107
+ include Base
108
+
109
+ def klass
110
+ target.class
111
+ end
112
+
113
+ def class
114
+ klass.__elasticsearch__
115
+ end
116
+
117
+ # Need to redefine `as_json` because we're not inheriting from `BasicObject`;
118
+ # see TODO note above.
119
+ #
120
+ def as_json(options={})
121
+ target.as_json(options)
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,70 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains modules and classes for wrapping the response from Elasticsearch
5
+ #
6
+ module Response
7
+
8
+ # Encapsulate the response returned from the Elasticsearch client
9
+ #
10
+ # Implements Enumerable and forwards its methods to the {#results} object.
11
+ #
12
+ class Response
13
+ attr_reader :klass, :search, :response,
14
+ :took, :timed_out, :shards
15
+
16
+ include Enumerable
17
+ extend Support::Forwardable
18
+
19
+ forward :results, :each, :empty?, :size, :slice, :[], :to_ary
20
+
21
+ def initialize(klass, search, options={})
22
+ @klass = klass
23
+ @search = search
24
+ end
25
+
26
+ # Returns the Elasticsearch response
27
+ #
28
+ # @return [Hash]
29
+ #
30
+ def response
31
+ @response ||= search.execute!
32
+ end
33
+
34
+ # Returns the collection of "hits" from Elasticsearch
35
+ #
36
+ # @return [Results]
37
+ #
38
+ def results
39
+ @results ||= Results.new(klass, self)
40
+ end
41
+
42
+ # Returns the collection of records from the database
43
+ #
44
+ # @return [Records]
45
+ #
46
+ def records
47
+ @records ||= Records.new(klass, self)
48
+ end
49
+
50
+ # Returns the "took" time
51
+ #
52
+ def took
53
+ response['took']
54
+ end
55
+
56
+ # Returns whether the response timed out
57
+ #
58
+ def timed_out
59
+ response['timed_out']
60
+ end
61
+
62
+ # Returns the statistics on shards
63
+ #
64
+ def shards
65
+ Hashie::Mash.new(response['_shards'])
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+ # Common funtionality for classes in the {Elasticsearch::Model::Response} module
5
+ #
6
+ module Base
7
+ attr_reader :klass, :response
8
+
9
+ # @param klass [Class] The name of the model class
10
+ # @param response [Hash] The full response returned from Elasticsearch client
11
+ # @param options [Hash] Optional parameters
12
+ #
13
+ def initialize(klass, response, options={})
14
+ @klass = klass
15
+ @response = response
16
+ end
17
+
18
+ # @abstract Implement this method in specific class
19
+ #
20
+ def results
21
+ raise NotImplemented, "Implement this method in #{klass}"
22
+ end
23
+
24
+ # @abstract Implement this method in specific class
25
+ #
26
+ def records
27
+ raise NotImplemented, "Implement this method in #{klass}"
28
+ end
29
+
30
+ # Returns the total number of hits
31
+ #
32
+ def total
33
+ response.response['hits']['total']
34
+ end
35
+
36
+ # Returns the max_score
37
+ #
38
+ def max_score
39
+ response.response['hits']['max_score']
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,96 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Pagination for search results/records
6
+ #
7
+ module Pagination
8
+ # Allow models to be paginated with the "kaminari" gem [https://github.com/amatsuda/kaminari]
9
+ #
10
+ module Kaminari
11
+ def self.included(base)
12
+ # Include the Kaminari configuration and paging method in response
13
+ #
14
+ base.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods
15
+ base.__send__ :include, ::Kaminari::PageScopeMethods
16
+
17
+ # Include the Kaminari paging methods in results and records
18
+ #
19
+ Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::ConfigurationMethods::ClassMethods
20
+ Elasticsearch::Model::Response::Results.__send__ :include, ::Kaminari::PageScopeMethods
21
+ Elasticsearch::Model::Response::Records.__send__ :include, ::Kaminari::PageScopeMethods
22
+
23
+ Elasticsearch::Model::Response::Results.__send__ :forward, :response, :limit_value, :offset_value, :total_count
24
+ Elasticsearch::Model::Response::Records.__send__ :forward, :response, :limit_value, :offset_value, :total_count
25
+
26
+ base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ # Define the `page` Kaminari method
28
+ #
29
+ def #{::Kaminari.config.page_method_name}(num=nil)
30
+ @results = nil
31
+ @records = nil
32
+ @response = nil
33
+ self.search.definition.update size: klass.default_per_page,
34
+ from: klass.default_per_page * ([num.to_i, 1].max - 1)
35
+ self
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ # Returns the current "limit" (`size`) value
41
+ #
42
+ def limit_value
43
+ case
44
+ when search.definition[:body] && search.definition[:body][:size]
45
+ search.definition[:body][:size]
46
+ when search.definition[:size]
47
+ search.definition[:size]
48
+ else
49
+ 0
50
+ end
51
+ end
52
+
53
+ # Returns the current "offset" (`from`) value
54
+ #
55
+ def offset_value
56
+ case
57
+ when search.definition[:body] && search.definition[:body][:from]
58
+ search.definition[:body][:from]
59
+ when search.definition[:from]
60
+ search.definition[:from]
61
+ else
62
+ 0
63
+ end
64
+ end
65
+
66
+ # Set the "limit" (`size`) value
67
+ #
68
+ def limit(value)
69
+ @results = nil
70
+ @records = nil
71
+ @response = nil
72
+ search.definition.update :size => value
73
+ self
74
+ end
75
+
76
+ # Set the "offset" (`from`) value
77
+ #
78
+ def offset(value)
79
+ @results = nil
80
+ @records = nil
81
+ @response = nil
82
+ search.definition.update :from => value
83
+ self
84
+ end
85
+
86
+ # Returns the total number of results
87
+ #
88
+ def total_count
89
+ results.total
90
+ end
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,71 @@
1
+ module Elasticsearch
2
+ module Model
3
+ module Response
4
+
5
+ # Encapsulates the collection of records returned from the database
6
+ #
7
+ # Implements Enumerable and forwards its methods to the {#records} object,
8
+ # which is provided by an {Elasticsearch::Model::Adapter::Adapter} implementation.
9
+ #
10
+ class Records
11
+ include Enumerable
12
+
13
+ extend Support::Forwardable
14
+ forward :records, :each, :empty?, :size, :slice, :[], :to_a, :to_ary
15
+
16
+ include Base
17
+
18
+ # @see Base#initialize
19
+ #
20
+ def initialize(klass, response, options={})
21
+ super
22
+
23
+ # Include module provided by the adapter in the singleton class ("metaclass")
24
+ #
25
+ adapter = Adapter.from_class(klass)
26
+ metaclass = class << self; self; end
27
+ metaclass.__send__ :include, adapter.records_mixin
28
+
29
+ self
30
+ end
31
+
32
+ # Returns the hit IDs
33
+ #
34
+ def ids
35
+ response.response['hits']['hits'].map { |hit| hit['_id'] }
36
+ end
37
+
38
+ # Returns the {Results} collection
39
+ #
40
+ def results
41
+ response.results
42
+ end
43
+
44
+ # Yields [record, hit] pairs to the block
45
+ #
46
+ def each_with_hit(&block)
47
+ records.zip(results).each(&block)
48
+ end
49
+
50
+ # Yields [record, hit] pairs and returns the result
51
+ #
52
+ def map_with_hit(&block)
53
+ records.zip(results).map(&block)
54
+ end
55
+
56
+ # Delegate methods to `@records`
57
+ #
58
+ def method_missing(method_name, *arguments)
59
+ records.respond_to?(method_name) ? records.__send__(method_name, *arguments) : super
60
+ end
61
+
62
+ # Respond to methods from `@records`
63
+ #
64
+ def respond_to?(method_name, include_private = false)
65
+ records.respond_to?(method_name) || super
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+ end