elasticsearch-model 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|