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