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