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,68 @@
1
+ # Mongoid and Elasticsearch
2
+ # =========================
3
+ #
4
+ # http://mongoid.org/en/mongoid/index.html
5
+
6
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7
+
8
+ require 'pry'
9
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
10
+
11
+ require 'benchmark'
12
+ require 'logger'
13
+ require 'ansi/core'
14
+ require 'mongoid'
15
+
16
+ require 'elasticsearch/model'
17
+ require 'elasticsearch/model/callbacks'
18
+
19
+ Mongoid.logger.level = Logger::DEBUG
20
+ Moped.logger.level = Logger::DEBUG
21
+
22
+ Mongoid.connect_to 'articles'
23
+
24
+ Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:9250', log: true
25
+
26
+ class Article
27
+ include Mongoid::Document
28
+ field :id, type: String
29
+ field :title, type: String
30
+ field :published_at, type: DateTime
31
+ attr_accessible :id, :title, :published_at if respond_to? :attr_accessible
32
+
33
+ def as_indexed_json(options={})
34
+ as_json(except: [:id, :_id])
35
+ end
36
+ end
37
+
38
+ # Extend the model with Elasticsearch support
39
+ #
40
+ Article.__send__ :include, Elasticsearch::Model
41
+ # Article.__send__ :include, Elasticsearch::Model::Callbacks
42
+
43
+ # Store data
44
+ #
45
+ Article.delete_all
46
+ Article.create id: '1', title: 'Foo'
47
+ Article.create id: '2', title: 'Bar'
48
+ Article.create id: '3', title: 'Foo Foo'
49
+
50
+ # Index data
51
+ #
52
+ client = Elasticsearch::Client.new host:'localhost:9250', log:true
53
+
54
+ client.indices.delete index: 'articles' rescue nil
55
+ client.bulk index: 'articles',
56
+ type: 'article',
57
+ body: Article.all.map { |a| { index: { _id: a.id, data: a.attributes } } },
58
+ refresh: true
59
+
60
+ # puts Benchmark.realtime { 9_875.times { |i| Article.create title: "Foo #{i}" } }
61
+
62
+ puts '', '-'*Pry::Terminal.width!
63
+
64
+ response = Article.search 'foo';
65
+
66
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
67
+ input: StringIO.new('response.records.to_a'),
68
+ quiet: true)
@@ -0,0 +1,70 @@
1
+ # Ohm for Redis and Elasticsearch
2
+ # ===============================
3
+ #
4
+ # https://github.com/soveran/ohm#example
5
+
6
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7
+
8
+ require 'pry'
9
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
10
+
11
+ require 'logger'
12
+ require 'ansi/core'
13
+ require 'active_model'
14
+ require 'ohm'
15
+
16
+ require 'elasticsearch/model'
17
+
18
+ class Article < Ohm::Model
19
+ # Include JSON serialization from ActiveModel
20
+ include ActiveModel::Serializers::JSON
21
+
22
+ attribute :title
23
+ attribute :published_at
24
+ end
25
+
26
+ # Extend the model with Elasticsearch support
27
+ #
28
+ Article.__send__ :include, Elasticsearch::Model
29
+
30
+ # Register a custom adapter
31
+ #
32
+ module Elasticsearch
33
+ module Model
34
+ module Adapter
35
+ module Ohm
36
+ Adapter.register self,
37
+ lambda { |klass| defined?(::Ohm::Model) and klass.ancestors.include?(::Ohm::Model) }
38
+ module Records
39
+ def records
40
+ klass.fetch(@ids)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # Configure the Elasticsearch client to log operations
49
+ #
50
+ Elasticsearch::Model.client = Elasticsearch::Client.new log: true
51
+
52
+ puts '', '-'*Pry::Terminal.width!
53
+
54
+ Article.all.map { |a| a.delete }
55
+ Article.create id: '1', title: 'Foo'
56
+ Article.create id: '2', title: 'Bar'
57
+ Article.create id: '3', title: 'Foo Foo'
58
+
59
+ Article.__elasticsearch__.client.indices.delete index: 'articles' rescue nil
60
+ Article.__elasticsearch__.client.bulk index: 'articles',
61
+ type: 'article',
62
+ body: Article.all.map { |a| { index: { _id: a.id, data: a.attributes } } },
63
+ refresh: true
64
+
65
+
66
+ response = Article.search 'foo', index: 'articles', type: 'article';
67
+
68
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
69
+ input: StringIO.new('response.records.to_a'),
70
+ quiet: true)
@@ -0,0 +1,52 @@
1
+ # Riak and Elasticsearch
2
+ # ======================
3
+ #
4
+ # https://github.com/basho-labs/ripple
5
+
6
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7
+
8
+ require 'pry'
9
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
10
+
11
+ require 'logger'
12
+ require 'ripple'
13
+
14
+ require 'elasticsearch/model'
15
+
16
+ # Documents are stored as JSON objects in Riak but have rich
17
+ # semantics, including validations and associations.
18
+ class Article
19
+ include Ripple::Document
20
+
21
+ property :title, String
22
+ property :published_at, Time, :default => proc { Time.now }
23
+ end
24
+
25
+ # Extend the model with Elasticsearch support
26
+ #
27
+ Article.__send__ :include, Elasticsearch::Model
28
+
29
+ # Create documents in Riak
30
+ #
31
+ Article.destroy_all
32
+ Article.create id: '1', title: 'Foo'
33
+ Article.create id: '2', title: 'Bar'
34
+ Article.create id: '3', title: 'Foo Foo'
35
+
36
+ # Index data into Elasticsearch
37
+ #
38
+ client = Elasticsearch::Client.new log:true
39
+
40
+ client.indices.delete index: 'articles' rescue nil
41
+ client.bulk index: 'articles',
42
+ type: 'article',
43
+ body: Article.all.map { |a|
44
+ { index: { _id: a.key, data: JSON.parse(a.robject.raw_data) } }
45
+ }.as_json,
46
+ refresh: true
47
+
48
+ response = Article.search 'foo';
49
+
50
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
51
+ input: StringIO.new('response.records.to_a'),
52
+ quiet: true)
@@ -0,0 +1,11 @@
1
+ # Usage:
2
+ #
3
+ # $ BUNDLE_GEMFILE=./gemfiles/3.gemfile bundle install
4
+ # $ BUNDLE_GEMFILE=./gemfiles/3.gemfile bundle exec rake test:integration
5
+
6
+ source 'https://rubygems.org'
7
+
8
+ gem 'activerecord', '~> 3.2'
9
+ gem 'mongoid', '>= 3.0'
10
+
11
+ gemspec path: '../'
@@ -0,0 +1,11 @@
1
+ # Usage:
2
+ #
3
+ # $ BUNDLE_GEMFILE=./gemfiles/4.gemfile bundle install
4
+ # $ BUNDLE_GEMFILE=./gemfiles/4.gemfile bundle exec rake test:integration
5
+
6
+ source 'https://rubygems.org'
7
+
8
+ gem 'activerecord', '~> 4'
9
+ gem 'mongoid', '~> 4.0.0.alpha1'
10
+
11
+ gemspec path: '../'
@@ -1,6 +1,156 @@
1
- require "elasticsearch/model/version"
1
+ require 'forwardable'
2
+
3
+ require 'elasticsearch'
4
+
5
+ require 'hashie'
6
+
7
+ require 'elasticsearch/model/support/forwardable'
8
+
9
+ require 'elasticsearch/model/client'
10
+
11
+ require 'elasticsearch/model/adapter'
12
+ require 'elasticsearch/model/adapters/default'
13
+ require 'elasticsearch/model/adapters/active_record'
14
+ require 'elasticsearch/model/adapters/mongoid'
15
+
16
+ require 'elasticsearch/model/importing'
17
+ require 'elasticsearch/model/indexing'
18
+ require 'elasticsearch/model/naming'
19
+ require 'elasticsearch/model/serializing'
20
+ require 'elasticsearch/model/searching'
21
+ require 'elasticsearch/model/callbacks'
22
+
23
+ require 'elasticsearch/model/proxy'
24
+
25
+ require 'elasticsearch/model/response'
26
+ require 'elasticsearch/model/response/base'
27
+ require 'elasticsearch/model/response/result'
28
+ require 'elasticsearch/model/response/results'
29
+ require 'elasticsearch/model/response/records'
30
+ require 'elasticsearch/model/response/pagination'
31
+
32
+ require 'elasticsearch/model/version'
33
+
34
+ if defined?(::Kaminari)
35
+ Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari
36
+ end
2
37
 
