elasticsearch-documents 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 +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
|