elasticsearch-model 0.1.1 → 0.1.2
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 +15 -0
- data/CHANGELOG.md +10 -0
- data/README.md +11 -3
- data/elasticsearch-model.gemspec +1 -0
- data/lib/elasticsearch/model.rb +4 -1
- data/lib/elasticsearch/model/adapters/active_record.rb +10 -5
- data/lib/elasticsearch/model/adapters/default.rb +6 -0
- data/lib/elasticsearch/model/adapters/mongoid.rb +6 -4
- data/lib/elasticsearch/model/importing.rb +25 -4
- data/lib/elasticsearch/model/indexing.rb +2 -0
- data/lib/elasticsearch/model/response/pagination.rb +65 -0
- data/lib/elasticsearch/model/response/records.rb +2 -2
- data/lib/elasticsearch/model/response/result.rb +19 -6
- data/lib/elasticsearch/model/version.rb +1 -1
- data/test/integration/active_record_basic_test.rb +32 -0
- data/test/integration/active_record_import_test.rb +28 -3
- data/test/integration/mongoid_basic_test.rb +12 -0
- data/test/unit/adapter_active_record_test.rb +32 -3
- data/test/unit/adapter_default_test.rb +14 -4
- data/test/unit/adapter_mongoid_test.rb +15 -0
- data/test/unit/importing_test.rb +34 -1
- data/test/unit/indexing_test.rb +6 -0
- data/test/unit/{response_pagination_test.rb → response_pagination_kaminari_test.rb} +1 -1
- data/test/unit/response_pagination_will_paginate_test.rb +189 -0
- data/test/unit/response_result_test.rb +42 -4
- metadata +23 -55
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NTEzZGEzOThmZGEyNGQ1NWU1NmQ5M2MxZjY2MTY3Zjc4MjY4YWUzMQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MzViYTk0YjhmZjA3ODI1MTlhOWZhNDU4NzdjZTQyZDIxNDcxODYwZg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ODRhOGMxYzBmMTZlYWU4YzJmNTRjZmRjZjQ5Y2VkY2I5M2E0ZTFhZWZjYTVl
|
10
|
+
ODcyMDUyMzkyOGE5MmI2ZDcwNWIwNTEwMTY4YmZlMzIyOWU2NDU2OGI2NmM4
|
11
|
+
MGFkODMxM2I1NmZmNmRmMDRiNzViOGIyZDhkYzU2MGMxNDQ2NzA=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NGQ2YTM3YWVkNzVkYWU5MmNkNjJmMjZjZTM2MGMxZGU0NGY3Y2U5MmRlZTU5
|
14
|
+
NWZjOGY2ZDdmODIyMzYzNDkzOTM4M2E4M2YwYTkwN2FmMjZlNmQ2MTIyODRk
|
15
|
+
YjM4ZTQ1NWNkN2VkY2UzNDU4NThjYTY0NmMyOWY5NDA4MmU5MjA=
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## 0.1.2
|
2
|
+
|
3
|
+
* Properly delegate existence methods like `result.foo?` to `result._source.foo`
|
4
|
+
* Exception is raised when `type` is not passed to Mappings#new
|
5
|
+
* Allow passing an ActiveRecord scope to the `import` method
|
6
|
+
* Added, that `each_with_hit` and `map_with_hit` in `Elasticsearch::Model::Response::Records` call `to_a`
|
7
|
+
* Added support for [`will_paginate`](https://github.com/mislav/will_paginate) pagination library
|
8
|
+
* Added the ability to transform models during indexing
|
9
|
+
* Added explicit `type` and `id` methods to Response::Result, aliasing `_type` and `_id`
|
10
|
+
|
1
11
|
## 0.1.1
|
2
12
|
|
3
13
|
* Improved documentation and tests
|
data/README.md
CHANGED
@@ -194,6 +194,13 @@ response.any? { |r| r.title =~ /fox|dog/ }
|
|
194
194
|
# => true
|
195
195
|
```
|
196
196
|
|
197
|
+
To use `Array`'s methods (including any _ActiveSupport_ extensions), just call `to_a` on the object:
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
response.to_a.last.title
|
201
|
+
# "Fast black dogs"
|
202
|
+
```
|
203
|
+
|
197
204
|
#### Search results as database records
|
198
205
|
|
199
206
|
Instead of returning documents from Elasticsearch, the `records` method will return a collection
|
@@ -245,9 +252,10 @@ response.records.each_with_hit { |record, hit| puts "* #{record.title}: #{hit._s
|
|
245
252
|
#### Pagination
|
246
253
|
|
247
254
|
You can implement pagination with the `from` and `size` search parameters. However, search results
|
248
|
-
can be automatically paginated with the [`kaminari`](http://rubygems.org/gems/kaminari)
|
255
|
+
can be automatically paginated with the [`kaminari`](http://rubygems.org/gems/kaminari) or
|
256
|
+
[`will_paginate`](https://github.com/mislav/will_paginate) gems.
|
249
257
|
|
250
|
-
If Kaminari is loaded, use the familiar paging methods:
|
258
|
+
If Kaminari or WillPaginate is loaded, use the familiar paging methods:
|
251
259
|
|
252
260
|
```ruby
|
253
261
|
response.page(2).results
|
@@ -264,7 +272,7 @@ In a Rails controller, use the the `params[:page]` parameter to paginate through
|
|
264
272
|
@articles.next_page
|
265
273
|
# => 3
|
266
274
|
```
|
267
|
-
To initialize and include the pagination support manually:
|
275
|
+
To initialize and include the Kaminari pagination support manually:
|
268
276
|
|
269
277
|
```ruby
|
270
278
|
Kaminari::Hooks.init
|
data/elasticsearch-model.gemspec
CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |s|
|
|
36
36
|
|
37
37
|
s.add_development_dependency "oj"
|
38
38
|
s.add_development_dependency "kaminari"
|
39
|
+
s.add_development_dependency "will_paginate"
|
39
40
|
# NOTE: Do not add Mongoid here, keep only in 3/4 files
|
40
41
|
|
41
42
|
s.add_development_dependency "minitest", "~> 4.0"
|
data/lib/elasticsearch/model.rb
CHANGED
@@ -31,8 +31,11 @@ require 'elasticsearch/model/response/pagination'
|
|
31
31
|
|
32
32
|
require 'elasticsearch/model/ext/active_record'
|
33
33
|
|
34
|
-
|
34
|
+
case
|
35
|
+
when defined?(::Kaminari)
|
35
36
|
Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::Kaminari
|
37
|
+
when defined?(::WillPaginate)
|
38
|
+
Elasticsearch::Model::Response::Response.__send__ :include, Elasticsearch::Model::Response::Pagination::WillPaginate
|
36
39
|
end
|
37
40
|
|
38
41
|
module Elasticsearch
|
@@ -83,15 +83,20 @@ module Elasticsearch
|
|
83
83
|
# @see http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ActiveRecord::Batches.find_in_batches
|
84
84
|
#
|
85
85
|
def __find_in_batches(options={}, &block)
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
named_scope = options.delete(:scope)
|
87
|
+
|
88
|
+
scope = named_scope ? self.__send__(named_scope) : self
|
89
|
+
|
90
|
+
scope.find_in_batches(options) do |batch|
|
91
|
+
yield batch
|
89
92
|
end
|
90
93
|
end
|
91
|
-
end
|
92
94
|
|
95
|
+
def __transform
|
96
|
+
lambda { |model| { index: { _id: model.id, data: model.__elasticsearch__.as_indexed_json } } }
|
97
|
+
end
|
98
|
+
end
|
93
99
|
end
|
94
|
-
|
95
100
|
end
|
96
101
|
end
|
97
102
|
end
|
@@ -36,6 +36,12 @@ module Elasticsearch
|
|
36
36
|
def __find_in_batches(options={}, &block)
|
37
37
|
raise NotImplemented, "Method not implemented for default adapter"
|
38
38
|
end
|
39
|
+
|
40
|
+
# @abstract Implement this method in your adapter
|
41
|
+
#
|
42
|
+
def __transform
|
43
|
+
raise NotImplemented, "Method not implemented for default adapter"
|
44
|
+
end
|
39
45
|
end
|
40
46
|
|
41
47
|
end
|
@@ -70,17 +70,19 @@ module Elasticsearch
|
|
70
70
|
items << item
|
71
71
|
|
72
72
|
if items.length % options[:batch_size] == 0
|
73
|
-
|
74
|
-
yield batch_for_bulk
|
73
|
+
yield items
|
75
74
|
items = []
|
76
75
|
end
|
77
76
|
end
|
78
77
|
|
79
78
|
unless items.empty?
|
80
|
-
|
81
|
-
yield batch_for_bulk
|
79
|
+
yield items
|
82
80
|
end
|
83
81
|
end
|
82
|
+
|
83
|
+
def __transform
|
84
|
+
lambda {|a| { index: { _id: a.id.to_s, data: a.as_indexed_json } }}
|
85
|
+
end
|
84
86
|
end
|
85
87
|
|
86
88
|
end
|
@@ -64,11 +64,29 @@ module Elasticsearch
|
|
64
64
|
#
|
65
65
|
# Article.import index: 'my-new-index', type: 'my-other-type'
|
66
66
|
#
|
67
|
+
# @example Pass an ActiveRecord scope to limit the imported records
|
68
|
+
#
|
69
|
+
# Article.import scope: 'published'
|
70
|
+
#
|
71
|
+
# @example Transform records during the import with a lambda
|
72
|
+
#
|
73
|
+
# transform = lambda do |a|
|
74
|
+
# {index: {_id: a.id, _parent: a.author_id, data: a.__elasticsearch__.as_indexed_json}}
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# Article.import transform: transform
|
78
|
+
#
|
67
79
|
def import(options={}, &block)
|
68
80
|
errors = 0
|
69
|
-
refresh = options.delete(:refresh)
|
70
|
-
target_index = options.delete(:index)
|
71
|
-
target_type = options.delete(:type)
|
81
|
+
refresh = options.delete(:refresh) || false
|
82
|
+
target_index = options.delete(:index) || index_name
|
83
|
+
target_type = options.delete(:type) || document_type
|
84
|
+
transform = options.delete(:transform) || __transform
|
85
|
+
|
86
|
+
unless transform.respond_to?(:call)
|
87
|
+
raise ArgumentError,
|
88
|
+
"Pass an object responding to `call` as the :transport option, #{transform.class} given"
|
89
|
+
end
|
72
90
|
|
73
91
|
if options.delete(:force)
|
74
92
|
self.create_index! force: true, index: target_index
|
@@ -78,7 +96,7 @@ module Elasticsearch
|
|
78
96
|
response = client.bulk \
|
79
97
|
index: target_index,
|
80
98
|
type: target_type,
|
81
|
-
body: batch
|
99
|
+
body: __batch_to_bulk(batch, transform)
|
82
100
|
|
83
101
|
yield response if block_given?
|
84
102
|
|
@@ -90,6 +108,9 @@ module Elasticsearch
|
|
90
108
|
return errors
|
91
109
|
end
|
92
110
|
|
111
|
+
def __batch_to_bulk(batch, transform)
|
112
|
+
batch.map { |model| transform.call(model) }
|
113
|
+
end
|
93
114
|
end
|
94
115
|
|
95
116
|
end
|
@@ -93,6 +93,71 @@ module Elasticsearch
|
|
93
93
|
results.total
|
94
94
|
end
|
95
95
|
end
|
96
|
+
|
97
|
+
# Allow models to be paginated with the "will_paginate" gem [https://github.com/mislav/will_paginate]
|
98
|
+
#
|
99
|
+
module WillPaginate
|
100
|
+
def self.included(base)
|
101
|
+
base.__send__ :include, ::WillPaginate::CollectionMethods
|
102
|
+
|
103
|
+
# Include the paging methods in results and records
|
104
|
+
#
|
105
|
+
methods = [:current_page, :per_page, :total_entries, :total_pages, :previous_page, :next_page, :out_of_bounds?]
|
106
|
+
Elasticsearch::Model::Response::Results.__send__ :delegate, *methods, to: :response
|
107
|
+
Elasticsearch::Model::Response::Records.__send__ :delegate, *methods, to: :response
|
108
|
+
end
|
109
|
+
|
110
|
+
# Main pagination method
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
#
|
114
|
+
# Article.search('foo').paginate(page: 1, per_page: 30)
|
115
|
+
#
|
116
|
+
def paginate(options)
|
117
|
+
page = [options[:page].to_i, 1].max
|
118
|
+
per_page = (options[:per_page] || klass.per_page).to_i
|
119
|
+
|
120
|
+
search.definition.update size: per_page,
|
121
|
+
from: (page - 1) * per_page
|
122
|
+
self
|
123
|
+
end
|
124
|
+
|
125
|
+
# Return the current page
|
126
|
+
#
|
127
|
+
def current_page
|
128
|
+
search.definition[:from] / per_page + 1 if search.definition[:from] && per_page
|
129
|
+
end
|
130
|
+
|
131
|
+
# Pagination method
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
#
|
135
|
+
# Article.search('foo').page(2)
|
136
|
+
#
|
137
|
+
def page(num)
|
138
|
+
paginate(page: num, per_page: per_page) # shorthand
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return or set the "size" value
|
142
|
+
#
|
143
|
+
# @example
|
144
|
+
#
|
145
|
+
# Article.search('foo').per_page(15).page(2)
|
146
|
+
#
|
147
|
+
def per_page(num = nil)
|
148
|
+
if num.nil?
|
149
|
+
search.definition[:size]
|
150
|
+
else
|
151
|
+
paginate(page: current_page, per_page: num) # shorthand
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the total number of results
|
156
|
+
#
|
157
|
+
def total_entries
|
158
|
+
results.total
|
159
|
+
end
|
160
|
+
end
|
96
161
|
end
|
97
162
|
|
98
163
|
end
|
@@ -43,13 +43,13 @@ module Elasticsearch
|
|
43
43
|
# Yields [record, hit] pairs to the block
|
44
44
|
#
|
45
45
|
def each_with_hit(&block)
|
46
|
-
records.zip(results).each(&block)
|
46
|
+
records.to_a.zip(results).each(&block)
|
47
47
|
end
|
48
48
|
|
49
49
|
# Yields [record, hit] pairs and returns the result
|
50
50
|
#
|
51
51
|
def map_with_hit(&block)
|
52
|
-
records.zip(results).map(&block)
|
52
|
+
records.to_a.zip(results).map(&block)
|
53
53
|
end
|
54
54
|
|
55
55
|
# Delegate methods to `@records`
|
@@ -17,14 +17,28 @@ module Elasticsearch
|
|
17
17
|
@result = Hashie::Mash.new(attributes)
|
18
18
|
end
|
19
19
|
|
20
|
+
# Return document `_id` as `id`
|
21
|
+
#
|
22
|
+
def id
|
23
|
+
@result['_id']
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return document `_type` as `_type`
|
27
|
+
#
|
28
|
+
def type
|
29
|
+
@result['_type']
|
30
|
+
end
|
31
|
+
|
20
32
|
# Delegate methods to `@result` or `@result._source`
|
21
33
|
#
|
22
|
-
def method_missing(
|
34
|
+
def method_missing(name, *arguments)
|
23
35
|
case
|
24
|
-
when
|
25
|
-
@result.__send__
|
26
|
-
when @result.
|
27
|
-
@result.
|
36
|
+
when name.to_s.end_with?('?')
|
37
|
+
@result.__send__(name, *arguments) || ( @result._source && @result._source.__send__(name, *arguments) )
|
38
|
+
when @result.respond_to?(name)
|
39
|
+
@result.__send__ name, *arguments
|
40
|
+
when @result._source && @result._source.respond_to?(name)
|
41
|
+
@result._source.__send__ name, *arguments
|
28
42
|
else
|
29
43
|
super
|
30
44
|
end
|
@@ -43,7 +57,6 @@ module Elasticsearch
|
|
43
57
|
end
|
44
58
|
|
45
59
|
# TODO: #to_s, #inspect, with support for Pry
|
46
|
-
|
47
60
|
end
|
48
61
|
end
|
49
62
|
end
|
@@ -52,6 +52,19 @@ module Elasticsearch
|
|
52
52
|
assert_equal 'Test', response.records.first.title
|
53
53
|
end
|
54
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
|
+
|
55
68
|
should "iterate over results" do
|
56
69
|
response = Article.search('title:test')
|
57
70
|
|
@@ -59,6 +72,13 @@ module Elasticsearch
|
|
59
72
|
assert_equal [1, 2], response.records.map(&:id)
|
60
73
|
end
|
61
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
|
+
|
62
82
|
should "access results from records" do
|
63
83
|
response = Article.search('title:test')
|
64
84
|
|
@@ -68,6 +88,18 @@ module Elasticsearch
|
|
68
88
|
end
|
69
89
|
end
|
70
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
|
+
|
71
103
|
should "remove document from index on destroy" do
|
72
104
|
article = Article.first
|
73
105
|
|
@@ -7,9 +7,12 @@ module Elasticsearch
|
|
7
7
|
class ::ImportArticle < ActiveRecord::Base
|
8
8
|
include Elasticsearch::Model
|
9
9
|
|
10
|
+
scope :popular, -> { where('views >= 50') }
|
11
|
+
|
10
12
|
mapping do
|
11
13
|
indexes :title, type: 'string'
|
12
14
|
indexes :views, type: 'integer'
|
15
|
+
indexes :numeric, type: 'integer'
|
13
16
|
indexes :created_at, type: 'date'
|
14
17
|
end
|
15
18
|
end
|
@@ -19,7 +22,8 @@ module Elasticsearch
|
|
19
22
|
ActiveRecord::Schema.define(:version => 1) do
|
20
23
|
create_table :import_articles do |t|
|
21
24
|
t.string :title
|
22
|
-
t.
|
25
|
+
t.integer :views
|
26
|
+
t.string :numeric # For the sake of invalid data sent to Elasticsearch
|
23
27
|
t.datetime :created_at, :default => 'NOW()'
|
24
28
|
end
|
25
29
|
end
|
@@ -28,7 +32,7 @@ module Elasticsearch
|
|
28
32
|
ImportArticle.__elasticsearch__.create_index! force: true
|
29
33
|
ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow'
|
30
34
|
|
31
|
-
100.times { |i| ImportArticle.create! title: "Test #{i}" }
|
35
|
+
100.times { |i| ImportArticle.create! title: "Test #{i}", views: i }
|
32
36
|
end
|
33
37
|
|
34
38
|
should "import all the documents" do
|
@@ -49,8 +53,17 @@ module Elasticsearch
|
|
49
53
|
assert_equal 100, ImportArticle.search('*').results.total
|
50
54
|
end
|
51
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
|
+
|
52
65
|
should "report and not store/index invalid documents" do
|
53
|
-
ImportArticle.create! title: "Test INVALID",
|
66
|
+
ImportArticle.create! title: "Test INVALID", numeric: "INVALID"
|
54
67
|
|
55
68
|
assert_equal 101, ImportArticle.count
|
56
69
|
|
@@ -68,6 +81,18 @@ module Elasticsearch
|
|
68
81
|
ImportArticle.__elasticsearch__.refresh_index!
|
69
82
|
assert_equal 100, ImportArticle.search('*').results.total
|
70
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
|
71
96
|
end
|
72
97
|
|
73
98
|
end
|