3
38
  module Elasticsearch
39
+
40
+ # Elasticsearch integration for Ruby models
41
+ # =========================================
42
+ #
43
+ # `Elasticsearch::Model` contains modules for integrating the Elasticsearch search and analytical engine
44
+ # with ActiveModel-based classes, or models, for the Ruby programming language.
45
+ #
46
+ # It facilitates importing your data into an index, automatically updating it when a record changes,
47
+ # searching the specific index, setting up the index mapping or the model JSON serialization.
48
+ #
49
+ # When the `Elasticsearch::Model` module is included in your class, it automatically extends it
50
+ # with the functionality; see {Elasticsearch::Model.included}. Most methods are available via
51
+ # the `__elasticsearch__` class and instance method proxies.
52
+ #
53
+ # It is possible to include/extend the model with the corresponding
54
+ # modules directly, if that is desired:
55
+ #
56
+ # MyModel.__send__ :extend, Elasticsearch::Model::Client::ClassMethods
57
+ # MyModel.__send__ :include, Elasticsearch::Model::Client::InstanceMethods
58
+ # MyModel.__send__ :extend, Elasticsearch::Model::Searching::ClassMethods
59
+ # # ...
60
+ #
4
61
  module Model
62
+
63
+ # Adds the `Elasticsearch::Model` functionality to the including class.
64
+ #
65
+ # * Creates the `__elasticsearch__` class and instance methods, pointing to the proxy object
66
+ # * Includes the necessary modules in the proxy classes
67
+ # * Sets up delegation for crucial methods such as `search`, etc.
68
+ #
69
+ # @example Include the module in the `Article` model definition
70
+ #
71
+ # class Article < ActiveRecord::Base
72
+ # include Elasticsearch::Model
73
+ # end
74
+ #
75
+ # @example Inject the module into the `Article` model during run time
76
+ #
77
+ # Article.__send__ :include, Elasticsearch::Model
78
+ #
79
+ #
80
+ def self.included(base)
81
+ base.class_eval do
82
+ include Elasticsearch::Model::Proxy
83
+
84
+ Elasticsearch::Model::Proxy::ClassMethodsProxy.class_eval do
85
+ include Elasticsearch::Model::Client::ClassMethods
86
+ include Elasticsearch::Model::Naming::ClassMethods
87
+ include Elasticsearch::Model::Indexing::ClassMethods
88
+ include Elasticsearch::Model::Searching::ClassMethods
89
+ end
90
+
91
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval do
92
+ include Elasticsearch::Model::Client::InstanceMethods
93
+ include Elasticsearch::Model::Naming::InstanceMethods
94
+ include Elasticsearch::Model::Indexing::InstanceMethods
95
+ include Elasticsearch::Model::Serializing::InstanceMethods
96
+ end
97
+
98
+ Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1
99
+ def as_indexed_json(options={})
100
+ target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super
101
+ end
102
+ CODE
103
+
104
+ # Delegate important methods to the `__elasticsearch__` proxy, unless they are defined already
105
+ #
106
+ extend Support::Forwardable
107
+ forward :'self.__elasticsearch__', :search unless respond_to?(:search)
108
+ forward :'self.__elasticsearch__', :mapping unless respond_to?(:mapping)
109
+ forward :'self.__elasticsearch__', :mappings unless respond_to?(:mappings)
110
+ forward :'self.__elasticsearch__', :settings unless respond_to?(:settings)
111
+ forward :'self.__elasticsearch__', :index_name unless respond_to?(:index_name)
112
+ forward :'self.__elasticsearch__', :document_type unless respond_to?(:document_type)
113
+ forward :'self.__elasticsearch__', :import unless respond_to?(:import)
114
+
115
+ # Mix the importing module into the proxy
116
+ #
117
+ self.__elasticsearch__.class_eval do
118
+ include Elasticsearch::Model::Importing::ClassMethods
119
+ include Adapter.from_class(base).importing_mixin
120
+ end
121
+ end
122
+ end
123
+
124
+ module ClassMethods
125
+
126
+ # Get the client common for all models
127
+ #
128
+ # @example Get the client
129
+ #
130
+ # Elasticsearch::Model.client
131
+ # => #<Elasticsearch::Transport::Client:0x007f96a7d0d000 @transport=... >
132
+ #
133
+ def client
134
+ @client ||= Elasticsearch::Client.new
135
+ end
136
+
137
+ # Set the client for all models
138
+ #
139
+ # @example Configure (set) the client for all models
140
+ #
141
+ # Elasticsearch::Model.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
142
+ # => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
143
+ #
144
+ # @note You have to set the client before you call Elasticsearch methods on the model,
145
+ # or set it directly on the model; see {Elasticsearch::Model::Client::ClassMethods#client}
146
+ #
147
+ def client=(client)
148
+ @client = client
149
+ end
150
+
151
+ end
152
+ extend ClassMethods
153
+
154
+ class NotImplemented < NoMethodError; end
5
155
  end
