elasticsearch-model 0.0.1 → 0.1.0.rc1

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