elasticsearch-documents 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 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