elasticsearch-model-queryable 0.1.5

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