es-elasticity 0.2.11 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58ea71d50e68bd34f58ea88ebf80197f1fc04083
4
- data.tar.gz: 740f4b8b9bc70011fa146245bf2b5f30600f77cd
3
+ metadata.gz: c7bc6c25943feeda4da146553b189c61bbdad216
4
+ data.tar.gz: 9f07d0aa831e187b094481e96178fce66c923139
5
5
  SHA512:
6
- metadata.gz: 0d9f5f2c133c66e27725e8e95051c780a407900f19ad278feae655de91305054e807bfa9fe87b591e57f991540d745790cf4f075f3cfd35f7c3d11c6150f09c2
7
- data.tar.gz: ac2228701bf716f8febb33027a75640477b46cd276f47cdaeb1ffee4eed25ad7649588d9e4dd07f37756d576eb775efaab4bf796f5743c4469c6ef888b50d16b
6
+ metadata.gz: 5d9109393c9161297e7a103d821f0a91599b8dae175174b228380f82d3db975a9d3d32902cc5ea671da301bc90f8d4705db4ed232ef019d0427c82bf49f032dd
7
+ data.tar.gz: bc7fb37f2ded19d2dd98e636a2f346aa078b3bee37c4e49ff3d46278b3d6e1f1d0407e385f04f344887b9bafd6f2b6c8f667fc983f972c5e6ab6725719b4c7be
data/.gitignore CHANGED
@@ -13,3 +13,4 @@
13
13
  *.a
14
14
  mkmf.log
15
15
  *.log
16
+ .DS_Store
data/.travis.yml CHANGED
@@ -1,7 +1,11 @@
1
1
  language: ruby
2
+ before_install:
3
+ - gem update bundler
2
4
  rvm:
3
5
  - 2.1.1
4
- - 2.0.0
6
+ - 2.1.2
7
+ - 2.1.3
8
+ - 2.1.4
5
9
  services:
6
10
  - elasticsearch
7
11
  env:
data/elasticity.gemspec CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "oj"
27
27
  spec.add_development_dependency "pry"
28
28
  spec.add_development_dependency "codeclimate-test-reporter"
29
+ spec.add_development_dependency "redis"
29
30
 
30
31
  spec.add_dependency "activesupport", "~> 4.0"
31
32
  spec.add_dependency "activemodel", "~> 4.0"
data/lib/elasticity.rb CHANGED
@@ -1,5 +1,33 @@
1
- require "elasticity_base"
2
- require "elasticity/index"
3
- require "elasticity/document"
4
- require "elasticity/search"
5
- require "elasticity/multi_search"
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
+ autoload :Bulk, "elasticity/bulk"
12
+ autoload :Config, "elasticity/config"
13
+ autoload :Document, "elasticity/document"
14
+ autoload :InstrumentedClient, "elasticity/instrumented_client"
15
+ autoload :LogSubscriber, "elasticity/log_subscriber"
16
+ autoload :MultiSearch, "elasticity/multi_search"
17
+ autoload :Search, "elasticity/search"
18
+ autoload :Strategies, "elasticity/strategies"
19
+
20
+ def self.configure
21
+ @config = Config.new
22
+ yield(@config)
23
+ end
24
+
25
+ def self.config
26
+ return @config if defined?(@config)
27
+ @config = Config.new
28
+ end
29
+ end
30
+
31
+ if defined?(Rails)
32
+ require "elasticity/railtie"
33
+ end
@@ -0,0 +1,53 @@
1
+ module Elasticity
2
+ class Bulk
3
+ def initialize(client)
4
+ @client = client
5
+ @operations = []
6
+ end
7
+
8
+ def index(index_name, type, id, attributes)
9
+ @operations << { index: { _index: index_name, _type: type, _id: id, data: attributes }}
10
+ end
11
+
12
+ def delete(index_name, type, id)
13
+ @operations << { delete: { _index: index_name, _type: type, _id: id }}
14
+ end
15
+
16
+ def execute
17
+ @client.bulk(body: @operations)
18
+ end
19
+
20
+ class Index < Bulk
21
+ def initialize(client, index_name)
22
+ super(client)
23
+ @index_name = index_name
24
+ end
25
+
26
+ def index(type, id, attributes)
27
+ super(@index_name, type, id, attributes)
28
+ end
29
+
30
+ def delete(type, id)
31
+ super(@index_name, type, id)
32
+ end
33
+ end
34
+
35
+ class Alias < Bulk
36
+ def initialize(client, update_alias, delete_indexes)
37
+ super(client)
38
+ @update_alias = update_alias
39
+ @delete_indexes = delete_indexes
40
+ end
41
+
42
+ def index(type, id, attributes)
43
+ super(@update_alias, type, id, attributes)
44
+ end
45
+
46
+ def delete(type, id)
47
+ @delete_indexes.each do |index|
48
+ super(index, type, id)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ module Elasticity
2
+ class Config
3
+ def client=(client)
4
+ @client = Elasticity::InstrumentedClient.new(client)
5
+ end
6
+
7
+ def client
8
+ return @client if defined?(@client)
9
+ self.client = Elasticsearch::Client.new
10
+ @client
11
+ end
12
+
13
+ attr_writer :settings, :namespace, :pretty_json
14
+
15
+ def settings
16
+ return @settings if defined?(@settings)
17
+ @settings = {}
18
+ end
19
+
20
+ def namespace
21
+ @namespace
22
+ end
23
+
24
+ def pretty_json
25
+ @pretty_json || false
26
+ end
27
+ end
28
+ end
@@ -2,89 +2,131 @@ module Elasticity
2
2
  class Document
