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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 227f98096d3f8725368150c8a275001d4b308fe8
4
+ data.tar.gz: 5385ffd8dd90ca8397731a1ab73f0f63cda66a59
5
+ SHA512:
6
+ metadata.gz: 86ae14cde369b32a520d6502af4a33b717f357c22cc7eaaf4c5cf2d585c93371dd38522588f447360f3ee3463b89da9264cebcea41531fe892de953793c7564a
7
+ data.tar.gz: 696b7ba51d63adf247a37b5cd633985b2303a5914c838c0509f003197346d94eabc5933dd7ac64560610637aa5e1ba6ef1f834d775686dcc04e53a8f3d8b3641
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ mongoid-elasticsearch
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
data/.travis.yml ADDED
@@ -0,0 +1,19 @@
1
+ services:
2
+ - mongodb
3
+ - elasticsearch
4
+
5
+ notifications:
6
+ email: false
7
+
8
+ language: ruby
9
+ rvm:
10
+ - 1.9.3
11
+ - 2.0.0
12
+ - jruby-19mode
13
+ # mongoid 4 has problems
14
+ #- rbx-19mode
15
+
16
+ gemfile:
17
+ - Gemfile
18
+ - gemfiles/mongoid-4.0.gemfile
19
+
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongoid_rating.gemspec
4
+ gem "mongoid", github: "mongoid/mongoid"
5
+ gemspec
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 glebtv
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Mongoid::Elasticsearch
2
+
3
+ [![Build Status](https://travis-ci.org/rs-pro/mongoid-elasticsearch.png?branch=master)](https://travis-ci.org/rs-pro/mongoid-elasticsearch)
4
+ [![Coverage Status](https://coveralls.io/repos/rs-pro/mongoid-elasticsearch/badge.png?branch=master)](https://coveralls.io/r/rs-pro/mongoid-elasticsearch?branch=master)
5
+ [![Gem Version](https://badge.fury.io/rb/mongoid-elasticsearch.png)](http://badge.fury.io/rb/mongoid-elasticsearch)
6
+ [![Dependency Status](https://gemnasium.com/rs-pro/mongoid-elasticsearch.png)](https://gemnasium.com/rs-pro/mongoid-elasticsearch)
7
+
8
+
9
+ Use [Elasticsearch](http://www.elasticsearch.org/) with mongoid with just a few
10
+ lines of code
11
+
12
+ Allows easy usage of [the new Elasticsearch gem](https://github.com/elasticsearch/elasticsearch-ruby)
13
+ with [Mongoid 4](https://github.com/mongoid/mongoid)
14
+
15
+ ## Features
16
+
17
+ - Uses new elasticsearch gem
18
+ - Has a simple high-level API
19
+ - No weird undocumented DSL, just raw JSON for queries and index definitions
20
+ - Allows for full power of elasticsearch when it's necessary
21
+ - Indexes are automatically created if they don't exist on app boot
22
+ - Works out of the box with zero configuration
23
+ - Multi-model search with real model instances and pagination
24
+ - Whole test suite is run against a real ES instance, no mocks
25
+
26
+ This gem is very simple and does not try hide any part of the ES REST api, it
27
+ just adds some shortcuts for prefixing index names, automatic updating of the index
28
+ when models are added\changed, search with pagination, wrapping results in
29
+ a model instance, ES 0.90.3 new completion suggester, etc (new features coming
30
+ soon)
31
+
32
+ ## Why use it (alternatives list):
33
+
34
+ - [(re)Tire](https://github.com/karmi/retire) - retired
35
+ - [RubberBand](https://github.com/grantr/rubberband) - EOL
36
+ - [Elasticsearch gem](https://github.com/elasticsearch/elasticsearch-ruby) - too low-level and complex
37
+
38
+ ## Installation
39
+
40
+ Add this line to your application's Gemfile:
41
+
42
+ gem 'mongoid-elasticsearch'
43
+
44
+ And then execute:
45
+
46
+ $ bundle
47
+
48
+ Or install it yourself as:
49
+
50
+ $ gem install mongoid-elasticsearch
51
+
52
+ ## Usage
53
+
54
+ ### Basic:
55
+
56
+ class Post
57
+ include Mongoid::Document
58
+ include Mongoid::Elasticsearch
59
+ elasticsearch!
60
+ end
61
+
62
+ Post.es.search 'test text' # shortcut for Post.es.search({q: 'test text'})
63
+ result = Post.es.search query: {...}, facets: {...} etc
64
+ result.raw_response
65
+ result.results # by default returns an Enumerable with Post instances exactly
66
+ # like they were loaded from MongoDB
67
+ Post.es.index.create # create index (done automatically on app boot)
68
+ Post.es.index.destroy # drop index
69
+ Post.es.index.reset # recreate index
70
+ Post.es.index.refresh # force index update (useful for specs)
71
+ Post.es.client # Elasticsearch::Client instance
72
+ Post.es.completion('te') # requires ES 0.90.3
73
+
74
+ # Search multiple models
75
+ # By default only searches in indexes managed by Mongoid::Elasticsearch
76
+ # to ignore other apps indexes in same ES instance
77
+ response = Mongoid::Elasticsearch.search 'test'
78
+
79
+
80
+ more docs: http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions
81
+
82
+ ES docs: http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/index.html
83
+
84
+ ### Advanced:
85
+
86
+ include Mongoid::Elasticsearch
87
+ # define index_options (raw elasticsearch index definition
88
+ #
89
+ elasticsearch! index_name: 'mongoid_es_news', prefix_name: false, index_options: {}, index_mappings: {
90
+ name: {
91
+ type: 'multi_field',
92
+ fields: {
93
+ name: {type: 'string', analyzer: 'snowball'},
94
+ raw: {type: 'string', index: :not_analyzed},
95
+ suggest: {type: 'completion'}
96
+ }
97
+ },
98
+ tags: {type: 'string', include_in_all: false}
99
+ }, wrapper: :load
100
+ # customize what gets sent to elasticsearch:
101
+ def as_indexed_json
102
+ # id field is properly added automatically
103
+ {
104
+ name: name,
105
+ excerpt: excerpt
106
+ }
107
+ end
108
+
109
+ See more examples in specs.
110
+
111
+ ## Contributing
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: run specs.'
6
+ task :default => :spec
7
+
8
+ desc "Run specs"
9
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mongoid", github: "mongoid/mongoid", branch: "master"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,18 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ module Callbacks
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ after_save do
8
+ es_update
9
+ end
10
+
11
+ after_destroy do
12
+ es_update
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,74 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ class Es
4
+ attr_reader :klass, :version
5
+
6
+ def initialize(klass)
7
+ @klass = klass
8
+ @version = Gem::Version.new(client.info['version']['number'])
9
+ end
10
+
11
+ def client
12
+ # dup is needed because Elasticsearch::Client.new changes options hash inplace
13
+ @client ||= ::Elasticsearch::Client.new klass.es_client_options.dup
14
+ end
15
+
16
+ def index
17
+ @index ||= Index.new(self)
18
+ end
19
+
20
+ def search(query, options = {})
21
+ if query.is_a?(String)
22
+ query = {q: Utils.clean(query)}
23
+ end
24
+
25
+ page = options[:page]
26
+ options[:per_page] ||= 50
27
+ per_page = options[:per_page]
28
+
29
+ query[:size] = ( per_page.to_i ) if per_page
30
+ query[:from] = ( page.to_i <= 1 ? 0 : (per_page.to_i * (page.to_i-1)) ) if page && per_page
31
+
32
+ Response.new(client, query.merge(type_options(true)), false, klass, klass.es_wrapper, options)
33
+ end
34
+
35
+ def options_for(obj, escape = false)
36
+ {id: obj.id.to_s}.merge type_options(escape)
37
+ end
38
+
39
+ def type_options(escape = false)
40
+ {index: index.name, type: escape ? Utils.escape(index.type) : index.type}
41
+ end
42
+
43
+ def index_item(obj)
44
+ client.index({body: obj.as_indexed_json}.merge(options_for(obj, true)))
45
+ end
46
+
47
+ def remove_item(obj)
48
+ client.delete(options_for(obj, true).merge(ignore: 404))
49
+ end
50
+
51
+ def all(options = {})
52
+ search({match_all: {}}, options)
53
+ end
54
+
55
+ def completion_supported?
56
+ @version > Gem::Version.new('0.90.2')
57
+ end
58
+
59
+ def completion(text, field = "suggest")
60
+ raise "Completion not supported in ES #{@version}" unless completion_supported?
61
+ body = {
62
+ q: {
63
+ text: Utils.clean(text),
64
+ completion: {
65
+ field: field
66
+ }
67
+ }
68
+ }
69
+ results = client.suggest(index: index.name, body: body)
70
+ results['q'][0]['options']
71
+ end
72
+ end
73
+ end
74
+ end
@@ -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
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,21 @@
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_update
11
+ if destroyed?
12
+ self.class.es.remove_item(self)
13
+ else
14
+ self.class.es.index_item(self)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -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[: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
+ current_page == total_pages
58
+ end
59
+
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,176 @@
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, wrapper, options)
15
+ @client = client
16
+ @query = query
17
+ @multi = multi
18
+ @model = model
19
+ @wrapper = 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'].map { |d| d.update '_type' => Utils.unescape(d['_type']) }
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
+ @model.find(hits.map { |h| h['_id'] })
59
+ end
60
+ when :mash
61
+ hits.map do |h|
62
+ m = Hashie::Mash.new(h)
63
+ m.id = BSON::ObjectId.from_string(h['_id'])
64
+ m._id = m.id
65
+ m
66
+ end
67
+ when :model
68
+ if @multi
69
+ multi_without_load
70
+ else
71
+ hits.map do |h|
72
+ model_from_hash(h)
73
+ end
74
+ end
75
+ else
76
+ hits
77
+ end
78
+
79
+ end
80
+ end
81
+
82
+ def error
83
+ raw_response['error']
84
+ end
85
+
86
+ def success?
87
+ error.to_s.empty?
88
+ end
89
+
90
+ def failure?
91
+ !success?
92
+ end
93
+
94
+ def each(&block)
95
+ results.each(&block)
96
+ end
97
+
98
+ def to_ary
99
+ results
100
+ end
101
+
102
+ def inspect
103
+ "#<Mongoid::Elasticsearch::Response @size:#{size} @results:#{results.inspect} @error=#{success? ? "none" : error} @raw_response=#{raw_response}>"
104
+ end
105
+
106
+ def count
107
+ # returns approximate counts, for now just using search_type: 'count',
108
+ # which is exact
109
+ # @total ||= @client.count(@query)['count']
110
+
111
+ @total ||= @client.search(@query.merge(search_type: 'count'))['hits']['total']
112
+ end
113
+
114
+ def size
115
+ results.size
116
+ end
117
+ alias_method :length, :size
118
+
119
+ private
120
+ def model_from_hash(h)
121
+ source = h.delete('_source')
122
+ m = @model.new(h.merge(source))
123
+ m.new_record = false
124
+ m
125
+ end
126
+
127
+ def find_klass(type)
128
+ raise NoMethodError, "You have tried to eager load the model instances, " +
129
+ "but Mongoid::Elasticsearch cannot find the model class because " +
130
+ "document has no _type property." unless type
131
+
132
+ begin
133
+ klass = type.camelize.singularize.constantize
134
+ rescue NameError => e
135
+ raise NameError, "You have tried to eager load the model instances, but " +
136
+ "Mongoid::Elasticsearch cannot find the model class '#{type.camelize}' " +
137
+ "based on _type '#{type}'.", e.backtrace
138
+ end
139
+ end
140
+
141
+ def multi_with_load
142
+ return [] if hits.empty?
143
+
144
+ records = {}
145
+ hits.group_by { |item| item['_type'] }.each do |type, items|
146
+ klass = find_klass(type)
147
+ records[type] = klass.find(items.map { |h| h['_id'] })
148
+ end
149
+
150
+ # Reorder records to preserve the order from search results
151
+ hits.map do |item|
152
+ records[item['_type']].detect do |record|
153
+ record.id.to_s == item['_id'].to_s
154
+ end
155
+ end
156
+ end
157
+
158
+ def multi_without_load
159
+ hits.map do |h|
160
+ klass = find_klass(h['_type'])
161
+ source = h.delete('_source')
162
+ begin
163
+ m = klass.new(h.merge(source))
164
+ rescue Mongoid::Errors::UnknownAttribute
165
+ klass.class_eval <<-RUBY, __FILE__, __LINE__+1
166
+ attr_accessor :_type, :_score, :_source
167
+ RUBY
168
+ m = klass.new(h.merge(source))
169
+ end
170
+ m.new_record = false
171
+ m
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,14 @@
1
+ # Monkeypatch for slashes to work
2
+
3
+ module Elasticsearch
4
+ module API
5
+ # Generic utility methods
6
+ #
7
+ module Utils
8
+ alias_method :__pathify_without_slashes, :__pathify
9
+ def __pathify(*segments)
10
+ __pathify_without_slashes(*segments).gsub('%252F', '%2F')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # source: https://github.com/karmi/retire/blob/master/lib/tire/utils.rb
2
+ require 'uri'
3
+
4
+ module Mongoid
5
+ module Elasticsearch
6
+ module Utils
7
+
8
+ def escape(s)
9
+ URI.encode_www_form_component(s.to_s)
10
+ end
11
+
12
+ def unescape(s)
13
+ s = s.to_s.respond_to?(:force_encoding) ? s.to_s.force_encoding(Encoding::UTF_8) : s.to_s
14
+ URI.decode_www_form_component(s)
15
+ end
16
+
17
+ def clean(s)
18
+ s.to_s.gsub(/\W+/, ' ').gsub(/ +/, '').strip
19
+ end
20
+
21
+ extend self
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Elasticsearch
3
+ VERSION = "0.2.1"
4
+ end
5
+ end