elastic_record 0.8.2 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/elastic_record.gemspec +1 -1
- data/lib/elastic_record/index/deferred.rb +81 -0
- data/lib/elastic_record/index.rb +6 -4
- data/lib/elastic_record/model.rb +3 -1
- data/lib/elastic_record/relation/merging.rb +6 -5
- data/lib/elastic_record/relation/search_methods.rb +8 -8
- data/lib/elastic_record/searches_many/association.rb +134 -0
- data/lib/elastic_record/searches_many/autosave.rb +72 -0
- data/lib/elastic_record/searches_many/builder.rb +42 -0
- data/lib/elastic_record/searches_many/collection_proxy.rb +20 -0
- data/lib/elastic_record/searches_many/reflection.rb +41 -0
- data/lib/elastic_record/searches_many.rb +91 -0
- data/lib/elastic_record.rb +3 -1
- data/test/elastic_record/callbacks_test.rb +2 -7
- data/test/elastic_record/config_test.rb +1 -1
- data/test/elastic_record/index/documents_test.rb +1 -1
- data/test/elastic_record/index/manage_test.rb +5 -1
- data/test/elastic_record/index/percolator_test.rb +5 -0
- data/test/elastic_record/relation/batches_test.rb +1 -3
- data/test/elastic_record/relation/delegation_test.rb +0 -5
- data/test/elastic_record/relation/finder_methods_test.rb +2 -4
- data/test/elastic_record/relation/none_test.rb +0 -5
- data/test/elastic_record/relation/search_methods_test.rb +19 -0
- data/test/elastic_record/relation_test.rb +0 -7
- data/test/elastic_record/searches_many/autosave_test.rb +31 -0
- data/test/elastic_record/searches_many/collection_proxy_test.rb +23 -0
- data/test/elastic_record/searches_many/reflection_test.rb +20 -0
- data/test/elastic_record/searches_many_test.rb +65 -0
- data/test/helper.rb +16 -1
- data/test/support/connect.rb +1 -1
- data/test/support/models/test_model.rb +98 -0
- data/test/support/models/warehouse.rb +6 -0
- data/test/support/{widget.rb → models/widget.rb} +10 -19
- metadata +16 -3
data/elastic_record.gemspec
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
class Index
|
3
|
+
module Deferred
|
4
|
+
class DeferredConnection
|
5
|
+
class DeferredAction < Struct.new(:method, :args, :block)
|
6
|
+
def run(index)
|
7
|
+
index.send(method, *args, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :index
|
12
|
+
attr_accessor :deferred_actions
|
13
|
+
attr_accessor :writes_made
|
14
|
+
|
15
|
+
def initialize(index)
|
16
|
+
self.index = index
|
17
|
+
reset!
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset!
|
21
|
+
if writes_made
|
22
|
+
begin
|
23
|
+
index.disable_deferring!
|
24
|
+
index.reset
|
25
|
+
ensure
|
26
|
+
index.enable_deferring!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
self.deferred_actions = []
|
30
|
+
self.writes_made = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def flush!
|
34
|
+
deferred_actions.each do |queued_action|
|
35
|
+
self.writes_made = true
|
36
|
+
queued_action.run(index.real_connection)
|
37
|
+
end
|
38
|
+
deferred_actions.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
READ_METHODS = [:json_get, :head]
|
43
|
+
def method_missing(method, *args, &block)
|
44
|
+
super unless index.real_connection.respond_to?(method)
|
45
|
+
|
46
|
+
if READ_METHODS.include?(method)
|
47
|
+
flush!
|
48
|
+
index.real_connection.json_post "/#{index.alias_name}/_refresh"
|
49
|
+
index.real_connection.send(method, *args, &block)
|
50
|
+
else
|
51
|
+
deferred_actions << DeferredAction.new(method, args, block)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def enable_deferring!
|
57
|
+
@deferring_enabled = true
|
58
|
+
end
|
59
|
+
|
60
|
+
def disable_deferring!
|
61
|
+
@deferring_enabled = false
|
62
|
+
end
|
63
|
+
|
64
|
+
def connection
|
65
|
+
if @deferring_enabled
|
66
|
+
deferred_connection
|
67
|
+
else
|
68
|
+
real_connection
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset_deferring!
|
73
|
+
deferred_connection.reset!
|
74
|
+
end
|
75
|
+
|
76
|
+
def deferred_connection
|
77
|
+
@deferred_connection ||= DeferredConnection.new(self)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/elastic_record/index.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'elastic_record/index/deferred'
|
1
2
|
require 'elastic_record/index/documents'
|
2
3
|
require 'elastic_record/index/manage'
|
3
4
|
require 'elastic_record/index/mapping'
|
@@ -10,6 +11,7 @@ module ElasticRecord
|
|
10
11
|
include Manage
|
11
12
|
include Mapping
|
12
13
|
include Percolator
|
14
|
+
include Deferred
|
13
15
|
|
14
16
|
attr_accessor :model
|
15
17
|
attr_accessor :disabled
|
@@ -35,13 +37,13 @@ module ElasticRecord
|
|
35
37
|
@disabled = false
|
36
38
|
end
|
37
39
|
|
40
|
+
def real_connection
|
41
|
+
model.elastic_connection
|
42
|
+
end
|
43
|
+
|
38
44
|
private
|
39
45
|
def new_index_name
|
40
46
|
"#{alias_name}_#{Time.now.to_i}"
|
41
47
|
end
|
42
|
-
|
43
|
-
def connection
|
44
|
-
model.elastic_connection
|
45
|
-
end
|
46
48
|
end
|
47
49
|
end
|
data/lib/elastic_record/model.rb
CHANGED
@@ -18,16 +18,17 @@ module ElasticRecord
|
|
18
18
|
@values = other.values
|
19
19
|
end
|
20
20
|
|
21
|
-
def normal_values
|
22
|
-
Relation::MULTI_VALUE_METHODS + Relation::SINGLE_VALUE_METHODS
|
23
|
-
end
|
24
|
-
|
25
21
|
def merge
|
26
|
-
|
22
|
+
Relation::SINGLE_VALUE_METHODS.each do |name|
|
27
23
|
value = values[name]
|
28
24
|
relation.send("#{name}!", value) unless value.blank?
|
29
25
|
end
|
30
26
|
|
27
|
+
Relation::MULTI_VALUE_METHODS.each do |name|
|
28
|
+
value = values[name]
|
29
|
+
relation.send("#{name}!", *value) unless value.blank?
|
30
|
+
end
|
31
|
+
|
31
32
|
relation
|
32
33
|
end
|
33
34
|
end
|
@@ -35,7 +35,7 @@ module ElasticRecord
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def filter!(*args)
|
38
|
-
self.filter_values += args
|
38
|
+
self.filter_values += args
|
39
39
|
self
|
40
40
|
end
|
41
41
|
|
@@ -137,9 +137,9 @@ module ElasticRecord
|
|
137
137
|
if query && filter
|
138
138
|
arelastic.query.filtered(query, filter)
|
139
139
|
elsif query
|
140
|
-
query
|
140
|
+
Arelastic::Searches::Query.new(query)
|
141
141
|
elsif filter
|
142
|
-
arelastic.query.constant_score(filter)
|
142
|
+
arelastic.query.constant_score(Arelastic::Searches::Filter.new(filter))
|
143
143
|
else
|
144
144
|
arelastic.query.match_all
|
145
145
|
end
|
@@ -150,9 +150,7 @@ module ElasticRecord
|
|
150
150
|
query = Arelastic::Queries::QueryString.new query
|
151
151
|
end
|
152
152
|
|
153
|
-
|
154
|
-
Arelastic::Searches::Query.new query
|
155
|
-
end
|
153
|
+
query
|
156
154
|
end
|
157
155
|
|
158
156
|
def build_filter(filters)
|
@@ -161,6 +159,8 @@ module ElasticRecord
|
|
161
159
|
filters.map do |filter|
|
162
160
|
if filter.is_a?(Arelastic::Filters::Filter)
|
163
161
|
nodes << filter
|
162
|
+
elsif filter.is_a?(ElasticRecord::Relation)
|
163
|
+
nodes << Arelastic::Filters::HasChild.new(filter.elastic_index.type, filter.as_elastic['query'])
|
164
164
|
else
|
165
165
|
filter.each do |field, terms|
|
166
166
|
case terms
|
@@ -176,9 +176,9 @@ module ElasticRecord
|
|
176
176
|
end
|
177
177
|
|
178
178
|
if nodes.size == 1
|
179
|
-
|
179
|
+
nodes.first
|
180
180
|
elsif nodes.size > 1
|
181
|
-
Arelastic::
|
181
|
+
Arelastic::Filters::And.new(nodes)
|
182
182
|
end
|
183
183
|
end
|
184
184
|
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
3
|
+
module ElasticRecord
|
4
|
+
module SearchesMany
|
5
|
+
class Association
|
6
|
+
attr_reader :owner, :reflection, :collection
|
7
|
+
|
8
|
+
delegate :klass, :options, to: :reflection
|
9
|
+
|
10
|
+
def initialize(owner, reflection)
|
11
|
+
@owner = owner
|
12
|
+
@reflection = reflection
|
13
|
+
@collection = []
|
14
|
+
@loaded = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def writer(other_records)
|
18
|
+
other_records = other_records.map do |other_record|
|
19
|
+
other_record.is_a?(Hash) ? klass.new(other_record) : other_record
|
20
|
+
end
|
21
|
+
|
22
|
+
if reflection.counter_cache_column
|
23
|
+
owner.send("#{reflection.counter_cache_column}=", other_records.size)
|
24
|
+
end
|
25
|
+
|
26
|
+
if reflection.touch_column
|
27
|
+
owner.send("#{reflection.touch_column}=", Time.current)
|
28
|
+
end
|
29
|
+
|
30
|
+
delete(load_collection - other_records)
|
31
|
+
merge_collections(load_collection, other_records)
|
32
|
+
concat(other_records - load_collection)
|
33
|
+
end
|
34
|
+
|
35
|
+
def reader
|
36
|
+
CollectionProxy.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def loaded?
|
40
|
+
@loaded
|
41
|
+
end
|
42
|
+
|
43
|
+
def concat(*records)
|
44
|
+
load_collection if owner.new_record?
|
45
|
+
|
46
|
+
result = true
|
47
|
+
|
48
|
+
records.flatten.each do |record|
|
49
|
+
add_to_collection(record) do |r|
|
50
|
+
result &&= record.save unless owner.new_record?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
result && records
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(records)
|
58
|
+
if options[:autosave] || owner.new_record?
|
59
|
+
records.each(&:mark_for_destruction)
|
60
|
+
else
|
61
|
+
record.destroy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def scope
|
66
|
+
search = klass.elastic_search.filter "#{reflection.belongs_to}_id" => owner.id
|
67
|
+
if options[:as]
|
68
|
+
search.filter! "#{reflection.belongs_to}_type" => owner.class.name
|
69
|
+
end
|
70
|
+
search
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_collection
|
74
|
+
unless @loaded
|
75
|
+
@collection = merge_collections(persisted_collection, collection)
|
76
|
+
@loaded = true
|
77
|
+
end
|
78
|
+
|
79
|
+
loaded = true
|
80
|
+
collection
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def load_persisted_collection?
|
85
|
+
!loaded? || owner.new_record?
|
86
|
+
end
|
87
|
+
|
88
|
+
def persisted_collection
|
89
|
+
scope.to_a
|
90
|
+
end
|
91
|
+
|
92
|
+
def merge_collections(existing_records, new_records)
|
93
|
+
return existing_records if new_records.empty?
|
94
|
+
return new_records if existing_records.empty?
|
95
|
+
|
96
|
+
existing_records.map! do |existing_record|
|
97
|
+
if new_record = new_records.delete(existing_record)
|
98
|
+
(existing_record.attributes.keys - new_record.changes.keys).each do |name|
|
99
|
+
new_record.send("#{name}=", existing_record.send(name))
|
100
|
+
end
|
101
|
+
|
102
|
+
new_record
|
103
|
+
else
|
104
|
+
existing_record
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
existing_records + new_records
|
109
|
+
end
|
110
|
+
|
111
|
+
def add_to_collection(record)
|
112
|
+
callback(:before_add, record)
|
113
|
+
|
114
|
+
record.send("#{reflection.belongs_to}=", owner)
|
115
|
+
yield(record) if block_given?
|
116
|
+
@collection << record
|
117
|
+
|
118
|
+
callback(:after_add, record)
|
119
|
+
|
120
|
+
record
|
121
|
+
end
|
122
|
+
|
123
|
+
def callback(method, record)
|
124
|
+
reflection.callbacks[method].each do |callback|
|
125
|
+
if callback.is_a?(Symbol)
|
126
|
+
owner.send(callback, record)
|
127
|
+
else
|
128
|
+
callback.call(owner, record)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module SearchesMany
|
3
|
+
module Autosave
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def add_autosave_callbacks(reflection)
|
8
|
+
add_autosave_after_save_callbacks(reflection)
|
9
|
+
add_autosave_validation_callbacks(reflection)
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_autosave_after_save_callbacks(reflection)
|
13
|
+
before_save { save_autosave_records(reflection) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_autosave_validation_callbacks(reflection)
|
17
|
+
validate { validate_autosave_records(reflection) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def save_autosave_records(reflection)
|
23
|
+
if association = searches_many_instance_get(reflection.name)
|
24
|
+
associated_records_to_autosave(association).each do |record|
|
25
|
+
if record.marked_for_destruction?
|
26
|
+
record.destroy
|
27
|
+
elsif record.changed?
|
28
|
+
record.save
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_autosave_records(reflection)
|
35
|
+
if association = searches_many_instance_get(reflection.name)
|
36
|
+
associated_records_to_autosave(association).each do |record|
|
37
|
+
unless record.valid?
|
38
|
+
record.errors.each do |attribute, message|
|
39
|
+
attribute = "#{reflection.name}.#{attribute}"
|
40
|
+
errors[attribute] << message
|
41
|
+
errors[attribute].uniq!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def associated_records_to_autosave(association)
|
49
|
+
if association.loaded?
|
50
|
+
association.load_collection
|
51
|
+
else
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Marks this record to be destroyed as part of the parents save transaction.
|
57
|
+
# This does _not_ actually destroy the record instantly, rather child record will be destroyed
|
58
|
+
# when <tt>parent.save</tt> is called.
|
59
|
+
#
|
60
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
61
|
+
def mark_for_destruction
|
62
|
+
@marked_for_destruction = true
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns whether or not this record will be destroyed as part of the parents save transaction.
|
66
|
+
#
|
67
|
+
# Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
|
68
|
+
def marked_for_destruction?
|
69
|
+
@marked_for_destruction
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module SearchesMany
|
3
|
+
class Builder
|
4
|
+
def self.build(model, name, options)
|
5
|
+
new(model, name, options).build
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :model, :name, :options
|
9
|
+
def initialize(model, name, options)
|
10
|
+
@model, @name, @options = model, name, options
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
define_writer
|
15
|
+
define_reader
|
16
|
+
|
17
|
+
reflection = ElasticRecord::SearchesMany::Reflection.new(model, name, options)
|
18
|
+
model.searches_many_reflections = model.searches_many_reflections.merge(name => reflection)
|
19
|
+
|
20
|
+
model.add_autosave_callbacks(reflection) # if options[:autosave]
|
21
|
+
end
|
22
|
+
|
23
|
+
def mixin
|
24
|
+
model.generated_searches_many_methods
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_writer
|
28
|
+
name = self.name
|
29
|
+
mixin.redefine_method("#{name}=") do |records|
|
30
|
+
searches_many_association(name).writer(records)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def define_reader
|
35
|
+
name = self.name
|
36
|
+
mixin.redefine_method(name) do
|
37
|
+
searches_many_association(name).reader
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module SearchesMany
|
3
|
+
class CollectionProxy < ElasticRecord::Relation
|
4
|
+
def initialize(association)
|
5
|
+
@association = association
|
6
|
+
super association.klass, association.klass.arelastic
|
7
|
+
merge! association.scope
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_a
|
11
|
+
@association.load_collection.reject(&:destroyed?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def <<(*records)
|
15
|
+
@association.concat(records) && self
|
16
|
+
end
|
17
|
+
alias_method :push, :<<
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ElasticRecord
|
2
|
+
module SearchesMany
|
3
|
+
class Reflection
|
4
|
+
attr_reader :model, :name, :options
|
5
|
+
attr_reader :callbacks
|
6
|
+
def initialize(model, name, options)
|
7
|
+
@model, @name, @options = model, name, options
|
8
|
+
@callbacks = define_callbacks(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def klass
|
12
|
+
klass_name.constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
def klass_name
|
16
|
+
name.to_s.classify
|
17
|
+
end
|
18
|
+
|
19
|
+
def belongs_to
|
20
|
+
options[:as] ? options[:as].to_s : model.name.to_s.demodulize.underscore
|
21
|
+
end
|
22
|
+
|
23
|
+
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
|
24
|
+
def define_callbacks(options)
|
25
|
+
Hash[CALLBACKS.map { |callback_name| [callback_name, Array(options[callback_name.to_sym])] }]
|
26
|
+
end
|
27
|
+
|
28
|
+
def touch_column
|
29
|
+
if options[:touch]
|
30
|
+
options[:touch] == true ? :updated_at : options[:touch].to_sym
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def counter_cache_column
|
35
|
+
if options[:counter_cache]
|
36
|
+
(options[:counter_cache] == true ? "#{name}_count" : options[:counter_cache]).to_sym
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'elastic_record/searches_many/association'
|
2
|
+
require 'elastic_record/searches_many/autosave'
|
3
|
+
require 'elastic_record/searches_many/builder'
|
4
|
+
require 'elastic_record/searches_many/collection_proxy'
|
5
|
+
require 'elastic_record/searches_many/reflection'
|
6
|
+
|
7
|
+
module ElasticRecord
|
8
|
+
module SearchesMany
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
extend ClassMethods
|
12
|
+
|
13
|
+
class_attribute :searches_many_reflections
|
14
|
+
self.searches_many_reflections = {}
|
15
|
+
|
16
|
+
include ElasticRecord::SearchesMany::Autosave
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Specifies a one-to-many association. The following methods for retrieval and query of
|
22
|
+
# collections of associated objects will be added:
|
23
|
+
#
|
24
|
+
# [collection]
|
25
|
+
# Returns an array of all the associated objects.
|
26
|
+
# [collection=objects]
|
27
|
+
# Replaces the collections content by deleting and adding objects as appropriate.
|
28
|
+
# [collection_params=objects]
|
29
|
+
# Support for nested assignment from a form
|
30
|
+
# === Options
|
31
|
+
# [:as]
|
32
|
+
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
33
|
+
# [:touch]
|
34
|
+
# Specify to update the owner when changed. Specify <tt>true</tt>
|
35
|
+
# to update the updated_at field. If you specify a symbol, that attribute
|
36
|
+
# will be updated with the current time in addition to the updated_at/on attribute.
|
37
|
+
# [:autosave]
|
38
|
+
# If true, always save the associated objects or destroy them if marked for destruction, when
|
39
|
+
# saving the parent object.
|
40
|
+
# [:counter_cache]
|
41
|
+
# Caches the number of belonging objects on the associate class. This requires that a column
|
42
|
+
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
43
|
+
# is used on the associate class (such as a Post class). You can also specify a custom counter
|
44
|
+
# cache column by providing a column name instead of a +true+/+false+ value to this
|
45
|
+
# option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
46
|
+
#
|
47
|
+
# === Example
|
48
|
+
#
|
49
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
50
|
+
# * <tt>Firm#clients</tt>
|
51
|
+
# * <tt>Firm#clients=(objects)</tt>
|
52
|
+
# * <tt>Firm#client_params=(params)</tt>
|
53
|
+
def searches_many(name, options = {})
|
54
|
+
ElasticRecord::SearchesMany::Builder.build(self, name, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def generated_searches_many_methods
|
58
|
+
@generated_searches_many_methods ||= begin
|
59
|
+
mod = const_set(:GeneratedSearchesManyMethods, Module.new)
|
60
|
+
include mod
|
61
|
+
mod
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the searches_many instance for the given name, instantiating it if it doesn't already exist
|
67
|
+
def searches_many_association(name)
|
68
|
+
association = searches_many_instance_get(name)
|
69
|
+
|
70
|
+
if association.nil?
|
71
|
+
association = ElasticRecord::SearchesMany::Association.new(self, searches_many_reflections[name])
|
72
|
+
searches_many_instance_set(name, association)
|
73
|
+
end
|
74
|
+
|
75
|
+
association
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def searches_many_cache
|
80
|
+
@searches_many_cache ||= {}
|
81
|
+
end
|
82
|
+
|
83
|
+
def searches_many_instance_get(name)
|
84
|
+
searches_many_cache[name.to_sym]
|
85
|
+
end
|
86
|
+
|
87
|
+
def searches_many_instance_set(name, association)
|
88
|
+
searches_many_cache[name.to_sym] = association
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/elastic_record.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'arelastic'
|
2
2
|
require 'active_support/core_ext/object/blank' # required because ActiveModel depends on this but does not require it
|
3
|
+
require 'active_support/concern'
|
3
4
|
require 'active_model'
|
4
5
|
|
5
6
|
module ElasticRecord
|
@@ -10,6 +11,7 @@ module ElasticRecord
|
|
10
11
|
autoload :Lucene, 'elastic_record/lucene'
|
11
12
|
autoload :Model, 'elastic_record/model'
|
12
13
|
autoload :Relation, 'elastic_record/relation'
|
14
|
+
autoload :SearchesMany, 'elastic_record/searches_many'
|
13
15
|
autoload :Searching, 'elastic_record/searching'
|
14
16
|
|
15
17
|
class << self
|
@@ -19,4 +21,4 @@ module ElasticRecord
|
|
19
21
|
end
|
20
22
|
end
|
21
23
|
|
22
|
-
require 'elastic_record/railtie' if defined?(Rails)
|
24
|
+
require 'elastic_record/railtie' if defined?(Rails)
|
@@ -1,16 +1,11 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class ElasticRecord::CallbacksTest < MiniTest::Spec
|
4
|
-
def setup
|
5
|
-
super
|
6
|
-
Widget.elastic_index.reset
|
7
|
-
end
|
8
|
-
|
9
4
|
def test_added_to_index
|
10
5
|
widget = Widget.new id: '10', color: 'green'
|
11
6
|
refute Widget.elastic_index.record_exists?(widget.id)
|
12
7
|
|
13
|
-
widget.
|
8
|
+
widget.save
|
14
9
|
|
15
10
|
assert Widget.elastic_index.record_exists?(widget.id)
|
16
11
|
end
|
@@ -21,7 +16,7 @@ class ElasticRecord::CallbacksTest < MiniTest::Spec
|
|
21
16
|
|
22
17
|
assert Widget.elastic_index.record_exists?(widget.id)
|
23
18
|
|
24
|
-
widget.
|
19
|
+
widget.destroy
|
25
20
|
|
26
21
|
refute Widget.elastic_index.record_exists?(widget.id)
|
27
22
|
end
|
@@ -2,7 +2,7 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class ElasticRecord::Relation::BatchesTest < MiniTest::Spec
|
4
4
|
def setup
|
5
|
-
|
5
|
+
super
|
6
6
|
create_widgets
|
7
7
|
end
|
8
8
|
|
@@ -37,7 +37,5 @@ class ElasticRecord::Relation::BatchesTest < MiniTest::Spec
|
|
37
37
|
Widget.new(id: 10, color: 'blue'),
|
38
38
|
Widget.new(id: 15, color: 'green'),
|
39
39
|
]
|
40
|
-
|
41
|
-
Widget.elastic_index.refresh
|
42
40
|
end
|
43
41
|
end
|
@@ -1,13 +1,8 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class ElasticRecord::Relation::DelegationTest < MiniTest::Spec
|
4
|
-
def setup
|
5
|
-
Widget.elastic_index.reset
|
6
|
-
end
|
7
|
-
|
8
4
|
def test_delegate_to_array
|
9
5
|
Widget.elastic_index.index_document('5', color: 'red')
|
10
|
-
Widget.elastic_index.refresh
|
11
6
|
|
12
7
|
records = []
|
13
8
|
Widget.elastic_relation.each do |record|
|
@@ -2,7 +2,7 @@ require 'helper'
|
|
2
2
|
|
3
3
|
class ElasticRecord::Relation::FinderMethodsTest < MiniTest::Spec
|
4
4
|
def setup
|
5
|
-
|
5
|
+
super
|
6
6
|
create_widgets
|
7
7
|
end
|
8
8
|
|
@@ -37,8 +37,6 @@ class ElasticRecord::Relation::FinderMethodsTest < MiniTest::Spec
|
|
37
37
|
Widget.elastic_index.bulk_add [
|
38
38
|
Widget.new(color: 'red', id: '05'),
|
39
39
|
Widget.new(color: 'blue', id: '10'),
|
40
|
-
]
|
41
|
-
|
42
|
-
Widget.elastic_index.refresh
|
40
|
+
]
|
43
41
|
end
|
44
42
|
end
|
@@ -57,6 +57,25 @@ class ElasticRecord::Relation::SearchMethodsTest < MiniTest::Spec
|
|
57
57
|
assert_equal expected, relation.as_elastic['query']
|
58
58
|
end
|
59
59
|
|
60
|
+
def test_filter_with_another_relation
|
61
|
+
relation.filter! Widget.elastic_search.query('red')
|
62
|
+
|
63
|
+
expected = {
|
64
|
+
"constant_score" => {
|
65
|
+
"filter" => {
|
66
|
+
"has_child" => {
|
67
|
+
"type" => "widget",
|
68
|
+
"query" => {
|
69
|
+
"query_string" => {"query"=>"red"}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
assert_equal expected, relation.as_elastic['query']
|
77
|
+
end
|
78
|
+
|
60
79
|
def test_query_with_only_query
|
61
80
|
relation.query!('foo')
|
62
81
|
|
@@ -1,11 +1,6 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class ElasticRecord::RelationTest < MiniTest::Spec
|
4
|
-
def setup
|
5
|
-
super
|
6
|
-
Widget.elastic_index.reset
|
7
|
-
end
|
8
|
-
|
9
4
|
def test_count
|
10
5
|
create_widgets [Widget.new(id: 5, color: 'red'), Widget.new(id: 10, color: 'blue')]
|
11
6
|
|
@@ -32,7 +27,6 @@ class ElasticRecord::RelationTest < MiniTest::Spec
|
|
32
27
|
create_widgets [Widget.new(id: 10, color: 'blue')]
|
33
28
|
|
34
29
|
# explain = Widget.elastic_relation.filter(color: 'blue').explain('10')
|
35
|
-
# p "explain = #{explain}"
|
36
30
|
end
|
37
31
|
|
38
32
|
def test_to_hits
|
@@ -70,6 +64,5 @@ class ElasticRecord::RelationTest < MiniTest::Spec
|
|
70
64
|
private
|
71
65
|
def create_widgets(widgets)
|
72
66
|
Widget.elastic_index.bulk_add(widgets)
|
73
|
-
Widget.elastic_index.refresh
|
74
67
|
end
|
75
68
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ElasticRecord::SearchesMany::AutosaveTest < MiniTest::Spec
|
4
|
+
def test_save_associations_callback
|
5
|
+
warehouse = Warehouse.new
|
6
|
+
widget = Widget.new
|
7
|
+
warehouse.widgets = [widget]
|
8
|
+
assert widget.new_record?
|
9
|
+
|
10
|
+
warehouse.save
|
11
|
+
|
12
|
+
assert widget.persisted?
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_validate_associations_callback
|
16
|
+
warehouse = Warehouse.new
|
17
|
+
widget = Widget.new color: 123
|
18
|
+
warehouse.widgets = [widget]
|
19
|
+
|
20
|
+
assert warehouse.invalid?
|
21
|
+
assert_equal ["is invalid"], warehouse.errors['widgets.color']
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_mark_for_destruction
|
25
|
+
widget = Widget.new
|
26
|
+
|
27
|
+
widget.mark_for_destruction
|
28
|
+
|
29
|
+
assert widget.marked_for_destruction?
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ElasticRecord::SearchesMany::CollectionProxyTest < MiniTest::Spec
|
4
|
+
def test_add_to_new_record
|
5
|
+
warehouse = Warehouse.new
|
6
|
+
widget = Widget.new
|
7
|
+
|
8
|
+
warehouse.widgets << widget
|
9
|
+
|
10
|
+
assert widget.new_record?
|
11
|
+
assert_equal [widget], warehouse.widgets
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_add_to_persisted_record
|
15
|
+
warehouse = Warehouse.create
|
16
|
+
widget = Widget.new
|
17
|
+
|
18
|
+
warehouse.widgets << widget
|
19
|
+
|
20
|
+
assert !widget.new_record?
|
21
|
+
assert_equal [widget], warehouse.widgets
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ElasticRecord::SearchesMany::ReflectionTest < MiniTest::Spec
|
4
|
+
def test_touch_column
|
5
|
+
assert_nil reflection_class.new(Warehouse, :widgets, {}).touch_column
|
6
|
+
assert_equal :updated_at, reflection_class.new(Warehouse, :widgets, touch: true).touch_column
|
7
|
+
assert_equal :my_column, reflection_class.new(Warehouse, :widgets, touch: :my_column).touch_column
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_counter_cache_column
|
11
|
+
assert_nil reflection_class.new(Warehouse, :widgets, {}).counter_cache_column
|
12
|
+
assert_equal :widgets_count, reflection_class.new(Warehouse, :widgets, counter_cache: true).counter_cache_column
|
13
|
+
assert_equal :my_column, reflection_class.new(Warehouse, :widgets, counter_cache: :my_column).counter_cache_column
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def reflection_class
|
18
|
+
ElasticRecord::SearchesMany::Reflection
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class ElasticRecord::SearchesManyTest < MiniTest::Spec
|
4
|
+
def test_reader
|
5
|
+
warehouse = Warehouse.create
|
6
|
+
related_widget = Widget.create warehouse: warehouse
|
7
|
+
unrelated_widget = Widget.create
|
8
|
+
|
9
|
+
assert_equal [related_widget], warehouse.widgets
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_write_with_objects
|
13
|
+
warehouse = Warehouse.new
|
14
|
+
widget = Widget.new
|
15
|
+
|
16
|
+
warehouse.widgets = [widget]
|
17
|
+
|
18
|
+
assert widget.new_record?
|
19
|
+
assert_equal warehouse.id, widget.warehouse_id
|
20
|
+
# assert_equal 1, warehouse.widgets_count
|
21
|
+
# assert_in_delta Time.current, warehouse.widgets_updated_at, 5
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_write_with_attributes
|
25
|
+
warehouse = Warehouse.new
|
26
|
+
|
27
|
+
warehouse.widgets = [
|
28
|
+
{
|
29
|
+
color: 'blue',
|
30
|
+
name: 'Toy'
|
31
|
+
}
|
32
|
+
]
|
33
|
+
|
34
|
+
widgets = warehouse.widgets
|
35
|
+
assert_equal 1, widgets.size
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_write_marks_destroyed
|
39
|
+
warehouse = Warehouse.new
|
40
|
+
widget = Widget.create warehouse: warehouse
|
41
|
+
|
42
|
+
warehouse.widgets = []
|
43
|
+
|
44
|
+
association = warehouse.searches_many_association(:widgets)
|
45
|
+
assert_equal 1, association.reader.size
|
46
|
+
assert association.reader.first.marked_for_destruction?
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_write_existing_record
|
50
|
+
widget = Widget.create name: 'Toy', color: 'green'
|
51
|
+
warehouse = Warehouse.new widgets: [widget]
|
52
|
+
|
53
|
+
warehouse.widgets = [
|
54
|
+
{
|
55
|
+
id: widget.id,
|
56
|
+
color: "blue"
|
57
|
+
}
|
58
|
+
]
|
59
|
+
|
60
|
+
widgets = warehouse.widgets
|
61
|
+
assert_equal 1, widgets.size
|
62
|
+
assert_equal "blue", widgets.first.color
|
63
|
+
assert_equal "Toy", widgets.first.name
|
64
|
+
end
|
65
|
+
end
|
data/test/helper.rb
CHANGED
@@ -3,8 +3,13 @@ Bundler.require
|
|
3
3
|
|
4
4
|
require 'minitest/autorun'
|
5
5
|
|
6
|
-
require 'support/widget'
|
7
6
|
require 'support/connect'
|
7
|
+
require 'support/models/test_model'
|
8
|
+
require 'support/models/warehouse'
|
9
|
+
require 'support/models/widget'
|
10
|
+
Widget.elastic_index.reset
|
11
|
+
|
12
|
+
ElasticRecord::Config.model_names = %w(Warehouse Widget)
|
8
13
|
|
9
14
|
FakeWeb.allow_net_connect = %r[^https?://127.0.0.1]
|
10
15
|
|
@@ -13,6 +18,16 @@ module MiniTest
|
|
13
18
|
def setup
|
14
19
|
super
|
15
20
|
FakeWeb.clean_registry
|
21
|
+
|
22
|
+
ElasticRecord::Config.models.each do |model|
|
23
|
+
model.elastic_index.enable_deferring!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def teardown
|
28
|
+
ElasticRecord::Config.models.each do |model|
|
29
|
+
model.elastic_index.reset_deferring!
|
30
|
+
end
|
16
31
|
end
|
17
32
|
end
|
18
33
|
end
|
data/test/support/connect.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ElasticRecord::Config.servers = '127.0.0.1:9200'
|
1
|
+
ElasticRecord::Config.servers = '127.0.0.1:9200'
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module TestModel
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
extend ActiveModel::Naming
|
6
|
+
extend ActiveModel::Callbacks
|
7
|
+
define_model_callbacks :save, :destroy
|
8
|
+
|
9
|
+
include ActiveModel::Dirty
|
10
|
+
include ActiveModel::Validations
|
11
|
+
|
12
|
+
include ElasticRecord::Model
|
13
|
+
include ElasticRecord::Callbacks
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def find(ids)
|
18
|
+
ids.map { |id| new(id: id, color: 'red') }
|
19
|
+
end
|
20
|
+
|
21
|
+
def base_class
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def create(attributes = {})
|
26
|
+
record = new(attributes)
|
27
|
+
record.save
|
28
|
+
record
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_attributes(attributes)
|
32
|
+
define_attribute_methods attributes
|
33
|
+
|
34
|
+
attributes.each do |attribute|
|
35
|
+
define_method attribute do
|
36
|
+
instance_variable_get("@#{attribute}")
|
37
|
+
end
|
38
|
+
|
39
|
+
define_method "#{attribute}=" do |value|
|
40
|
+
send("#{attribute}_will_change!")
|
41
|
+
instance_variable_set("@#{attribute}", value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
define_method 'attributes' do
|
46
|
+
Hash[attributes.map { |attr| [attr.to_s, send(attr)] }]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(attrs = {})
|
52
|
+
self.attributes = attrs
|
53
|
+
end
|
54
|
+
|
55
|
+
def attributes=(attrs)
|
56
|
+
attrs.each do |key, val|
|
57
|
+
send("#{key}=", val)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def id=(value)
|
62
|
+
@id = value
|
63
|
+
end
|
64
|
+
|
65
|
+
def id
|
66
|
+
@id ||= rand(10000).to_s
|
67
|
+
end
|
68
|
+
|
69
|
+
def save
|
70
|
+
@persisted = true
|
71
|
+
run_callbacks :save
|
72
|
+
end
|
73
|
+
|
74
|
+
def destroy
|
75
|
+
@destroyed = true
|
76
|
+
run_callbacks :destroy
|
77
|
+
end
|
78
|
+
|
79
|
+
def ==(other)
|
80
|
+
id == other.id
|
81
|
+
end
|
82
|
+
|
83
|
+
def changed?
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
def new_record?
|
88
|
+
!@persisted
|
89
|
+
end
|
90
|
+
|
91
|
+
def persisted?
|
92
|
+
@persisted
|
93
|
+
end
|
94
|
+
|
95
|
+
def destroyed?
|
96
|
+
@destroyed
|
97
|
+
end
|
98
|
+
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
class Widget
|
2
|
-
|
3
|
-
extend ActiveModel::Callbacks
|
4
|
-
define_model_callbacks :save, :destroy
|
2
|
+
include TestModel
|
5
3
|
|
6
|
-
|
7
|
-
|
4
|
+
validates :color, format: {with: /[a-z]/}
|
5
|
+
|
6
|
+
define_attributes [:name, :color, :warehouse_id]
|
8
7
|
|
9
8
|
self.elastic_index.mapping[:properties].update(
|
10
9
|
name: {
|
@@ -16,14 +15,13 @@ class Widget
|
|
16
15
|
},
|
17
16
|
color: {
|
18
17
|
type: 'string', index: 'not_analyzed'
|
18
|
+
},
|
19
|
+
warehouse_id: {
|
20
|
+
type: 'string', index: 'not_analyzed'
|
19
21
|
}
|
20
22
|
)
|
21
|
-
|
22
|
-
class << self
|
23
|
-
def find(ids)
|
24
|
-
ids.map { |id| new(id: id, color: 'red') }
|
25
|
-
end
|
26
23
|
|
24
|
+
class << self
|
27
25
|
def anon(&block)
|
28
26
|
Class.new(self) do
|
29
27
|
def self.name
|
@@ -33,16 +31,9 @@ class Widget
|
|
33
31
|
instance_eval(&block)
|
34
32
|
end
|
35
33
|
end
|
36
|
-
|
37
|
-
def base_class
|
38
|
-
self
|
39
|
-
end
|
40
34
|
end
|
41
35
|
|
42
|
-
|
43
|
-
|
44
|
-
attributes.each do |key, val|
|
45
|
-
send("#{key}=", val)
|
46
|
-
end
|
36
|
+
def warehouse=(other)
|
37
|
+
self.warehouse_id = other.id
|
47
38
|
end
|
48
39
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elastic_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-10-
|
12
|
+
date: 2012-10-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: arelastic
|
@@ -62,6 +62,7 @@ files:
|
|
62
62
|
- lib/elastic_record/config.rb
|
63
63
|
- lib/elastic_record/connection.rb
|
64
64
|
- lib/elastic_record/index.rb
|
65
|
+
- lib/elastic_record/index/deferred.rb
|
65
66
|
- lib/elastic_record/index/documents.rb
|
66
67
|
- lib/elastic_record/index/manage.rb
|
67
68
|
- lib/elastic_record/index/mapping.rb
|
@@ -79,6 +80,12 @@ files:
|
|
79
80
|
- lib/elastic_record/relation/none.rb
|
80
81
|
- lib/elastic_record/relation/search_methods.rb
|
81
82
|
- lib/elastic_record/relation/value_methods.rb
|
83
|
+
- lib/elastic_record/searches_many.rb
|
84
|
+
- lib/elastic_record/searches_many/association.rb
|
85
|
+
- lib/elastic_record/searches_many/autosave.rb
|
86
|
+
- lib/elastic_record/searches_many/builder.rb
|
87
|
+
- lib/elastic_record/searches_many/collection_proxy.rb
|
88
|
+
- lib/elastic_record/searches_many/reflection.rb
|
82
89
|
- lib/elastic_record/searching.rb
|
83
90
|
- lib/elastic_record/tasks/index.rake
|
84
91
|
- test/elastic_record/callbacks_test.rb
|
@@ -99,10 +106,16 @@ files:
|
|
99
106
|
- test/elastic_record/relation/none_test.rb
|
100
107
|
- test/elastic_record/relation/search_methods_test.rb
|
101
108
|
- test/elastic_record/relation_test.rb
|
109
|
+
- test/elastic_record/searches_many/autosave_test.rb
|
110
|
+
- test/elastic_record/searches_many/collection_proxy_test.rb
|
111
|
+
- test/elastic_record/searches_many/reflection_test.rb
|
112
|
+
- test/elastic_record/searches_many_test.rb
|
102
113
|
- test/elastic_record/searching_test.rb
|
103
114
|
- test/helper.rb
|
104
115
|
- test/support/connect.rb
|
105
|
-
- test/support/
|
116
|
+
- test/support/models/test_model.rb
|
117
|
+
- test/support/models/warehouse.rb
|
118
|
+
- test/support/models/widget.rb
|
106
119
|
homepage: http://github.com/matthuhiggins/elastic_record
|
107
120
|
licenses:
|
108
121
|
- MIT
|