3
3
  include ::ActiveModel::Model
4
4
 
5
- # Returns the instance of Elasticity::Index associated with this document.
6
- def self.index
7
- return @index if @index.present?
8
- @index = Index.new(Elasticity.config.client, self.namespaced_index_name)
5
+ class NotConfigured < StandardError; end
6
+
7
+ Config = Struct.new(:index_base_name, :document_type, :mapping, :strategy)
8
+
9
+ # Configure the given klass, changing default parameters and resetting
10
+ # some of the internal state.
11
+ def self.configure
12
+ @config = Config.new
13
+ @config.strategy = Strategies::SingleIndex
14
+ yield(@config)
15
+ end
16
+
17
+ # Returns the stategy class being used.
18
+ # Check Elasticity::Strategies for more information.
19
+ def self.strategy
20
+ if @config.nil? || @config.strategy.nil?
21
+ raise NotConfigured, "#{self} has not been configured, make sure you call the configure method"
22
+ end
23
+
24
+ return @strategy if defined?(@strategy)
25
+
26
+ if namespace = Elasticity.config.namespace
27
+ index_base_name = "#{namespace}_#{@config.index_base_name}"
28
+ end
29
+
30
+ @strategy = @config.strategy.new(Elasticity.config.client, index_base_name)
31
+ end
32
+
33
+ # Document type
34
+ def self.document_type
35
+ if @config.nil? || @config.document_type.blank?
36
+ raise NotConfigured, "#{self} has not been configured, make sure you call the configure method"
37
+ end
38
+
39
+ @config.document_type
40
+ end
41
+
42
+ # Document type
43
+ def self.mapping
44
+ if @config.nil? || @config.mapping.blank?
45
+ raise NotConfigured, "#{self} has not been configured, make sure you call the configure method"
46
+ end
47
+
48
+ @config.mapping
9
49
  end
10
50
 
11
51
  # Creates the index for this document
12
52
  def self.create_index
13
- self.index.create_if_undefined(settings: Elasticity.config.settings, mappings: { document_type => @mappings })
53
+ self.strategy.create_if_undefined(settings: Elasticity.config.settings, mappings: { document_type => mapping })
14
54
  end
15
55
 
16
56
  # Re-creates the index for this document
17
57
  def self.recreate_index
18
- self.index.recreate(settings: Elasticity.config.settings, mappings: { document_type => @mappings })
58
+ self.strategy.recreate(settings: Elasticity.config.settings, mappings: { document_type => mapping })
19
59
  end
20
60
 
21
61
  # Deletes the index
22
62
  def self.delete_index
23
- self.index.delete
63
+ self.strategy.delete
24
64
  end
25
65
 
26
- # Sets the index name to something else than the default
27
- def self.index_name=(name)
28
- @index_name = name
29
- @index = nil
66
+ # Does the index exist?
67
+ def self.index_exists?
68
+ !self.strategy.missing?
30
69
  end
31
70
 
32
- # Namespaced index name
33
- def self.namespaced_index_name
34
- name = @index_name || self.name.underscore.pluralize
35
-
36
- if namespace = Elasticity.config.namespace
37
- name = "#{namespace}_#{name}"
38
- end
39
-
40
- name
71
+ # Remap
72
+ def self.remap!
73
+ self.strategy.remap(settings: Elasticity.config.settings, mappings: { document_type => mapping })
41
74
  end
