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 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in elasticsearch-documents.gemspec
4
+ gemspec
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,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task default: :spec
6
+
@@ -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,8 @@
1
+ module Elasticsearch
2
+ module Extensions
3
+ module Documents
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
8
+
@@ -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,2 @@
1
+ require "elasticsearch/extensions/documents"
2
+
@@ -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
+
@@ -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