elasticsearch-persistence-queryable 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +16 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +678 -0
- data/Rakefile +57 -0
- data/elasticsearch-persistence.gemspec +57 -0
- data/examples/music/album.rb +34 -0
- data/examples/music/artist.rb +50 -0
- data/examples/music/artists/_form.html.erb +8 -0
- data/examples/music/artists/artists_controller.rb +67 -0
- data/examples/music/artists/artists_controller_test.rb +53 -0
- data/examples/music/artists/index.html.erb +57 -0
- data/examples/music/artists/show.html.erb +51 -0
- data/examples/music/assets/application.css +226 -0
- data/examples/music/assets/autocomplete.css +48 -0
- data/examples/music/assets/blank_cover.png +0 -0
- data/examples/music/assets/form.css +113 -0
- data/examples/music/index_manager.rb +60 -0
- data/examples/music/search/index.html.erb +93 -0
- data/examples/music/search/search_controller.rb +41 -0
- data/examples/music/search/search_controller_test.rb +9 -0
- data/examples/music/search/search_helper.rb +15 -0
- data/examples/music/suggester.rb +45 -0
- data/examples/music/template.rb +392 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +7 -0
- data/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +6 -0
- data/examples/notes/.gitignore +7 -0
- data/examples/notes/Gemfile +28 -0
- data/examples/notes/README.markdown +36 -0
- data/examples/notes/application.rb +238 -0
- data/examples/notes/config.ru +7 -0
- data/examples/notes/test.rb +118 -0
- data/lib/elasticsearch/per_thread_registry.rb +53 -0
- data/lib/elasticsearch/persistence/client.rb +51 -0
- data/lib/elasticsearch/persistence/inheritence.rb +9 -0
- data/lib/elasticsearch/persistence/model/base.rb +95 -0
- data/lib/elasticsearch/persistence/model/callbacks.rb +37 -0
- data/lib/elasticsearch/persistence/model/errors.rb +9 -0
- data/lib/elasticsearch/persistence/model/find.rb +155 -0
- data/lib/elasticsearch/persistence/model/gateway_delegation.rb +23 -0
- data/lib/elasticsearch/persistence/model/hash_wrapper.rb +17 -0
- data/lib/elasticsearch/persistence/model/rails.rb +39 -0
- data/lib/elasticsearch/persistence/model/store.rb +271 -0
- data/lib/elasticsearch/persistence/model.rb +148 -0
- data/lib/elasticsearch/persistence/null_relation.rb +56 -0
- data/lib/elasticsearch/persistence/query_cache.rb +68 -0
- data/lib/elasticsearch/persistence/querying.rb +21 -0
- data/lib/elasticsearch/persistence/relation/delegation.rb +130 -0
- data/lib/elasticsearch/persistence/relation/finder_methods.rb +39 -0
- data/lib/elasticsearch/persistence/relation/merger.rb +179 -0
- data/lib/elasticsearch/persistence/relation/query_builder.rb +279 -0
- data/lib/elasticsearch/persistence/relation/query_methods.rb +362 -0
- data/lib/elasticsearch/persistence/relation/search_option_methods.rb +44 -0
- data/lib/elasticsearch/persistence/relation/spawn_methods.rb +61 -0
- data/lib/elasticsearch/persistence/relation.rb +110 -0
- data/lib/elasticsearch/persistence/repository/class.rb +71 -0
- data/lib/elasticsearch/persistence/repository/find.rb +73 -0
- data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
- data/lib/elasticsearch/persistence/repository/response/results.rb +105 -0
- data/lib/elasticsearch/persistence/repository/search.rb +156 -0
- data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
- data/lib/elasticsearch/persistence/repository/store.rb +94 -0
- data/lib/elasticsearch/persistence/repository.rb +77 -0
- data/lib/elasticsearch/persistence/scoping/default.rb +137 -0
- data/lib/elasticsearch/persistence/scoping/named.rb +70 -0
- data/lib/elasticsearch/persistence/scoping.rb +52 -0
- data/lib/elasticsearch/persistence/version.rb +5 -0
- data/lib/elasticsearch/persistence.rb +157 -0
- data/lib/elasticsearch/rails_compatibility.rb +17 -0
- data/lib/rails/generators/elasticsearch/model/model_generator.rb +21 -0
- data/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +9 -0
- data/lib/rails/generators/elasticsearch_generator.rb +2 -0
- data/lib/rails/instrumentation/railtie.rb +31 -0
- data/lib/rails/instrumentation.rb +10 -0
- data/test/integration/model/model_basic_test.rb +157 -0
- data/test/integration/repository/custom_class_test.rb +85 -0
- data/test/integration/repository/customized_class_test.rb +82 -0
- data/test/integration/repository/default_class_test.rb +114 -0
- data/test/integration/repository/virtus_model_test.rb +114 -0
- data/test/test_helper.rb +53 -0
- data/test/unit/model_base_test.rb +48 -0
- data/test/unit/model_find_test.rb +148 -0
- data/test/unit/model_gateway_test.rb +99 -0
- data/test/unit/model_rails_test.rb +88 -0
- data/test/unit/model_store_test.rb +514 -0
- data/test/unit/persistence_test.rb +32 -0
- data/test/unit/repository_class_test.rb +51 -0
- data/test/unit/repository_client_test.rb +32 -0
- data/test/unit/repository_find_test.rb +388 -0
- data/test/unit/repository_indexing_test.rb +37 -0
- data/test/unit/repository_module_test.rb +146 -0
- data/test/unit/repository_naming_test.rb +146 -0
- data/test/unit/repository_response_results_test.rb +98 -0
- data/test/unit/repository_search_test.rb +117 -0
- data/test/unit/repository_serialize_test.rb +57 -0
- data/test/unit/repository_store_test.rb +303 -0
- metadata +487 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
require 'elasticsearch/persistence/relation/merger'
|
4
|
+
|
5
|
+
module Elasticsearch
|
6
|
+
module Persistence
|
7
|
+
|
8
|
+
module SpawnMethods
|
9
|
+
def spawn
|
10
|
+
clone
|
11
|
+
end
|
12
|
+
|
13
|
+
def merge(other)
|
14
|
+
if other.is_a?(Array)
|
15
|
+
to_a & other
|
16
|
+
elsif other
|
17
|
+
spawn.merge!(other)
|
18
|
+
else
|
19
|
+
self
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def merge!(other) # :nodoc:
|
24
|
+
if !other.is_a?(Relation) && other.respond_to?(:to_proc)
|
25
|
+
instance_exec(&other)
|
26
|
+
else
|
27
|
+
klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger
|
28
|
+
klass.new(self, other).merge
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Removes from the query the condition(s) specified in +skips+.
|
33
|
+
#
|
34
|
+
# Post.order('id asc').except(:order) # discards the order condition
|
35
|
+
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
|
36
|
+
def except(*skips)
|
37
|
+
relation_with values.except(*skips)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Removes any condition from the query other than the one(s) specified in +onlies+.
|
41
|
+
#
|
42
|
+
# Post.order('id asc').only(:where) # discards the order condition
|
43
|
+
# Post.order('id asc').only(:where, :order) # uses the specified order
|
44
|
+
def only(*onlies)
|
45
|
+
if onlies.any? { |o| o == :where }
|
46
|
+
onlies << :bind
|
47
|
+
end
|
48
|
+
relation_with values.slice(*onlies)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def relation_with(values) # :nodoc:
|
54
|
+
result = Relation.create(klass, values)
|
55
|
+
result.extend(*extending_values) if extending_values.any?
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
class Relation
|
4
|
+
|
5
|
+
MULTI_VALUE_METHODS = [:order, :where, :or_filter, :filter, :bind, :extending, :unscope, :skip_callbacks]
|
6
|
+
SINGLE_VALUE_METHODS = [:limit, :offset, :routing, :size]
|
7
|
+
|
8
|
+
INVALID_METHODS_FOR_DELETE_ALL = [:limit, :offset]
|
9
|
+
|
10
|
+
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
|
11
|
+
|
12
|
+
include FinderMethods, SpawnMethods, QueryMethods, SearchOptionMethods, Delegation
|
13
|
+
|
14
|
+
attr_reader :klass, :loaded
|
15
|
+
alias :model :klass
|
16
|
+
alias :loaded? :loaded
|
17
|
+
|
18
|
+
delegate :blank?, :empty?, :any?, :many?, to: :results
|
19
|
+
|
20
|
+
def initialize(klass, values={})
|
21
|
+
@klass = klass
|
22
|
+
@values = values
|
23
|
+
@offsets = {}
|
24
|
+
@loaded = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def build(*args)
|
28
|
+
@klass.new *args
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_a
|
32
|
+
load
|
33
|
+
@records
|
34
|
+
end
|
35
|
+
alias :results :to_a
|
36
|
+
|
37
|
+
def as_json(options = nil)
|
38
|
+
to_a.as_json(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_elastic
|
42
|
+
query_builder.to_elastic
|
43
|
+
end
|
44
|
+
|
45
|
+
def create(*args, &block)
|
46
|
+
scoping { @klass.create!(*args, &block) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def scoping
|
50
|
+
previous, klass.current_scope = klass.current_scope, self
|
51
|
+
yield
|
52
|
+
ensure
|
53
|
+
klass.current_scope = previous
|
54
|
+
end
|
55
|
+
|
56
|
+
def load
|
57
|
+
exec_queries unless loaded?
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
alias :fetch :load
|
62
|
+
|
63
|
+
def delete(opts=nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
def exec_queries
|
67
|
+
# Run safety callback
|
68
|
+
klass.circuit_breaker_callbacks.each do |cb|
|
69
|
+
current_scope_values = self.send("#{cb[:options][:in]}_values")
|
70
|
+
next if skip_callbacks_values.include? cb[:name]
|
71
|
+
valid = if cb[:callback].nil?
|
72
|
+
current_scope_values.collect(&:keys).flatten.include? cb[:name]
|
73
|
+
else
|
74
|
+
cb[:callback].call(current_scope_values.collect(&:keys).flatten, current_scope_values)
|
75
|
+
end
|
76
|
+
|
77
|
+
raise Elasticsearch::Persistence::Model::QueryOptionMissing, "#{cb[:name]} #{cb[:options][:message]}" unless valid
|
78
|
+
end
|
79
|
+
|
80
|
+
@records = @klass.fetch_results(query_builder)
|
81
|
+
|
82
|
+
@loaded = true
|
83
|
+
@records
|
84
|
+
end
|
85
|
+
|
86
|
+
def values
|
87
|
+
Hash[@values]
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
entries = to_a.results.take([size_value.to_i + 1, 11].compact.min).map!(&:inspect)
|
92
|
+
message = {}
|
93
|
+
message = {total: to_a.total, max: to_a.total}
|
94
|
+
message.merge!(aggregations: results.aggregations.keys) unless results.aggregations.nil?
|
95
|
+
message = message.each_pair.collect { |k,v| "#{k}: #{v}" }
|
96
|
+
message.unshift entries.join(', ') unless entries.size.zero?
|
97
|
+
"#<#{self.class.name} #{message.join(', ')}>"
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def query_builder
|
105
|
+
QueryBuilder.new(values)
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
# The default repository class, to be used either directly, or as a gateway in a custom repository class
|
6
|
+
#
|
7
|
+
# @example Standalone use
|
8
|
+
#
|
9
|
+
# repository = Elasticsearch::Persistence::Repository::Class.new
|
10
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
11
|
+
# repository.save(my_object)
|
12
|
+
# # => {"_index"=> ... }
|
13
|
+
#
|
14
|
+
# @example Shortcut use
|
15
|
+
#
|
16
|
+
# repository = Elasticsearch::Persistence::Repository.new
|
17
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
18
|
+
#
|
19
|
+
# @example Configuration via a block
|
20
|
+
#
|
21
|
+
# repository = Elasticsearch::Persistence::Repository.new do
|
22
|
+
# index 'my_notes'
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
26
|
+
# # > repository.save(my_object)
|
27
|
+
# # => {"_index"=> ... }
|
28
|
+
#
|
29
|
+
# @example Accessing the gateway in a custom class
|
30
|
+
#
|
31
|
+
# class MyRepository
|
32
|
+
# include Elasticsearch::Persistence::Repository
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# repository = MyRepository.new
|
36
|
+
#
|
37
|
+
# repository.gateway.client.info
|
38
|
+
# # => {"status"=>200, "name"=>"Venom", ... }
|
39
|
+
#
|
40
|
+
class Class
|
41
|
+
include Elasticsearch::Persistence::Client::ClassMethods
|
42
|
+
include Elasticsearch::Persistence::Repository::Naming
|
43
|
+
include Elasticsearch::Persistence::Repository::Serialize
|
44
|
+
include Elasticsearch::Persistence::Repository::Store
|
45
|
+
include Elasticsearch::Persistence::Repository::Find
|
46
|
+
include Elasticsearch::Persistence::Repository::Search
|
47
|
+
|
48
|
+
include Elasticsearch::Model::Indexing::ClassMethods
|
49
|
+
|
50
|
+
attr_reader :options
|
51
|
+
|
52
|
+
def initialize(options={}, &block)
|
53
|
+
@options = options
|
54
|
+
index_name options.delete(:index)
|
55
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return the "host" class, if this repository is a gateway hosted in another class
|
59
|
+
#
|
60
|
+
# @return [nil, Class]
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
#
|
64
|
+
def host
|
65
|
+
options[:host]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
class DocumentNotFound < StandardError; end
|
5
|
+
|
6
|
+
# Retrieves one or more domain objects from the repository
|
7
|
+
#
|
8
|
+
module Find
|
9
|
+
|
10
|
+
# Retrieve a single object or multiple objects from Elasticsearch by ID or IDs
|
11
|
+
#
|
12
|
+
# @example Retrieve a single object by ID
|
13
|
+
#
|
14
|
+
# repository.find(1)
|
15
|
+
# # => <Note ...>
|
16
|
+
#
|
17
|
+
# @example Retrieve multiple objects by IDs
|
18
|
+
#
|
19
|
+
# repository.find(1, 2)
|
20
|
+
# # => [<Note ...>, <Note ...>
|
21
|
+
#
|
22
|
+
# @return [Object,Array]
|
23
|
+
#
|
24
|
+
def find(*args)
|
25
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
26
|
+
ids = args
|
27
|
+
|
28
|
+
if args.size == 1
|
29
|
+
id = args.pop
|
30
|
+
id.is_a?(Array) ? __find_many(id, options) : __find_one(id, options)
|
31
|
+
else
|
32
|
+
__find_many args, options
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return if object exists in the repository
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
#
|
40
|
+
# repository.exists?(1)
|
41
|
+
# => true
|
42
|
+
#
|
43
|
+
# @return [true, false]
|
44
|
+
#
|
45
|
+
def exists?(id, options={})
|
46
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
47
|
+
client.exists( { index: index_name, id: id }.merge(options) )
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
#
|
52
|
+
def __find_one(id, options={})
|
53
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
54
|
+
document = client.get( { index: index_name, id: id }.merge(options) )
|
55
|
+
|
56
|
+
deserialize(document)
|
57
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
|
58
|
+
raise DocumentNotFound, e.message, caller
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
def __find_many(ids, options={})
|
64
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
65
|
+
documents = client.mget( { index: index_name, body: { ids: ids } }.merge(options) )
|
66
|
+
|
67
|
+
documents['docs'].map { |document| document['found'] ? deserialize(document) : nil }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
# Wraps all naming-related features of the repository (index name, the domain object class, etc)
|
6
|
+
#
|
7
|
+
module Naming
|
8
|
+
|
9
|
+
# Get or set the class used to initialize domain objects when deserializing them
|
10
|
+
#
|
11
|
+
def klass name=nil
|
12
|
+
@klass = name || @klass
|
13
|
+
end
|
14
|
+
|
15
|
+
# Set the class used to initialize domain objects when deserializing them
|
16
|
+
#
|
17
|
+
def klass=klass
|
18
|
+
@klass = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
# Get or set the index name used when storing and retrieving documents
|
22
|
+
#
|
23
|
+
def index_name name=nil
|
24
|
+
@index_name = name || @index_name || begin
|
25
|
+
if respond_to?(:host) && host && host.is_a?(Module)
|
26
|
+
self.host.to_s.underscore.gsub(/\//, '-')
|
27
|
+
else
|
28
|
+
self.class.to_s.underscore.gsub(/\//, '-')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end; alias :index :index_name
|
32
|
+
|
33
|
+
# Set the index name used when storing and retrieving documents
|
34
|
+
#
|
35
|
+
def index_name=(name)
|
36
|
+
@index_name = name
|
37
|
+
end; alias :index= :index_name=
|
38
|
+
|
39
|
+
# Get or set the document type used when storing and retrieving documents
|
40
|
+
#
|
41
|
+
def document_type name=nil
|
42
|
+
@document_type = name || @document_type || (klass ? klass.to_s.underscore : nil)
|
43
|
+
end; alias :type :document_type
|
44
|
+
|
45
|
+
# Set the document type used when storing and retrieving documents
|
46
|
+
#
|
47
|
+
def document_type=(name)
|
48
|
+
@document_type = name
|
49
|
+
end; alias :type= :document_type=
|
50
|
+
|
51
|
+
# Get the Ruby class from the Elasticsearch `_type`
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# repository.__get_klass_from_type 'note'
|
55
|
+
# => Note
|
56
|
+
#
|
57
|
+
# @return [Class] The class corresponding to the passed type
|
58
|
+
# @raise [NameError] if the class cannot be found
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
#
|
62
|
+
def __get_klass_from_type(type)
|
63
|
+
klass = type.classify
|
64
|
+
klass.constantize
|
65
|
+
rescue NameError => e
|
66
|
+
raise NameError, "Attempted to get class '#{klass}' from the '#{type}' type, but no such class can be found."
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the Elasticsearch `_type` from the Ruby class
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# repository.__get_type_from_class Note
|
73
|
+
# => "note"
|
74
|
+
#
|
75
|
+
# @return [String] The type corresponding to the passed class
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
#
|
79
|
+
def __get_type_from_class(klass)
|
80
|
+
klass.to_s.underscore
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get a document ID from the document (assuming Hash or Hash-like object)
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# repository.__get_id_from_document title: 'Test', id: 'abc123'
|
87
|
+
# => "abc123"
|
88
|
+
#
|
89
|
+
# @api private
|
90
|
+
#
|
91
|
+
def __get_id_from_document(document)
|
92
|
+
document[:id] || document['id'] || document[:_id] || document['_id']
|
93
|
+
end
|
94
|
+
|
95
|
+
# Extract a document ID from the document (assuming Hash or Hash-like object)
|
96
|
+
#
|
97
|
+
# @note Calling this method will *remove* the `id` or `_id` key from the passed object.
|
98
|
+
#
|
99
|
+
# @example
|
100
|
+
# options = { title: 'Test', id: 'abc123' }
|
101
|
+
# repository.__extract_id_from_document options
|
102
|
+
# # => "abc123"
|
103
|
+
# options
|
104
|
+
# # => { title: 'Test' }
|
105
|
+
#
|
106
|
+
# @api private
|
107
|
+
#
|
108
|
+
def __extract_id_from_document(document)
|
109
|
+
document.delete(:id) || document.delete('id') || document.delete(:_id) || document.delete('_id')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
module Response # :nodoc:
|
5
|
+
|
6
|
+
# Encapsulates the domain objects and documents returned from Elasticsearch when searching
|
7
|
+
#
|
8
|
+
# Implements `Enumerable` and forwards its methods to the {#results} object.
|
9
|
+
#
|
10
|
+
class Results
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
delegate :aggregations, to: :response
|
14
|
+
|
15
|
+
attr_reader :repository, :loaded
|
16
|
+
alias :loaded? :loaded
|
17
|
+
|
18
|
+
# @param repository [Elasticsearch::Persistence::Repository::Class] The repository instance
|
19
|
+
# @param response [Hash] The full response returned from the Elasticsearch client
|
20
|
+
# @param options [Hash] Optional parameters
|
21
|
+
#
|
22
|
+
def initialize(repository, response, options={})
|
23
|
+
@repository = repository
|
24
|
+
@response = Elasticsearch::Persistence::Model::HashWrapper.new(response)
|
25
|
+
@options = options
|
26
|
+
@loaded = false
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(method_name, *arguments, &block)
|
30
|
+
results.respond_to?(method_name) ? results.__send__(method_name, *arguments, &block) : super
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to?(method_name, include_private = false)
|
34
|
+
results.respond_to?(method_name) || super
|
35
|
+
end
|
36
|
+
|
37
|
+
def inner_hits
|
38
|
+
response['hits']['hits'].collect { |d| d['inner_hits'] }
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# The number of total hits for a query
|
43
|
+
#
|
44
|
+
def total
|
45
|
+
response['hits']['total']
|
46
|
+
end
|
47
|
+
|
48
|
+
# The maximum score for a query
|
49
|
+
#
|
50
|
+
def max_score
|
51
|
+
response['hits']['max_score']
|
52
|
+
end
|
53
|
+
|
54
|
+
# Yields [object, hit] pairs to the block
|
55
|
+
#
|
56
|
+
def each_with_hit(&block)
|
57
|
+
results.zip(response['hits']['hits']).each(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Yields [object, hit] pairs and returns the result
|
61
|
+
#
|
62
|
+
def map_with_hit(&block)
|
63
|
+
results.zip(response['hits']['hits']).map(&block)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the collection of domain objects
|
67
|
+
#
|
68
|
+
# @example Iterate over the results
|
69
|
+
#
|
70
|
+
# results.map { |r| r.attributes[:title] }
|
71
|
+
# => ["Fox", "Dog"]
|
72
|
+
#
|
73
|
+
# @return [Array]
|
74
|
+
#
|
75
|
+
def results
|
76
|
+
@results ||= response['hits']['hits'].map do |document|
|
77
|
+
repository.deserialize(document.to_hash)
|
78
|
+
end
|
79
|
+
@loaded = true
|
80
|
+
@results
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete(opts=nil)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Access the response returned from Elasticsearch by the client
|
87
|
+
#
|
88
|
+
# @example Access the aggregations in the response
|
89
|
+
#
|
90
|
+
# results = repository.search query: { match: { title: 'fox dog' } },
|
91
|
+
# aggregations: { titles: { terms: { field: 'title' } } }
|
92
|
+
# results.response.aggregations.titles.buckets.map { |term| "#{term['key']}: #{term['doc_count']}" }
|
93
|
+
# # => ["brown: 1", "dog: 1", ...]
|
94
|
+
#
|
95
|
+
# @return [Hashie::Mash]
|
96
|
+
#
|
97
|
+
def response
|
98
|
+
@response
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
# Returns a collection of domain objects by an Elasticsearch query
|
6
|
+
#
|
7
|
+
module Search
|
8
|
+
include Elasticsearch::Persistence::QueryCache
|
9
|
+
# Returns a collection of domain objects by an Elasticsearch query
|
10
|
+
#
|
11
|
+
# Pass the query either as a string or a Hash-like object
|
12
|
+
#
|
13
|
+
# @example Return objects matching a simple query
|
14
|
+
#
|
15
|
+
# repository.search('fox or dog')
|
16
|
+
#
|
17
|
+
# @example Return objects matching a query in the Elasticsearch DSL
|
18
|
+
#
|
19
|
+
# repository.search(query: { match: { title: 'fox dog' } })
|
20
|
+
#
|
21
|
+
# @example Define additional search parameters, such as highlighted excerpts
|
22
|
+
#
|
23
|
+
# results = repository.search(query: { match: { title: 'fox dog' } }, highlight: { fields: { title: {} } })
|
24
|
+
# results.map_with_hit { |d,h| h.highlight.title.join }
|
25
|
+
# # => ["quick brown <em>fox</em>", "fast white <em>dog</em>"]
|
26
|
+
#
|
27
|
+
# @example Perform aggregations as part of the request
|
28
|
+
#
|
29
|
+
# results = repository.search query: { match: { title: 'fox dog' } },
|
30
|
+
# aggregations: { titles: { terms: { field: 'title' } } }
|
31
|
+
# results.response.aggregations.titles.buckets.map { |term| "#{term['key']}: #{term['doc_count']}" }
|
32
|
+
# # => ["brown: 1", "dog: 1", ... ]
|
33
|
+
#
|
34
|
+
# @example Pass additional options to the search request, such as `size`
|
35
|
+
#
|
36
|
+
# repository.search query: { match: { title: 'fox dog' } }, size: 25
|
37
|
+
# # GET http://localhost:9200/notes/note/_search
|
38
|
+
# # > {"query":{"match":{"title":"fox dog"}},"size":25}
|
39
|
+
#
|
40
|
+
# @return [Elasticsearch::Persistence::Repository::Response::Results]
|
41
|
+
#
|
42
|
+
def search(query_or_definition, options = {})
|
43
|
+
request = { index: index_name, body: query_or_definition.to_hash }
|
44
|
+
|
45
|
+
case
|
46
|
+
when query_or_definition.respond_to?(:to_hash)
|
47
|
+
request.merge!(body: query_or_definition.to_hash)
|
48
|
+
when query_or_definition.is_a?(String)
|
49
|
+
request.merge!(q: query_or_definition)
|
50
|
+
else
|
51
|
+
raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" +
|
52
|
+
" -- #{query_or_definition.class} given."
|
53
|
+
end
|
54
|
+
|
55
|
+
response = cache_query(to_curl(request.merge(options)), klass) { client.search(request.merge(options)) }
|
56
|
+
|
57
|
+
Response::Results.new(self, response)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the number of domain object in the index
|
61
|
+
#
|
62
|
+
# @example Return the number of all domain objects
|
63
|
+
#
|
64
|
+
# repository.count
|
65
|
+
# # => 2
|
66
|
+
#
|
67
|
+
# @example Return the count of domain object matching a simple query
|
68
|
+
#
|
69
|
+
# repository.count('fox or dog')
|
70
|
+
# # => 1
|
71
|
+
#
|
72
|
+
# @example Return the count of domain object matching a query in the Elasticsearch DSL
|
73
|
+
#
|
74
|
+
# repository.search(query: { match: { title: 'fox dog' } })
|
75
|
+
# # => 1
|
76
|
+
#
|
77
|
+
# @return [Integer]
|
78
|
+
#
|
79
|
+
def count(query_or_definition = nil, options = {})
|
80
|
+
query_or_definition ||= { query: { match_all: {} } }
|
81
|
+
|
82
|
+
request = { index: index_name, body: query_or_definition.to_hash }
|
83
|
+
response = cache_query(to_curl(request.merge(options), "_count"), klass) { client.count(request.merge(options)) }
|
84
|
+
|
85
|
+
response
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
## TODO: Not happy with where this is living right now.
|
91
|
+
#
|
92
|
+
def to_curl(arguments = {}, end_point = "_search")
|
93
|
+
host = client.transport.options[:hosts]&.first || client.transport.options[:url]
|
94
|
+
arguments[:index] = "_all" if !arguments[:index] && arguments[:type]
|
95
|
+
|
96
|
+
valid_params = [
|
97
|
+
:analyzer,
|
98
|
+
:analyze_wildcard,
|
99
|
+
:default_operator,
|
100
|
+
:df,
|
101
|
+
:explain,
|
102
|
+
:fields,
|
103
|
+
:from,
|
104
|
+
:ignore_indices,
|
105
|
+
:ignore_unavailable,
|
106
|
+
:allow_no_indices,
|
107
|
+
:expand_wildcards,
|
108
|
+
:lenient,
|
109
|
+
:lowercase_expanded_terms,
|
110
|
+
:preference,
|
111
|
+
:q,
|
112
|
+
:routing,
|
113
|
+
:scroll,
|
114
|
+
:search_type,
|
115
|
+
:size,
|
116
|
+
:sort,
|
117
|
+
:source,
|
118
|
+
:_source,
|
119
|
+
:_source_include,
|
120
|
+
:_source_exclude,
|
121
|
+
:stats,
|
122
|
+
:suggest_field,
|
123
|
+
:suggest_mode,
|
124
|
+
:suggest_size,
|
125
|
+
:suggest_text,
|
126
|
+
:timeout,
|
127
|
+
:version,
|
128
|
+
]
|
129
|
+
|
130
|
+
method = "GET"
|
131
|
+
path = Elasticsearch::API::Utils.__pathify(Elasticsearch::API::Utils.__listify(arguments[:index]), end_point)
|
132
|
+
|
133
|
+
params = Elasticsearch::API::Utils.__validate_and_extract_params arguments, valid_params
|
134
|
+
body = arguments[:body]
|
135
|
+
|
136
|
+
params[:fields] = Elasticsearch::API::Utils.__listify(params[:fields]) if params[:fields]
|
137
|
+
|
138
|
+
url = path
|
139
|
+
|
140
|
+
unless host.is_a? String
|
141
|
+
host_parts = "#{host[:protocol].to_s}://#{host[:host]}"
|
142
|
+
host_parts = "#{host_parts}:#{host[:port]}" if host[:port]
|
143
|
+
else
|
144
|
+
host_parts = host
|
145
|
+
end
|
146
|
+
|
147
|
+
trace_url = "#{host_parts}/#{url}"
|
148
|
+
trace_url += "?#{::Faraday::Utils::ParamsHash[params].to_query}" unless params.blank?
|
149
|
+
trace_body = body ? " -d '#{body.to_json}'" : ""
|
150
|
+
|
151
|
+
Rainbow("curl -X #{method.to_s.upcase} '#{CGI.unescape(trace_url)}'#{trace_body}\n").color :white
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|