42
75
 
43
- # The document type to be used, it's inferred by the class name.
44
- def self.document_type
45
- return @document_type if defined?(@document_type)
46
- @document_type = self.name.demodulize.underscore
76
+ # Flushes the index, forcing any writes
77
+ def self.flush_index
78
+ self.strategy.flush
47
79
  end
48
80
 
49
- # Sets the document type to something different than the default
50
- def self.document_type=(document_type)
51
- @document_type = document_type
52
- end
81
+ # Creates a instance of a document from a ElasticSearch hit data.
82
+ def self.from_hit(hit_data)
83
+ attrs = hit_data["_source"].merge(_id: hit_data['_id'])
84
+
85
+ if hit_data["highlight"]
86
+ highlighted_attrs = attrs.dup
87
+ attrs_set = Set.new
53
88
 
54
- # Sets the mapping for this model, which will be used to create the associated index and
55
- # generate accessor methods.
56
- def self.mappings=(mappings)
57
- raise "Can't re-define mappings in runtime" if defined?(@mappings)
58
- @mappings = mappings
89
+ hit_data["highlight"].each do |name, v|
90
+ name = name.gsub(/\..*\z/, '')
91
+ next if attrs_set.include?(name)
92
+ highlighted_attrs[name] = v
93
+ attrs_set << name
94
+ end
95
+
96
+ highlighted = new(highlighted_attrs)
97
+ end
98
+
99
+ new(attrs.merge(highlighted: highlighted))
59
100
  end
60
101
 
61
102
  # Searches the index using the parameters provided in the body hash, following the same
62
103
  # structure Elasticsearch expects.
63
104
  # Returns a DocumentSearch object.
64
105
  def self.search(body)
65
- DocumentSearchProxy.new(Search.new(index, document_type, body), self)
106
+ search = self.strategy.search(self.document_type, body)
107
+ Search::DocumentProxy.new(search, self)
66
108
  end
67
109
 
68
110
  # Fetches one specific document from the index by ID.
69
111
  def self.get(id)
70
- if doc = index.get_document(document_type, id)
112
+ if doc = self.strategy.get_document(document_type, id)
71
113
  new(doc["_source"].merge(_id: doc['_id']))
72
114
  end
73
115
  end
74
116
 
75
117
  # Removes one specific document from the index.
76
118
  def self.delete(id)
77
- index.delete_document(document_type, id)
119
+ self.strategy.delete_document(document_type, id)
78
120
  end
79
121
 
80
122
  # Removes entries based on a search
81
123
  def self.delete_by_search(search)
82
- index.delete_by_query(document_type, search.body)
124
+ self.strategy.delete_by_query(document_type, search.body)
83
125
  end
84
126
 
85
127
  # Bulk index the provided documents
86
128
  def self.bulk_index(documents)
87
- index.bulk do |b|
129
+ self.strategy.bulk do |b|
88
130
  documents.each do |doc|
89
131
  b.index(self.document_type, doc._id, doc.to_document)
90
132
  end
@@ -124,7 +166,15 @@ module Elasticity
124
166
 
125
167
  # Update this object on the index, creating or updating the document.
126
168
  def update
127
- self.class.index.index_document(self.class.document_type, _id, to_document)
169
+ self._id, @created = self.class.strategy.index_document(self.class.document_type, _id, to_document)
170
+ end
171
+
172
+ def delete
173
+ self.class.delete(self._id)
174
+ end
175
+
176
+ def created?
177
+ @created || false
128
178
  end
129
179
  end
130
180
  end
