elasticsearch-model 0.1.9 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -69,7 +69,7 @@ module Elasticsearch
69
69
  # Returns a Hashie::Mash of the aggregations
70
70
  #
71
71
  def aggregations
72
- response['aggregations'] ? Hashie::Mash.new(response['aggregations']) : nil
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,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Model
3
- VERSION = "0.1.9"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  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 and add_index(:answers, :question_id)
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 and add_index(:comments, :post_id)
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
- has_many :comments
79
- end
80
-
81
- # ----- Search integration via Concern module -----------------------------------------------------
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
- # Customize the JSON serialization for Elasticsearch
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('title:code')
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: { dates: { date_histogram: { field: 'created_at', interval: 'hour' } } },
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, response.aggregations.dates.buckets.first.doc_count
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