es-elasticity 0.2.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 +15 -0
- data/.rspec +1 -0
- data/.simplecov +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +120 -0
- data/Rakefile +4 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/elasticity.gemspec +32 -0
- data/lib/elasticity.rb +5 -0
- data/lib/elasticity/document.rb +100 -0
- data/lib/elasticity/index.rb +114 -0
- data/lib/elasticity/multi_search.rb +46 -0
- data/lib/elasticity/search.rb +159 -0
- data/lib/elasticity/version.rb +3 -0
- data/lib/elasticity_base.rb +60 -0
- data/spec/rspec_config.rb +35 -0
- data/spec/units/document_spec.rb +100 -0
- data/spec/units/index_spec.rb +97 -0
- data/spec/units/multi_search_spec.rb +37 -0
- data/spec/units/search_spec.rb +108 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4316eca2c0cf5953a781ed8233e13eb6b6ae90fd
|
4
|
+
data.tar.gz: f37edd93770533fc4cb5c05b515438abf481aea9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dfc1d0d24057f8e8a3bb890ff6b2812d3db20dbd0972ba07aa957e454bdc61f261fba3eca868397db40e14c6ae649ffb3314312ae288b005c532a7e855bd14d9
|
7
|
+
data.tar.gz: f76f11327b69c509442cda2a6788c5e13252e44a86146d6c5424981508fab1e480592f95a028aee8bf708768418de347255445e6c4d4a7bea3cfedcceb40dc4a
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-Ilib -Ispec -rrspec_config --color --format progress --order rand
|
data/.simplecov
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Rodrigo Kochenburger
|
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,120 @@
|
|
1
|
+
# Elasticity
|
2
|
+
|
3
|
+
Elasticity provides a higher level abstraction on top of [elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby) gem.
|
4
|
+
|
5
|
+
Mainly, it provides a model-oriented approach to ElasticSearch, similar to what [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord) provides to relational databases. It leverages [ActiveModel](https://github.com/rails/rails/tree/master/activemodel) to provide a familiar format for Rails developers.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'elasticity'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install elasticity
|
22
|
+
|
23
|
+
## Usage overview
|
24
|
+
|
25
|
+
The first thing to do, is setup a model class for a index and document type that inherits from `Elasticity::Document`.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class Search::User < Elasticity::Document
|
29
|
+
# All models automatically have the id attribute but you need to define the others.
|
30
|
+
attr_accessor :name, :birthdate
|
31
|
+
|
32
|
+
# Define the index mapping for the index and document type this model represents.
|
33
|
+
self.mappings = {
|
34
|
+
properties: {
|
35
|
+
name: { type: "string" },
|
36
|
+
birthdate: { type: "date" },
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
# Defines a search method.
|
41
|
+
def self.adults
|
42
|
+
date = Date.today - 21.years
|
43
|
+
|
44
|
+
# This is the query that will be submited to ES, same format ES would expect,
|
45
|
+
# translated to a Ruby hash.
|
46
|
+
body = {
|
47
|
+
filter: {
|
48
|
+
{ range: { birthdate: { gte: date.iso8601 }}},
|
49
|
+
},
|
50
|
+
}
|
51
|
+
|
52
|
+
# Creates a search object from the body and return it. The returned object is a
|
53
|
+
# lazy evaluated search that behaves like a collection, being automatically
|
54
|
+
# triggered when data is iterated over.
|
55
|
+
self.search(body)
|
56
|
+
end
|
57
|
+
|
58
|
+
# to_document is the only required method that needs to be implemented so an
|
59
|
+
# instance of this model can be indexed.
|
60
|
+
def to_document
|
61
|
+
{
|
62
|
+
name: self.name,
|
63
|
+
birthdate: self.birthdate.iso8601,
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Then instances of that model can be indexed pretty easily by just calling the `update` method.
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
# Creates a new document on the index
|
73
|
+
u = Search::User.new(id: 1, name: "John", birthdate: Date.civil(1985, 10, 31))
|
74
|
+
u.update
|
75
|
+
|
76
|
+
# Updates the same document on the index
|
77
|
+
u.name = "Jonh Jon"
|
78
|
+
u.update
|
79
|
+
```
|
80
|
+
|
81
|
+
Getting the results of a search is also pretty straightforward:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
# Get the search object, which is an instance of `Elasticity::DocumentSearchProxy`.
|
85
|
+
# Search is not performed until data is accessed.
|
86
|
+
adults = User.adults
|
87
|
+
|
88
|
+
# Iterating over the results will trigger the query
|
89
|
+
adults.each do |user|
|
90
|
+
# do something with user
|
91
|
+
end
|
92
|
+
|
93
|
+
# Or you can also, map the results back to an ActiveRecord relation.
|
94
|
+
# In this case, only the ids will be fetched.
|
95
|
+
adults.active_recors(Database::User) # => Array of Database::User instances
|
96
|
+
```
|
97
|
+
|
98
|
+
## Design Goals
|
99
|
+
|
100
|
+
- Provide model specific for ElasticSearch documents instead of an ActiveRecord mixin;
|
101
|
+
- proper separation of concerns and de-coupling;
|
102
|
+
- lazy search evaluation and easy composition of multi-searches;
|
103
|
+
- easy of debug;
|
104
|
+
- higher level API that resembles ElasticSearch API;
|
105
|
+
|
106
|
+
## Roadmap
|
107
|
+
|
108
|
+
- [ ] Write more detailed documentation section for:
|
109
|
+
- [ ] Model definition
|
110
|
+
- [ ] Indexing, Bulk Indexing and Delete By Query
|
111
|
+
- [ ] Search and Multi Search
|
112
|
+
- [ ] ActiveRecord integration
|
113
|
+
|
114
|
+
## Contributing
|
115
|
+
|
116
|
+
1. Fork it ( https://github.com/[my-github-username]/elasticity/fork )
|
117
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
118
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
119
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
120
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rake', 'rake')
|
data/bin/rspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/elasticity.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'elasticity/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "es-elasticity"
|
8
|
+
spec.version = Elasticity::VERSION
|
9
|
+
spec.authors = ["Rodrigo Kochenburger"]
|
10
|
+
spec.email = ["rodrigo@doximity.com"]
|
11
|
+
spec.summary = %q{ActiveModel-based library for working with ElasticSearch}
|
12
|
+
spec.description = %q{Elasticity provides a higher level abstraction on top of [elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby) gem}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
# spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.executables = []
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
25
|
+
spec.add_development_dependency "simplecov", "~> 0.7.1"
|
26
|
+
spec.add_development_dependency "oj"
|
27
|
+
spec.add_development_dependency "pry"
|
28
|
+
|
29
|
+
spec.add_dependency "activesupport", "~> 4.0.0"
|
30
|
+
spec.add_dependency "activemodel", "~> 4.0.0"
|
31
|
+
spec.add_dependency "elasticsearch", "~> 1.0.5"
|
32
|
+
end
|
data/lib/elasticity.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Elasticity
|
2
|
+
class Document
|
3
|
+
include ::ActiveModel::Model
|
4
|
+
|
5
|
+
# Returns the instance of Elasticity::Index associated with this document.
|
6
|
+
def self.index
|
7
|
+
return @index if defined?(@index)
|
8
|
+
|
9
|
+
index_name = self.name.underscore.pluralize
|
10
|
+
|
11
|
+
if namespace = Elasticity.config.namespace
|
12
|
+
index_name = "#{namespace}_#{index_name}"
|
13
|
+
end
|
14
|
+
|
15
|
+
@index = Index.new(Elasticity.config.client, index_name)
|
16
|
+
@index.create_if_undefined(settings: Elasticity.config.settings, mappings: @mappings)
|
17
|
+
@index
|
18
|
+
end
|
19
|
+
|
20
|
+
# The index name to be used for indexing and storing data for this document model.
|
21
|
+
# By default, it's the class name converted to underscore and plural.
|
22
|
+
def self.index_name
|
23
|
+
self.index.name
|
24
|
+
end
|
25
|
+
|
26
|
+
# The document type to be used, it's inferred by the class name.
|
27
|
+
def self.document_type
|
28
|
+
self.name.underscore
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets the mapping for this model, which will be used to create the associated index and
|
32
|
+
# generate accessor methods.
|
33
|
+
def self.mappings=(mappings)
|
34
|
+
raise "Can't re-define mappings in runtime" if defined?(@mappings)
|
35
|
+
@mappings = mappings
|
36
|
+
end
|
37
|
+
|
38
|
+
# Searches the index using the parameters provided in the body hash, following the same
|
39
|
+
# structure ElasticSearch expects.
|
40
|
+
# Returns a DocumentSearch object.
|
41
|
+
def self.search(body)
|
42
|
+
DocumentSearchProxy.new(Search.new(index, document_type, body), self)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches one specific document from the index by ID.
|
46
|
+
def self.get(id)
|
47
|
+
if doc = index.get_document(document_type, id)
|
48
|
+
new(doc["_source"])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Removes one specific document from the index.
|
53
|
+
def self.delete(id)
|
54
|
+
index.delete_document(document_type, id)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Removes entries based on a search
|
58
|
+
def self.delete_by_search(search)
|
59
|
+
index.delete_by_query(document_type, search.body)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Bulk index the provided documents
|
63
|
+
def self.bulk_index(documents)
|
64
|
+
index.bulk do |b|
|
65
|
+
documents.each do |doc|
|
66
|
+
b.index(self.document_type, doc.id, doc.to_document)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Define common attributes for all documents
|
72
|
+
attr_accessor :id
|
73
|
+
|
74
|
+
# Creates a new Document instance with the provided attributes.
|
75
|
+
def initialize(attributes = {})
|
76
|
+
super(attributes)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Defines equality by comparing the ID and values of each instance variable.
|
80
|
+
def ==(other)
|
81
|
+
return false if id != other.id
|
82
|
+
|
83
|
+
instance_variables.all? do |ivar|
|
84
|
+
instance_variable_get(ivar) == other.instance_variable_get(ivar)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# IMPLEMENT
|
89
|
+
# Returns a hash with the attributes as they should be stored in the index.
|
90
|
+
# This will be stored as _source attributes on ElasticSearch.
|
91
|
+
def to_document
|
92
|
+
raise NotImplementedError, "to_document needs to be implemented for #{self.class}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Update this object on the index, creating or updating the document.
|
96
|
+
def update
|
97
|
+
self.class.index.index_document(self.class.document_type, id, to_document)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Elasticity
|
2
|
+
class Index
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(client, index_name)
|
6
|
+
@client = client
|
7
|
+
@name = index_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(index_def)
|
11
|
+
args = { index: @name, body: index_def }
|
12
|
+
instrument("index_create", args) { @client.indices.create(args) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_if_undefined(index_def)
|
16
|
+
create(index_def) unless @client.indices.exists(index: @name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete
|
20
|
+
args = { index: @name }
|
21
|
+
instrument("index_delete", args) { @client.indices.delete(args) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete_if_defined
|
25
|
+
delete if @client.indices.exists(index: @name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def recreate(index_def = nil)
|
29
|
+
index_def ||= { settings: settings, mappings: mappings }
|
30
|
+
delete_if_defined
|
31
|
+
create(index_def)
|
32
|
+
end
|
33
|
+
|
34
|
+
def index_document(type, id, attributes)
|
35
|
+
args = { index: @name, type: type, id: id, body: attributes }
|
36
|
+
instrument("index_document", args) { @client.index(args) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def delete_document(type, id)
|
40
|
+
args = { index: @name, type: type, id: id }
|
41
|
+
instrument("delete_document", args) { @client.delete(args) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def get_document(type, id)
|
45
|
+
args = { index: @name, type: type, id: id }
|
46
|
+
instrument("get_document", args) { @client.get(args) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def search(type, body)
|
50
|
+
args = { index: @name, type: type, body: body }
|
51
|
+
instrument("search", args) { @client.search(args) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_by_query(type, body)
|
55
|
+
args = { index: @name, type: type, body: body }
|
56
|
+
instrument("delete_by_query", args) { @client.delete_by_query(args) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def bulk
|
60
|
+
b = Bulk.new(@client, @name)
|
61
|
+
yield b
|
62
|
+
b.execute
|
63
|
+
end
|
64
|
+
|
65
|
+
def settings
|
66
|
+
args = { index: @name }
|
67
|
+
settings = instrument("settings", args) { @client.indices.get_settings(args) }
|
68
|
+
settings[@name]["settings"] if settings[@name]
|
69
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def mappings
|
74
|
+
args = { index: @name }
|
75
|
+
mappings = instrument("mappings", args) { @client.indices.get_mapping(args) }
|
76
|
+
mappings[@name]["mappings"] if mappings[@name]
|
77
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def flush
|
82
|
+
args = { index: @name }
|
83
|
+
instrument("flush", args) { @client.indices.flush(args) }
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def instrument(name, extra = {})
|
89
|
+
ActiveSupport::Notifications.instrument("elasticity.#{name}", args: extra) do
|
90
|
+
yield
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Bulk
|
95
|
+
def initialize(client, name)
|
96
|
+
@client = client
|
97
|
+
@name = name
|
98
|
+
@operations = []
|
99
|
+
end
|
100
|
+
|
101
|
+
def index(type, id, attributes)
|
102
|
+
@operations << { index: { _index: @name, _type: type, _id: id, data: attributes }}
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete(type, id)
|
106
|
+
@operations << { delete: { _index: @name, _type: type, _id: id }}
|
107
|
+
end
|
108
|
+
|
109
|
+
def execute
|
110
|
+
@client.bulk(body: @operations)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Elasticity
|
2
|
+
class MultiSearch
|
3
|
+
def initialize
|
4
|
+
@searches = []
|
5
|
+
yield self if block_given?
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(name, search, documents: nil, active_records: nil)
|
9
|
+
mapper = case
|
10
|
+
when documents && active_records
|
11
|
+
raise ArgumentError, "you can only pass either :documents or :active_records as an option"
|
12
|
+
when documents
|
13
|
+
Search::DocumentMapper.new(documents)
|
14
|
+
when active_records
|
15
|
+
Search::ActiveRecordMapper.new(active_records)
|
16
|
+
else
|
17
|
+
raise ArgumentError, "you need to provide either :documents or :active_records as an option"
|
18
|
+
end
|
19
|
+
|
20
|
+
@searches << [name, search, mapper]
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](name)
|
24
|
+
@results ||= fetch
|
25
|
+
@results[name]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch
|
31
|
+
multi_body = @searches.map do |name, search, _|
|
32
|
+
{ index: search.index.name, type: search.document_type, body: search.body }
|
33
|
+
end
|
34
|
+
|
35
|
+
results = {}
|
36
|
+
|
37
|
+
responses = Array(Elasticity.config.client.msearch(body: multi_body)["responses"])
|
38
|
+
responses.each_with_index do |resp, idx|
|
39
|
+
name, search, mapper = @searches[idx]
|
40
|
+
results[name] = Search::Result.new(resp, mapper)
|
41
|
+
end
|
42
|
+
|
43
|
+
results
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Elasticity
|
2
|
+
# Search provides a simple interface for defining a search against an ElasticSearch
|
3
|
+
# index and fetching the results in different ways and mappings.
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# search = Elasticity::Search.new("people", "person", {...})
|
7
|
+
# search.documents(Person)
|
8
|
+
class Search
|
9
|
+
attr_reader :index, :document_type, :body
|
10
|
+
|
11
|
+
# Creates a new Search definitions for the given index, document_type and criteria. The
|
12
|
+
# search is not performend until methods are called, each method represents a different
|
13
|
+
# way of fetching and mapping the data.
|
14
|
+
#
|
15
|
+
# The body parameter is a hash following the exact same syntax as ElasticSearch's JSON
|
16
|
+
# query language.
|
17
|
+
def initialize(index, document_type, body)
|
18
|
+
@index = index
|
19
|
+
@document_type = document_type.freeze
|
20
|
+
@body = body.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
# Execute the search, fetching only ids from ElasticSearch and then mapping the results
|
24
|
+
# into ActiveRecord models using the provided relation.
|
25
|
+
def active_records(relation)
|
26
|
+
return @active_record if defined?(@active_record)
|
27
|
+
response = @index.search(@document_type, @body.merge(_source: []))
|
28
|
+
@active_record = Result.new(response, ActiveRecordMapper.new(relation))
|
29
|
+
end
|
30
|
+
|
31
|
+
# Execute the search, fetching all documents from the index and mapping the stored attributes
|
32
|
+
# into instances of the provided class. It will call document_klass.new(attrs), where attrs
|
33
|
+
# are the stored attributes.
|
34
|
+
def documents(document_klass)
|
35
|
+
return @documents if defined?(@documents)
|
36
|
+
response = @index.search(@document_type, @body)
|
37
|
+
@documents = Result.new(response, DocumentMapper.new(document_klass))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Result is a collection representing the response from a search against an index. It's what gets
|
41
|
+
# returned by any of the Elasticity::Search methods and it provides a lazily-evaluated and
|
42
|
+
# lazily-mapped – using the provided mapper class.
|
43
|
+
#
|
44
|
+
# Example:
|
45
|
+
#
|
46
|
+
# response = {"took"=>0, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, "hits"=>{"total"=>2, "max_score"=>1.0, "hits"=>[
|
47
|
+
# {"_index"=>"my_index", "_type"=>"my_type", "_id"=>"1", "_score"=>1.0, "_source"=> { "id" => 1, "name" => "Foo" },
|
48
|
+
# {"_index"=>"my_index", "_type"=>"my_type", "_id"=>"2", "_score"=>1.0, "_source"=> { "id" => 2, "name" => "Bar" },
|
49
|
+
# ]}}
|
50
|
+
#
|
51
|
+
# class AttributesMapper
|
52
|
+
# def map(hits)
|
53
|
+
# hits.map { |h| h["_source"] }
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# r = Result.new(response, AttributesMapper.new)
|
58
|
+
# r.total # => 2
|
59
|
+
# r[0] # => { "id" => 1, "name" => "Foo" }
|
60
|
+
#
|
61
|
+
class Result
|
62
|
+
include Enumerable
|
63
|
+
|
64
|
+
def initialize(response, mapper)
|
65
|
+
@response = response
|
66
|
+
@mapper = mapper
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](idx)
|
70
|
+
mapping[idx]
|
71
|
+
end
|
72
|
+
|
73
|
+
def each(&block)
|
74
|
+
mapping.each(&block)
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_ary
|
78
|
+
mapping.to_ary
|
79
|
+
end
|
80
|
+
|
81
|
+
def size
|
82
|
+
mapping.size
|
83
|
+
end
|
84
|
+
|
85
|
+
# The total number of entries as returned by ES
|
86
|
+
def total
|
87
|
+
@response["hits"]["total"]
|
88
|
+
end
|
89
|
+
|
90
|
+
def empty?
|
91
|
+
total == 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def blank?
|
95
|
+
empty?
|
96
|
+
end
|
97
|
+
|
98
|
+
def mapping
|
99
|
+
return @mapping if defined?(@mapping)
|
100
|
+
hits = Array(@response["hits"]["hits"])
|
101
|
+
@mapping = @mapper.map(hits)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class DocumentMapper
|
106
|
+
def initialize(document_klass)
|
107
|
+
@document_klass = document_klass
|
108
|
+
end
|
109
|
+
|
110
|
+
def map(hits)
|
111
|
+
hits.map do |hit|
|
112
|
+
@document_klass.new(hit["_source"])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class ActiveRecordMapper
|
118
|
+
def initialize(relation)
|
119
|
+
@relation = relation
|
120
|
+
end
|
121
|
+
|
122
|
+
def map(hits)
|
123
|
+
ids = hits.map { |h| h["_source"]["id"] }
|
124
|
+
|
125
|
+
if ids.any?
|
126
|
+
id_col = "#{quote(@relation.table_name)}.#{quote(@relation.klass.primary_key)}"
|
127
|
+
@relation.where(id: ids).order("FIELD(#{id_col},#{ids.join(',')})")
|
128
|
+
else
|
129
|
+
@relation.none
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def quote(identifier)
|
136
|
+
@relation.connection.quote_column_name(identifier)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class DocumentSearchProxy < BasicObject
|
142
|
+
def initialize(search, document_klass)
|
143
|
+
@search = search
|
144
|
+
@document_klass = document_klass
|
145
|
+
end
|
146
|
+
|
147
|
+
def active_records(relation)
|
148
|
+
@search.active_records(relation)
|
149
|
+
end
|
150
|
+
|
151
|
+
def documents
|
152
|
+
@search.documents(@document_klass)
|
153
|
+
end
|
154
|
+
|
155
|
+
def method_missing(method_name, *args, &block)
|
156
|
+
documents.public_send(method_name, *args, &block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
require "active_support"
|
6
|
+
require "active_support/core_ext"
|
7
|
+
require "active_model"
|
8
|
+
require "elasticsearch"
|
9
|
+
|
10
|
+
module Elasticity
|
11
|
+
class Config
|
12
|
+
attr_writer :logger, :client, :settings, :namespace, :pretty_json
|
13
|
+
|
14
|
+
def logger
|
15
|
+
return @logger if defined?(@logger)
|
16
|
+
@logger = Logger.new(STDOUT)
|
17
|
+
end
|
18
|
+
|
19
|
+
def client
|
20
|
+
return @client if defined?(@client)
|
21
|
+
@client = Elasticsearch::Client.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def settings
|
25
|
+
return @settings if defined?(@settings)
|
26
|
+
@settings = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def namespace
|
30
|
+
@namespace
|
31
|
+
end
|
32
|
+
|
33
|
+
def pretty_json
|
34
|
+
@pretty_json || false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.configure
|
39
|
+
@config = Config.new
|
40
|
+
yield(@config)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.config
|
44
|
+
return @config if defined?(@config)
|
45
|
+
@config = Config.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
ActiveSupport::Notifications.subscribe(/^elasticity\./) do |name, start, finish, id, payload|
|
50
|
+
time = (finish - start)*1000
|
51
|
+
|
52
|
+
if logger = Elasticity.config.logger
|
53
|
+
logger.debug "#{name} #{"%.2f" % time}ms #{MultiJson.dump(payload[:args], pretty: Elasticity.config.pretty_json)}"
|
54
|
+
|
55
|
+
exception, message = payload[:exception]
|
56
|
+
if exception
|
57
|
+
logger.error "#{name} #{exception}: #{message}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "elasticity_base"
|
2
|
+
require "simplecov"
|
3
|
+
require "oj"
|
4
|
+
require "elasticity"
|
5
|
+
|
6
|
+
def elastic_search_client
|
7
|
+
return @elastic_search_client if defined?(@elastic_search_client)
|
8
|
+
@elastic_search_client = Elasticsearch::Client.new host: "http://0.0.0.0:9200"
|
9
|
+
end
|
10
|
+
|
11
|
+
logger = Logger.new("spec/spec.log", File::WRONLY | File::APPEND | File::CREAT)
|
12
|
+
|
13
|
+
RSpec.configure do |c|
|
14
|
+
c.disable_monkey_patching!
|
15
|
+
|
16
|
+
c.before(:suite) do
|
17
|
+
logger.info "rspec.init Starting test suite execution"
|
18
|
+
end
|
19
|
+
|
20
|
+
c.before(:each) do |example|
|
21
|
+
logger.info "rspec.spec #{example.full_description}"
|
22
|
+
|
23
|
+
if example.metadata[:elasticsearch]
|
24
|
+
client = elastic_search_client
|
25
|
+
else
|
26
|
+
client = double(:elasticsearch_client)
|
27
|
+
end
|
28
|
+
|
29
|
+
Elasticity.configure do |e|
|
30
|
+
e.logger = logger
|
31
|
+
e.client = client
|
32
|
+
e.namespace = "elasticity_test"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "elasticity/search"
|
2
|
+
|
3
|
+
RSpec.describe Elasticity::Document do
|
4
|
+
mappings = {
|
5
|
+
properties: {
|
6
|
+
id: { type: "integer" },
|
7
|
+
name: { type: "string" },
|
8
|
+
|
9
|
+
items: {
|
10
|
+
type: "nested",
|
11
|
+
properties: {
|
12
|
+
name: { type: "string" },
|
13
|
+
},
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
let :klass do
|
19
|
+
Class.new(described_class) do
|
20
|
+
# Override the name since this is an anonymous class
|
21
|
+
def self.name
|
22
|
+
"ClassName"
|
23
|
+
end
|
24
|
+
|
25
|
+
self.mappings = mappings
|
26
|
+
|
27
|
+
attr_accessor :name, :items
|
28
|
+
|
29
|
+
def to_document
|
30
|
+
{ id: id, name: name, items: items}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
let :index do
|
36
|
+
double(:index, create_if_undefined: nil, name: "elasticity_test_class_names")
|
37
|
+
end
|
38
|
+
|
39
|
+
before :each do
|
40
|
+
allow(Elasticity::Index).to receive(:new).and_return(index)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "requires subclasses to define to_document method" do
|
44
|
+
expect { Class.new(described_class).new.to_document }.to raise_error(NotImplementedError)
|
45
|
+
end
|
46
|
+
|
47
|
+
context "class" do
|
48
|
+
subject { klass }
|
49
|
+
|
50
|
+
it "extracts index name and document type from the class name" do
|
51
|
+
expect(subject.index_name).to eq "elasticity_test_class_names"
|
52
|
+
expect(subject.document_type).to eq "class_name"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "have an associated Index instance" do
|
56
|
+
client = double(:client)
|
57
|
+
settings = double(:settings)
|
58
|
+
|
59
|
+
Elasticity.config.settings = settings
|
60
|
+
Elasticity.config.client = client
|
61
|
+
|
62
|
+
expect(Elasticity::Index).to receive(:new).with(client, "elasticity_test_class_names").and_return(index)
|
63
|
+
expect(index).to receive(:create_if_undefined).with(settings: settings, mappings: mappings)
|
64
|
+
|
65
|
+
expect(subject.index).to be index
|
66
|
+
end
|
67
|
+
|
68
|
+
it "searches using DocumentSearch" do
|
69
|
+
body = double(:body)
|
70
|
+
search = double(:search)
|
71
|
+
expect(Elasticity::Search).to receive(:new).with(index, "class_name", body).and_return(search)
|
72
|
+
|
73
|
+
doc_search = double(:doc_search)
|
74
|
+
expect(Elasticity::DocumentSearchProxy).to receive(:new).with(search, subject).and_return(doc_search)
|
75
|
+
|
76
|
+
expect(subject.search(body)).to be doc_search
|
77
|
+
end
|
78
|
+
|
79
|
+
it "gets specific document from the index" do
|
80
|
+
doc = { "_source" => { "id" => 1, "name" => "Foo", "items" => [{ "name" => "Item1" }]}}
|
81
|
+
expect(index).to receive(:get_document).with("class_name", 1).and_return(doc)
|
82
|
+
expect(subject.get(1)).to eq klass.new(id: 1, name: "Foo", items: [{ "name" => "Item1" }])
|
83
|
+
end
|
84
|
+
|
85
|
+
it "deletes specific document from index" do
|
86
|
+
index_ret = double(:index_return)
|
87
|
+
expect(index).to receive(:delete_document).with("class_name", 1).and_return(index_ret)
|
88
|
+
expect(subject.delete(1)).to eq index_ret
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "instance" do
|
93
|
+
subject { klass.new id: 1, name: "Foo", items: [{ name: "Item1" }] }
|
94
|
+
|
95
|
+
it "stores the document in the index" do
|
96
|
+
expect(index).to receive(:index_document).with("class_name", 1, {id: 1, name: "Foo", items: [{ name: "Item1" }]})
|
97
|
+
subject.update
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "elasticity/index"
|
2
|
+
|
3
|
+
RSpec.describe Elasticity::Index, elasticsearch: true do
|
4
|
+
subject do
|
5
|
+
described_class.new(Elasticity.config.client, "test_index_name")
|
6
|
+
end
|
7
|
+
|
8
|
+
let :index_def do
|
9
|
+
{
|
10
|
+
mappings: {
|
11
|
+
document: {
|
12
|
+
properties: {
|
13
|
+
name: { type: "string" }
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
after do
|
21
|
+
subject.delete_if_defined
|
22
|
+
end
|
23
|
+
|
24
|
+
it "allows creating, recreating and deleting an index" do
|
25
|
+
subject.create(index_def)
|
26
|
+
expect(subject.mappings).to eq({"document"=>{"properties"=>{"name"=>{"type"=>"string"}}}})
|
27
|
+
|
28
|
+
subject.recreate
|
29
|
+
expect(subject.mappings).to eq({"document"=>{"properties"=>{"name"=>{"type"=>"string"}}}})
|
30
|
+
|
31
|
+
subject.delete
|
32
|
+
expect(subject.mappings).to be nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns nil for mapping and settings when index does not exist" do
|
36
|
+
expect(subject.mappings).to be nil
|
37
|
+
expect(subject.settings).to be nil
|
38
|
+
end
|
39
|
+
|
40
|
+
context "with existing index" do
|
41
|
+
before do
|
42
|
+
subject.create_if_undefined(index_def)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "allows adding, getting and removing documents from the index" do
|
46
|
+
subject.index_document("document", 1, name: "test")
|
47
|
+
|
48
|
+
doc = subject.get_document("document", 1)
|
49
|
+
expect(doc["_source"]["name"]).to eq("test")
|
50
|
+
|
51
|
+
subject.delete_document("document", 1)
|
52
|
+
expect { subject.get_document("document", 1) }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "allows batching index and delete actions" do
|
56
|
+
results_a = subject.bulk do |b|
|
57
|
+
b.index "document", 1, name: "foo"
|
58
|
+
end
|
59
|
+
expect(results_a).to include("errors"=>false, "items"=>[{"index"=>{"_index"=>"test_index_name", "_type"=>"document", "_id"=>"1", "_version"=>1, "status"=>201}}])
|
60
|
+
|
61
|
+
results_b = subject.bulk do |b|
|
62
|
+
b.index "document", 2, name: "bar"
|
63
|
+
b.delete "document", 1
|
64
|
+
end
|
65
|
+
expect(results_b).to include("errors"=>false, "items"=>[{"index"=>{"_index"=>"test_index_name", "_type"=>"document", "_id"=>"2", "_version"=>1, "status"=>201}}, {"delete"=>{"_index"=>"test_index_name", "_type"=>"document", "_id"=>"1", "_version"=>2, "status"=>200, "found"=>true}}])
|
66
|
+
|
67
|
+
subject.flush
|
68
|
+
|
69
|
+
expect { subject.get_document("document", 1) }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
70
|
+
expect(subject.get_document("document", 2)).to eq({"_index"=>"test_index_name", "_type"=>"document", "_id"=>"2", "_version"=>1, "found"=>true, "_source"=>{"name"=>"bar"}})
|
71
|
+
end
|
72
|
+
|
73
|
+
it "allows searching documents" do
|
74
|
+
subject.index_document("document", 1, name: "test")
|
75
|
+
subject.flush
|
76
|
+
results = subject.search("document", filter: { term: { name: "test" }})
|
77
|
+
|
78
|
+
expect(results["hits"]["total"]).to be 1
|
79
|
+
|
80
|
+
doc = results["hits"]["hits"][0]
|
81
|
+
expect(doc["_id"]).to eq "1"
|
82
|
+
expect(doc["_source"]).to eq({ "name" => "test" })
|
83
|
+
end
|
84
|
+
|
85
|
+
it "allows deleting by queryu" do
|
86
|
+
subject.index_document("document", 1, name: "foo")
|
87
|
+
subject.index_document("document", 2, name: "bar")
|
88
|
+
|
89
|
+
subject.delete_by_query("document", query: { term: { name: "foo" }})
|
90
|
+
|
91
|
+
expect { subject.get_document("document", 1) }.to raise_error(Elasticsearch::Transport::Transport::Errors::NotFound)
|
92
|
+
expect { subject.get_document("document", 2) }.to_not raise_error
|
93
|
+
|
94
|
+
subject.flush
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "elasticity/search"
|
2
|
+
require "elasticity/multi_search"
|
3
|
+
|
4
|
+
RSpec.describe Elasticity::MultiSearch do
|
5
|
+
let :klass do
|
6
|
+
Class.new do
|
7
|
+
include ActiveModel::Model
|
8
|
+
attr_accessor :id, :name
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
self.id == other.id && self.name == other.name
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
let :response do
|
17
|
+
{
|
18
|
+
"responses" => [
|
19
|
+
{ "hits" => { "total" => 2, "hits" => [{"_source" => { "id" => 1, "name" => "foo" }}, {"_source" => { "id" => 2, "name" => "bar" }}]}},
|
20
|
+
{ "hits" => { "total" => 1, "hits" => [{"_source" => { "id" => 3, "name" => "baz" }}]}},
|
21
|
+
]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
it "performs multi search" do
|
26
|
+
subject.add(:first, Elasticity::Search.new(double(:index, name: "index_first"), "document_first", { search: :first }), documents: klass)
|
27
|
+
subject.add(:second, Elasticity::Search.new(double(:index, name: "index_second"), "document_second", { search: :second }), documents: klass)
|
28
|
+
|
29
|
+
expect(Elasticity.config.client).to receive(:msearch).with(body: [
|
30
|
+
{ index: "index_first", type: "document_first", body: { search: :first } },
|
31
|
+
{ index: "index_second", type: "document_second", body: { search: :second } },
|
32
|
+
]).and_return(response)
|
33
|
+
|
34
|
+
expect(Array(subject[:first])). to eq [klass.new(id: 1, name: "foo"), klass.new(id: 2, name: "bar")]
|
35
|
+
expect(Array(subject[:second])). to eq [klass.new(id: 3, name: "baz")]
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "elasticity/search"
|
2
|
+
|
3
|
+
RSpec.describe "Search" do
|
4
|
+
let(:index) { double(:index) }
|
5
|
+
let(:document_type) { "document" }
|
6
|
+
let(:body) { {} }
|
7
|
+
|
8
|
+
let :full_response do
|
9
|
+
{ "hits" => { "total" => 2, "hits" => [
|
10
|
+
{"_source" => { "id" => 1, "name" => "foo" }},
|
11
|
+
{"_source" => { "id" => 2, "name" => "bar" }},
|
12
|
+
]}}
|
13
|
+
end
|
14
|
+
|
15
|
+
let :ids_response do
|
16
|
+
{ "hits" => { "total" => 2, "hits" => [
|
17
|
+
{"_source" => { "id" => 1, "name" => "foo" }},
|
18
|
+
{"_source" => { "id" => 2, "name" => "bar" }},
|
19
|
+
]}}
|
20
|
+
end
|
21
|
+
|
22
|
+
let :empty_response do
|
23
|
+
{ "hits" => { "total" => 0, "hits" => [] }}
|
24
|
+
end
|
25
|
+
|
26
|
+
let :klass do
|
27
|
+
Class.new do
|
28
|
+
include ActiveModel::Model
|
29
|
+
attr_accessor :id, :name
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
self.id == other.id && self.name == other.name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe Elasticity::Search do
|
38
|
+
subject do
|
39
|
+
described_class.new(index, document_type, body)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "searches the index and return document models" do
|
43
|
+
expect(index).to receive(:search).with(document_type, body).and_return(full_response)
|
44
|
+
|
45
|
+
docs = subject.documents(klass)
|
46
|
+
expected = [klass.new(id: 1, name: "foo"), klass.new(id: 2, name: "bar")]
|
47
|
+
|
48
|
+
expect(docs.total).to eq 2
|
49
|
+
expect(docs.size).to eq expected.size
|
50
|
+
|
51
|
+
expect(docs).to_not be_empty
|
52
|
+
expect(docs).to_not be_blank
|
53
|
+
|
54
|
+
expect(docs[0].name).to eq expected[0].name
|
55
|
+
expect(docs[1].name).to eq expected[1].name
|
56
|
+
|
57
|
+
expect(docs.each.first).to eq expected[0]
|
58
|
+
expect(Array(docs)).to eq expected
|
59
|
+
end
|
60
|
+
|
61
|
+
it "searches the index and return active record models" do
|
62
|
+
expect(index).to receive(:search).with(document_type, body.merge(_source: ["id"])).and_return(ids_response)
|
63
|
+
|
64
|
+
relation = double(:relation,
|
65
|
+
connection: double(:connection),
|
66
|
+
table_name: "table_name",
|
67
|
+
klass: double(:klass, primary_key: "id"),
|
68
|
+
)
|
69
|
+
allow(relation.connection).to receive(:quote_column_name) { |name| name }
|
70
|
+
|
71
|
+
expect(relation).to receive(:where).with(id: [1,2]).and_return(relation)
|
72
|
+
expect(relation).to receive(:order).with("FIELD(table_name.id,1,2)").and_return(relation)
|
73
|
+
|
74
|
+
expect(subject.active_records(relation).mapping).to be relation
|
75
|
+
end
|
76
|
+
|
77
|
+
it "return relation.none from activerecord relation with no matches" do
|
78
|
+
expect(index).to receive(:search).with(document_type, body.merge(_source: ["id"])).and_return(empty_response)
|
79
|
+
|
80
|
+
relation = double(:relation)
|
81
|
+
expect(relation).to receive(:none).and_return(relation)
|
82
|
+
|
83
|
+
expect(subject.active_records(relation).mapping).to be relation
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe Elasticity::DocumentSearchProxy do
|
88
|
+
let :search do
|
89
|
+
Elasticity::Search.new(index, "document", body)
|
90
|
+
end
|
91
|
+
|
92
|
+
subject do
|
93
|
+
described_class.new(search, klass)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "automatically maps the documents into the provided Document class" do
|
97
|
+
expect(index).to receive(:search).with(document_type, body).and_return(full_response)
|
98
|
+
expect(Array(subject)).to eq [klass.new(id: 1, name: "foo"), klass.new(id: 2, name: "bar")]
|
99
|
+
end
|
100
|
+
|
101
|
+
it "delegates active_records for the underlying search" do
|
102
|
+
records = double(:records)
|
103
|
+
rel = double(:relation)
|
104
|
+
expect(search).to receive(:active_records).with(rel).and_return(records)
|
105
|
+
expect(subject.active_records(rel)).to be records
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
metadata
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: es-elasticity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rodrigo Kochenburger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-07 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.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.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: 3.1.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: simplecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.7.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.7.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: oj
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activesupport
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 4.0.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 4.0.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activemodel
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 4.0.0
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 4.0.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: elasticsearch
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.0.5
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 1.0.5
|
139
|
+
description: Elasticity provides a higher level abstraction on top of [elasticsearch-ruby](https://github.com/elasticsearch/elasticsearch-ruby)
|
140
|
+
gem
|
141
|
+
email:
|
142
|
+
- rodrigo@doximity.com
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".simplecov"
|
150
|
+
- Gemfile
|
151
|
+
- LICENSE.txt
|
152
|
+
- README.md
|
153
|
+
- Rakefile
|
154
|
+
- bin/rake
|
155
|
+
- bin/rspec
|
156
|
+
- elasticity.gemspec
|
157
|
+
- lib/elasticity.rb
|
158
|
+
- lib/elasticity/document.rb
|
159
|
+
- lib/elasticity/index.rb
|
160
|
+
- lib/elasticity/multi_search.rb
|
161
|
+
- lib/elasticity/search.rb
|
162
|
+
- lib/elasticity/version.rb
|
163
|
+
- lib/elasticity_base.rb
|
164
|
+
- spec/rspec_config.rb
|
165
|
+
- spec/units/document_spec.rb
|
166
|
+
- spec/units/index_spec.rb
|
167
|
+
- spec/units/multi_search_spec.rb
|
168
|
+
- spec/units/search_spec.rb
|
169
|
+
homepage: ''
|
170
|
+
licenses:
|
171
|
+
- MIT
|
172
|
+
metadata: {}
|
173
|
+
post_install_message:
|
174
|
+
rdoc_options: []
|
175
|
+
require_paths:
|
176
|
+
- lib
|
177
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
requirements: []
|
188
|
+
rubyforge_project:
|
189
|
+
rubygems_version: 2.2.2
|
190
|
+
signing_key:
|
191
|
+
specification_version: 4
|
192
|
+
summary: ActiveModel-based library for working with ElasticSearch
|
193
|
+
test_files:
|
194
|
+
- spec/rspec_config.rb
|
195
|
+
- spec/units/document_spec.rb
|
196
|
+
- spec/units/index_spec.rb
|
197
|
+
- spec/units/multi_search_spec.rb
|
198
|
+
- spec/units/search_spec.rb
|
199
|
+
has_rdoc:
|