mongoid-elasticsearch 0.2.1

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.
@@ -0,0 +1,106 @@
1
+ require "mongoid/elasticsearch/version"
2
+
3
+ require 'elasticsearch'
4
+ require 'active_support/concern'
5
+
6
+ require 'mongoid/elasticsearch/utils'
7
+ require 'mongoid/elasticsearch/es'
8
+ require 'mongoid/elasticsearch/callbacks'
9
+ require 'mongoid/elasticsearch/index'
10
+ require 'mongoid/elasticsearch/indexing'
11
+ require 'mongoid/elasticsearch/response'
12
+ require 'mongoid/elasticsearch/slash_monkeypatch'
13
+
14
+ module Mongoid
15
+ module Elasticsearch
16
+ mattr_accessor :prefix
17
+ self.prefix = ''
18
+
19
+ mattr_accessor :client_options
20
+ self.client_options = {}
21
+
22
+ mattr_accessor :registered_indexes
23
+ self.registered_indexes = []
24
+
25
+ extend ActiveSupport::Concern
26
+ included do
27
+ def self.es
28
+ @__es__ ||= Mongoid::Elasticsearch::Es.new(self)
29
+ end
30
+
31
+ # Add elasticsearch to the model
32
+ # @option index_name [String] name of the index for this model
33
+ # @option index_options [Hash] Index options to be passed to Elasticsearch
34
+ # when creating an index
35
+ # @option client_options [Hash] Options for Elasticsearch::Client.new
36
+ # @option wrapper [Symbol] Select what wrapper to use for results
37
+ # possible options:
38
+ # :model - creates a new model instance, set its attributes, and marks it as persisted
39
+ # :mash - Hashie::Mash for object-like access (perfect for simple models, needs gem 'hashie')
40
+ # :none - raw hash
41
+ # :load - load models from Mongo by IDs
42
+ def self.elasticsearch!(options = {})
43
+ options = {
44
+ prefix_name: true,
45
+ index_name: nil,
46
+ client_options: {},
47
+ index_options: {},
48
+ index_mappings: nil,
49
+ wrapper: :model,
50
+ callbacks: true
51
+ }.merge(options)
52
+
53
+ if options[:wrapper] == :model
54
+ attr_accessor :_type, :_score, :_source
55
+ end
56
+
57
+ cattr_accessor :es_client_options, :es_index_name, :es_index_options, :es_wrapper
58
+
59
+ self.es_client_options = Mongoid::Elasticsearch.client_options.dup.merge(options[:client_options])
60
+ self.es_index_name = (options[:prefix_name] ? Mongoid::Elasticsearch.prefix : '') + (options[:index_name] || model_name.plural)
61
+ self.es_index_options = options[:index_options]
62
+ self.es_wrapper = options[:wrapper]
63
+
64
+ Mongoid::Elasticsearch.registered_indexes.push self.es_index_name
65
+
66
+ unless options[:index_mappings].nil?
67
+ self.es_index_options = self.es_index_options.deep_merge({
68
+ :mappings => {
69
+ es.index.type.to_sym => {
70
+ :properties => options[:index_mappings]
71
+ }
72
+ }
73
+ })
74
+ end
75
+
76
+ include Indexing
77
+ include Callbacks if options[:callbacks]
78
+
79
+ es.index.create
80
+ end
81
+ end
82
+
83
+ # search multiple models
84
+ def self.search(query, options = {}, wrapper = :model)
85
+ if query.is_a?(String)
86
+ query = {q: Utils.clean(query)}
87
+ end
88
+ # use `_all` or empty string to perform the operation on all indices
89
+ # regardless whether they are managed by Mongoid::Elasticsearch or not
90
+ unless query.key?(:index)
91
+ query.merge!(index: Mongoid::Elasticsearch.registered_indexes.join(','), ignore_indices: 'missing')
92
+ end
93
+
94
+ page = options[:page]
95
+ options[:per_page] ||= 50
96
+ per_page = options[:per_page]
97
+
98
+ query[:size] = ( per_page.to_i ) if per_page
99
+ query[:from] = ( page.to_i <= 1 ? 0 : (per_page.to_i * (page.to_i-1)) ) if page && per_page
100
+
101
+
102
+ client = ::Elasticsearch::Client.new Mongoid::Elasticsearch.client_options
103
+ Response.new(client, query, true, nil, wrapper, options)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,2 @@
1
+ require 'elasticsearch'
2
+ require 'mongoid/elasticsearch'
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid/elasticsearch/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongoid-elasticsearch"
8
+ spec.version = Mongoid::Elasticsearch::VERSION
9
+ spec.authors = ["glebtv"]
10
+ spec.email = ["glebtv@gmail.com"]
11
+ spec.description = %q{Simple and easy integration of mongoid with the new elasticsearch gem}
12
+ spec.summary = %q{Simple and easy integration of mongoid with the new elasticsearch gem}
13
+ spec.homepage = "https://github.com/rs-pro/mongoid-elasticsearch"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "mongoid", [">= 4.0", "< 5.0"]
22
+ spec.add_dependency "elasticsearch", "~> 0.4.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.3"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "kaminari"
28
+ spec.add_development_dependency "database_cleaner"
29
+ spec.add_development_dependency "coveralls"
30
+ end
@@ -0,0 +1,27 @@
1
+ class Article
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps::Short
4
+ include ActiveModel::ForbiddenAttributesProtection
5
+
6
+ field :name
7
+ field :tags
8
+
9
+ include Mongoid::Elasticsearch
10
+ i_fields = {
11
+ name: {type: 'string', analyzer: 'snowball'},
12
+ raw: {type: 'string', index: :not_analyzed}
13
+ }
14
+
15
+ if Gem::Version.new(::Elasticsearch::Client.new.info['version']['number']) > Gem::Version.new('0.90.2')
16
+ i_fields[:suggest] = {type: 'completion'}
17
+ end
18
+
19
+ elasticsearch! index_name: 'mongoid_es_news', prefix_name: false, index_mappings: {
20
+ name: {
21
+ type: 'multi_field',
22
+ fields: i_fields
23
+ },
24
+ tags: {type: 'string', include_in_all: false}
25
+ }, wrapper: :load
26
+ end
27
+
@@ -0,0 +1,22 @@
1
+ module Namespaced
2
+ class Model
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps::Short
5
+ include ActiveModel::ForbiddenAttributesProtection
6
+
7
+ field :name
8
+
9
+ include Mongoid::Elasticsearch
10
+ elasticsearch! index_options: {
11
+ 'namespaced/model' => {
12
+ mappings: {
13
+ properties: {
14
+ name: {
15
+ type: 'string'
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ class Nowrapper
2
+ include Mongoid::Document
3
+
4
+ field :name, type: String
5
+
6
+ include Mongoid::Elasticsearch
7
+ elasticsearch! wrapper: :none
8
+ end
9
+
@@ -0,0 +1,13 @@
1
+ class Post
2
+ include Mongoid::Document
3
+
4
+ field :name, type: String
5
+ field :content, type: String
6
+
7
+ include Mongoid::Elasticsearch
8
+ elasticsearch!
9
+ def as_indexed_json
10
+ {name: name, content: content}
11
+ end
12
+ end
13
+
@@ -0,0 +1,293 @@
1
+ require "spec_helper"
2
+
3
+ describe Article do
4
+ it 'properly uses options' do
5
+ Article.es_index_name.should eq 'mongoid_es_news'
6
+ Article.es.index.name.should eq 'mongoid_es_news'
7
+ Article.es.index.type.should eq 'articles'
8
+ Article.es_wrapper.should eq :load
9
+ Article.es_client_options.should eq({})
10
+ end
11
+
12
+ context 'index operations' do
13
+ it 'creates and destroys index' do
14
+ Article.es.index.exists?.should be_true
15
+ Article.es.index.delete
16
+ Article.es.index.exists?.should be_false
17
+ Article.es.index.create
18
+ Article.es.index.exists?.should be_true
19
+ end
20
+ end
21
+
22
+ context 'adding to index' do
23
+ it 'successfuly saves mongoid document' do
24
+ article = Article.new(name: 'test article name')
25
+ article.save.should be_true
26
+ end
27
+ end
28
+
29
+ context 'deleting from index' do
30
+ it 'deletes document from index when model is destroyed' do
31
+ Article.create(name: 'test article name')
32
+ Article.es.index.refresh
33
+ Article.es.all.count.should eq 1
34
+
35
+ Article.first.destroy
36
+ Article.es.index.refresh
37
+ Article.es.all.count.should eq 0
38
+ end
39
+ end
40
+
41
+ context 'searching' do
42
+ before :each do
43
+ @article_1 = Article.create!(name: 'test article name likes', tags: 'likely')
44
+ @article_2 = Article.create!(name: 'tests likely an another article title')
45
+ @article_3 = Article.create!(name: 'a strange name for this stuff')
46
+ Article.es.index.refresh
47
+ end
48
+
49
+ it 'searches and returns models' do
50
+ results = Article.es.search q: 'likely'
51
+ results.count.should eq 1
52
+ results.to_a.count.should eq 1
53
+ results.first.id.should eq @article_2.id
54
+ results.first.name.should eq @article_2.name
55
+ end
56
+
57
+ if Article.es.completion_supported?
58
+ it 'completion' do
59
+ Article.es.completion('te', 'name.suggest').should eq [
60
+ {"text"=>"test article name likes", "score"=>1.0},
61
+ {"text"=>"tests likely an another article title", "score"=>1.0}
62
+ ]
63
+ end
64
+ else
65
+ pending "completion suggester not supported in ES version #{Article.es.version}"
66
+ end
67
+ end
68
+
69
+ context 'pagination' do
70
+ before :each do
71
+ @articles = []
72
+ 10.times { @articles << Article.create!(name: 'test') }
73
+ Article.es.index.refresh
74
+ end
75
+
76
+ it '#search' do
77
+ Article.es.search('test', per_page: 7, page: 2).to_a.size.should eq 3
78
+ end
79
+
80
+ it '#all' do
81
+ result = Article.es.all(per_page: 7, page: 2)
82
+ result.num_pages.should eq 2
83
+ result.current_page.should eq 2
84
+ result.total_entries.should eq 10
85
+ result.previous_page.should eq 1
86
+ result.next_page.should be_nil
87
+ result.to_a.size.should eq 3
88
+ result.out_of_bounds?.should be_false
89
+ result.first_page?.should be_false
90
+ result.last_page?.should be_true
91
+
92
+ p1 = Article.es.all(per_page: 7, page: 1)
93
+ p1.out_of_bounds?.should be_false
94
+ p1.first_page?.should be_true
95
+ p1.last_page?.should be_false
96
+ p1.current_page.should eq 1
97
+ p1.next_page.should eq 2
98
+
99
+ p3 = Article.es.all(per_page: 7, page: 3)
100
+ p3.out_of_bounds?.should be_true
101
+
102
+ p1.length.should eq 7
103
+ all = (result.to_a + p1.to_a).map(&:id).map(&:to_s).sort
104
+ all.length.should eq 10
105
+ all.should eq @articles.map(&:id).map(&:to_s).sort
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ describe Post do
112
+ it 'properly uses options' do
113
+ Post.es_index_name.should eq 'mongoid_es_test_posts'
114
+ Post.es_wrapper.should eq :model
115
+ Post.es_client_options.should eq({})
116
+ end
117
+
118
+ context 'index operations' do
119
+ it 'does not create index with empty definition (ES will do it for us)' do
120
+ Post.es.index.exists?.should be_false
121
+ Post.es.index.create
122
+ Post.es.index.exists?.should be_false
123
+ end
124
+ it 'ES autocreates index on first index' do
125
+ Post.es.index.exists?.should be_false
126
+ Post.create!(name: 'test post')
127
+ Post.es.index.exists?.should be_true
128
+ end
129
+ end
130
+
131
+ context 'adding to index' do
132
+ it 'successfuly saves mongoid document' do
133
+ article = Post.new(name: 'test article name')
134
+ article.save.should be_true
135
+ end
136
+ end
137
+
138
+ context 'searching' do
139
+ before :each do
140
+ @post_1 = Post.create!(name: 'test article name')
141
+ @post_2 = Post.create!(name: 'another article title')
142
+ Post.es.index.refresh
143
+ end
144
+
145
+ it 'searches and returns models' do
146
+ Post.es.search('article').first.class.should eq Post
147
+ sleep 1
148
+ Post.es.search('article').count.should eq 2
149
+ Post.es.search('another').count.should eq 1
150
+ Post.es.search('another').first.id.should eq @post_2.id
151
+ end
152
+ end
153
+
154
+ context 'pagination' do
155
+ before :each do
156
+ 10.times { Post.create(name: 'test') }
157
+ Post.es.index.refresh
158
+ end
159
+
160
+ it '#search' do
161
+ Post.es.search('test').size.should eq 10
162
+ Post.es.search('test', per_page: 7, page: 2).to_a.size.should eq 3
163
+ end
164
+
165
+ it '#all' do
166
+ Post.es.all.size.should eq 10
167
+ Post.es.all(per_page: 7, page: 2).to_a.size.should eq 3
168
+ end
169
+ end
170
+ end
171
+
172
+ describe Nowrapper do
173
+ it 'properly uses options' do
174
+ Nowrapper.es_index_name.should eq 'mongoid_es_test_nowrappers'
175
+ Nowrapper.es_wrapper.should eq :none
176
+ end
177
+
178
+ context 'searching' do
179
+ before :each do
180
+ @post_1 = Nowrapper.create!(name: 'test article name')
181
+ @post_2 = Nowrapper.create!(name: 'another article title')
182
+ Nowrapper.es.index.refresh
183
+ end
184
+
185
+ it 'searches and returns hashes' do
186
+ # #count uses _count
187
+ Nowrapper.es.search('article').count.should eq 2
188
+ # #size and #length fetch results
189
+ Nowrapper.es.search('article').length.should eq 2
190
+ Nowrapper.es.search('article').first.class.should eq Hash
191
+ end
192
+ end
193
+ end
194
+
195
+ describe "Multisearch" do
196
+ before :each do
197
+ @post_1 = Post.create!(name: 'test article name')
198
+ Post.es.index.refresh
199
+
200
+ @article_1 = Article.create!(name: 'test article name likes', tags: 'likely')
201
+ @article_2 = Article.create!(name: 'test likely an another article title')
202
+ Article.es.index.refresh
203
+
204
+ @ns_1 = Namespaced::Model.create!(name: 'test article name likes')
205
+ Namespaced::Model.es.index.refresh
206
+ end
207
+ it 'works' do
208
+ response = Mongoid::Elasticsearch.search 'test'
209
+ response.length.should eq 4
210
+ response.to_a.map(&:class).map(&:name).uniq.sort.should eq ['Article', 'Namespaced::Model', 'Post']
211
+ response.select { |r| r.class == Article && r.id == @article_1.id }.first.should_not be_nil
212
+ end
213
+ end
214
+
215
+ describe Namespaced::Model do
216
+ it 'properly uses options' do
217
+ Namespaced::Model.es_index_name.should eq 'mongoid_es_test_namespaced_models'
218
+ Namespaced::Model.es.index.name.should eq 'mongoid_es_test_namespaced_models'
219
+ Namespaced::Model.es.index.type.should eq 'namespaced/models'
220
+ Namespaced::Model.es_wrapper.should eq :model
221
+ Namespaced::Model.es_client_options.should eq({})
222
+ end
223
+
224
+ context 'index operations' do
225
+ it 'creates and destroys index' do
226
+ Namespaced::Model.es.index.exists?.should be_true
227
+ Namespaced::Model.es.index.delete
228
+ Namespaced::Model.es.index.exists?.should be_false
229
+ Namespaced::Model.es.index.create
230
+ Namespaced::Model.es.index.exists?.should be_true
231
+ end
232
+ end
233
+
234
+ context 'adding to index' do
235
+ it 'successfuly saves mongoid document' do
236
+ article = Namespaced::Model.new(name: 'test article name')
237
+ article.save.should be_true
238
+ end
239
+ it 'successfuly destroys mongoid document' do
240
+ article = Namespaced::Model.create(name: 'test article name')
241
+ Namespaced::Model.es.index.refresh
242
+ Namespaced::Model.es.all.count.should eq 1
243
+ article.destroy
244
+ Namespaced::Model.es.index.refresh
245
+ Namespaced::Model.es.all.count.should eq 0
246
+ end
247
+ end
248
+
249
+ context 'searching' do
250
+ before :each do
251
+ @article_1 = Namespaced::Model.create!(name: 'test article name likes')
252
+ @article_2 = Namespaced::Model.create!(name: 'tests likely an another article title')
253
+ @article_3 = Namespaced::Model.create!(name: 'a strange name for this stuff')
254
+ Namespaced::Model.es.index.refresh
255
+ end
256
+
257
+ it 'searches and returns models' do
258
+ results = Namespaced::Model.es.search q: 'likely'
259
+ results.count.should eq 1
260
+ results.to_a.count.should eq 1
261
+ results.first.id.should eq @article_2.id
262
+ results.first.name.should eq @article_2.name
263
+ end
264
+ end
265
+
266
+ context 'pagination' do
267
+ before :each do
268
+ @articles = []
269
+ 20.times { @articles << Namespaced::Model.create!(name: 'test') }
270
+ Namespaced::Model.es.index.refresh
271
+ end
272
+
273
+ it '#search' do
274
+ Namespaced::Model.es.search('test', per_page: 10, page: 2).to_a.size.should eq 10
275
+ Namespaced::Model.es.search('test', per_page: 30, page: 2).to_a.size.should eq 0
276
+ Namespaced::Model.es.search('test', per_page: 2, page: 2).to_a.size.should eq 2
277
+ end
278
+
279
+ it '#all' do
280
+ result = Namespaced::Model.es.all(per_page: 7, page: 3)
281
+ result.num_pages.should eq 3
282
+ result.to_a.size.should eq 6
283
+ p1 = Namespaced::Model.es.all(per_page: 7, page: 1).to_a
284
+ p2 = Namespaced::Model.es.all(per_page: 7, page: 2).to_a
285
+ p1.length.should eq 7
286
+ p2.length.should eq 7
287
+ all = (result.to_a + p1 + p2).map(&:id).map(&:to_s).sort
288
+ all.length.should eq 20
289
+ all.should eq @articles.map(&:id).map(&:to_s).sort
290
+ end
291
+ end
292
+ end
293
+
@@ -0,0 +1,56 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
6
+
7
+ MODELS = File.join(File.dirname(__FILE__), "models")
8
+
9
+ require "rubygems"
10
+ require "rspec"
11
+ require "mongoid"
12
+ require "database_cleaner"
13
+
14
+ require "mongoid-elasticsearch"
15
+
16
+ Mongoid::Elasticsearch.prefix = "mongoid_es_test_"
17
+
18
+ Dir["#{MODELS}/*.rb"].each { |f| require f }
19
+
20
+ Mongoid.configure do |config|
21
+ config.connect_to "mongoid_elasticsearch_test"
22
+ end
23
+ Mongoid.logger = Logger.new($stdout)
24
+
25
+
26
+ DatabaseCleaner.orm = "mongoid"
27
+
28
+ RSpec.configure do |config|
29
+ config.before(:all) do
30
+ DatabaseCleaner.strategy = :truncation
31
+ Article.es.index.reset
32
+ Post.es.index.reset
33
+ Nowrapper.es.index.reset
34
+ Namespaced::Model.es.index.reset
35
+ end
36
+
37
+ config.before(:each) do
38
+ DatabaseCleaner.start
39
+ end
40
+
41
+ config.after(:each) do
42
+ DatabaseCleaner.clean
43
+ Article.es.index.reset
44
+ Post.es.index.reset
45
+ Nowrapper.es.index.reset
46
+ Namespaced::Model.es.index.reset
47
+ end
48
+
49
+ config.after(:all) do
50
+ DatabaseCleaner.clean
51
+ Article.es.index.delete
52
+ Post.es.index.delete
53
+ Nowrapper.es.index.delete
54
+ Namespaced::Model.es.index.delete
55
+ end
56
+ end