stretchie 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 027f752423a87c8f0e75f9d8b137b0a1d3542bed
4
+ data.tar.gz: be1f1ca84334a7b2a7fdc0456d0263b27d9529ec
5
+ SHA512:
6
+ metadata.gz: 8362eaead32327b7a0c4c6c1e282bbaa566e364c695b954aec94207695ac1e453f3f0add51895b3251a9bec09300cfbed2bdcf20b0e6d79d8549e330d5b0fc0d
7
+ data.tar.gz: bac222dde6a7d2cc3406661ec3c3dc63d579173a1b6f820e45506a267b881e0acb38572d51e62203d58a25921e7b3a5a64d50d02e598b77be0dd86b9194dac43
data/.gitignore ADDED
@@ -0,0 +1,24 @@
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
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .DS_Store
24
+
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stretchie.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Adam Bregenzer
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,187 @@
1
+ Stretchie
2
+ =========
3
+
4
+ Comfortable searching pants for ActiveRecord Models. Stretchie simplifies
5
+ using elastic search in your models and provides hooks to ease testing.
6
+
7
+
8
+ Defining Indices
9
+ ----------------
10
+
11
+ Stretchie simply builds on elasticsearch-model, allowing you to pull the
12
+ details out into a concern.
13
+
14
+ With a model like this:
15
+ ```ruby
16
+ class User < ActiveRecord::Base
17
+ attr_accessor :name, :email, :misc_id
18
+ end
19
+ ```
20
+
21
+ Your concern could look like this:
22
+ ```ruby
23
+ module Search
24
+ module User
25
+ extend ActiveSupport::Concern
26
+ include Stretchie::Pants
27
+
28
+ included do
29
+ settings index: { number_of_shards: 1 } do
30
+ mappings dynamic: 'false' do
31
+ indexes :name, analyzer: 'whitespace', index_options: 'offsets'
32
+ indexes :email
33
+ indexes :misc_id
34
+ indexes :sortable_name
35
+ end
36
+ end
37
+ end
38
+
39
+ def as_indexed_json(options={})
40
+ json = as_json(only: [:name, :email, :misc_id])
41
+ json['sortable_name'] = self.name.downcase
42
+ json
43
+ end
44
+ end
45
+ end
46
+ ```
47
+
48
+ And you would add this to your model:
49
+ ```ruby
50
+ include Search::User
51
+ ```
52
+
53
+ If you have liked models, you can have them re-index automatically with `index_dependent_models`:
54
+ ```ruby
55
+ class User < ActiveRecord::Base
56
+ include Search::User
57
+ attr_accessor :name, :email
58
+ has_may :tags
59
+ end
60
+
61
+ class Tag < ActiveRecord::Base
62
+ include Search::Tag
63
+ attr_accessor :name
64
+
65
+ belongs_to :user
66
+ end
67
+
68
+ module Search
69
+ module Tag
70
+ extend ActiveSupport::Concern
71
+ include Stretchie::Pants
72
+
73
+ included do
74
+ settings index: { number_of_shards: 1 } do
75
+ mappings dynamic: 'false' do
76
+ indexes :name, analyzer: 'whitespace', index_options: 'offsets'
77
+ indexes :sortable_name
78
+ end
79
+ end
80
+ end
81
+
82
+ def as_indexed_json(options={})
83
+ json = as_json(only: [:name])
84
+ json['sortable_name'] = self.name.downcase
85
+ json
86
+ end
87
+
88
+ def index_dependent_models
89
+ self.users
90
+ end
91
+ end
92
+ end
93
+ ```
94
+
95
+
96
+ Maintaining Indices
97
+ -------------------
98
+
99
+ Create / Update your indices:
100
+ ```ruby
101
+ Stretchie.update_indices
102
+ Stretchie.update_indices :users
103
+ ```
104
+
105
+ Delete your indices:
106
+ ```ruby
107
+ Stretchie.delete_indices
108
+ Stretchie.delete_indices :users
109
+ ```
110
+
111
+ Refresh your indices:
112
+ ```ruby
113
+ Stretchie.refresh_indices
114
+ Stretchie.refresh_indices :users
115
+ ```
116
+
117
+
118
+ Maintaining Documents in the Index
119
+ ----------------------------------
120
+
121
+ To add or update changes:
122
+ ```ruby
123
+ user = User.create(name: 'Adam Bregenzer', email: 'adam@bregenzer.net')
124
+ user.update_in_index
125
+ ```
126
+
127
+ To remove:
128
+ ```ruby
129
+ user.delete_from_index
130
+ ```
131
+
132
+
133
+ Searching
134
+ ---------
135
+
136
+ You can do a simple search:
137
+ ```ruby
138
+ User.search 'Adam'
139
+ User.search 'Adam', limit: 10, skip: 20, order: {'name' => 'asc'}
140
+ ```
141
+
142
+ You can scope searches:
143
+ ```ruby
144
+ Tag.search 'rails', terms: {'user_id': current_user.id}
145
+ ```
146
+
147
+ You can search specific fields:
148
+ ```ruby
149
+ User.field_search :email, 'adam@bregenzer.net'
150
+ ```
151
+
152
+ You can search however you want:
153
+ ```ruby
154
+ User.query_search {'match' => {'name' => 'Foo'}}
155
+ ```
156
+
157
+
158
+ Talk to Me!
159
+ -----------
160
+
161
+ Let me know what you think, if you use it, etc.
162
+
163
+
164
+ Installation
165
+ ------------
166
+
167
+ Add this line to your application's Gemfile:
168
+
169
+ gem 'stretchie'
170
+
171
+ And then execute:
172
+
173
+ $ bundle
174
+
175
+ Or install it yourself as:
176
+
177
+ $ gem install stretchie
178
+
179
+
180
+ Contributing
181
+ ------------
182
+
183
+ 1. Fork it ( https://github.com/adambregenzer/stretchie/fork )
184
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
185
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
186
+ 4. Push to the branch (`git push origin my-new-feature`)
187
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
@@ -0,0 +1,219 @@
1
+ require 'active_support'
2
+ require 'elasticsearch/model'
3
+
4
+ module Stretchie
5
+ module Pants
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include Elasticsearch::Model
10
+
11
+
12
+ # Add the current model to the list of searchable models
13
+ Stretchie::add_model self
14
+
15
+ # Have the index name include the rails environment
16
+ index_name "#{document_type}-#{ENV['RAILS_ENV']}"
17
+ end
18
+
19
+
20
+ # Update the document in the ElasticSearch index
21
+ def update_in_index
22
+ __elasticsearch__.index_document
23
+
24
+ index_dependent_models.map(&:update_in_index)
25
+ end
26
+
27
+
28
+ # Delete the document from the ElasticSearch index
29
+ def delete_from_index
30
+ begin
31
+ __elasticsearch__.delete_document
32
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
33
+ nil
34
+ end
35
+
36
+ index_dependent_models.map(&:update_in_index)
37
+ end
38
+
39
+
40
+ # Override to trigger dependent models to be re-indexed, should return
41
+ # an array of models to run .update_in_index on.
42
+ def index_dependent_models
43
+ []
44
+ end
45
+
46
+
47
+ module ClassMethods
48
+
49
+ # Search the index for a matching term
50
+ def search(q, options={})
51
+ fields = options.fetch(:fields, nil)
52
+
53
+ query_string = {'query' => q}
54
+
55
+ # If asked, limit the search to specific fields
56
+ if !fields.nil?
57
+ if !fields.kind_of? Array
58
+ fields = [fields.to_s]
59
+ end
60
+
61
+ query_string['fields'] = fields
62
+ end
63
+
64
+ # Perform a default query search
65
+ query = {
66
+ 'bool' => {
67
+ 'must' => [{'query_string' => query_string}]
68
+ }
69
+ }
70
+
71
+ begin
72
+ run_query query, options
73
+ rescue Elasticsearch::Transport::Transport::Errors::BadRequest
74
+ # This is probably because the query couldn't be parsed, clean it up
75
+ # and try again
76
+ query_string['query'] = sanitize(query_string['query'])
77
+
78
+ run_query query, options
79
+ end
80
+ end
81
+
82
+
83
+ # Search the specified field for a match
84
+ def field_search(field, q, options={})
85
+ # Search for a match based on prefix or a string match as a backup
86
+ query = {
87
+ 'bool' => {
88
+ 'minimum_should_match' => 1,
89
+ 'should' => [
90
+ {'prefix' => {field => q}},
91
+ {'match' => {field => q}}
92
+ ]
93
+ }
94
+ }
95
+
96
+ run_query query, options
97
+ end
98
+
99
+
100
+ # Execute a query hash in ElasticSearch
101
+ def query_search(query, options={})
102
+ run_query query, options
103
+ end
104
+
105
+
106
+ #
107
+ # Private Methods
108
+ #
109
+
110
+
111
+ # Run a query hash in ElasticSearch
112
+ def run_query(query, options={})
113
+ query = prepare_query query, options
114
+
115
+ # Run the query
116
+ response = self.__elasticsearch__.search query
117
+
118
+ Hashie::Mash.new({
119
+ records: response.records,
120
+ total_entries: response.results.total
121
+ })
122
+ end
123
+ private :run_query
124
+
125
+
126
+ # sanitize query string for Lucene. Useful if the original query raises an
127
+ # exception due to invalid DSL parse.
128
+ #
129
+ # http://stackoverflow.com/questions/16205341/symbols-in-query-string-for-elasticsearch
130
+ #
131
+ # @return self [Stretchie::QueryString] the modified QueryString.
132
+ def sanitize(q)
133
+ # Escape special characters
134
+ # http://lucene.apache.org/core/4_8_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description#Escaping_Special_Characters
135
+ escaped_characters = Regexp.escape('\\+-&|!(){}[]^~*?:\/')
136
+ q = q.gsub(/([#{escaped_characters}])/, '\\\\\1')
137
+
138
+ # AND, OR and NOT are used by lucene as logical operators. We need
139
+ # to escape them
140
+ ['AND', 'OR', 'NOT'].each do |word|
141
+ escaped_word = word.split('').map {|char| "\\#{char}" }.join('')
142
+ q = q.gsub(/\s*\b(#{word.upcase})\b\s*/, " #{escaped_word} ")
143
+ end
144
+
145
+ # Escape odd quotes
146
+ if q.count('"') % 2 == 1
147
+ q = q.gsub(/(.*)"(.*)/, '\1\"\2')
148
+ end
149
+
150
+ q
151
+ end
152
+ private :sanitize
153
+
154
+
155
+ # Prepare a query hash for ElasticSearch
156
+ def prepare_query(q_hash, options={})
157
+ terms = options.fetch(:terms, {})
158
+ limit = options.fetch(:limit, -1)
159
+ skip = options.fetch(:skip, -1)
160
+ order = options.fetch(:order, {})
161
+
162
+ # Load the query but don't return any field data since we don't need it
163
+ query = {
164
+ 'fields' => [],
165
+ 'query' => q_hash
166
+ }
167
+
168
+ # If we have terms that must match (such as user_id) set them
169
+ if terms.length > 0
170
+ if q_hash.include?('term')
171
+ q_hash['term'].merge! terms
172
+ else
173
+ if q_hash.include?('bool')
174
+ if !q_hash['bool'].include?('must')
175
+ q_hash['bool']['must'] = []
176
+ end
177
+
178
+ q_hash['bool']['must'] << {term: terms}
179
+ else
180
+ query['query'] = {
181
+ 'bool' => {
182
+ 'must' => [q_hash]
183
+ }
184
+ }
185
+
186
+ query['query']['bool']['must'] << {term: terms}
187
+ end
188
+ end
189
+ end
190
+
191
+ # Set the limit
192
+ if limit > 0
193
+ query['size'] = limit
194
+ end
195
+
196
+ # Set the number of records to skip
197
+ if skip > 0
198
+ query['from'] = skip
199
+ end
200
+
201
+ # Set the sort order, sorting by _score last
202
+ if !query.include? 'sort'
203
+ query['sort'] = []
204
+ end
205
+
206
+ order.map do |k, v|
207
+ query['sort'] << {k => v}
208
+ end
209
+
210
+ if query['sort'].select { |x| x.keys.first == '_score' }.count == 0
211
+ query['sort'] << {'_score' => 'desc'}
212
+ end
213
+
214
+ query
215
+ end
216
+ private :prepare_query
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,3 @@
1
+ module Stretchie
2
+ VERSION = '0.0.1'
3
+ end
data/lib/stretchie.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'set'
2
+
3
+ # ElasticSearch
4
+ # require 'elasticsearch'
5
+
6
+ # Stretchie Core
7
+ require 'stretchie/version'
8
+
9
+ # Lets put our Stretchie::Pants on
10
+ require 'stretchie/pants'
11
+
12
+
13
+ # Defaults
14
+ module Stretchie
15
+
16
+ # Set containing all registered models
17
+ @@models = Set.new
18
+
19
+
20
+ # Add a model to the set
21
+ def self.add_model model
22
+ @@models << model
23
+ end
24
+
25
+
26
+ # Returns a set containing all registered models
27
+ def self.models
28
+ @@models
29
+ end
30
+
31
+
32
+ # Create or update all indices across all registered models
33
+ def self.update_indices(*args)
34
+ result = true
35
+
36
+ if args.length > 0
37
+ args = args.map { |m| m.to_s.singularize.capitalize.constantize }
38
+ _models = models.select { |m| args.include? m }
39
+ else
40
+ _models = models
41
+ end
42
+
43
+ _models.map do |m|
44
+ create = m.__elasticsearch__.create_index!
45
+ if !create.nil?
46
+ result &= create['acknowledged']
47
+ else
48
+ m.mappings.to_hash.map do |map, body|
49
+ client = m.__elasticsearch__.client
50
+ update = client.indices.put_mapping index: m.index_name, type: map,
51
+ body: {map => body}
52
+
53
+ if update.kind_of?(Hash)
54
+ result &= update['acknowledged']
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ result
61
+ end
62
+
63
+
64
+ # Refresh all indices across all registered models
65
+ def self.refresh_indices(*args)
66
+ result = true
67
+
68
+ if args.length > 0
69
+ args = args.map { |m| m.to_s.singularize.capitalize.constantize }
70
+ _models = models.select { |m| args.include? m }
71
+ else
72
+ _models = models
73
+ end
74
+
75
+ _models.map do |m|
76
+ refresh = m.__elasticsearch__.refresh_index!
77
+ if refresh.kind_of?(Hash) && refresh.include?('_shards')
78
+ result &= refresh['_shards']['failed'] == 0
79
+ end
80
+ end
81
+
82
+ result
83
+ end
84
+
85
+
86
+ # Delete all indices across all registered models
87
+ def self.delete_indices(*args)
88
+ result = true
89
+
90
+ if args.length > 0
91
+ args = args.map { |m| m.to_s.singularize.capitalize.constantize }
92
+ _models = models.select { |m| args.include? m }
93
+ else
94
+ _models = models
95
+ end
96
+
97
+ _models.map do |m|
98
+ delete = m.__elasticsearch__.delete_index!
99
+
100
+ if !delete.nil?
101
+ result &= delete['acknowledged']
102
+ end
103
+ end
104
+
105
+ result
106
+ end
107
+ end
@@ -0,0 +1,21 @@
1
+ require 'simplecov'
2
+ SimpleCov.start do
3
+ add_filter '/spec/'
4
+ end
5
+
6
+ ENV['RAILS_ENV'] ||= 'test'
7
+
8
+ require_relative '../lib/stretchie'
9
+
10
+ pwd = File.expand_path File.dirname(__FILE__)
11
+ Dir[File.join(pwd, 'support/**/*.rb')].each { |f| require_relative f }
12
+
13
+ RSpec.configure do |config|
14
+ if config.files_to_run.one?
15
+ config.default_formatter = 'doc'
16
+ end
17
+
18
+ config.order = :random
19
+ Kernel.srand config.seed
20
+ end
21
+
@@ -0,0 +1,225 @@
1
+ #encoding: UTF-8
2
+
3
+ describe Stretchie::Pants do
4
+
5
+ before :each do
6
+ @user = User.create(name: 'Foo Bar', email: 'baz@baz.com', misc_id: 100)
7
+ @user.update_in_index
8
+ @user1 = User.create(name: 'Red Green', email: 'blue@white.com' )
9
+ @user1.update_in_index
10
+ @user2 = User.create(name: 'Red Purple', email: 'red@white.com' )
11
+ @user2.update_in_index
12
+ Stretchie.refresh_indices
13
+ end
14
+
15
+
16
+ context '.delete_from_index' do
17
+ it 'removes document from index' do
18
+ @user.delete_from_index
19
+ Stretchie.refresh_indices
20
+ results = User.search 'Foo'
21
+ results = results.records.to_a
22
+ expect(results.length).to eq(0)
23
+ expect(results).to eq([])
24
+ @user.delete_from_index
25
+ end
26
+ end
27
+ context '.update_in_index' do
28
+ it 'adds documents to the index' do
29
+ @user.delete_from_index
30
+ Stretchie.refresh_indices
31
+ results = User.search 'Foo'
32
+ results = results.records.to_a
33
+ expect(results.length).to eq(0)
34
+ expect(results).to eq([])
35
+
36
+ @user.update_in_index
37
+ Stretchie.refresh_indices
38
+ results = User.search 'Foo'
39
+ results = results.records.to_a
40
+ expect(results.length).to eq(1)
41
+ expect(results).to eq([@user])
42
+ end
43
+ end
44
+
45
+ context '.search' do
46
+
47
+ it 'searches all fields' do
48
+ results = User.search 'Foo'
49
+ results = results.records.to_a
50
+ expect(results.length).to eq(1)
51
+ expect(results).to eq([@user])
52
+
53
+ results = User.search 'baz@baz.com'
54
+ results = results.records.to_a
55
+ expect(results.length).to eq(1)
56
+ expect(results).to eq([@user])
57
+ end
58
+
59
+ it 'searches all fields with invalid query characters' do
60
+ results = User.search 'Foo"'
61
+ results = results.records.to_a
62
+ expect(results.length).to eq(1)
63
+ expect(results).to eq([@user])
64
+ end
65
+
66
+ it 'searches specific fields' do
67
+ results = User.search 'Foo', fields: :name
68
+ results = results.records.to_a
69
+ expect(results.length).to eq(1)
70
+ expect(results).to eq([@user])
71
+ end
72
+
73
+ it 'requires specific terms' do
74
+ results = User.search 'baz@baz.com', terms: {misc_id: 100}
75
+ results = results.records.to_a
76
+ expect(results.length).to eq(1)
77
+ expect(results).to eq([@user])
78
+ end
79
+
80
+ it 'supports limit' do
81
+ results = User.search 'Red', limit: 1, order: {'name' => 'asc'}
82
+ results = results.records.to_a
83
+ expect(results.length).to eq(1)
84
+ expect(results).to eq([@user1])
85
+ end
86
+
87
+ it 'supports skip' do
88
+ results = User.search 'Red', limit: 1, skip: 1, order: {'name' => 'asc'}
89
+ results = results.records.to_a
90
+ expect(results.length).to eq(1)
91
+ expect(results).to eq([@user2])
92
+ end
93
+
94
+ it 'supports order' do
95
+ results = User.search 'Red', order: {'name' => 'asc'}
96
+ results = results.records.to_a
97
+ expect(results.length).to eq(2)
98
+ expect(results).to eq([@user1, @user2])
99
+ end
100
+ end
101
+
102
+
103
+ context '.field_search' do
104
+ it 'searches all fields' do
105
+ results = User.field_search :name, 'Foo'
106
+ results = results.records.to_a
107
+ expect(results.length).to eq(1)
108
+ expect(results).to eq([@user])
109
+
110
+ results = User.field_search :name, 'baz@baz.com'
111
+ results = results.records.to_a
112
+ expect(results.length).to eq(0)
113
+ expect(results).to eq([])
114
+ end
115
+
116
+ it 'requires specific terms' do
117
+ results = User.field_search :name, 'Foo', terms: {misc_id: 100}
118
+ results = results.records.to_a
119
+ expect(results.length).to eq(1)
120
+ expect(results).to eq([@user])
121
+ end
122
+
123
+ it 'supports limit' do
124
+ results = User.field_search :name, 'Red', limit: 1, order: {'name' => 'asc'}
125
+ results = results.records.to_a
126
+ expect(results.length).to eq(1)
127
+ expect(results).to eq([@user1])
128
+ end
129
+
130
+ it 'supports skip' do
131
+ results = User.field_search :name, 'Red', limit: 1, skip: 1, order: {'name' => 'asc'}
132
+ results = results.records.to_a
133
+ expect(results.length).to eq(1)
134
+ expect(results).to eq([@user2])
135
+ end
136
+
137
+ it 'supports order' do
138
+ results = User.field_search :name, 'Red', order: {'name' => 'asc'}
139
+ results = results.records.to_a
140
+ expect(results.length).to eq(2)
141
+ expect(results).to eq([@user1, @user2])
142
+ end
143
+ end
144
+
145
+
146
+ context '.query_search' do
147
+ it 'performs a custom search' do
148
+ query = {
149
+ 'match' => {
150
+ 'name' => 'Foo'
151
+ }
152
+ }
153
+
154
+ results = User.query_search query
155
+ results = results.records.to_a
156
+ expect(results.length).to eq(1)
157
+ expect(results).to eq([@user])
158
+ end
159
+
160
+ it 'requires specific terms' do
161
+ query = {
162
+ 'match' => {
163
+ 'name' => 'Foo'
164
+ }
165
+ }
166
+
167
+ results = User.query_search query, terms: {misc_id: 100}
168
+ results = results.records.to_a
169
+ expect(results.length).to eq(1)
170
+ expect(results).to eq([@user])
171
+ end
172
+
173
+ it 'requires additional terms' do
174
+ query = {
175
+ 'term' => {
176
+ 'name' => 'Foo'
177
+ }
178
+ }
179
+
180
+ results = User.query_search query, terms: {misc_id: 100}
181
+ results = results.records.to_a
182
+ expect(results.length).to eq(1)
183
+ expect(results).to eq([@user])
184
+ end
185
+
186
+ it 'supports limit' do
187
+ query = {
188
+ 'match' => {
189
+ 'name' => 'Red'
190
+ }
191
+ }
192
+
193
+ results = User.query_search query, limit: 1, order: {'name' => 'asc'}
194
+ results = results.records.to_a
195
+ expect(results.length).to eq(1)
196
+ expect(results).to eq([@user1])
197
+ end
198
+
199
+ it 'supports skip' do
200
+ query = {
201
+ 'match' => {
202
+ 'name' => 'Red'
203
+ }
204
+ }
205
+
206
+ results = User.query_search query, limit: 1, skip: 1, order: {'name' => 'asc'}
207
+ results = results.records.to_a
208
+ expect(results.length).to eq(1)
209
+ expect(results).to eq([@user2])
210
+ end
211
+
212
+ it 'supports order' do
213
+ query = {
214
+ 'match' => {
215
+ 'name' => 'Red'
216
+ }
217
+ }
218
+
219
+ results = User.query_search query, order: {'name' => 'asc'}
220
+ results = results.records.to_a
221
+ expect(results.length).to eq(2)
222
+ expect(results).to eq([@user1, @user2])
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,83 @@
1
+ #encoding: UTF-8
2
+
3
+ describe Stretchie do
4
+
5
+ context '.update_indices' do
6
+
7
+ it 'creates all indices' do
8
+ Stretchie.delete_indices
9
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
10
+ expect(indices).not_to include(User.index_name)
11
+ result = Stretchie.update_indices
12
+ expect(result).to eq(true)
13
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
14
+ expect(indices).to include(User.index_name)
15
+ end
16
+
17
+ it 'creates specific indices' do
18
+ Stretchie.delete_indices
19
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
20
+ expect(indices).not_to include(User.index_name)
21
+
22
+ result = Stretchie.update_indices :users
23
+ expect(result).to eq(true)
24
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
25
+ expect(indices).to include(User.index_name)
26
+ end
27
+
28
+ it 'updates all indices' do
29
+ Stretchie.delete_indices
30
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
31
+ expect(indices).not_to include(User.index_name)
32
+
33
+ Stretchie.update_indices
34
+ result = Stretchie.update_indices
35
+ expect(result).to eq(true)
36
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
37
+ expect(indices).to include(User.index_name)
38
+ end
39
+
40
+ it 'updates specific indices' do
41
+ Stretchie.delete_indices :users
42
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
43
+ expect(indices).not_to include(User.index_name)
44
+
45
+ Stretchie.update_indices :users
46
+ result = Stretchie.update_indices :users
47
+ expect(result).to eq(true)
48
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
49
+ expect(indices).to include(User.index_name)
50
+ end
51
+ end
52
+
53
+
54
+ context '.delete_indices' do
55
+
56
+ it 'deletes all indices' do
57
+ Stretchie.update_indices
58
+ Stretchie.delete_indices
59
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
60
+ expect(indices).not_to include(User.index_name)
61
+ end
62
+
63
+ it 'deletes specific indices' do
64
+ Stretchie.delete_indices :users
65
+ indices = User.__elasticsearch__.client.indices.status['indices'].keys
66
+ expect(indices).not_to include(User.index_name)
67
+ end
68
+ end
69
+
70
+
71
+ context '.refresh_indices' do
72
+
73
+ it 'refreshes all indices' do
74
+ result = Stretchie.refresh_indices
75
+ expect(result).to eq(true)
76
+ end
77
+
78
+ it 'refreshes specific indices' do
79
+ result = Stretchie.refresh_indices :users
80
+ expect(result).to eq(true)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.configure do |config|
2
+ config.before(:each) do
3
+ Stretchie.update_indices
4
+ end
5
+
6
+ config.append_after(:each) do
7
+ Stretchie.delete_indices
8
+ end
9
+ end
@@ -0,0 +1,50 @@
1
+ require 'active_record'
2
+ require 'logger'
3
+
4
+ ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
5
+ ActiveRecord::Base.logger = Logger.new STDOUT
6
+ ActiveRecord::Base.logger.level = Logger::WARN
7
+ ActiveSupport::LogSubscriber.colorize_logging = false
8
+
9
+
10
+ ActiveRecord::Schema.define do
11
+ self.verbose = false
12
+
13
+ create_table :users do |t|
14
+ t.string :name
15
+ t.string :email
16
+ t.integer :misc_id
17
+ end
18
+ end
19
+
20
+
21
+ module Search
22
+ module User
23
+ extend ActiveSupport::Concern
24
+ include Stretchie::Pants
25
+
26
+ included do
27
+ settings index: { number_of_shards: 1 } do
28
+ mappings dynamic: 'false' do
29
+ indexes :name, analyzer: 'whitespace', index_options: 'offsets'
30
+ indexes :email
31
+ indexes :misc_id
32
+ indexes :sortable_name
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+ def as_indexed_json(options={})
39
+ json = as_json(only: [:name, :email, :misc_id])
40
+ json['sortable_name'] = self.name.downcase
41
+ json
42
+ end
43
+ end
44
+ end
45
+
46
+
47
+ class User < ActiveRecord::Base
48
+ include Search::User
49
+ attr_accessor :name, :email, :misc_id
50
+ end
data/stretchie.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'stretchie/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'stretchie'
10
+ spec.version = Stretchie::VERSION
11
+ spec.authors = ['Adam Bregenzer']
12
+ spec.email = ['adam@bregenzer.net']
13
+ spec.summary = 'An ActiveModel concern for integrating ElasticSearch.'
14
+ spec.description = 'Comfortable searching pants for ActiveRecord Models. Stretchie simplifies using elastic search in your models and provides hooks to ease testing.'
15
+ spec.homepage = 'https://github.com/adambregenzer/stretchie'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'rails', '~> 4.1', '>= 4.1.1'
24
+ spec.add_dependency 'elasticsearch-model', '~> 0.1', '>= 0.1.4'
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.6'
27
+ spec.add_development_dependency 'rake', '~> 10.3', '>= 10.3.2'
28
+ spec.add_development_dependency 'rspec', '~> 3.0', '>= 3.0.0'
29
+ spec.add_development_dependency 'simplecov', '~> 0.8', '>= 0.8.2'
30
+ spec.add_development_dependency 'sqlite3', '~> 1.3', '>= 1.3.9'
31
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stretchie
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Adam Bregenzer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 4.1.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '4.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 4.1.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: elasticsearch-model
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 0.1.4
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '0.1'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 0.1.4
53
+ - !ruby/object:Gem::Dependency
54
+ name: bundler
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.6'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.6'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rake
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '10.3'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 10.3.2
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.3'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 10.3.2
87
+ - !ruby/object:Gem::Dependency
88
+ name: rspec
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '3.0'
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.0.0
97
+ type: :development
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.0.0
107
+ - !ruby/object:Gem::Dependency
108
+ name: simplecov
109
+ requirement: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '0.8'
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.8.2
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.8'
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 0.8.2
127
+ - !ruby/object:Gem::Dependency
128
+ name: sqlite3
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - "~>"
132
+ - !ruby/object:Gem::Version
133
+ version: '1.3'
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 1.3.9
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '1.3'
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: 1.3.9
147
+ description: Comfortable searching pants for ActiveRecord Models. Stretchie simplifies
148
+ using elastic search in your models and provides hooks to ease testing.
149
+ email:
150
+ - adam@bregenzer.net
151
+ executables: []
152
+ extensions: []
153
+ extra_rdoc_files: []
154
+ files:
155
+ - ".gitignore"
156
+ - ".rspec"
157
+ - Gemfile
158
+ - LICENSE.txt
159
+ - README.md
160
+ - Rakefile
161
+ - lib/stretchie.rb
162
+ - lib/stretchie/pants.rb
163
+ - lib/stretchie/version.rb
164
+ - spec/spec_helper.rb
165
+ - spec/stretchie_pants_spec.rb
166
+ - spec/stretchie_spec.rb
167
+ - spec/support/elastic_search.rb
168
+ - spec/support/model.rb
169
+ - stretchie.gemspec
170
+ homepage: https://github.com/adambregenzer/stretchie
171
+ licenses:
172
+ - MIT
173
+ metadata: {}
174
+ post_install_message:
175
+ rdoc_options: []
176
+ require_paths:
177
+ - lib
178
+ required_ruby_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ required_rubygems_version: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ requirements: []
189
+ rubyforge_project:
190
+ rubygems_version: 2.2.2
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: An ActiveModel concern for integrating ElasticSearch.
194
+ test_files:
195
+ - spec/spec_helper.rb
196
+ - spec/stretchie_pants_spec.rb
197
+ - spec/stretchie_spec.rb
198
+ - spec/support/elastic_search.rb
199
+ - spec/support/model.rb