elasticsearch-model 0.1.9 → 2.0.0
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +53 -10
- data/Rakefile +24 -15
- data/elasticsearch-model.gemspec +8 -11
- data/examples/activerecord_article.rb +1 -1
- data/examples/activerecord_associations.rb +7 -6
- data/gemfiles/5.0.gemfile +12 -0
- data/lib/elasticsearch/model.rb +25 -1
- data/lib/elasticsearch/model/adapters/active_record.rb +6 -3
- data/lib/elasticsearch/model/naming.rb +26 -2
- data/lib/elasticsearch/model/response.rb +1 -1
- data/lib/elasticsearch/model/response/aggregations.rb +36 -0
- data/lib/elasticsearch/model/version.rb +1 -1
- data/test/integration/active_record_associations_parent_child.rb +11 -3
- data/test/integration/active_record_associations_test.rb +73 -59
- data/test/integration/active_record_basic_test.rb +43 -26
- data/test/integration/active_record_custom_serialization_test.rb +5 -0
- data/test/integration/active_record_import_test.rb +19 -13
- data/test/integration/active_record_namespaced_model_test.rb +5 -0
- data/test/integration/active_record_pagination_test.rb +18 -14
- data/test/integration/dynamic_index_name_test.rb +5 -0
- data/test/integration/multiple_models_test.rb +29 -25
- data/test/unit/indexing_test.rb +4 -4
- data/test/unit/module_test.rb +11 -0
- data/test/unit/naming_inheritance_test.rb +94 -0
- data/test/unit/response_aggregations_test.rb +46 -0
- data/test/unit/response_result_test.rb +1 -1
- metadata +34 -29
@@ -69,7 +69,7 @@ module Elasticsearch
|
|
69
69
|
# Returns a Hashie::Mash of the aggregations
|
70
70
|
#
|
71
71
|
def aggregations
|
72
|
-
|
72
|
+
Aggregations.new(response['aggregations'])
|
73
73
|
end
|
74
74
|
|
75
75
|
# Returns a Hashie::Mash of the suggestions
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Response
|
4
|
+
|
5
|
+
class Aggregations < Hashie::Mash
|
6
|
+
def initialize(attributes={})
|
7
|
+
__redefine_enumerable_methods super(attributes)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Fix the problem of Hashie::Mash returning unexpected values for `min` and `max` methods
|
11
|
+
#
|
12
|
+
# People can define names for aggregations such as `min` and `max`, but these
|
13
|
+
# methods are defined in `Enumerable#min` and `Enumerable#max`
|
14
|
+
#
|
15
|
+
# { foo: 'bar' }.min
|
16
|
+
# # => [:foo, "bar"]
|
17
|
+
#
|
18
|
+
# Therefore, any Hashie::Mash instance value has the `min` and `max`
|
19
|
+
# methods redefined to return the real value
|
20
|
+
#
|
21
|
+
def __redefine_enumerable_methods(h)
|
22
|
+
if h.respond_to?(:each_pair)
|
23
|
+
h.each_pair { |k, v| v = __redefine_enumerable_methods(v) }
|
24
|
+
end
|
25
|
+
if h.is_a?(Hashie::Mash)
|
26
|
+
class << h
|
27
|
+
define_method(:min) { self[:min] }
|
28
|
+
define_method(:max) { self[:max] }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'active_record'
|
3
3
|
|
4
|
+
# Needed for ActiveRecord 3.x ?
|
5
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
6
|
+
|
7
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
8
|
+
|
4
9
|
class Question < ActiveRecord::Base
|
5
10
|
include Elasticsearch::Model
|
6
11
|
|
@@ -66,14 +71,17 @@ module Elasticsearch
|
|
66
71
|
t.string :title
|
67
72
|
t.text :text
|
68
73
|
t.string :author
|
69
|
-
t.timestamps
|
74
|
+
t.timestamps null: false
|
70
75
|
end
|
76
|
+
|
71
77
|
create_table :answers do |t|
|
72
78
|
t.text :text
|
73
79
|
t.string :author
|
74
80
|
t.references :question
|
75
|
-
t.timestamps
|
76
|
-
end
|
81
|
+
t.timestamps null: false
|
82
|
+
end
|
83
|
+
|
84
|
+
add_index(:answers, :question_id) unless index_exists?(:answers, :question_id)
|
77
85
|
end
|
78
86
|
|
79
87
|
Question.delete_all
|
@@ -1,10 +1,67 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'active_record'
|
3
3
|
|
4
|
+
# Needed for ActiveRecord 3.x ?
|
5
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
6
|
+
|
7
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
8
|
+
|
4
9
|
module Elasticsearch
|
5
10
|
module Model
|
6
11
|
class ActiveRecordAssociationsIntegrationTest < Elasticsearch::Test::IntegrationTestCase
|
7
12
|
|
13
|
+
# ----- Search integration via Concern module -----------------------------------------------------
|
14
|
+
|
15
|
+
module Searchable
|
16
|
+
extend ActiveSupport::Concern
|
17
|
+
|
18
|
+
included do
|
19
|
+
include Elasticsearch::Model
|
20
|
+
include Elasticsearch::Model::Callbacks
|
21
|
+
|
22
|
+
# Set up the mapping
|
23
|
+
#
|
24
|
+
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
|
25
|
+
mapping do
|
26
|
+
indexes :title, analyzer: 'snowball'
|
27
|
+
indexes :created_at, type: 'date'
|
28
|
+
|
29
|
+
indexes :authors do
|
30
|
+
indexes :first_name
|
31
|
+
indexes :last_name
|
32
|
+
indexes :full_name, type: 'multi_field' do
|
33
|
+
indexes :full_name
|
34
|
+
indexes :raw, analyzer: 'keyword'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
indexes :categories, analyzer: 'keyword'
|
39
|
+
|
40
|
+
indexes :comments, type: 'nested' do
|
41
|
+
indexes :text
|
42
|
+
indexes :author
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Customize the JSON serialization for Elasticsearch
|
48
|
+
#
|
49
|
+
def as_indexed_json(options={})
|
50
|
+
{
|
51
|
+
title: title,
|
52
|
+
text: text,
|
53
|
+
categories: categories.map(&:title),
|
54
|
+
authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]),
|
55
|
+
comments: comments.as_json(only: [:text, :author])
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Update document in the index after touch
|
60
|
+
#
|
61
|
+
after_touch() { __elasticsearch__.index_document }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
8
65
|
context "ActiveRecord associations" do
|
9
66
|
setup do
|
10
67
|
|
@@ -13,7 +70,7 @@ module Elasticsearch
|
|
13
70
|
ActiveRecord::Schema.define(version: 1) do
|
14
71
|
create_table :categories do |t|
|
15
72
|
t.string :title
|
16
|
-
t.timestamps
|
73
|
+
t.timestamps null: false
|
17
74
|
end
|
18
75
|
|
19
76
|
create_table :categories_posts, id: false do |t|
|
@@ -22,28 +79,30 @@ module Elasticsearch
|
|
22
79
|
|
23
80
|
create_table :authors do |t|
|
24
81
|
t.string :first_name, :last_name
|
25
|
-
t.timestamps
|
82
|
+
t.timestamps null: false
|
26
83
|
end
|
27
84
|
|
28
85
|
create_table :authorships do |t|
|
29
86
|
t.string :first_name, :last_name
|
30
87
|
t.references :post
|
31
88
|
t.references :author
|
32
|
-
t.timestamps
|
89
|
+
t.timestamps null: false
|
33
90
|
end
|
34
91
|
|
35
92
|
create_table :comments do |t|
|
36
93
|
t.string :text
|
37
94
|
t.string :author
|
38
95
|
t.references :post
|
39
|
-
t.timestamps
|
40
|
-
end
|
96
|
+
t.timestamps null: false
|
97
|
+
end
|
98
|
+
|
99
|
+
add_index(:comments, :post_id) unless index_exists?(:comments, :post_id)
|
41
100
|
|
42
101
|
create_table :posts do |t|
|
43
102
|
t.string :title
|
44
103
|
t.text :text
|
45
104
|
t.boolean :published
|
46
|
-
t.timestamps
|
105
|
+
t.timestamps null: false
|
47
106
|
end
|
48
107
|
end
|
49
108
|
|
@@ -56,6 +115,8 @@ module Elasticsearch
|
|
56
115
|
class Author < ActiveRecord::Base
|
57
116
|
has_many :authorships
|
58
117
|
|
118
|
+
after_update { self.authorships.each(&:touch) }
|
119
|
+
|
59
120
|
def full_name
|
60
121
|
[first_name, last_name].compact.join(' ')
|
61
122
|
end
|
@@ -74,60 +135,13 @@ module Elasticsearch
|
|
74
135
|
has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
|
75
136
|
after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
|
76
137
|
has_many :authorships
|
77
|
-
has_many :authors, through: :authorships
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
module Searchable
|
84
|
-
extend ActiveSupport::Concern
|
85
|
-
|
86
|
-
included do
|
87
|
-
include Elasticsearch::Model
|
88
|
-
include Elasticsearch::Model::Callbacks
|
89
|
-
|
90
|
-
# Set up the mapping
|
91
|
-
#
|
92
|
-
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
|
93
|
-
mapping do
|
94
|
-
indexes :title, analyzer: 'snowball'
|
95
|
-
indexes :created_at, type: 'date'
|
96
|
-
|
97
|
-
indexes :authors do
|
98
|
-
indexes :first_name
|
99
|
-
indexes :last_name
|
100
|
-
indexes :full_name, type: 'multi_field' do
|
101
|
-
indexes :full_name
|
102
|
-
indexes :raw, analyzer: 'keyword'
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
indexes :categories, analyzer: 'keyword'
|
107
|
-
|
108
|
-
indexes :comments, type: 'nested' do
|
109
|
-
indexes :text
|
110
|
-
indexes :author
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
138
|
+
has_many :authors, through: :authorships,
|
139
|
+
after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
|
140
|
+
after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
|
141
|
+
has_many :comments, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
|
142
|
+
after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
|
114
143
|
|
115
|
-
|
116
|
-
#
|
117
|
-
def as_indexed_json(options={})
|
118
|
-
{
|
119
|
-
title: title,
|
120
|
-
text: text,
|
121
|
-
categories: categories.map(&:title),
|
122
|
-
authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]),
|
123
|
-
comments: comments.as_json(only: [:text, :author])
|
124
|
-
}
|
125
|
-
end
|
126
|
-
|
127
|
-
# Update document in the index after touch
|
128
|
-
#
|
129
|
-
after_touch() { __elasticsearch__.index_document }
|
130
|
-
end
|
144
|
+
after_touch() { __elasticsearch__.index_document }
|
131
145
|
end
|
132
146
|
|
133
147
|
# Include the search integration
|
@@ -3,45 +3,53 @@ require 'active_record'
|
|
3
3
|
|
4
4
|
puts "ActiveRecord #{ActiveRecord::VERSION::STRING}", '-'*80
|
5
5
|
|
6
|
+
# Needed for ActiveRecord 3.x ?
|
7
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
8
|
+
|
9
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
10
|
+
|
6
11
|
module Elasticsearch
|
7
12
|
module Model
|
8
13
|
class ActiveRecordBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase
|
14
|
+
|
15
|
+
class ::Article < ActiveRecord::Base
|
16
|
+
include Elasticsearch::Model
|
17
|
+
include Elasticsearch::Model::Callbacks
|
18
|
+
|
19
|
+
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
|
20
|
+
mapping do
|
21
|
+
indexes :title, type: 'string', analyzer: 'snowball'
|
22
|
+
indexes :body, type: 'string'
|
23
|
+
indexes :clicks, type: 'integer'
|
24
|
+
indexes :created_at, type: 'date'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def as_indexed_json(options = {})
|
29
|
+
attributes
|
30
|
+
.symbolize_keys
|
31
|
+
.slice(:title, :body, :clicks, :created_at)
|
32
|
+
.merge(suggest_title: title)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
9
36
|
context "ActiveRecord basic integration" do
|
10
37
|
setup do
|
11
38
|
ActiveRecord::Schema.define(:version => 1) do
|
12
39
|
create_table :articles do |t|
|
13
40
|
t.string :title
|
14
41
|
t.string :body
|
42
|
+
t.integer :clicks, :default => 0
|
15
43
|
t.datetime :created_at, :default => 'NOW()'
|
16
44
|
end
|
17
45
|
end
|
18
46
|
|
19
|
-
class ::Article < ActiveRecord::Base
|
20
|
-
include Elasticsearch::Model
|
21
|
-
include Elasticsearch::Model::Callbacks
|
22
|
-
|
23
|
-
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
|
24
|
-
mapping do
|
25
|
-
indexes :title, type: 'string', analyzer: 'snowball'
|
26
|
-
indexes :body, type: 'string'
|
27
|
-
indexes :created_at, type: 'date'
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def as_indexed_json(options = {})
|
32
|
-
attributes
|
33
|
-
.symbolize_keys
|
34
|
-
.slice(:title, :body, :created_at)
|
35
|
-
.merge(suggest_title: title)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
47
|
Article.delete_all
|
40
48
|
Article.__elasticsearch__.create_index! force: true
|
41
49
|
|
42
|
-
::Article.create! title: 'Test', body: ''
|
43
|
-
::Article.create! title: 'Testing Coding', body: ''
|
44
|
-
::Article.create! title: 'Coding', body: ''
|
50
|
+
::Article.create! title: 'Test', body: '', clicks: 1
|
51
|
+
::Article.create! title: 'Testing Coding', body: '', clicks: 2
|
52
|
+
::Article.create! title: 'Coding', body: '', clicks: 3
|
45
53
|
|
46
54
|
Article.__elasticsearch__.refresh_index!
|
47
55
|
end
|
@@ -98,7 +106,10 @@ module Elasticsearch
|
|
98
106
|
end
|
99
107
|
|
100
108
|
should "preserve the search results order for records" do
|
101
|
-
response = Article.search
|
109
|
+
response = Article.search query: { match: { title: 'code' }}, sort: { clicks: :desc }
|
110
|
+
|
111
|
+
assert_equal response.records[0].clicks, 3
|
112
|
+
assert_equal response.records[1].clicks, 2
|
102
113
|
|
103
114
|
response.records.each_with_hit do |r, h|
|
104
115
|
assert_equal h._id, r.id.to_s
|
@@ -217,11 +228,17 @@ module Elasticsearch
|
|
217
228
|
|
218
229
|
should "allow dot access to response" do
|
219
230
|
response = Article.search query: { match: { title: { query: 'test' } } },
|
220
|
-
aggregations: {
|
231
|
+
aggregations: {
|
232
|
+
dates: { date_histogram: { field: 'created_at', interval: 'hour' } },
|
233
|
+
clicks: { global: {}, aggregations: { min: { min: { field: 'clicks' } } } }
|
234
|
+
},
|
221
235
|
suggest: { text: 'tezt', title: { term: { field: 'title', suggest_mode: 'always' } } }
|
222
236
|
|
223
237
|
response.response.respond_to?(:aggregations)
|
224
|
-
assert_equal 2,
|
238
|
+
assert_equal 2, response.aggregations.dates.buckets.first.doc_count
|
239
|
+
assert_equal 3, response.aggregations.clicks.doc_count
|
240
|
+
assert_equal 1.0, response.aggregations.clicks.min.value
|
241
|
+
assert_nil response.aggregations.clicks.max
|
225
242
|
|
226
243
|
response.response.respond_to?(:suggest)
|
227
244
|
assert_equal 1, response.suggestions.title.first.options.size
|
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'active_record'
|
3
3
|
|
4
|
+
# Needed for ActiveRecord 3.x ?
|
5
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
6
|
+
|
7
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
8
|
+
|
4
9
|
module Elasticsearch
|
5
10
|
module Model
|
6
11
|
class ActiveRecordCustomSerializationTest < Elasticsearch::Test::IntegrationTestCase
|
@@ -1,9 +1,28 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'active_record'
|
3
3
|
|
4
|
+
# Needed for ActiveRecord 3.x ?
|
5
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
6
|
+
|
7
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
8
|
+
|
4
9
|
module Elasticsearch
|
5
10
|
module Model
|
6
11
|
class ActiveRecordImportIntegrationTest < Elasticsearch::Test::IntegrationTestCase
|
12
|
+
|
13
|
+
class ::ImportArticle < ActiveRecord::Base
|
14
|
+
include Elasticsearch::Model
|
15
|
+
|
16
|
+
scope :popular, -> { where('views >= 50') }
|
17
|
+
|
18
|
+
mapping do
|
19
|
+
indexes :title, type: 'string'
|
20
|
+
indexes :views, type: 'integer'
|
21
|
+
indexes :numeric, type: 'integer'
|
22
|
+
indexes :created_at, type: 'date'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
7
26
|
context "ActiveRecord importing" do
|
8
27
|
setup do
|
9
28
|
ActiveRecord::Schema.define(:version => 1) do
|
@@ -15,19 +34,6 @@ module Elasticsearch
|
|
15
34
|
end
|
16
35
|
end
|
17
36
|
|
18
|
-
class ::ImportArticle < ActiveRecord::Base
|
19
|
-
include Elasticsearch::Model
|
20
|
-
|
21
|
-
scope :popular, -> { where('views >= 50') }
|
22
|
-
|
23
|
-
mapping do
|
24
|
-
indexes :title, type: 'string'
|
25
|
-
indexes :views, type: 'integer'
|
26
|
-
indexes :numeric, type: 'integer'
|
27
|
-
indexes :created_at, type: 'date'
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
37
|
ImportArticle.delete_all
|
32
38
|
ImportArticle.__elasticsearch__.create_index! force: true
|
33
39
|
ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
|
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'active_record'
|
3
3
|
|
4
|
+
# Needed for ActiveRecord 3.x ?
|
5
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected?
|
6
|
+
|
7
|
+
::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5'
|
8
|
+
|
4
9
|
module Elasticsearch
|
5
10
|
module Model
|
6
11
|
class ActiveRecordNamespacedModelIntegrationTest < Elasticsearch::Test::IntegrationTestCase
|