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,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