esse 0.2.0 → 0.2.3

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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/esse/cli/event_listener.rb +13 -0
  3. data/lib/esse/cli/generate.rb +53 -14
  4. data/lib/esse/cli/index/base_operation.rb +5 -13
  5. data/lib/esse/cli/index/close.rb +1 -1
  6. data/lib/esse/cli/index/create.rb +1 -1
  7. data/lib/esse/cli/index/delete.rb +1 -1
  8. data/lib/esse/cli/index/import.rb +6 -2
  9. data/lib/esse/cli/index/open.rb +1 -1
  10. data/lib/esse/cli/index/reset.rb +1 -1
  11. data/lib/esse/cli/index/update_aliases.rb +2 -2
  12. data/lib/esse/cli/index/update_mapping.rb +9 -5
  13. data/lib/esse/cli/index/update_settings.rb +1 -1
  14. data/lib/esse/cli/index.rb +11 -4
  15. data/lib/esse/cli/templates/collection.rb.erb +29 -0
  16. data/lib/esse/cli/templates/config.rb.erb +13 -3
  17. data/lib/esse/cli/templates/document.rb.erb +34 -0
  18. data/lib/esse/cli/templates/index.rb.erb +63 -114
  19. data/lib/esse/cli/templates/mappings.json +27 -0
  20. data/lib/esse/cli/templates/settings.json +62 -0
  21. data/lib/esse/cli.rb +5 -0
  22. data/lib/esse/cluster.rb +93 -12
  23. data/lib/esse/cluster_engine.rb +42 -0
  24. data/lib/esse/collection.rb +18 -0
  25. data/lib/esse/config.rb +14 -2
  26. data/lib/esse/core.rb +28 -7
  27. data/lib/esse/deprecations/cluster.rb +27 -0
  28. data/lib/esse/deprecations/deprecate.rb +29 -0
  29. data/lib/esse/deprecations/index.rb +37 -0
  30. data/lib/esse/deprecations/index_backend_delegator.rb +217 -0
  31. data/lib/esse/deprecations/repository.rb +34 -0
  32. data/lib/esse/deprecations/repository_backend_delegator.rb +110 -0
  33. data/lib/esse/deprecations/serializer.rb +14 -0
  34. data/lib/esse/deprecations.rb +7 -0
  35. data/lib/esse/document.rb +91 -0
  36. data/lib/esse/dynamic_template.rb +43 -0
  37. data/lib/esse/errors.rb +60 -2
  38. data/lib/esse/events/event.rb +4 -19
  39. data/lib/esse/events.rb +13 -2
  40. data/lib/esse/hash_document.rb +38 -0
  41. data/lib/esse/import/bulk.rb +106 -0
  42. data/lib/esse/import/request_body.rb +60 -0
  43. data/lib/esse/index/aliases.rb +50 -0
  44. data/lib/esse/index/attributes.rb +107 -0
  45. data/lib/esse/index/base.rb +17 -53
  46. data/lib/esse/index/documents.rb +236 -0
  47. data/lib/esse/index/indices.rb +171 -0
  48. data/lib/esse/index/inheritance.rb +30 -0
  49. data/lib/esse/index/mappings.rb +6 -19
  50. data/lib/esse/index/object_document_mapper.rb +36 -0
  51. data/lib/esse/index/plugins.rb +42 -0
  52. data/lib/esse/index/search.rb +27 -0
  53. data/lib/esse/index/settings.rb +2 -2
  54. data/lib/esse/index/type.rb +51 -11
  55. data/lib/esse/index.rb +14 -9
  56. data/lib/esse/index_mapping.rb +10 -2
  57. data/lib/esse/index_setting.rb +3 -1
  58. data/lib/esse/null_document.rb +35 -0
  59. data/lib/esse/plugins.rb +12 -0
  60. data/lib/esse/primitives/hstring.rb +1 -1
  61. data/lib/esse/{index_type → repository}/actions.rb +1 -1
  62. data/lib/esse/repository/documents.rb +13 -0
  63. data/lib/esse/repository/object_document_mapper.rb +157 -0
  64. data/lib/esse/repository.rb +17 -0
  65. data/lib/esse/search/query.rb +105 -0
  66. data/lib/esse/search/response.rb +46 -0
  67. data/lib/esse/template_loader.rb +1 -1
  68. data/lib/esse/transport/aliases.rb +36 -0
  69. data/lib/esse/transport/documents.rb +199 -0
  70. data/lib/esse/transport/health.rb +30 -0
  71. data/lib/esse/transport/indices.rb +192 -0
  72. data/lib/esse/transport/search.rb +48 -0
  73. data/lib/esse/transport.rb +44 -0
  74. data/lib/esse/version.rb +1 -1
  75. data/lib/esse.rb +20 -5
  76. metadata +55 -50
  77. data/lib/esse/backend/index/aliases.rb +0 -73
  78. data/lib/esse/backend/index/close.rb +0 -54
  79. data/lib/esse/backend/index/create.rb +0 -67
  80. data/lib/esse/backend/index/delete.rb +0 -39
  81. data/lib/esse/backend/index/documents.rb +0 -23
  82. data/lib/esse/backend/index/existance.rb +0 -22
  83. data/lib/esse/backend/index/open.rb +0 -54
  84. data/lib/esse/backend/index/refresh.rb +0 -43
  85. data/lib/esse/backend/index/reset.rb +0 -33
  86. data/lib/esse/backend/index/update.rb +0 -143
  87. data/lib/esse/backend/index.rb +0 -54
  88. data/lib/esse/backend/index_type/documents.rb +0 -214
  89. data/lib/esse/backend/index_type.rb +0 -37
  90. data/lib/esse/cli/templates/type_collection.rb.erb +0 -41
  91. data/lib/esse/cli/templates/type_mappings.json +0 -6
  92. data/lib/esse/cli/templates/type_serializer.rb.erb +0 -23
  93. data/lib/esse/index/backend.rb +0 -14
  94. data/lib/esse/index/naming.rb +0 -64
  95. data/lib/esse/index_type/backend.rb +0 -14
  96. data/lib/esse/index_type/mappings.rb +0 -42
  97. data/lib/esse/index_type.rb +0 -15
  98. data/lib/esse/object_document_mapper.rb +0 -110
