elasticsearch-model-queryable 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +13 -0
  6. data/README.md +695 -0
  7. data/Rakefile +59 -0
  8. data/elasticsearch-model.gemspec +57 -0
  9. data/examples/activerecord_article.rb +77 -0
  10. data/examples/activerecord_associations.rb +162 -0
  11. data/examples/couchbase_article.rb +66 -0
  12. data/examples/datamapper_article.rb +71 -0
  13. data/examples/mongoid_article.rb +68 -0
  14. data/examples/ohm_article.rb +70 -0
  15. data/examples/riak_article.rb +52 -0
  16. data/gemfiles/3.0.gemfile +12 -0
  17. data/gemfiles/4.0.gemfile +11 -0
  18. data/lib/elasticsearch/model/adapter.rb +145 -0
  19. data/lib/elasticsearch/model/adapters/active_record.rb +104 -0
  20. data/lib/elasticsearch/model/adapters/default.rb +50 -0
  21. data/lib/elasticsearch/model/adapters/mongoid.rb +92 -0
  22. data/lib/elasticsearch/model/callbacks.rb +35 -0
  23. data/lib/elasticsearch/model/client.rb +61 -0
  24. data/lib/elasticsearch/model/ext/active_record.rb +14 -0
  25. data/lib/elasticsearch/model/hash_wrapper.rb +15 -0
  26. data/lib/elasticsearch/model/importing.rb +144 -0
  27. data/lib/elasticsearch/model/indexing.rb +472 -0
  28. data/lib/elasticsearch/model/naming.rb +101 -0
  29. data/lib/elasticsearch/model/proxy.rb +127 -0
  30. data/lib/elasticsearch/model/response/base.rb +44 -0
  31. data/lib/elasticsearch/model/response/pagination.rb +173 -0
  32. data/lib/elasticsearch/model/response/records.rb +69 -0
  33. data/lib/elasticsearch/model/response/result.rb +63 -0
  34. data/lib/elasticsearch/model/response/results.rb +31 -0
  35. data/lib/elasticsearch/model/response.rb +71 -0
  36. data/lib/elasticsearch/model/searching.rb +107 -0
  37. data/lib/elasticsearch/model/serializing.rb +35 -0
  38. data/lib/elasticsearch/model/version.rb +5 -0
  39. data/lib/elasticsearch/model.rb +157 -0
  40. data/test/integration/active_record_associations_parent_child.rb +139 -0
  41. data/test/integration/active_record_associations_test.rb +307 -0
  42. data/test/integration/active_record_basic_test.rb +179 -0
  43. data/test/integration/active_record_custom_serialization_test.rb +62 -0
  44. data/test/integration/active_record_import_test.rb +100 -0
  45. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  46. data/test/integration/active_record_pagination_test.rb +132 -0
  47. data/test/integration/mongoid_basic_test.rb +193 -0
  48. data/test/test_helper.rb +63 -0
  49. data/test/unit/adapter_active_record_test.rb +140 -0
  50. data/test/unit/adapter_default_test.rb +41 -0
  51. data/test/unit/adapter_mongoid_test.rb +102 -0
  52. data/test/unit/adapter_test.rb +69 -0
  53. data/test/unit/callbacks_test.rb +31 -0
  54. data/test/unit/client_test.rb +27 -0
  55. data/test/unit/importing_test.rb +176 -0
  56. data/test/unit/indexing_test.rb +478 -0
  57. data/test/unit/module_test.rb +57 -0
  58. data/test/unit/naming_test.rb +76 -0
  59. data/test/unit/proxy_test.rb +89 -0
  60. data/test/unit/response_base_test.rb +40 -0
  61. data/test/unit/response_pagination_kaminari_test.rb +189 -0
  62. data/test/unit/response_pagination_will_paginate_test.rb +208 -0
  63. data/test/unit/response_records_test.rb +91 -0
  64. data/test/unit/response_result_test.rb +90 -0
  65. data/test/unit/response_results_test.rb +31 -0
  66. data/test/unit/response_test.rb +67 -0
  67. data/test/unit/searching_search_request_test.rb +78 -0
  68. data/test/unit/searching_test.rb +41 -0
  69. data/test/unit/serializing_test.rb +17 -0
  70. metadata +466 -0