6
156
  end
@@ -0,0 +1,145 @@
1
+ module Elasticsearch
2
+ module Model
3
+
4
+ # Contains an adapter which provides OxM-specific implementations for common behaviour:
5
+ #
6
+ # * {Adapter::Adapter#records_mixin Fetching records from the database}
7
+ # * {Adapter::Adapter#callbacks_mixin Model callbacks for automatic index updates}
8
+ # * {Adapter::Adapter#importing_mixin Efficient bulk loading from the database}
9
+ #
10
+ # @see Elasticsearch::Model::Adapter::Default
11
+ # @see Elasticsearch::Model::Adapter::ActiveRecord
12
+ # @see Elasticsearch::Model::Adapter::Mongoid
13
+ #
14
+ module Adapter
15
+
16
+ # Returns an adapter based on the Ruby class passed
17
+ #
18
+ # @example Create an adapter for an ActiveRecord-based model
19
+ #
20
+ # class Article < ActiveRecord::Base; end
21
+ #
22
+ # myadapter = Elasticsearch::Model::Adapter.from_class(Article)
23
+ # myadapter.adapter
24
+ # # => Elasticsearch::Model::Adapter::ActiveRecord
25
+ #
26
+ # @see Adapter.adapters The list of included adapters
27
+ # @see Adapter.register Register a custom adapter
28
+ #
29
+ def from_class(klass)
30
+ Adapter.new(klass)
31
+ end; module_function :from_class
32
+
33
+ # Returns registered adapters
34
+ #
35
+ # @see ::Elasticsearch::Model::Adapter::Adapter.adapters
36
+ #
37
+ def adapters
38
+ Adapter.adapters
39
+ end; module_function :adapters
40
+
41
+ # Registers an adapter
42
+ #
43
+ # @see ::Elasticsearch::Model::Adapter::Adapter.register
44
+ #
45
+ def register(name, condition)
46
+ Adapter.register(name, condition)
47
+ end; module_function :register
48
+
49
+ # Contains an adapter for specific OxM or architecture.
50
+ #
51
+ class Adapter
52
+ attr_reader :klass
53
+
54
+ def initialize(klass)
55
+ @klass = klass
56
+ end
57
+
58
+ # Registers an adapter for specific condition
59
+ #
60
+ # @param name [Module] The module containing the implemented interface
61
+ # @param condition [Proc] An object with a `call` method which is evaluated in {.adapter}
62
+ #
63
+ # @example Register an adapter for DataMapper
64
+ #
65
+ # module DataMapperAdapter
66
+ #
67
+ # # Implement the interface for fetching records
68
+ # #
69
+ # module Records
70
+ # def records
71
+ # klass.all(id: @ids)
72
+ # end
73
+ #
74
+ # # ...
75
+ # end
76
+ # end
77
+ #
78
+ # # Register the adapter
79
+ # #
80
+ # Elasticsearch::Model::Adapter.register(
81
+ # DataMapperAdapter,
82
+ # lambda { |klass|
83
+ # defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource)
84
+ # }
85
+ # )
86
+ #
87
+ def self.register(name, condition)
88
+ self.adapters[name] = condition
89
+ end
90
+
91
+ # Return the collection of registered adapters
92
+ #
93
+ # @example Return the currently registered adapters
94
+ #
95
+ # Elasticsearch::Model::Adapter.adapters
96
+ # # => {
97
+ # # Elasticsearch::Model::Adapter::ActiveRecord => #<Proc:0x007...(lambda)>,
98
+ # # Elasticsearch::Model::Adapter::Mongoid => #<Proc:0x007... (lambda)>,
99
+ # # }
100
+ #
101
+ # @return [Hash] The collection of adapters
102
+ #
103
+ def self.adapters
104
+ @adapters ||= {}
105
+ end
106
+
107
+ # Return the module with {Default::Records} interface implementation
108
+ #
109
+ # @api private
110
+ #
111
+ def records_mixin
112
+ adapter.const_get(:Records)
113
+ end
114
+
115
+ # Return the module with {Default::Callbacks} interface implementation
116
+ #
117
+ # @api private
118
+ #
119
+ def callbacks_mixin
120
+ adapter.const_get(:Callbacks)
121
+ end
122
+
123
+ # Return the module with {Default::Importing} interface implementation
124
+ #
125
+ # @api private
126
+ #
127
+ def importing_mixin
128
+ adapter.const_get(:Importing)
129
+ end
130
+
131
+ # Returns the adapter module
132
+ #
133
+ # @api private
134
+ #
135
+ def adapter
136
+ @adapter ||= begin
137
+ self.class.adapters.find( lambda {[]} ) { |name, condition| condition.call(klass) }.first \
138
+ || Elasticsearch::Model::Adapter::Default
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+ end
145
+ end