mongoid-elasticsearch 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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