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.
- data/.gitignore +3 -0
- data/LICENSE.txt +1 -1
- data/README.md +669 -8
- data/Rakefile +52 -0
- data/elasticsearch-model.gemspec +48 -17
- data/examples/activerecord_article.rb +77 -0
- data/examples/activerecord_associations.rb +153 -0
- data/examples/couchbase_article.rb +66 -0
- data/examples/datamapper_article.rb +71 -0
- data/examples/mongoid_article.rb +68 -0
- data/examples/ohm_article.rb +70 -0
- data/examples/riak_article.rb +52 -0
- data/gemfiles/3.gemfile +11 -0
- data/gemfiles/4.gemfile +11 -0
- data/lib/elasticsearch/model.rb +151 -1
- data/lib/elasticsearch/model/adapter.rb +145 -0
- data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
- data/lib/elasticsearch/model/adapters/default.rb +44 -0
- data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
- data/lib/elasticsearch/model/callbacks.rb +35 -0
- data/lib/elasticsearch/model/client.rb +61 -0
- data/lib/elasticsearch/model/importing.rb +94 -0
- data/lib/elasticsearch/model/indexing.rb +332 -0
- data/lib/elasticsearch/model/naming.rb +101 -0
- data/lib/elasticsearch/model/proxy.rb +127 -0
- data/lib/elasticsearch/model/response.rb +70 -0
- data/lib/elasticsearch/model/response/base.rb +44 -0
- data/lib/elasticsearch/model/response/pagination.rb +96 -0
- data/lib/elasticsearch/model/response/records.rb +71 -0
- data/lib/elasticsearch/model/response/result.rb +50 -0
- data/lib/elasticsearch/model/response/results.rb +32 -0
- data/lib/elasticsearch/model/searching.rb +107 -0
- data/lib/elasticsearch/model/serializing.rb +35 -0
- data/lib/elasticsearch/model/support/forwardable.rb +44 -0
- data/lib/elasticsearch/model/version.rb +1 -1
- data/test/integration/active_record_associations_parent_child.rb +138 -0
- data/test/integration/active_record_associations_test.rb +306 -0
- data/test/integration/active_record_basic_test.rb +139 -0
- data/test/integration/active_record_import_test.rb +74 -0
- data/test/integration/active_record_namespaced_model_test.rb +49 -0
- data/test/integration/active_record_pagination_test.rb +109 -0
- data/test/integration/mongoid_basic_test.rb +178 -0
- data/test/test_helper.rb +57 -0
- data/test/unit/adapter_active_record_test.rb +93 -0
- data/test/unit/adapter_default_test.rb +31 -0
- data/test/unit/adapter_mongoid_test.rb +87 -0
- data/test/unit/adapter_test.rb +69 -0
- data/test/unit/callbacks_test.rb +30 -0
- data/test/unit/client_test.rb +27 -0
- data/test/unit/importing_test.rb +97 -0
- data/test/unit/indexing_test.rb +364 -0
- data/test/unit/module_test.rb +46 -0
- data/test/unit/naming_test.rb +76 -0
- data/test/unit/proxy_test.rb +88 -0
- data/test/unit/response_base_test.rb +40 -0
- data/test/unit/response_pagination_test.rb +159 -0
- data/test/unit/response_records_test.rb +87 -0
- data/test/unit/response_result_test.rb +52 -0
- data/test/unit/response_results_test.rb +31 -0
- data/test/unit/response_test.rb +57 -0
- data/test/unit/searching_search_request_test.rb +73 -0
- data/test/unit/searching_test.rb +39 -0
- data/test/unit/serializing_test.rb +17 -0
- 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
|