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