elasticsearch-documents 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +217 -0
- data/Rakefile +6 -0
- data/elasticsearch-documents.gemspec +29 -0
- data/lib/elasticsearch/extensions/documents/document.rb +32 -0
- data/lib/elasticsearch/extensions/documents/index.rb +47 -0
- data/lib/elasticsearch/extensions/documents/indexer.rb +60 -0
- data/lib/elasticsearch/extensions/documents/queryable.rb +31 -0
- data/lib/elasticsearch/extensions/documents/utils.rb +31 -0
- data/lib/elasticsearch/extensions/documents/version.rb +8 -0
- data/lib/elasticsearch/extensions/documents.rb +50 -0
- data/lib/elasticsearch-documents.rb +2 -0
- data/spec/elasticsearch/extensions/documentor_spec.rb +50 -0
- data/spec/elasticsearch/extensions/documents/document_spec.rb +38 -0
- data/spec/elasticsearch/extensions/documents/index_spec.rb +98 -0
- data/spec/elasticsearch/extensions/documents/indexer_spec.rb +112 -0
- data/spec/elasticsearch/extensions/documents/queryable_spec.rb +55 -0
- data/spec/elasticsearch/extensions/documents/utils_spec.rb +39 -0
- data/spec/spec_helper.rb +28 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 09242c5b4dacbea5a8df8b92589cf7da49491d1c
|
4
|
+
data.tar.gz: b26f6777634d3e6e37207eafb0ed96e1def983d7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5d7bb17681d33ef53bc909f4089be04971ca9f3b8f355a689f179cf5918ec7e36d1365de24c35aa05920f8ea50df01cef220e36364e17e433c4ac4e856339432
|
7
|
+
data.tar.gz: c778f6377d6243a1999c4edb25daf9a0c3a768f1d8ccd42756c520021d0f6bc0b59e8fdbf37bc9e8f68a2ab7dab2ec6e0d2740f98ea1df9718d7eb516f2b0826
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Ryan Houston and Project 10 LLC (culturalist.com)
|
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,217 @@
|
|
1
|
+
# Elasticsearch::Extensions::Documents
|
2
|
+
|
3
|
+
A service wrapper to manage Elasticsearch index documents
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'elasticsearch-documents'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install elasticsearch-documents
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
Before making any calls to Elasticsearch you need to configure the `Documents`
|
22
|
+
extension.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
ES_MAPPINGS = {
|
26
|
+
user: {
|
27
|
+
_all: { analyzer: "snowball" },
|
28
|
+
properties: {
|
29
|
+
id: { type: "integer", index: :not_analyzed },
|
30
|
+
name: { type: "string", analyzer: "snowball" },
|
31
|
+
bio: { type: "string", analyzer: "snowball" },
|
32
|
+
updated_at: { type: "date", include_in_all: false }
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
ES_SETTINGS = {
|
38
|
+
index: {
|
39
|
+
number_of_shards: 3,
|
40
|
+
number_of_replicas: 2,
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
Elasticsearch::Extensions::Documents.configure do |config|
|
45
|
+
config.url = 'http://example.com:9200' # your elasticsearch endpoint
|
46
|
+
config.index_name = 'test_index' # the name of your index
|
47
|
+
config.mappings = ES_MAPPINGS # a hash containing your index mappings
|
48
|
+
config.settings = ES_SETTINGS # a hash containing your index settings
|
49
|
+
config.logger = Logger.new(STDOUT) # the logger to use. (defaults to Logger.new(STDOUT)
|
50
|
+
config.log = true # if the elasticsearch-ruby should provide logging
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
If you are using this extension with a Rails application this configuration
|
55
|
+
could live in an initializer like `config/initializers/elasticsearch.rb`.
|
56
|
+
|
57
|
+
## Usage
|
58
|
+
|
59
|
+
The `Documents` extension builds on the
|
60
|
+
`elasticsearch-ruby` Gem adding conventions and helper classes to aide in the
|
61
|
+
serialization and flow of data between your application code and the
|
62
|
+
elasticsearch-ruby interface. To accomplish this the application data models
|
63
|
+
will be serialized into `Document`s that can be indexed and searched with the
|
64
|
+
`elasticsearch-ruby` Gem.
|
65
|
+
|
66
|
+
### Saving a Document
|
67
|
+
If your application has a model called `User` that you wanted to index you would
|
68
|
+
create a `Document` that defined how the `User` is stored in the index.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class UserDocument < Elasticsearch::Extensions::Documents::Document
|
72
|
+
indexes_as_type :user
|
73
|
+
|
74
|
+
def as_hash
|
75
|
+
{
|
76
|
+
name: object.name,
|
77
|
+
title: object.title,
|
78
|
+
bio: object.bio,
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
user = User.new # could be a PORO or an ActiveRecord model
|
85
|
+
user_doc = UserDocument.new(user)
|
86
|
+
|
87
|
+
index = Elasticsearch::Extensions::Documents::Index.new
|
88
|
+
index.index(user_doc)
|
89
|
+
```
|
90
|
+
|
91
|
+
### Deleting a Document
|
92
|
+
Deleting a document is just as easy
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
user_doc = UserDocument.new(user)
|
96
|
+
index.delete(user_doc)
|
97
|
+
```
|
98
|
+
|
99
|
+
### Searching for Documents
|
100
|
+
Create classes which include `Elasticsearch::Extensions::Documents::Queryable`.
|
101
|
+
Then implement a `#as_hash` method to define the JSON structure of an
|
102
|
+
Elasticsearch Query using the [Query DSL][es-query-dsl]. This hash should be
|
103
|
+
formatted appropriately to be passed on to the
|
104
|
+
[Elasticsearch::Transport::Client#search][es-ruby-search-src] method.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
class GeneralSiteSearchQuery
|
108
|
+
include Elasticsearch::Extensions::Documents::Queryable
|
109
|
+
|
110
|
+
def as_hash
|
111
|
+
{
|
112
|
+
index: 'test_index',
|
113
|
+
body: {
|
114
|
+
query: {
|
115
|
+
query_string: {
|
116
|
+
analyzer: "snowball",
|
117
|
+
query: "something to search for",
|
118
|
+
}
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
You could elaborate on this class with a constructor that takes the search
|
127
|
+
term and other options specific to your use case as arguments.
|
128
|
+
|
129
|
+
You can then call the `#execute` method to run the query. The Elasticsearch JSON
|
130
|
+
response will be returned in whole wrapped in a
|
131
|
+
[`Hashie::Mash`](https://github.com/intridea/hashie) instance to allow
|
132
|
+
the results to be interacted with in object notation instead of hash notation.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
query = GeneralSiteSearchQuery.new
|
136
|
+
results = query.execute
|
137
|
+
results.hits.total
|
138
|
+
results.hits.max_score
|
139
|
+
results.hits.hits.each { |hit| puts hit._source }
|
140
|
+
```
|
141
|
+
|
142
|
+
You can also easily define a custom result format by overriding the
|
143
|
+
`#parse_results` method in your Queryable class.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class GeneralSiteSearchQuery
|
147
|
+
include Elasticsearch::Extensions::Documents::Queryable
|
148
|
+
|
149
|
+
def as_hash
|
150
|
+
# your query structure here
|
151
|
+
end
|
152
|
+
|
153
|
+
def parse_results(raw_results)
|
154
|
+
CustomQueryResults.new(raw_results)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
Here the `CustomQueryResults` gets passed the `Hashie::Mash` results object and
|
160
|
+
can parse and coerce that data into whatever structure is most useful for your
|
161
|
+
application.
|
162
|
+
|
163
|
+
|
164
|
+
### Index Management
|
165
|
+
|
166
|
+
The Indexer uses the `Elasticsearch::Extensions::Documents.configuration`
|
167
|
+
to create the index with the configured `#index_name`, `#mappings`, and
|
168
|
+
`#settings`.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
indexer = Elasticsearch::Extensions::Documents::Indexer.new
|
172
|
+
indexer.create_index
|
173
|
+
indexer.drop_index
|
174
|
+
```
|
175
|
+
|
176
|
+
The `Indexer` can `#bulk_index` documents sending multiple documents to
|
177
|
+
Elasticsearch in a single request. This may be more efficient when
|
178
|
+
programmatically re-indexing entire sets of documents.
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
user_documents = users.collect { |user| UserDocument.new(user) }
|
182
|
+
indexer.bulk_index(user_documents)
|
183
|
+
```
|
184
|
+
|
185
|
+
The `Indexer` accepts a block to the `#reindex` method to encapsulate the
|
186
|
+
processes of dropping the old index, creating a new index with the latest
|
187
|
+
configured mappings and settings, and bulk indexing a set of documents into the
|
188
|
+
newly created index. The content of the block should be the code that creates
|
189
|
+
your documents in batches and passes them to the `#bulk_index` method of the
|
190
|
+
`Indexer`.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
indexer.reindex do |indexer|
|
194
|
+
|
195
|
+
# For ActiveRecord you may want to find_in_batches
|
196
|
+
User.find_in_batches(batch_size: 500) do |batch|
|
197
|
+
documents = batch.map { |user| UserDocument.new(user) }
|
198
|
+
indexer.bulk_index(documents)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Otherwise you can add whatever logic you need to bulk index your documents
|
202
|
+
documents = users.map { |model| UserDocument.new(model) }
|
203
|
+
indexer.bulk_index(documents)
|
204
|
+
end
|
205
|
+
```
|
206
|
+
## Contributing
|
207
|
+
|
208
|
+
1. Fork it
|
209
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
210
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
211
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
212
|
+
5. Create new Pull Request
|
213
|
+
|
214
|
+
|
215
|
+
[es-query-dsl]: http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html
|
216
|
+
[es-ruby-search-src]: https://github.com/elasticsearch/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/search.rb
|
217
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elasticsearch/extensions/documents/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "elasticsearch-documents"
|
8
|
+
spec.version = Elasticsearch::Extensions::Documents::VERSION
|
9
|
+
spec.required_ruby_version = ">= 1.9.3"
|
10
|
+
|
11
|
+
spec.authors = ["Ryan Houston"]
|
12
|
+
spec.email = ["ryanhouston83@gmail.com"]
|
13
|
+
spec.summary = %q{A service wrapper to manage elasticsearch index documents}
|
14
|
+
spec.description = %q{Define mappings to turn model instances into indexable search documents}
|
15
|
+
spec.homepage = "http://github.com/RyanHouston/elasticsearch-documents"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "elasticsearch"
|
27
|
+
spec.add_runtime_dependency "hashie"
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Extensions
|
3
|
+
module Documents
|
4
|
+
class Document
|
5
|
+
attr_reader :object
|
6
|
+
|
7
|
+
def initialize(object)
|
8
|
+
@object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def indexes_as_type(type)
|
13
|
+
@index_type = type.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :index_type
|
17
|
+
alias :type :index_type
|
18
|
+
end
|
19
|
+
|
20
|
+
def id
|
21
|
+
object.id
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_hash
|
25
|
+
raise NotImplementedError, 'A subclass should define this method'
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'hashie/mash'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Extensions
|
5
|
+
module Documents
|
6
|
+
class Index
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
def initialize(client = nil)
|
10
|
+
@client = client || Documents.client
|
11
|
+
end
|
12
|
+
|
13
|
+
def index(document)
|
14
|
+
payload = {
|
15
|
+
index: Documents.index_name,
|
16
|
+
type: document.class.type,
|
17
|
+
id: document.id,
|
18
|
+
body: document.as_hash,
|
19
|
+
}
|
20
|
+
client.index payload
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete(document)
|
24
|
+
payload = {
|
25
|
+
index: Documents.index_name,
|
26
|
+
type: document.class.type,
|
27
|
+
id: document.id,
|
28
|
+
}
|
29
|
+
client.delete payload
|
30
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound => not_found
|
31
|
+
Documents.logger.info "[Documents] Attempted to delete missing document: #{not_found}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def search(query)
|
35
|
+
response = client.search(query.as_hash)
|
36
|
+
Hashie::Mash.new(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
def refresh
|
40
|
+
client.indices.refresh index: Documents.index_name
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Extensions
|
3
|
+
module Documents
|
4
|
+
class Indexer
|
5
|
+
attr_reader :client, :config
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@client = options.fetch(:client) { Documents.client }
|
9
|
+
@config = options.fetch(:config) { Documents.configuration }
|
10
|
+
end
|
11
|
+
|
12
|
+
def drop_index
|
13
|
+
client.indices.delete(index: config.index_name) if index_exists?
|
14
|
+
end
|
15
|
+
|
16
|
+
def index_exists?
|
17
|
+
client.indices.exists(index: config.index_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_index
|
21
|
+
client.indices.create(index: config.index_name, body: create_index_body) unless index_exists?
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_index_body
|
25
|
+
{}.tap do |body|
|
26
|
+
body[:settings] = config.settings if config.settings
|
27
|
+
body[:mappings] = config.mappings if config.mappings
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def reindex(&block)
|
32
|
+
drop_index
|
33
|
+
create_index
|
34
|
+
block.call(self) if block_given?
|
35
|
+
end
|
36
|
+
|
37
|
+
def bulk_index(documents)
|
38
|
+
client.bulk body: bulk_index_operations(documents)
|
39
|
+
end
|
40
|
+
|
41
|
+
def bulk_index_operations(documents)
|
42
|
+
documents.collect { |document| bulk_index_operation_hash(document) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def bulk_index_operation_hash(document)
|
46
|
+
{
|
47
|
+
index: {
|
48
|
+
_index: config.index_name,
|
49
|
+
_type: document.class.type,
|
50
|
+
_id: document.id,
|
51
|
+
data: document.as_hash,
|
52
|
+
}
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Extensions
|
3
|
+
module Documents
|
4
|
+
module Queryable
|
5
|
+
|
6
|
+
def as_hash
|
7
|
+
raise NotImplementedError, "#{self.class.name} should implement #as_hash method"
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute
|
11
|
+
raw_results = index.search(self)
|
12
|
+
parse_results(raw_results)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_results(raw_results)
|
16
|
+
raw_results
|
17
|
+
end
|
18
|
+
|
19
|
+
def index_name
|
20
|
+
Elasticsearch::Extensions::Documents.index_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def index
|
24
|
+
@index ||= Elasticsearch::Extensions::Documents::Index.new
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Extensions
|
3
|
+
module Documents
|
4
|
+
module Utils
|
5
|
+
|
6
|
+
def self.sanitize_for_query_string_query(query_string)
|
7
|
+
# http://stackoverflow.com/questions/16205341/symbols-in-query-string-for-elasticse
|
8
|
+
# Escape special characters
|
9
|
+
# http://lucene.apache.org/core/old_versioned_docs/versions/2_9_1/queryparsersyntax
|
10
|
+
escaped_characters = Regexp.escape('/\\+-&|!(){}[]^~*?:')
|
11
|
+
query_string = query_string.gsub(/([#{escaped_characters}])/, '\\\\\1')
|
12
|
+
|
13
|
+
# AND, OR and NOT are used by lucene as logical operators. We need
|
14
|
+
# to escape them
|
15
|
+
['AND', 'OR', 'NOT'].each do |word|
|
16
|
+
escaped_word = word.split('').map {|char| "\\#{char}" }.join('')
|
17
|
+
query_string = query_string.gsub(/\s*\b(#{word.upcase})\b\s*/, " #{escaped_word} ")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Escape odd quotes
|
21
|
+
quote_count = query_string.count '"'
|
22
|
+
query_string = query_string.gsub(/(.*)"(.*)/, '\1\"\2') if quote_count % 2 == 1
|
23
|
+
|
24
|
+
query_string
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "elasticsearch"
|
2
|
+
require "logger"
|
3
|
+
require "elasticsearch/extensions/documents/version"
|
4
|
+
require "elasticsearch/extensions/documents/document"
|
5
|
+
require "elasticsearch/extensions/documents/index"
|
6
|
+
require "elasticsearch/extensions/documents/indexer"
|
7
|
+
require "elasticsearch/extensions/documents/queryable"
|
8
|
+
require "elasticsearch/extensions/documents/utils"
|
9
|
+
|
10
|
+
module Elasticsearch
|
11
|
+
module Extensions
|
12
|
+
module Documents
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_accessor :client, :configuration
|
16
|
+
|
17
|
+
def client
|
18
|
+
@client ||= Elasticsearch::Client.new({
|
19
|
+
host: self.configuration.url,
|
20
|
+
log: self.configuration.log,
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
def configure
|
25
|
+
self.configuration ||= Configuration.new
|
26
|
+
yield configuration
|
27
|
+
end
|
28
|
+
|
29
|
+
def index_name
|
30
|
+
self.configuration.index_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def logger
|
34
|
+
self.configuration.logger
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Configuration
|
39
|
+
attr_accessor :url, :index_name, :mappings, :settings, :log, :logger
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@logger = Logger.new(STDOUT)
|
43
|
+
@log = true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Elasticsearch::Extensions
|
5
|
+
describe Documents do
|
6
|
+
|
7
|
+
let(:logger) { Logger.new(STDOUT) }
|
8
|
+
before do
|
9
|
+
Documents.configure do |config|
|
10
|
+
config.logger = logger
|
11
|
+
config.log = true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.configuration' do
|
16
|
+
subject(:config) { Documents.configuration }
|
17
|
+
|
18
|
+
specify { expect(config.url).to eql 'http://example.com:9200' }
|
19
|
+
specify { expect(config.index_name).to eql 'test_index' }
|
20
|
+
specify { expect(config.mappings).to eql( :fake_mappings ) }
|
21
|
+
specify { expect(config.settings).to eql( :fake_settings ) }
|
22
|
+
specify { expect(config.logger).to equal logger }
|
23
|
+
specify { expect(config.logger).to be_true }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.client' do
|
27
|
+
subject(:client) { Documents.client }
|
28
|
+
|
29
|
+
it 'creates an instance of Elasticsearch::Transport::Client' do
|
30
|
+
expect(client).to be_instance_of Elasticsearch::Transport::Client
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'caches the client instance' do
|
34
|
+
c1 = Documents.client
|
35
|
+
c2 = Documents.client
|
36
|
+
expect(c1).to equal c2
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '.index_name' do
|
41
|
+
specify { expect(Documents.index_name).to eq 'test_index' }
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.logger' do
|
45
|
+
specify { expect(Documents.logger).to equal logger }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Extensions
|
5
|
+
module Documents
|
6
|
+
Documents.configure {|config| config.index_name = 'test_index' }
|
7
|
+
|
8
|
+
describe Document do
|
9
|
+
let(:document_class) { Class.new(Elasticsearch::Extensions::Documents::Document) }
|
10
|
+
let(:model) { double(:model) }
|
11
|
+
subject(:document) { document_class.new(model) }
|
12
|
+
|
13
|
+
describe '.indexes_as_type' do
|
14
|
+
it 'sets the index type name' do
|
15
|
+
document_class.indexes_as_type('test_type')
|
16
|
+
expect(document_class.index_type).to eq 'test_type'
|
17
|
+
expect(document_class.type).to eq 'test_type'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#id' do
|
22
|
+
it 'delegates to the model' do
|
23
|
+
expect(model).to receive(:id)
|
24
|
+
document.id
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#as_hash' do
|
29
|
+
it 'raises an error if not redefined by a subclass' do
|
30
|
+
expect{ document.as_hash }.to raise_error NotImplementedError
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Extensions
|
5
|
+
module Documents
|
6
|
+
class TestDocument < Document
|
7
|
+
indexes_as_type :test_doc
|
8
|
+
def as_hash
|
9
|
+
{ valueA: :a, valueB: :b }
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Index do
|
14
|
+
let(:client) { double(:client) }
|
15
|
+
let(:model) { double(:model, id: 1) }
|
16
|
+
let(:document) { TestDocument.new(model) }
|
17
|
+
subject(:index) { Index.new(client) }
|
18
|
+
|
19
|
+
describe '#index' do
|
20
|
+
it 'adds or replaces a document in the search index' do
|
21
|
+
payload = {
|
22
|
+
index: 'test_index',
|
23
|
+
type: 'test_doc',
|
24
|
+
id: 1,
|
25
|
+
body: {valueA: :a, valueB: :b}
|
26
|
+
}
|
27
|
+
expect(client).to receive(:index).with(payload)
|
28
|
+
index.index document
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#delete' do
|
33
|
+
it 'removes a document from the search index' do
|
34
|
+
payload = {
|
35
|
+
index: 'test_index',
|
36
|
+
type: 'test_doc',
|
37
|
+
id: 1,
|
38
|
+
}
|
39
|
+
expect(client).to receive(:delete).with(payload)
|
40
|
+
index.delete document
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#search' do
|
45
|
+
let(:query_params) do
|
46
|
+
{
|
47
|
+
index: 'test_index',
|
48
|
+
query: {
|
49
|
+
query_string: "search term",
|
50
|
+
analyzer: "snowball",
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:response) do
|
56
|
+
{
|
57
|
+
"hits" => {
|
58
|
+
"total" => 4000,
|
59
|
+
"max_score" => 4.222,
|
60
|
+
"hits" => [
|
61
|
+
{"_index" => "test_index",
|
62
|
+
"_type" => "user",
|
63
|
+
"_id" => 42,
|
64
|
+
"_score" => 4.222,
|
65
|
+
"_source" => { "name" => "Joe" }
|
66
|
+
}
|
67
|
+
],
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
let(:query) { double(:query, as_hash: query_params) }
|
73
|
+
|
74
|
+
it 'passes on the query request body to the client' do
|
75
|
+
expect(client).to receive(:search).with(query_params)
|
76
|
+
index.search query
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'returns a Hashie::Mash instance' do
|
80
|
+
expect(client).to receive(:search).with(query_params).and_return(response)
|
81
|
+
response = index.search(query)
|
82
|
+
response.should be_kind_of Hashie::Mash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#refresh' do
|
87
|
+
it 'delegates to the client#indices' do
|
88
|
+
indices = double(:indices, refresh: true)
|
89
|
+
client.stub(:indices).and_return(indices)
|
90
|
+
expect(indices).to receive(:refresh).with(index: 'test_index')
|
91
|
+
index.refresh
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Extensions
|
5
|
+
module Documents
|
6
|
+
describe Indexer do
|
7
|
+
|
8
|
+
let(:indices) { double(:indices) }
|
9
|
+
let(:client) { double(:client, indices: indices) }
|
10
|
+
subject(:indexer) { Indexer.new(client: client) }
|
11
|
+
|
12
|
+
describe '#create_index' do
|
13
|
+
it 'creates the index if it does not exist' do
|
14
|
+
expected_client_params = {
|
15
|
+
index: 'test_index',
|
16
|
+
body: {
|
17
|
+
settings: :fake_settings,
|
18
|
+
mappings: :fake_mappings,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
indices.stub(:exists).and_return(false)
|
22
|
+
expect(indices).to receive(:create).with(expected_client_params)
|
23
|
+
indexer.create_index
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'does not create the index if it exists' do
|
27
|
+
indices.stub(:exists).and_return(true)
|
28
|
+
expect(indices).not_to receive(:create)
|
29
|
+
indexer.create_index
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#drop_index' do
|
34
|
+
it 'drops the index if it exists' do
|
35
|
+
indices.stub(:exists).and_return(true)
|
36
|
+
expect(indices).to receive(:delete)
|
37
|
+
indexer.drop_index
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not drop the index if it does not exist' do
|
41
|
+
indices.stub(:exists).and_return(false)
|
42
|
+
expect(indices).not_to receive(:delete)
|
43
|
+
indexer.drop_index
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#index_exists?' do
|
48
|
+
it 'delegates to the client indices' do
|
49
|
+
expect(indices).to receive(:exists).with(index: 'test_index')
|
50
|
+
indexer.index_exists?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#bulk_index' do
|
55
|
+
let(:models) { [double(:model, id: 1, a: 1, b: 2), double(:model, id: 2, a: 3, b: 4)] }
|
56
|
+
let(:documents) { models.map { |m| TestDocumentsDocument.new(m) } }
|
57
|
+
|
58
|
+
it 'passes operations to the client bulk action' do
|
59
|
+
expected_body = {
|
60
|
+
body: [
|
61
|
+
{
|
62
|
+
index: {
|
63
|
+
_index: 'test_index',
|
64
|
+
_type: 'documents_test',
|
65
|
+
_id: 1,
|
66
|
+
data: { a: 1, b: 2 },
|
67
|
+
}
|
68
|
+
},
|
69
|
+
{
|
70
|
+
index: {
|
71
|
+
_index: 'test_index',
|
72
|
+
_type: 'documents_test',
|
73
|
+
_id: 2,
|
74
|
+
data: { a: 3, b: 4 },
|
75
|
+
},
|
76
|
+
}
|
77
|
+
]
|
78
|
+
}
|
79
|
+
expect(client).to receive(:bulk).with(expected_body)
|
80
|
+
indexer.bulk_index(documents)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#reindex' do
|
85
|
+
|
86
|
+
it 'drops the index if exists' do
|
87
|
+
indices.stub(:exists).and_return(true)
|
88
|
+
expect(indexer).to receive(:drop_index)
|
89
|
+
indexer.reindex
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'creates a new index' do
|
93
|
+
indices.stub(:exists).and_return(false)
|
94
|
+
expect(indexer).to receive(:create_index)
|
95
|
+
indexer.reindex
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'calls a given block to batch index the documents' do
|
99
|
+
indexer.stub(:drop_index)
|
100
|
+
indexer.stub(:create_index)
|
101
|
+
documents = double(:documents)
|
102
|
+
expect(indexer).to receive(:bulk_index).with(documents)
|
103
|
+
indexer.reindex { |indexer| indexer.bulk_index(documents) }
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'elasticsearch/extensions/documents/queryable'
|
3
|
+
|
4
|
+
module Elasticsearch
|
5
|
+
module Extensions
|
6
|
+
module Documents
|
7
|
+
describe Queryable do
|
8
|
+
|
9
|
+
context 'when a subclass is not correctly implemented' do
|
10
|
+
class InvalidQuery
|
11
|
+
include Queryable
|
12
|
+
end
|
13
|
+
|
14
|
+
subject(:query) { InvalidQuery.new }
|
15
|
+
|
16
|
+
it 'raises an error when a query is run' do
|
17
|
+
expect { query.execute }.to raise_error NotImplementedError
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when the query class defines a custom result format' do
|
22
|
+
class CustomResultsQuery
|
23
|
+
include Queryable
|
24
|
+
|
25
|
+
def as_hash
|
26
|
+
{ query: 'foo' }
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_results(raw_results)
|
30
|
+
{ custom_format: raw_results }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
subject(:query) { CustomResultsQuery.new }
|
35
|
+
|
36
|
+
describe '#execute' do
|
37
|
+
it 'provides the search results in a custom format' do
|
38
|
+
query.index.stub(:search).and_return({ foo: :bar})
|
39
|
+
expect(query.execute).to eql({ custom_format: { foo: :bar } })
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#index_name' do
|
44
|
+
it 'provides the index_name from the config' do
|
45
|
+
expect(query.index_name).to eql 'test_index'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'elasticsearch/extensions/documents/utils'
|
3
|
+
|
4
|
+
module Elasticsearch
|
5
|
+
module Extensions
|
6
|
+
module Documents
|
7
|
+
describe Utils do
|
8
|
+
|
9
|
+
describe '.sanitize_for_query_string_query' do
|
10
|
+
subject { Utils.public_method(:sanitize_for_query_string_query) }
|
11
|
+
|
12
|
+
it 'escapes special syntax characters' do
|
13
|
+
%w(/ \\ + - & | ! ( ) { } [ ] ^ ~ * ? :).each do |char|
|
14
|
+
term = 'a' + char + 'b'
|
15
|
+
sanitized_term = 'a\\' + char + 'b'
|
16
|
+
expect(subject.call(term)).to eq sanitized_term
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'escapes boolean terms' do
|
21
|
+
term = 'this AND that'
|
22
|
+
expect(subject.call(term)).to eq 'this \A\N\D that'
|
23
|
+
term = 'this OR that'
|
24
|
+
expect(subject.call(term)).to eq 'this \O\R that'
|
25
|
+
term = 'this NOT that'
|
26
|
+
expect(subject.call(term)).to eq 'this \N\O\T that'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'escapes odd quotes' do
|
30
|
+
term = 'A "quoted" te"m'
|
31
|
+
expect(subject.call(term)).to eq 'A "quoted" te\"m'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'elasticsearch-documents'
|
2
|
+
|
3
|
+
RSpec.configure do |config|
|
4
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
5
|
+
config.run_all_when_everything_filtered = true
|
6
|
+
config.filter_run :focus
|
7
|
+
|
8
|
+
config.order = 'random'
|
9
|
+
end
|
10
|
+
|
11
|
+
Elasticsearch::Extensions::Documents.configure do |config|
|
12
|
+
config.url = 'http://example.com:9200'
|
13
|
+
config.index_name = 'test_index'
|
14
|
+
config.settings = :fake_settings
|
15
|
+
config.mappings = :fake_mappings
|
16
|
+
end
|
17
|
+
|
18
|
+
class TestDocumentsDocument < Elasticsearch::Extensions::Documents::Document
|
19
|
+
indexes_as_type :documents_test
|
20
|
+
|
21
|
+
def as_hash
|
22
|
+
{
|
23
|
+
a: object.a,
|
24
|
+
b: object.b,
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: elasticsearch-documents
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ryan Houston
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-03-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: elasticsearch
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: hashie
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Define mappings to turn model instances into indexable search documents
|
84
|
+
email:
|
85
|
+
- ryanhouston83@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .gitignore
|
91
|
+
- .rspec
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- elasticsearch-documents.gemspec
|
97
|
+
- lib/elasticsearch-documents.rb
|
98
|
+
- lib/elasticsearch/extensions/documents.rb
|
99
|
+
- lib/elasticsearch/extensions/documents/document.rb
|
100
|
+
- lib/elasticsearch/extensions/documents/index.rb
|
101
|
+
- lib/elasticsearch/extensions/documents/indexer.rb
|
102
|
+
- lib/elasticsearch/extensions/documents/queryable.rb
|
103
|
+
- lib/elasticsearch/extensions/documents/utils.rb
|
104
|
+
- lib/elasticsearch/extensions/documents/version.rb
|
105
|
+
- spec/elasticsearch/extensions/documentor_spec.rb
|
106
|
+
- spec/elasticsearch/extensions/documents/document_spec.rb
|
107
|
+
- spec/elasticsearch/extensions/documents/index_spec.rb
|
108
|
+
- spec/elasticsearch/extensions/documents/indexer_spec.rb
|
109
|
+
- spec/elasticsearch/extensions/documents/queryable_spec.rb
|
110
|
+
- spec/elasticsearch/extensions/documents/utils_spec.rb
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
homepage: http://github.com/RyanHouston/elasticsearch-documents
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.9.3
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 2.2.0
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: A service wrapper to manage elasticsearch index documents
|
136
|
+
test_files:
|
137
|
+
- spec/elasticsearch/extensions/documentor_spec.rb
|
138
|
+
- spec/elasticsearch/extensions/documents/document_spec.rb
|
139
|
+
- spec/elasticsearch/extensions/documents/index_spec.rb
|
140
|
+
- spec/elasticsearch/extensions/documents/indexer_spec.rb
|
141
|
+
- spec/elasticsearch/extensions/documents/queryable_spec.rb
|
142
|
+
- spec/elasticsearch/extensions/documents/utils_spec.rb
|
143
|
+
- spec/spec_helper.rb
|