@@ -0,0 +1,62 @@
1
+ {
2
+ "index": {
3
+ "max_ngram_diff": 5,
4
+ "max_shingle_diff": 3,
5
+ "analysis": {
6
+ "filter": {
7
+ "esse_search_shingle": {
8
+ "token_separator": "",
9
+ "output_unigrams_if_no_shingles": "true",
10
+ "output_unigrams": "false",
11
+ "type": "shingle"
12
+ },
13
+ "esse_index_shingle": {
14
+ "token_separator": "",
15
+ "type": "shingle"
16
+ },
17
+ "esse_stemmer": {
18
+ "type": "stemmer",
19
+ "language": "English"
20
+ }
21
+ },
22
+ "analyzer": {
23
+ "esse_index": {
24
+ "filter": [
25
+ "lowercase",
26
+ "asciifolding",
27
+ "english_stop",
28
+ "esse_index_shingle",
29
+ "esse_stemmer"
30
+ ],
31
+ "char_filter": [
32
+ "ampersand"
33
+ ],
34
+ "type": "custom",
35
+ "tokenizer": "standard"
36
+ },
37
+ "esse_search": {
38
+ "filter": [
39
+ "lowercase",
40
+ "asciifolding",
41
+ "english_stop",
42
+ "esse_search_shingle",
43
+ "esse_stemmer"
44
+ ],
45
+ "char_filter": [
46
+ "ampersand"
47
+ ],
48
+ "type": "custom",
49
+ "tokenizer": "standard"
50
+ }
51
+ },
52
+ "char_filter": {
53
+ "ampersand": {
54
+ "type": "mapping",
55
+ "mappings": [
56
+ "&=> and "
57
+ ]
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
data/lib/esse/cli.rb CHANGED
@@ -32,6 +32,7 @@ module Esse
32
32
  def initialize(*)
33
33
  super
34
34
 
35
+ after_initialize
35
36
  load_app_config(options[:require])
36
37
  setup_listeners if !options[:silent] && Esse.config.cli_event_listeners?
37
38
  end
@@ -71,6 +72,10 @@ module Esse
71
72
 
72
73
  private
73
74
 
75
+ def after_initialize
76
+ # esse plugins may override this method
77
+ end
78
+
74
79
  def setup_listeners
75
80
  Esse::Events.__bus__.events.keys.grep(/^elasticsearch/).each do |event_name|
76
81
  Esse::Events.subscribe(event_name) do |event|
data/lib/esse/cluster.rb CHANGED
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'cluster_engine'
4
+ require_relative 'transport'
5
+
3
6
  module Esse
4
7
  class Cluster
5
- ATTRIBUTES = %i[index_prefix index_settings client wait_for_status].freeze
8
+ ATTRIBUTES = %i[index_prefix settings mappings client wait_for_status readonly].freeze
6
9
  WAIT_FOR_STATUSES = %w[green yellow red].freeze
7
10
 
8
11
  # The index prefix. For example an index named UsersIndex.
9
12
  # With `index_prefix = 'app1'`. Final index/alias is: 'app1_users'
10
13
  attr_accessor :index_prefix
11
14
 
12
- # This settings will be passed through all indices during the mapping
13
- attr_accessor :index_settings
15
+ # This global settings will be passed through all indices
16
+ attr_accessor :settings
17
+
18
+ # This global mappings will be applied to all indices
19
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html
20
+ attr_accessor :mappings
14
21
 
15
22
  # if this option set, actions such as creating or deleting index,
16
23
  # importing data will wait for the status specified. Extremely useful
@@ -21,11 +28,17 @@ module Esse
21
28
  # wait_for_status: green
22
29
  attr_accessor :wait_for_status
23
30
 
31
+ # Disable all writes from the application to the underlying Elasticsearch instance while keeping the
32
+ # application running and handling search requests.
33
+ attr_writer :readonly
34
+
24
35
  attr_reader :id
25
36
 
26
37
  def initialize(id:, **options)
27
38
  @id = id.to_sym
28
- @index_settings = {}
39
+ @settings = {}
40
+ @mappings = {}
41
+ @readonly = false
29
42
  assign(options)
30
43
  end
31
44
 
@@ -41,34 +54,102 @@ module Esse
41
54
  end
42
55
 
43
56
  def client
44
- @client ||= Elasticsearch::Client.new
57
+ @client ||= if defined? Elasticsearch::Client
58
+ Elasticsearch::Client.new
59
+ elsif defined? OpenSearch::Client
60
+ OpenSearch::Client.new
61
+ else
62
+ raise Esse::Error, <<~ERROR
63
+ Elasticsearch::Client or OpenSearch::Client is not defined.
64
+ Please install elasticsearch or opensearch-ruby gem.
65
+ ERROR
66
+ end
67
+ end
68
+
69
+ # @return [Boolean] Return true if the cluster is readonly
70
+ def readonly?
71
+ !!@readonly
45
72
  end
46
73
 
47
- # Define the elasticsearch client connectio
48
- # @param es_client [Elasticsearch::Client, Hash] an instance of elasticsearch/api client or an hash
49
- # with the settings that will be used to initialize Elasticsearch::Client
74
+ # @raise [Esse::Transport::ReadonlyClusterError] if the cluster is readonly
75
+ # @return [void]
76
+ def throw_error_when_readonly!
77
+ raise Esse::Transport::ReadonlyClusterError if readonly?
78
+ end
79
+
80
+ # Define the elasticsearch client connection
81
+ # @param es_client [Elasticsearch::Client, OpenSearch::Client, Hash] an instance of elasticsearch/api client or an hash
82
+ # with the settings that will be used to initialize the Client
50
83
  def client=(es_client)
51
- @client = if es_client.is_a?(Hash)
84
+ @client = if es_client.is_a?(Hash) && defined?(Elasticsearch::Client)
52
85
  settings = es_client.each_with_object({}) { |(k, v), r| r[k.to_sym] = v }
53
86
  Elasticsearch::Client.new(settings)
87
+ elsif es_client.is_a?(Hash) && defined?(OpenSearch::Client)
88
+ settings = es_client.each_with_object({}) { |(k, v), r| r[k.to_sym] = v }
89
+ OpenSearch::Client.new(settings)
54
90
  else
55
91
  es_client
56
92
  end
57
93
  end
58
94
 
59
95
  def inspect
60
- attrs = ([:id] + ATTRIBUTES - [:client]).map do |method|
96
+ attrs = ([:id] + ATTRIBUTES - [:client, :readonly]).map do |method|
61
97
  value = public_send(method)
62
98
  format('%<k>s=%<v>p', k: method, v: value) if value
63
99
  end.compact
100
+ attrs << 'readonly=true' if readonly?
64
101
  attrs << format('client=%p', @client)
65
102
  format('#<Esse::Cluster %<attrs>s>', attrs: attrs.join(' '))
66
103
  end
67
104
 
68
- def wait_for_status!(status: wait_for_status)
105
+ # Wait until cluster is in a specific state
106
+ #
107
+ # @option [String] :status Wait until cluster is in a specific state (options: green, yellow, red)
108
+ # @option [String] :index Limit the information returned to a specific index
109
+ def wait_for_status!(status: nil, **kwargs)
110
+ status ||= wait_for_status
69
111
  return unless WAIT_FOR_STATUSES.include?(status.to_s)
70
112
 
71
- client.cluster.health(wait_for_status: status.to_s)
113
+ api.health(**kwargs, wait_for_status: status.to_s)
114
+ end
115
+
116
+ # @return [void]
117
+ def may_update_type!(hash, key: :type)
118
+ if (single_type = engine.mapping_default_type)
119
+ hash[key] = single_type
120
+ return
121
+ end
122
+ hash.delete(key) if engine.mapping_single_type?
123
+ end
124
+
125
+ def info
126
+ @info ||= begin
127
+ resp = client.info
128
+ {
129
+ distribution: (resp.dig('version', 'distribution') || 'elasticsearch'),
130
+ version: resp.dig('version', 'number'),
131
+ }
132
+ end
133
+ end
134
+
135
+ def engine
136
+ @engine ||=ClusterEngine.new(**info)
137
+ end
138
+ alias_method :warm_up!, :engine
139
+
140
+ # Build a search query for the given indices
141
+ #
142
+ # @param indices [Array<Esse::Index, String>] The indices class or the index name
143
+ # @return [Esse::Search::Query] The search query instance
144
+ def search(*indices, **kwargs, &block)
145
+ Esse::Search::Query.new(api, *indices, **kwargs, &block)
146
+ end
147
+
148
+ # Return the proxy object used to perform low level actions on the elasticsearch cluster through the official api client
149
+ #
150
+ # @return [Esse::Transport] The cluster api instance
151
+ def api
152
+ Esse::Transport.new(self)
72
153
  end
73
154
  end
74
155
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class ClusterEngine
5
+ OPENSEARCH_FORK_VERSION = '7.10.2'
6
+
7
+ attr_reader :version, :distribution
8
+
9
+ def initialize(distribution:, version:)
10
+ @distribution = distribution
11
+ @version = version
12
+ end
13
+
14
+ def engine_version
15
+ return @version unless opensearch?
16
+
17
+ OPENSEARCH_FORK_VERSION
18
+ end
19
+
20
+ def opensearch?
21
+ distribution == 'opensearch'
22
+ end
23
+
24
+ def elasticsearch?
25
+ distribution == 'elasticsearch'
26
+ end
27
+
28
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html
29
+ def mapping_single_type?
30
+ engine_version >= '6'
31
+ end
32
+
33
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/6.3/mapping.html
34
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/6.4/mapping.html
35
+ # @see https://www.elastic.co/guide/en/elasticsearch/reference/7.1/mapping.html
36
+ def mapping_default_type
37
+ return unless engine_version.to_i == 6
38
+
39
+ engine_version >= '6.4' ? :_doc : :doc
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Collection
5
+ include Enumerable
6
+ attr_reader :options
7
+
8
+ def initialize(**options)
9
+ @options = options
10
+ end
11
+
12
+ # @yield [<Array, Hash>] A batch of documents to be serialized and indexed.
13
+ # @abstract Override this method to yield each chunk of documents with optional metadata
14
+ def each
15
+ raise NotImplementedError, 'Override this method to iterate over the collection'
16
+ end
17
+ end
18
+ end
data/lib/esse/config.rb CHANGED
@@ -9,10 +9,14 @@ module Esse
9
9
  # conf.cluster(:v1) do |cluster|
10
10
  # cluster.index_prefix = 'backend'
11
11
  # cluster.client = Elasticsearch::Client.new
12
- # cluster.index_settings = {
12
+ # cluster.settings = {
13
13
  # number_of_shards: 2,
14
14
  # number_of_replicas: 0
15
15
  # }
16
+ # cluster.mappings = {
17
+ # dynamic_templates: [...]
18
+ # properties: { ... }
19
+ # }
16
20
  # end
17
21
  # end
18
22
  #
@@ -36,13 +40,17 @@ module Esse
36
40
  # end
37
41
  class Config
38
42
  DEFAULT_CLUSTER_ID = :default
39
- ATTRIBUTES = %i[indices_directory].freeze
43
+ ATTRIBUTES = %i[indices_directory bulk_wait_interval].freeze
40
44
 
41
45
  # The location of the indices. Defaults to the `app/indices`
42
46
  attr_reader :indices_directory
43
47
 
48
+ # wait a given period between posting pages to give Elasticsearch time to catch up.
49
+ attr_reader :bulk_wait_interval
50
+
44
51
  def initialize
45
52
  self.indices_directory = 'app/indices'
53
+ self.bulk_wait_interval = 0.1
46
54
  @clusters = {}
47
55
  cluster(DEFAULT_CLUSTER_ID) # initialize the :default client
48
56
  end
@@ -66,6 +74,10 @@ module Esse
66
74
  @indices_directory = value.is_a?(Pathname) ? value : Pathname.new(value)
67
75
  end
68
76
 
77
+ def bulk_wait_interval=(value)
78
+ @bulk_wait_interval = value.to_f
79
+ end
80
+
69
81
  def load(arg)
70
82
  case arg
71
83
  when Hash
data/lib/esse/core.rb CHANGED
@@ -1,22 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
- require 'multi_json'
5
- require 'elasticsearch'
6
-
7
3
  module Esse
8
4
  require_relative 'config'
9
5
  require_relative 'cluster'
10
6
  require_relative 'primitives'
11
- require_relative 'index_type'
7
+ require_relative 'collection'
8
+ require_relative 'document'
9
+ require_relative 'hash_document'
10
+ require_relative 'null_document'
11
+ require_relative 'repository'
12
12
  require_relative 'index_setting'
13
+ require_relative 'dynamic_template'
13
14
  require_relative 'index_mapping'
14
15
  require_relative 'template_loader'
15
- require_relative 'backend/index'
16
- require_relative 'backend/index_type'
16
+ require_relative 'import/request_body'
17
+ require_relative 'import/bulk'
17
18
  require_relative 'version'
18
19
  require_relative 'logging'
19
20
  require_relative 'events'
21
+ require_relative 'search/query'
22
+ require_relative 'search/response'
23
+ require_relative 'deprecations' # Should be last
20
24
  include Logging
21
25
 
22
26
  @single_threaded = false
@@ -68,4 +72,21 @@ module Esse
68
72
  end
69
73
  [id, modified]
70
74
  end
75
+
76
+ def self.eager_load_indices!
77
+ return false unless Esse.config.indices_directory.exist?
78
+
79
+ Dir[Esse.config.indices_directory.join('**/*_index.rb')].map { |path| Pathname.new(path) }.each do |path|
80
+ next unless path.extname == '.rb'
81
+
82
+ require(path.expand_path.to_s)
83
+ end
84
+ true
85
+ end
86
+
87
+ def self.document?(object)
88
+ return false unless object
89
+
90
+ !!(object.is_a?(Esse::Document) && object.id)
91
+ end
71
92
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Cluster
5
+ extend Esse::Deprecations::Deprecate
6
+
7
+ def index_settings
8
+ settings
9
+ end
10
+ deprecate :index_settings, :settings, 2023, 12
11
+
12
+ def index_settings=(value)
13
+ self.settings = value
14
+ end
15
+ deprecate :index_settings=, :settings=, 2023, 12
16
+
17
+ def index_mappings
18
+ mappings
19
+ end
20
+ deprecate :index_mappings, :mappings, 2023, 12
21
+
22
+ def index_mappings=(value)
23
+ self.mappings = value
24
+ end
25
+ deprecate :index_mappings=, :mappings=, 2023, 12
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ module Deprecations
5
+ module Deprecate
6
+ def self.extended(base)
7
+ base.extend Gem::Deprecate
8
+ base.include InstanceMethods
9
+ end
10
+
11
+ module InstanceMethods
12
+ def warning(method, repl, year, month)
13
+ msg = ["NOTE: #{method} is deprecated"]
14
+ msg << if repl == :none
15
+ ' with no replacement'
16
+ elsif repl.respond_to?(:call)
17
+ "; use #{repl.call} instead"
18
+ else
19
+ "; use #{repl} instead"
20
+ end
21
+ msg << '. It will be removed on or after %4d-%02d-01.' % [year, month]
22
+ msg << "\n#{method} called from #{Gem.location_of_caller(2).join(':')}"
23
+
24
+ warn "#{msg.join}." unless Gem::Deprecate.skip
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ class << self
6
+ extend Esse::Deprecations::Deprecate
7
+
8
+ def define_type(name, *args, **kwargs, &block)
9
+ repository(name, *args, **kwargs, &block)
10
+ end
11
+ deprecate :define_type, :repository, 2023, 12
12
+
13
+ def type_hash
14
+ repo_hash
15
+ end
16
+ deprecate :type_hash, :repo_hash, 2023, 12
17
+
18
+ def index_version
19
+ index_suffix
20
+ end
21
+ deprecate :index_version, :index_suffix, 2023, 12
22
+
23
+ def index_version=(value)
24
+ self.index_suffix = value
25
+ end
26
+ deprecate :index_version=, :index_suffix=, 2023, 12
27
+
28
+ def elasticsearch
29
+ Esse::Deprecations::IndexBackendDelegator.new(:elasticsearch, self)
30
+ end
31
+
32
+ def backend
33
+ Esse::Deprecations::IndexBackendDelegator.new(:backend, self)
34
+ end
35
+ end
36
+ end
37
+ end