@@ -0,0 +1,35 @@
1
+ module Elasticity
2
+ class InstrumentedClient
3
+ def initialize(client)
4
+ @client = client
5
+ end
6
+
7
+ # Generate wrapper methods for @client.indices
8
+ %w(exists create delete get_settings get_mapping flush get_alias get_aliases put_alias delete_alias exists_alias update_aliases).each do |method_name|
9
+ full_name = "index_#{method_name}"
10
+
11
+ define_method(full_name) do |*args, &block|
12
+ instrument(full_name, args) do
13
+ @client.indices.public_send(method_name, *args, &block)
14
+ end
15
+ end
16
+ end
17
+
18
+ # Generate wrapper methods for @client
19
+ %w(index delete get mget search msearch scroll delete_by_query bulk).each do |method_name|
20
+ define_method(method_name) do |*args, &block|
21
+ instrument(method_name, args) do
22
+ @client.public_send(method_name, *args, &block)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def instrument(name, args)
30
+ ActiveSupport::Notifications.instrument("#{name}.elasticity", args: args, backtrace: caller(1)) do
31
+ yield
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ require "active_support/subscriber"
2
+ require "active_support/log_subscriber"
3
+
4
+ module Elasticity
5
+ GRAY = "\e[90m"
6
+
7
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
8
+ %w(exists create delete get_settings get_mapping flush get_alias get_aliases put_alias delete_alias exists_alias update_aliases).each do |method_name|
9
+ define_method("index_#{method_name}") do |event|
10
+ log_event(event)
11
+ end
12
+ end
13
+
14
+ %w(index delete get search scroll delete_by_query bulk).each do |method_name|
15
+ define_method(method_name) do |event|
16
+ log_event(event)
17
+ end
18
+ end
19
+
20
+ def multi_search(event)
21
+ log_event(event)
22
+ end
23
+
24
+ private
25
+
26
+ def log_event(event)
27
+ bt = event.payload[:backtrace]
28
+
29
+ if bt.present? && defined?(Rails)
30
+ bt = Rails.backtrace_cleaner.clean(bt)
31
+ end
32
+
33
+ message = "#{event.transaction_id} #{event.name} #{"%.2f" % event.duration}ms #{MultiJson.dump(event.payload[:args], pretty: Elasticity.config.pretty_json)}"
34
+
35
+ if bt = event.payload[:backtrace]
36
+ bt = Rails.backtrace_cleaner.clean(bt) if defined?(Rails)
37
+ lines = bt[0,4].map { |l| color(l, GRAY) }.join("\n")
38
+ message << "\n#{lines}"
39
+ end
40
+
41
+ debug(message)
42
+
43
+ exception, message = event.payload[:exception]
44
+ if exception
45
+ error("#{event.transaction_id} #{event.name} ERROR #{exception}: #{message}")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -7,19 +7,18 @@ module Elasticity
7
7
  end
8
8
 
9
9
  def add(name, search, documents: nil, active_records: nil)
10
- mapper = case
11
- when documents && active_records
10
+ if !documents.nil? && !active_records.nil?
12
11
  raise ArgumentError, "you can only pass either :documents or :active_records as an option"
13
- when documents
14
- Search::DocumentMapper.new(documents)
15
- when active_records
16
- Search::ActiveRecordMapper.new(active_records)
17
- else
12
+ elsif documents.nil? && active_records.nil?
18
13
  raise ArgumentError, "you need to provide either :documents or :active_records as an option"
19
14
  end
20
15
 
21
- @searches[name] = { index: search.index.name, type: search.document_type, search: search.body }
22
- @mappers[name] = mapper
16
+ @searches[name] = {
17
+ search_definition: search.search_definition,
18
+ documents: documents,
19
+ active_records: active_records
20
+ }
21
+
23
22
  name
24
23
  end
25
24
 
@@ -31,18 +30,26 @@ module Elasticity
31
30
  private
32
31
 
33
32
  def fetch
34
- bodies = @searches.values.map(&:dup)
33
+ bodies = @searches.values.map do |hsh|
34
+ hsh[:search_definition].to_msearch_args
35
+ end
35
36
 
36
- response = ActiveSupport::Notifications.instrument("multi_search.elasticity", args: { body: @searches.values }) do
37
- Elasticity.config.client.msearch(body: bodies)
37
+ response = ActiveSupport::Notifications.instrument("multi_search.elasticity", args: { body: bodies }) do
38
+ Elasticity.config.client.msearch(body: bodies.map(&:dup))
38
39
  end
39
40
 
40
41
  results = {}
41
42
 
42
43
  @searches.keys.each_with_index do |name, idx|
43
- resp = response["responses"][idx]
44
- mapper = @mappers[name]
45
- results[name] = Search::Result.new(resp, mapper)
44
+ resp = response["responses"][idx]
45
+ search = @searches[name]
46
+
47
+ results[name] = case
48
+ when search[:documents]
49
+ resp["hits"]["hits"].map { |hit| search[:documents].from_hit(hit) }
50
+ when search[:active_records]
51
+ Search::ActiveRecordProxy.from_hits(search[:active_records], resp["hits"]["hits"])
52
+ end
46
53
  end
47
54
 
48
55
  results