elasticsearch-persistence-queryable 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/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
|