wj-mongoid-elasticsearch 0.0.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,62 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ class Index
4
+ def initialize(es)
5
+ @es = es
6
+ end
7
+
8
+ def klass
9
+ @es.klass
10
+ end
11
+
12
+ def name
13
+ klass.es_index_name
14
+ end
15
+
16
+ def type
17
+ klass.model_name.collection.singularize
18
+ end
19
+
20
+ def options
21
+ klass.es_index_options
22
+ end
23
+
24
+ def indices
25
+ @es.client.indices
26
+ end
27
+
28
+ def exists?
29
+ indices.exists index: name
30
+ end
31
+
32
+ def create
33
+ unless options == {} || exists?
34
+ force_create
35
+ end
36
+ end
37
+
38
+ def force_create
39
+ indices.create index: name, body: options
40
+ end
41
+
42
+ def delete
43
+ if exists?
44
+ force_delete
45
+ end
46
+ end
47
+
48
+ def force_delete
49
+ indices.delete index: name
50
+ end
51
+
52
+ def refresh
53
+ indices.refresh index: name
54
+ end
55
+
56
+ def reset
57
+ delete
58
+ create
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,25 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ module Indexing
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ def as_indexed_json
7
+ serializable_hash.reject { |k, v| %w(_id c_at u_at created_at updated_at).include?(k) }
8
+ end
9
+
10
+ def es_index?
11
+ true
12
+ end
13
+
14
+ def es_update
15
+ if destroyed? || !es_index?
16
+ self.class.es.remove_item(self)
17
+ else
18
+ self.class.es.index_item(self)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,56 @@
1
+ # Quick fix for https://github.com/elasticsearch/elasticsearch-ruby/issues/43
2
+
3
+ require 'elasticsearch/api/actions/search'
4
+ module Elasticsearch
5
+ module API
6
+ module Actions
7
+ def search(arguments={})
8
+ arguments[:index] = '_all' if ! arguments[:index] && arguments[:type]
9
+
10
+ valid_params = [
11
+ :analyzer,
12
+ :analyze_wildcard,
13
+ :default_operator,
14
+ :df,
15
+ :explain,
16
+ :fields,
17
+ :from,
18
+ :ignore_indices,
19
+ :ignore_unavailable,
20
+ :allow_no_indices,
21
+ :expand_wildcards,
22
+ :indices_boost,
23
+ :lenient,
24
+ :lowercase_expanded_terms,
25
+ :preference,
26
+ :q,
27
+ :routing,
28
+ :scroll,
29
+ :search_type,
30
+ :size,
31
+ :sort,
32
+ :source,
33
+ :_source,
34
+ :_source_include,
35
+ :_source_exclude,
36
+ :stats,
37
+ :suggest_field,
38
+ :suggest_mode,
39
+ :suggest_size,
40
+ :suggest_text,
41
+ :timeout,
42
+ :version ]
43
+
44
+ method = 'GET'
45
+ path = Utils.__pathify( Utils.__listify(arguments[:index]), Utils.__listify(arguments[:type]), '_search' )
46
+
47
+ params = Utils.__validate_and_extract_params arguments, valid_params
48
+ body = arguments[:body]
49
+
50
+ params[:fields] = Utils.__listify(params[:fields]) if params[:fields]
51
+
52
+ perform_request(method, path, params, body).body
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ # credits: https://github.com/karmi/retire/blob/master/lib/tire/results/pagination.rb
2
+
3
+
4
+ module Mongoid
5
+ module Elasticsearch
6
+ # Adds support for WillPaginate and Kaminari
7
+ module Pagination
8
+ def total_entries
9
+ total
10
+ end
11
+
12
+ def per_page
13
+ (@options[:per_page] || @options[:per] || @options[:size] || 10 ).to_i
14
+ end
15
+
16
+ def total_pages
17
+ ( total.to_f / per_page ).ceil
18
+ end
19
+
20
+ def current_page
21
+ if @options[:page]
22
+ @options[:page].to_i
23
+ else
24
+ (per_page + @options[:from].to_i) / per_page
25
+ end
26
+ end
27
+
28
+ def previous_page
29
+ current_page > 1 ? (current_page - 1) : nil
30
+ end
31
+
32
+ def next_page
33
+ current_page < total_pages ? (current_page + 1) : nil
34
+ end
35
+
36
+ def offset
37
+ per_page * (current_page - 1)
38
+ end
39
+
40
+ def out_of_bounds?
41
+ current_page > total_pages
42
+ end
43
+
44
+ # Kaminari support
45
+ #
46
+ alias :limit_value :per_page
47
+ alias :total_count :total_entries
48
+ alias :num_pages :total_pages
49
+ alias :offset_value :offset
50
+ alias :out_of_range? :out_of_bounds?
51
+
52
+ def first_page?
53
+ current_page == 1
54
+ end
55
+
56
+ def last_page?
57
+ total_pages == 0 || current_page == total_pages
58
+ end
59
+
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,11 @@
1
+ module Mongoid::Elasticsearch
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ require File.expand_path('../tasks', __FILE__)
5
+ end
6
+
7
+ config.after_initialize do
8
+ Mongoid::Elasticsearch.create_all_indexes! if Mongoid::Elasticsearch.autocreate_indexes
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,193 @@
1
+ # partially based on https://github.com/karmi/retire/blob/master/lib/tire/results/collection.rb
2
+
3
+ require 'mongoid/elasticsearch/pagination'
4
+
5
+ module Mongoid
6
+ module Elasticsearch
7
+ class Response
8
+ include Enumerable
9
+ include Pagination
10
+
11
+ attr_reader :time, :total, :options, :facets, :max
12
+ attr_reader :response
13
+
14
+ def initialize(client, query, multi, model, options)
15
+ @client = client
16
+ @query = query
17
+ @multi = multi
18
+ @model = model
19
+ @wrapper = options[:wrapper]
20
+ @options = options
21
+ end
22
+
23
+ def perform!
24
+ response = @client.search(@query)
25
+ @options = options
26
+ @time = response['took'].to_i
27
+ @total = response['hits']['total'].to_i rescue nil
28
+ @facets = response['facets']
29
+ @max_score = response['hits']['max_score'].to_f rescue nil
30
+ response
31
+ end
32
+
33
+ def total
34
+ if @total.nil?
35
+ perform!
36
+ @total
37
+ else
38
+ @total
39
+ end
40
+ end
41
+
42
+ def raw_response
43
+ @raw_response ||= perform!
44
+ end
45
+
46
+ def hits
47
+ @hits ||= raw_response['hits']['hits']
48
+ end
49
+
50
+ def results
51
+ return [] if failure?
52
+ @results ||= begin
53
+ case @wrapper
54
+ when :load
55
+ if @multi
56
+ multi_with_load
57
+ else
58
+ records = @model.find(hits.map { |h| h['_id'] })
59
+ hits.map do |item|
60
+ records.detect do |record|
61
+ record.id.to_s == item['_id'].to_s
62
+ end
63
+ end
64
+ end
65
+ when :mash
66
+ hits.map do |h|
67
+ s = h.delete('_source')
68
+ m = Hashie::Mash.new(h.merge(s))
69
+ if defined?(Moped::BSON)
70
+ m.id = Moped::BSON::ObjectId.from_string(h['_id'])
71
+ else
72
+ m.id = BSON::ObjectId.from_string(h['_id'])
73
+ end
74
+ m._id = m.id
75
+ m
76
+ end
77
+ when :model
78
+ multi_without_load
79
+ else
80
+ hits
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+ def error
87
+ raw_response['error']
88
+ end
89
+
90
+ def success?
91
+ error.to_s.empty?
92
+ end
93
+
94
+ def failure?
95
+ !success?
96
+ end
97
+
98
+ def each(&block)
99
+ results.each(&block)
100
+ end
101
+
102
+ def to_ary
103
+ results
104
+ end
105
+
106
+ def inspect
107
+ "#<Mongoid::Elasticsearch::Response @size:#{@results.nil? ? 'not run yet' : size} @results:#{@results.inspect} @raw_response=#{@raw_response}>"
108
+ end
109
+
110
+ def count
111
+ # returns approximate counts, for now just using search_type: 'count',
112
+ # which is exact
113
+ # @total ||= @client.count(@query)['count']
114
+
115
+ @total ||= @client.search(@query.merge(search_type: 'count'))['hits']['total']
116
+ end
117
+
118
+ def size
119
+ results.size
120
+ end
121
+ alias_method :length, :size
122
+
123
+ private
124
+
125
+ def find_klass(type)
126
+ raise NoMethodError, "You have tried to eager load the model instances, " +
127
+ "but Mongoid::Elasticsearch cannot find the model class because " +
128
+ "document has no _type property." unless type
129
+
130
+ begin
131
+ klass = type.camelize.singularize.constantize
132
+ rescue NameError => e
133
+ raise NameError, "You have tried to eager load the model instances, but " +
134
+ "Mongoid::Elasticsearch cannot find the model class '#{type.camelize}' " +
135
+ "based on _type '#{type}'.", e.backtrace
136
+ end
137
+ end
138
+
139
+ def multi_with_load
140
+ return [] if hits.empty?
141
+
142
+ records = {}
143
+ hits.group_by { |item| item['_type'] }.each do |type, items|
144
+ klass = find_klass(type)
145
+ records[type] = klass.find(items.map { |h| h['_id'] })
146
+ end
147
+
148
+ # Reorder records to preserve the order from search results
149
+ hits.map do |item|
150
+ records[item['_type']].detect do |record|
151
+ record.id.to_s == item['_id'].to_s
152
+ end
153
+ end
154
+ end
155
+
156
+ def multi_without_load
157
+ hits.map do |h|
158
+ klass = find_klass(h['_type'])
159
+ h[:_highlight] = h.delete('highlight') if h.key?('highlight')
160
+ source = h.delete('_source')
161
+ if defined?(Moped::BSON)
162
+ source.each do |k,v|
163
+ if v.is_a?(Hash) && v.has_key?("$oid")
164
+ source[k] = Moped::BSON::ObjectId.from_string(v["$oid"])
165
+ end
166
+ end
167
+ else
168
+ source.each do |k,v|
169
+ if v.is_a?(Hash) && v.has_key?("$oid")
170
+ source[k] = BSON::ObjectId.from_string(v["$oid"])
171
+ end
172
+ end
173
+ end
174
+ begin
175
+ m = klass.new(h.merge(source))
176
+ if defined?(Moped::BSON)
177
+ m.id = Moped::BSON::ObjectId.from_string(h['_id'])
178
+ else
179
+ m.id = BSON::ObjectId.from_string(h['_id'])
180
+ end
181
+ rescue Mongoid::Errors::UnknownAttribute
182
+ klass.class_eval <<-RUBY, __FILE__, __LINE__+1
183
+ attr_accessor :_type, :_score, :_source, :_highlight
184
+ RUBY
185
+ m = klass.new(h.merge(source))
186
+ end
187
+ m.new_record = false
188
+ m
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,28 @@
1
+ # require this file to load the tasks
2
+ require 'rake'
3
+
4
+ # Require sitemap_generator at runtime. If we don't do this the ActionView helpers are included
5
+ # before the Rails environment can be loaded by other Rake tasks, which causes problems
6
+ # for those tasks when rendering using ActionView.
7
+ namespace :es do
8
+ # Require sitemap_generator only. When installed as a plugin the require will fail, so in
9
+ # that case, load the environment first.
10
+ task :require do
11
+ Rake::Task['environment'].invoke
12
+ require 'ruby-progressbar'
13
+ end
14
+ desc "create all indexes"
15
+ task :create => ['es:require'] do
16
+ Mongoid::Elasticsearch.create_all_indexes!
17
+ end
18
+ desc "reindex all ES models"
19
+ task :reindex => ['es:require'] do
20
+ Mongoid::Elasticsearch.registered_models.each do |model_name|
21
+ pb = nil
22
+ model_name.constantize.es.index_all do |steps, step|
23
+ pb = ProgressBar.create(title: model_name, total: steps, format: '%t: %p%% %a |%b>%i| %E') if pb.nil?
24
+ pb.increment
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ # coding: utf-8
2
+ # source: https://github.com/karmi/retire/blob/master/lib/tire/utils.rb
3
+ require 'uri'
4
+
5
+ module Mongoid
6
+ module Elasticsearch
7
+ module Utils
8
+ def clean(s)
9
+ s.to_s.gsub(/\P{Word}+/, ' ').gsub(/ +/, ' ').strip
10
+ end
11
+
12
+ extend self
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,129 @@
1
+ require 'elasticsearch'
2
+ require 'mongoid/elasticsearch/version'
3
+
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
+
13
+ require 'mongoid/elasticsearch/monkeypatches'
14
+
15
+ module Mongoid
16
+ module Elasticsearch
17
+ mattr_accessor :autocreate_indexes
18
+ self.autocreate_indexes = true
19
+
20
+ mattr_accessor :prefix
21
+ self.prefix = ''
22
+
23
+ mattr_accessor :client_options
24
+ self.client_options = {}
25
+
26
+ mattr_accessor :registered_indexes
27
+ self.registered_indexes = []
28
+
29
+ mattr_accessor :registered_models
30
+ self.registered_models = []
31
+
32
+ extend ActiveSupport::Concern
33
+ included do
34
+ def self.es
35
+ @__es__ ||= Mongoid::Elasticsearch::Es.new(self)
36
+ end
37
+
38
+ # Add elasticsearch to the model
39
+ # @option index_name [String] name of the index for this model
40
+ # @option index_options [Hash] Index options to be passed to Elasticsearch
41
+ # when creating an index
42
+ # @option client_options [Hash] Options for Elasticsearch::Client.new
43
+ # @option wrapper [Symbol] Select what wrapper to use for results
44
+ # possible options:
45
+ # :model - creates a new model instance, set its attributes, and marks it as persisted
46
+ # :mash - Hashie::Mash for object-like access (perfect for simple models, needs gem 'hashie')
47
+ # :none - raw hash
48
+ # :load - load models from Mongo by IDs
49
+ def self.elasticsearch!(options = {})
50
+ options = {
51
+ prefix_name: true,
52
+ index_name: nil,
53
+ client_options: {},
54
+ index_options: {},
55
+ index_mappings: nil,
56
+ wrapper: :model,
57
+ callbacks: true,
58
+ skip_create: false
59
+ }.merge(options)
60
+
61
+ if options[:wrapper] == :model
62
+ attr_accessor :_type, :_score, :_source
63
+ end
64
+
65
+ cattr_accessor :es_client_options, :es_index_name, :es_index_options, :es_wrapper, :es_skip_create
66
+
67
+ self.es_client_options = Mongoid::Elasticsearch.client_options.dup.merge(options[:client_options])
68
+ self.es_index_name = (options[:prefix_name] ? Mongoid::Elasticsearch.prefix : '') + (options[:index_name] || model_name.plural)
69
+ self.es_index_options = options[:index_options]
70
+ self.es_wrapper = options[:wrapper]
71
+ self.es_skip_create = options[:skip_create]
72
+
73
+ Mongoid::Elasticsearch.registered_indexes.push self.es_index_name
74
+ Mongoid::Elasticsearch.registered_indexes.uniq!
75
+
76
+ Mongoid::Elasticsearch.registered_models.push self.name
77
+ Mongoid::Elasticsearch.registered_models.uniq!
78
+
79
+ unless options[:index_mappings].nil?
80
+ self.es_index_options = self.es_index_options.deep_merge({
81
+ :mappings => {
82
+ es.index.type.to_sym => {
83
+ :properties => options[:index_mappings]
84
+ }
85
+ }
86
+ })
87
+ end
88
+
89
+ include Indexing
90
+ include Callbacks if options[:callbacks]
91
+ end
92
+ end
93
+
94
+ def self.create_all_indexes!
95
+ # puts "creating ES indexes"
96
+ Mongoid::Elasticsearch.registered_models.each do |model_name|
97
+ model = model_name.constantize
98
+ model.es.index.create unless model.es_skip_create
99
+ end
100
+ end
101
+
102
+ # search multiple models
103
+ def self.search(query, options = {})
104
+ if query.is_a?(String)
105
+ query = {q: Utils.clean(query)}
106
+ end
107
+ # use `_all` or empty string to perform the operation on all indices
108
+ # regardless whether they are managed by Mongoid::Elasticsearch or not
109
+ unless query.key?(:index)
110
+ query.merge!(index: Mongoid::Elasticsearch.registered_indexes.join(','), ignore_indices: 'missing', ignore_unavailable: true)
111
+ end
112
+
113
+ page = options[:page]
114
+ per_page = options[:per_page]
115
+
116
+ query[:size] = ( per_page.to_i ) if per_page
117
+ query[:from] = ( page.to_i <= 1 ? 0 : (per_page.to_i * (page.to_i-1)) ) if page && per_page
118
+
119
+ options[:wrapper] ||= :model
120
+
121
+ client = ::Elasticsearch::Client.new Mongoid::Elasticsearch.client_options.dup
122
+ Response.new(client, query, true, nil, options)
123
+ end
124
+ end
125
+ end
126
+
127
+ if defined? Rails
128
+ require 'mongoid/elasticsearch/railtie'
129
+ end
@@ -0,0 +1 @@
1
+ require 'mongoid/elasticsearch'
@@ -0,0 +1,34 @@
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 = "wj-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", [">= 3.0", "< 6.0"]
22
+ spec.add_dependency "elasticsearch", "~> 1.0.13"
23
+ spec.add_dependency "ruby-progressbar"
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "kaminari"
29
+ spec.add_development_dependency "database_cleaner"
30
+ spec.add_development_dependency "coveralls"
31
+ spec.add_development_dependency "hashie"
32
+ spec.add_development_dependency "mongoid-slug", '~> 5.0.0'
33
+ spec.add_development_dependency "glebtv-httpclient"
34
+ end
@@ -0,0 +1,32 @@
1
+ class Article
2
+ include Mongoid::Document
3
+ include Mongoid::Timestamps::Short
4
+ #include ActiveModel::ForbiddenAttributesProtection
5
+
6
+ field :name
7
+
8
+ include Mongoid::Slug
9
+ slug :name
10
+
11
+ field :tags
12
+
13
+ include Mongoid::Elasticsearch
14
+ i_fields = {
15
+ name: {type: 'string', analyzer: 'snowball'},
16
+ raw: {type: 'string', index: :not_analyzed}
17
+ }
18
+
19
+ if Gem::Version.new(::Elasticsearch::Client.new.info['version']['number']) > Gem::Version.new('0.90.2')
20
+ i_fields[:suggest] = {type: 'completion'}
21
+ end
22
+
23
+ elasticsearch! index_name: 'mongoid_es_news', prefix_name: false, index_mappings: {
24
+ name: {
25
+ type: 'multi_field',
26
+ fields: i_fields
27
+ },
28
+ _slugs: {type: 'string', index: :not_analyzed},
29
+ tags: {type: 'string', include_in_all: false}
30
+ }, wrapper: :load
31
+ end
32
+
@@ -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 NoAutocreate
2
+ include Mongoid::Document
3
+
4
+ field :name, type: String
5
+
6
+ include Mongoid::Elasticsearch
7
+ elasticsearch! skip_create: true
8
+ end
9
+