stretchie 0.0.1

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