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 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) gem.
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
@@ -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"
@@ -31,8 +31,11 @@ require 'elasticsearch/model/response/pagination'
31
31
 
32
32
  require 'elasticsearch/model/ext/active_record'
33
33
 
34
- if defined?(::Kaminari)
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
- find_in_batches(options) do |batch|
87
- batch_for_bulk = batch.map { |a| { index: { _id: a.id, data: a.__elasticsearch__.as_indexed_json } } }
88
- yield batch_for_bulk
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
- batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
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
- batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
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) || false
70
- target_index = options.delete(:index) || index_name
71
- target_type = options.delete(:type) || document_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
@@ -37,6 +37,8 @@ module Elasticsearch
37
37
  attr_accessor :options
38
38
 
39
39
  def initialize(type, options={})
40
+ raise ArgumentError, "`type` is missing" if type.nil?
41
+
40
42
  @type = type
41
43
  @options = options
42
44
  @mapping = {}
@@ -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(method_name, *arguments)
34
+ def method_missing(name, *arguments)
23
35
  case
24
- when @result.respond_to?(method_name.to_sym)
25
- @result.__send__ method_name.to_sym, *arguments
26
- when @result._source && @result._source.respond_to?(method_name.to_sym)
27
- @result._source.__send__ method_name.to_sym, *arguments
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
@@ -1,5 +1,5 @@
1
1
  module Elasticsearch
2
2
  module Model
3
- VERSION = "0.1.1"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  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.string :views # For the sake of invalid data sent to Elasticsearch
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", views: "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