wj-mongoid-elasticsearch 0.0.1

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