@@ -0,0 +1,179 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ puts "ActiveRecord #{ActiveRecord::VERSION::STRING}", '-'*80
5
+
6
+ module Elasticsearch
7
+ module Model
8
+ class ActiveRecordBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase
9
+ context "ActiveRecord basic integration" do
10
+ setup do
11
+ ActiveRecord::Schema.define(:version => 1) do
12
+ create_table :articles do |t|
13
+ t.string :title
14
+ t.datetime :created_at, :default => 'NOW()'
15
+ end
16
+ end
17
+
18
+ class ::Article < ActiveRecord::Base
19
+ include Elasticsearch::Model
20
+ include Elasticsearch::Model::Callbacks
21
+
22
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
23
+ mapping do
24
+ indexes :title, type: 'string', analyzer: 'snowball'
25
+ indexes :created_at, type: 'date'
26
+ end
27
+ end
28
+ end
29
+
30
+ Article.delete_all
31
+ Article.__elasticsearch__.create_index! force: true
32
+
33
+ ::Article.create! title: 'Test'
34
+ ::Article.create! title: 'Testing Coding'
35
+ ::Article.create! title: 'Coding'
36
+
37
+ Article.__elasticsearch__.refresh_index!
38
+ end
39
+
40
+ should "index and find a document" do
41
+ response = Article.search('title:test')
42
+
43
+ assert response.any?, "Response should not be empty: #{response.to_a.inspect}"
44
+
45
+ assert_equal 2, response.results.size
46
+ assert_equal 2, response.records.size
47
+
48
+ assert_instance_of Elasticsearch::Model::Response::Result, response.results.first
49
+ assert_instance_of Article, response.records.first
50
+
51
+ assert_equal 'Test', response.results.first.title
52
+ assert_equal 'Test', response.records.first.title
53
+ end
54
+
55
+ should "provide access to result" do
56
+ response = Article.search query: { match: { title: 'test' } }, highlight: { fields: { title: {} } }
57
+
58
+ assert_equal 'Test', response.results.first.title
59
+
60
+ assert_equal true, response.results.first.title?
61
+ assert_equal false, response.results.first.boo?
62
+
63
+ assert_equal true, response.results.first.highlight?
64
+ assert_equal true, response.results.first.highlight.title?
65
+ assert_equal false, response.results.first.highlight.boo?
66
+ end
67
+
68
+ should "iterate over results" do
69
+ response = Article.search('title:test')
70
+
71
+ assert_equal ['1', '2'], response.results.map(&:_id)
72
+ assert_equal [1, 2], response.records.map(&:id)
73
+ end
74
+
75
+ should "return _id and _type as #id and #type" do
76
+ response = Article.search('title:test')
77
+
78
+ assert_equal '1', response.results.first.id
79
+ assert_equal 'article', response.results.first.type
80
+ end
81
+
82
+ should "access results from records" do
83
+ response = Article.search('title:test')
84
+
85
+ response.records.each_with_hit do |r, h|
86
+ assert_not_nil h._score
87
+ assert_not_nil h._source.title
88
+ end
89
+ end
90
+
91
+ should "preserve the search results order for records" do
92
+ response = Article.search('title:code')
93
+
94
+ response.records.each_with_hit do |r, h|
95
+ assert_equal h._id, r.id.to_s
96
+ end
97
+
98
+ response.records.map_with_hit do |r, h|
99
+ assert_equal h._id, r.id.to_s
100
+ end
101
+ end
102
+
103
+ should "remove document from index on destroy" do
104
+ article = Article.first
105
+
106
+ article.destroy
107
+ assert_equal 2, Article.count
108
+
109
+ Article.__elasticsearch__.refresh_index!
110
+
111
+ response = Article.search 'title:test'
112
+
113
+ assert_equal 1, response.results.size
114
+ assert_equal 1, response.records.size
115
+ end
116
+
117
+ should "index updates to the document" do
118
+ article = Article.first
119
+
120
+ article.title = 'Writing'
121
+ article.save
122
+
123
+ Article.__elasticsearch__.refresh_index!
124
+
125
+ response = Article.search 'title:write'
126
+
127
+ assert_equal 1, response.results.size
128
+ assert_equal 1, response.records.size
129
+ end
130
+
131
+ should "return results for a DSL search" do
132
+ response = Article.search query: { match: { title: { query: 'test' } } }
133
+
134
+ assert_equal 2, response.results.size
135
+ assert_equal 2, response.records.size
136
+ end
137
+
138
+ should "return a paged collection" do
139
+ response = Article.search query: { match: { title: { query: 'test' } } },
140
+ size: 2,
141
+ from: 1
142
+
143
+ assert_equal 1, response.results.size
144
+ assert_equal 1, response.records.size
145
+
146
+ assert_equal 'Testing Coding', response.results.first.title
147
+ assert_equal 'Testing Coding', response.records.first.title
148
+ end
149
+
150
+ should "allow chaining SQL commands on response.records" do
151
+ response = Article.search query: { match: { title: { query: 'test' } } }
152
+
153
+ assert_equal 2, response.records.size
154
+ assert_equal 1, response.records.where(title: 'Test').size
155
+ assert_equal 'Test', response.records.where(title: 'Test').first.title
156
+ end
157
+
158
+ should "allow ordering response.records in SQL" do
159
+ response = Article.search query: { match: { title: { query: 'test' } } }
160
+
161
+ if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
162
+ assert_equal 'Testing Coding', response.records.order(title: :desc).first.title
163
+ else
164
+ assert_equal 'Testing Coding', response.records.order('title DESC').first.title
165
+ end
166
+ end
167
+
168
+ should "allow dot access to response" do
169
+ response = Article.search query: { match: { title: { query: 'test' } } },
170
+ aggregations: { dates: { date_histogram: { field: 'created_at', interval: 'hour' } } }
171
+
172
+ response.response.respond_to?(:aggregations)
173
+ assert_equal 2, response.response.aggregations.dates.buckets.first.doc_count
174
+ end
175
+ end
176
+
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,62 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class ActiveRecordCustomSerializationTest < Elasticsearch::Test::IntegrationTestCase
7
+ context "ActiveRecord model with custom JSON serialization" do
8
+ setup do
9
+ class ::ArticleWithCustomSerialization < ActiveRecord::Base
10
+ include Elasticsearch::Model
11
+ include Elasticsearch::Model::Callbacks
12
+
13
+ mapping do
14
+ indexes :title
15
+ end
16
+
17
+ def as_indexed_json(options={})
18
+ # as_json(options.merge root: false).slice('title')
19
+ { title: self.title }
20
+ end
21
+ end
22
+
23
+ ActiveRecord::Schema.define(:version => 1) do
24
+ create_table ArticleWithCustomSerialization.table_name do |t|
25
+ t.string :title
26
+ t.string :status
27
+ end
28
+ end
29
+
30
+ ArticleWithCustomSerialization.delete_all
31
+ ArticleWithCustomSerialization.__elasticsearch__.create_index! force: true
32
+ end
33
+
34
+ should "index only the title attribute when creating" do
35
+ ArticleWithCustomSerialization.create! title: 'Test', status: 'green'
36
+
37
+ a = ArticleWithCustomSerialization.__elasticsearch__.client.get \
38
+ index: 'article_with_custom_serializations',
39
+ type: 'article_with_custom_serialization',
40
+ id: '1'
41
+
42
+ assert_equal( { 'title' => 'Test' }, a['_source'] )
43
+ end
44
+
45
+ should "index only the title attribute when updating" do
46
+ ArticleWithCustomSerialization.create! title: 'Test', status: 'green'
47
+
48
+ article = ArticleWithCustomSerialization.first
49
+ article.update_attributes title: 'UPDATED', status: 'red'
50
+
51
+ a = ArticleWithCustomSerialization.__elasticsearch__.client.get \
52
+ index: 'article_with_custom_serializations',
53
+ type: 'article_with_custom_serialization',
54
+ id: '1'
55
+
56
+ assert_equal( { 'title' => 'UPDATED' }, a['_source'] )
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,100 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class ActiveRecordImportIntegrationTest < Elasticsearch::Test::IntegrationTestCase
7
+ context "ActiveRecord importing" do
8
+ setup do
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+ create_table :import_articles do |t|
11
+ t.string :title
12
+ t.integer :views
13
+ t.string :numeric # For the sake of invalid data sent to Elasticsearch
14
+ t.datetime :created_at, :default => 'NOW()'
15
+ end
16
+ end
17
+
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
+ ImportArticle.delete_all
32
+ ImportArticle.__elasticsearch__.create_index! force: true
33
+ ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
34
+
35
+ 100.times { |i| ImportArticle.create! title: "Test #{i}", views: i }
36
+ end
37
+
38
+ should "import all the documents" do
39
+ assert_equal 100, ImportArticle.count
40
+
41
+ ImportArticle.__elasticsearch__.refresh_index!
42
+ assert_equal 0, ImportArticle.search('*').results.total
43
+
44
+ batches = 0
45
+ errors = ImportArticle.import(batch_size: 10) do |response|
46
+ batches += 1
47
+ end
48
+
49
+ assert_equal 0, errors
50
+ assert_equal 10, batches
51
+
52
+ ImportArticle.__elasticsearch__.refresh_index!
53
+ assert_equal 100, ImportArticle.search('*').results.total
54
+ end
55
+
56
+ should "import only documents from a specific scope" do
57
+ assert_equal 100, ImportArticle.count
58
+
59
+ assert_equal 0, ImportArticle.import(scope: 'popular')
60
+
61
+ ImportArticle.__elasticsearch__.refresh_index!
62
+ assert_equal 50, ImportArticle.search('*').results.total
63
+ end
64
+
65
+ should "report and not store/index invalid documents" do
66
+ ImportArticle.create! title: "Test INVALID", numeric: "INVALID"
67
+
68
+ assert_equal 101, ImportArticle.count
69
+
70
+ ImportArticle.__elasticsearch__.refresh_index!
71
+ assert_equal 0, ImportArticle.search('*').results.total
72
+
73
+ batches = 0
74
+ errors = ImportArticle.__elasticsearch__.import(batch_size: 10) do |response|
75
+ batches += 1
76
+ end
77
+
78
+ assert_equal 1, errors
79
+ assert_equal 11, batches
80
+
81
+ ImportArticle.__elasticsearch__.refresh_index!
82
+ assert_equal 100, ImportArticle.search('*').results.total
83
+ end
84
+
85
+ should "transform documents with the option" do
86
+ assert_equal 100, ImportArticle.count
87
+
88
+ assert_equal 0, ImportArticle.import( transform: ->(a) {{ index: { data: { name: a.title, foo: 'BAR' } }}} )
89
+
90
+ ImportArticle.__elasticsearch__.refresh_index!
91
+ assert_contains ImportArticle.search('*').results.first._source.keys, 'name'
92
+ assert_contains ImportArticle.search('*').results.first._source.keys, 'foo'
93
+ assert_equal 100, ImportArticle.search('test').results.total
94
+ assert_equal 100, ImportArticle.search('bar').results.total
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,49 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class ActiveRecordNamespacedModelIntegrationTest < Elasticsearch::Test::IntegrationTestCase
7
+ context "Namespaced ActiveRecord model integration" do
8
+ setup do
9
+ ActiveRecord::Schema.define(:version => 1) do
10
+ create_table :articles do |t|
11
+ t.string :title
12
+ end
13
+ end
14
+
15
+ module ::MyNamespace
16
+ class Article < ActiveRecord::Base
17
+ include Elasticsearch::Model
18
+ include Elasticsearch::Model::Callbacks
19
+
20
+ mapping { indexes :title }
21
+ end
22
+ end
23
+
24
+ MyNamespace::Article.delete_all
25
+ MyNamespace::Article.__elasticsearch__.create_index! force: true
26
+
27
+ MyNamespace::Article.create! title: 'Test'
28
+
29
+ MyNamespace::Article.__elasticsearch__.refresh_index!
30
+ end
31
+
32
+ should "have proper index name and document type" do
33
+ assert_equal "my_namespace-articles", MyNamespace::Article.index_name
34
+ assert_equal "article", MyNamespace::Article.document_type
35
+ end
36
+
37
+ should "save document into index on save and find it" do
38
+ response = MyNamespace::Article.search 'title:test'
39
+
40
+ assert response.any?, "No results returned: #{response.inspect}"
41
+ assert_equal 1, response.size
42
+
43
+ assert_equal 'Test', response.results.first.title
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,132 @@
1
+ require 'test_helper'
2
+ require 'active_record'
3
+
4
+ module Elasticsearch
5
+ module Model
6
+ class ActiveRecordPaginationTest < Elasticsearch::Test::IntegrationTestCase
7
+ context "ActiveRecord pagination" do
8
+ setup do
9
+ class ::ArticleForPagination < ActiveRecord::Base
10
+ include Elasticsearch::Model
11
+
12
+ scope :published, -> { where(published: true) }
13
+
14
+ settings index: { number_of_shards: 1, number_of_replicas: 0 } do
15
+ mapping do
16
+ indexes :title, type: 'string', analyzer: 'snowball'
17
+ indexes :created_at, type: 'date'
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::Schema.define(:version => 1) do
23
+ create_table ::ArticleForPagination.table_name do |t|
24
+ t.string :title
25
+ t.datetime :created_at, :default => 'NOW()'
26
+ t.boolean :published
27
+ end
28
+ end
29
+
30
+ Kaminari::Hooks.init
31
+
32
+ ArticleForPagination.delete_all
33
+ ArticleForPagination.__elasticsearch__.create_index! force: true
34
+
35
+ 68.times do |i|
36
+ ::ArticleForPagination.create! title: "Test #{i}", published: (i % 2 == 0)
37
+ end
38
+
39
+ ArticleForPagination.import
40
+ ArticleForPagination.__elasticsearch__.refresh_index!
41
+ end
42
+
43
+ should "be on the first page by default" do
44
+ records = ArticleForPagination.search('title:test').page(1).records
45
+
46
+ assert_equal 25, records.size
47
+ assert_equal 1, records.current_page
48
+ assert_equal nil, records.prev_page
49
+ assert_equal 2, records.next_page
50
+ assert_equal 3, records.total_pages
51
+
52
+ assert records.first_page?, "Should be the first page"
53
+ assert ! records.last_page?, "Should NOT be the last page"
54
+ assert ! records.out_of_range?, "Should NOT be out of range"
55
+ end
56
+
57
+ should "load next page" do
58
+ records = ArticleForPagination.search('title:test').page(2).records
59
+
60
+ assert_equal 25, records.size
61
+ assert_equal 2, records.current_page
62
+ assert_equal 1, records.prev_page
63
+ assert_equal 3, records.next_page
64
+ assert_equal 3, records.total_pages
65
+
66
+ assert ! records.first_page?, "Should NOT be the first page"
67
+ assert ! records.last_page?, "Should NOT be the last page"
68
+ assert ! records.out_of_range?, "Should NOT be out of range"
69
+ end
70
+
71
+ should "load last page" do
72
+ records = ArticleForPagination.search('title:test').page(3).records
73
+
74
+ assert_equal 18, records.size
75
+ assert_equal 3, records.current_page
76
+ assert_equal 2, records.prev_page
77
+ assert_equal nil, records.next_page
78
+ assert_equal 3, records.total_pages
79
+
80
+ assert ! records.first_page?, "Should NOT be the first page"
81
+ assert records.last_page?, "Should be the last page"
82
+ assert ! records.out_of_range?, "Should NOT be out of range"
83
+ end
84
+
85
+ should "not load invalid page" do
86
+ records = ArticleForPagination.search('title:test').page(6).records
87
+
88
+ assert_equal 0, records.size
89
+ assert_equal 6, records.current_page
90
+ assert_equal 5, records.prev_page
91
+ assert_equal nil, records.next_page
92
+ assert_equal 3, records.total_pages
93
+
94
+ assert ! records.first_page?, "Should NOT be the first page"
95
+ assert records.last_page?, "Should be the last page"
96
+ assert records.out_of_range?, "Should be out of range"
97
+ end
98
+
99
+ should "be combined with scopes" do
100
+ records = ArticleForPagination.search('title:test').page(2).records.published
101
+ assert records.all? { |r| r.published? }
102
+ assert_equal 12, records.size
103
+ end
104
+
105
+ should "set the limit per request" do
106
+ records = ArticleForPagination.search('title:test').limit(50).page(2).records
107
+
108
+ assert_equal 18, records.size
109
+ assert_equal 2, records.current_page
110
+ assert_equal 1, records.prev_page
111
+ assert_equal nil, records.next_page
112
+ assert_equal 2, records.total_pages
113
+
114
+ assert records.last_page?, "Should be the last page"
115
+ end
116
+
117
+ context "with specific model settings" do
118
+ teardown do
119
+ ArticleForPagination.instance_variable_set(:@_default_per_page, nil)
120
+ end
121
+
122
+ should "respect paginates_per" do
123
+ ArticleForPagination.paginates_per 50
124
+
125
+ assert_equal 50, ArticleForPagination.search('*').page(1).records.size
126
+ end
127
+ end
128
+ end
129
+
130
+ end
131
+ end
132
+ end