opensearch-sugar 1.0.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.agents/skills/diataxis/SKILL.md +142 -0
  3. data/.agents/skills/diataxis/references/examples.md +420 -0
  4. data/.agents/skills/diataxis/references/explanation-template.md +96 -0
  5. data/.agents/skills/diataxis/references/framework.md +400 -0
  6. data/.agents/skills/diataxis/references/how-to-guide-template.md +105 -0
  7. data/.agents/skills/diataxis/references/reference-template.md +110 -0
  8. data/.agents/skills/diataxis/references/tutorial-template.md +101 -0
  9. data/.agents/skills/diataxis/scripts/generate_index.py +139 -0
  10. data/.rspec +3 -0
  11. data/.standard.yml +3 -0
  12. data/AGENTS.md +120 -0
  13. data/CHANGELOG.md +5 -0
  14. data/Dockerfile.opensearch +4 -0
  15. data/Increase_Coverage.md +311 -0
  16. data/README.md +143 -0
  17. data/Rakefile +27 -0
  18. data/Steepfile +23 -0
  19. data/adrs/ADR-000-template.md +87 -0
  20. data/adrs/ADR-001-simpledelegator-for-client.md +138 -0
  21. data/adrs/ADR-002-facade-pattern-for-index.md +126 -0
  22. data/adrs/ADR-003-repository-pattern-for-models.md +148 -0
  23. data/adrs/ADR-004-integration-tests-no-mocking.md +91 -0
  24. data/adrs/ADR-005-exceptions-over-result-objects.md +107 -0
  25. data/adrs/ADR-006-ssl-on-by-default.md +95 -0
  26. data/adrs/ADR-007-selective-sugar-surface.md +118 -0
  27. data/adrs/ADR-008-integration-test-design.md +178 -0
  28. data/compose.yml +2 -0
  29. data/compose_opensearch.yml +31 -0
  30. data/docs/HOWTO.md +844 -0
  31. data/docs/REFERENCE.md +725 -0
  32. data/docs/TUTORIAL.md +327 -0
  33. data/docs/alias-api-design-notes.md +119 -0
  34. data/lib/opensearch/sugar/client.rb +300 -0
  35. data/lib/opensearch/sugar/index/include/utilities.rb +6 -0
  36. data/lib/opensearch/sugar/index.rb +339 -0
  37. data/lib/opensearch/sugar/models.rb +209 -0
  38. data/lib/opensearch/sugar/version.rb +8 -0
  39. data/lib/opensearch/sugar.rb +61 -0
  40. data/old_docs/DELEGATED_METHODS_ANALYSIS.md +361 -0
  41. data/old_docs/EXPLANATION.md +685 -0
  42. data/old_docs/README.md +155 -0
  43. data/old_docs/docs/CLI-PROPOSAL.md +257 -0
  44. data/old_docs/docs/HOWTO.md +798 -0
  45. data/old_docs/docs/REFERENCE.md +901 -0
  46. data/old_docs/docs/TUTORIAL.md +493 -0
  47. data/sig/opensearch/sugar.rbs +162 -0
  48. metadata +240 -0
@@ -0,0 +1,209 @@
1
+ module OpenSearch::Sugar
2
+ # Manages ML models deployed via the
3
+ # {https://opensearch.org/docs/latest/ml-commons-plugin/index/ OpenSearch ML Commons plugin}.
4
+ #
5
+ # Provides methods to register, deploy, list, and delete models, as well as to build
6
+ # text-embedding ingest pipelines that use a deployed model.
7
+ #
8
+ # Access an instance via {OpenSearch::Sugar::Client#models}:
9
+ #
10
+ # @example
11
+ # models = client.models
12
+ # model = models.register(name: "all-MiniLM-L6-v2", version: "1.0.0")
13
+ # puts model.id
14
+ class Models
15
+ # Struct representing a deployed ML model with +:name+, +:version+, and +:id+ members.
16
+ #
17
+ # @!attribute [r] name
18
+ # @return [String] The model name
19
+ # @!attribute [r] version
20
+ # @return [String] The model version string
21
+ # @!attribute [r] id
22
+ # @return [String] The internal OpenSearch model ID (used in API calls)
23
+ ML_INFO = Struct.new(:name, :version, :id)
24
+
25
+ # @param os [OpenSearch::Sugar::Client] The client used for ML API calls
26
+ def initialize(os)
27
+ @os = os
28
+ end
29
+
30
+ # Registers and deploys an ML model via the ML Commons plugin.
31
+ #
32
+ # Idempotent — if a model matching +name+ is already registered, returns the
33
+ # existing {ML_INFO} without re-registering. Polls the task status every 5 seconds
34
+ # until deployment completes or fails.
35
+ #
36
+ # @param name [String] The model name (e.g. +"all-MiniLM-L6-v2"+)
37
+ # @param version [String] The model version (e.g. +"1.0.0"+)
38
+ # @param format [String] The model format (default: +"TORCH_SCRIPT"+)
39
+ # @return [ML_INFO, nil] The registered model info, or +nil+ if lookup fails after registration
40
+ # @raise [RuntimeError] If the registration task reports a +FAILED+ state
41
+ # @see https://opensearch.org/docs/latest/ml-commons-plugin/api/model-apis/register-model/ OpenSearch register model API
42
+ # @example
43
+ # model = client.models.register(name: "all-MiniLM-L6-v2", version: "1.0.0")
44
+ # puts model.id
45
+ def register(name:, version:, format: "TORCH_SCRIPT")
46
+ config = {
47
+ name: name,
48
+ version: version,
49
+ model_format: format
50
+ }
51
+
52
+ current = self[name]
53
+ return current if current
54
+
55
+ resp = @os.http.post("/_plugins/_ml/models/_register?deploy=true", body: config)
56
+ taskid = resp["task_id"]
57
+ loop do
58
+ model_install_response = @os.http.get("_plugins/_ml/tasks/#{taskid}")
59
+ break if model_install_response["state"] == "COMPLETED"
60
+ raise model_install_response["error"].to_s if model_install_response["state"] == "FAILED"
61
+ sleep(5)
62
+ end
63
+ self[name]
64
+ end
65
+
66
+ # Alias for {#register}.
67
+ # @see #register
68
+ alias_method :deploy, :register
69
+
70
+ # Get info about the latest version of a model by name, id, or partial name
71
+ # @todo make sure models are unique by nickname if nickname is found
72
+ # @param id_or_fullname_or_nickname [String] Exact model name, exact model ID, or
73
+ # a case-insensitive partial name. When multiple partial matches exist, returns the
74
+ # one with the highest version.
75
+ # @return [ML_INFO, nil] Matching model info, or +nil+ if not found
76
+ # @example Look up by exact name
77
+ # model = client.models["all-MiniLM-L6-v2"]
78
+ # @example Look up by partial name
79
+ # model = client.models["MiniLM"]
80
+ def [](id_or_fullname_or_nickname)
81
+ mlm = list
82
+ name = mlm.find { |x| x.name == id_or_fullname_or_nickname }
83
+ return name if name
84
+
85
+ id = mlm.find { |m| m.id == id_or_fullname_or_nickname }
86
+ return id if id
87
+
88
+ nickname_pattern = Regexp.new(id_or_fullname_or_nickname, "i")
89
+ nicks = mlm.find_all { |m| nickname_pattern.match(m.name) }.sort { |a, b| b.version <=> a.version }
90
+ nicks.first # could be nil
91
+ end
92
+
93
+ # Returns all deployed ML models in the cluster.
94
+ #
95
+ # @return [Array<ML_INFO>] Unique name/version/id triples for all deployed models
96
+ # @example
97
+ # client.models.list.each { |m| puts "#{m.name} #{m.version} (#{m.id})" }
98
+ def list
99
+ lst = raw_list.dig("hits", "hits").map { |x| x["_source"] }.each_with_object([]) do |ml, a|
100
+ model = ML_INFO.new(ml["name"], ml["model_version"], ml["model_id"])
101
+ a << model
102
+ end
103
+ lst.uniq
104
+ end
105
+
106
+ # Returns the raw OpenSearch response from the ML models search endpoint.
107
+ #
108
+ # This is the unprocessed response from +/_plugins/_ml/models/_search+, filtered
109
+ # to chunk 0 to avoid returning embedding chunks. Prefer {#list} or {#[]} for
110
+ # normal usage.
111
+ #
112
+ # @return [Hash] Raw OpenSearch search response
113
+ def raw_list
114
+ @os.http.get("/_plugins/_ml/models/_search",
115
+ body: {"query" => {"term" => {"chunk_number" => 0}}})
116
+ end
117
+
118
+ # Undeploys (unloads from memory) a model without deleting its registration.
119
+ #
120
+ # @param name_or_id [String] Model name, ID, or partial name accepted by {#[]}
121
+ # @return [Hash] The OpenSearch undeploy response
122
+ # @raise [NoMethodError] If no model matching +name_or_id+ is found
123
+ # @example
124
+ # client.models.undeploy!("all-MiniLM-L6-v2")
125
+ def undeploy!(name_or_id)
126
+ m = self[name_or_id]
127
+ @os.http.post("/_plugins/_ml/models/#{m.id}/_undeploy")
128
+ end
129
+
130
+ # Undeploys and permanently deletes a model from the cluster.
131
+ #
132
+ # @param name_or_id [String] Model name, ID, or partial name accepted by {#[]}
133
+ # @return [Hash] The OpenSearch delete response
134
+ # @raise [NoMethodError] If no model matching +name_or_id+ is found
135
+ # @example
136
+ # client.models.delete!("all-MiniLM-L6-v2")
137
+ def delete!(name_or_id)
138
+ m = self[name_or_id]
139
+ undeploy!(m.id)
140
+ @os.http.delete("/_plugins/_ml/models/#{m.id}")
141
+ end
142
+
143
+ # Deletes an ingest pipeline by name.
144
+ #
145
+ # @param pipeline_name [String] The name of the pipeline to delete
146
+ # @return [Hash] The OpenSearch acknowledgement response
147
+ # @example
148
+ # client.models.delete_pipeline!("my-embedding-pipeline")
149
+ def delete_pipeline!(pipeline_name)
150
+ @os.ingest.delete_pipeline(id: pipeline_name)
151
+ end
152
+
153
+ # Creates a text-embedding ingest pipeline backed by a deployed ML model.
154
+ #
155
+ # The pipeline uses the ML Commons +text_embedding+ processor to generate embeddings
156
+ # for each field in +field_map+. Because +text_embedding+ writes to a temporary field,
157
+ # the pipeline also inserts +copy+ processors to move the resulting +.knn+ vectors to
158
+ # the intended target fields.
159
+ #
160
+ # @param name [String] The name to give the ingest pipeline
161
+ # @param model [String] Model name, ID, or partial name accepted by {#[]}
162
+ # @param description [String] Human-readable description of the pipeline
163
+ # @param field_map [Hash{String => String}] Mapping of source text fields to target
164
+ # vector fields (e.g. +{ "title" => "title_embedding" }+)
165
+ # @return [Hash] The OpenSearch response
166
+ # @raise [RuntimeError] If no model matching +model+ is found
167
+ # @see https://opensearch.org/docs/latest/ml-commons-plugin/semantic-search/ OpenSearch semantic search docs
168
+ # @example
169
+ # client.models.create_pipeline(
170
+ # name: "product-embeddings",
171
+ # model: "all-MiniLM-L6-v2",
172
+ # description: "Generate embeddings for product titles",
173
+ # field_map: { "title" => "title_knn" }
174
+ # )
175
+ def create_pipeline(name:, model:, description:, field_map:)
176
+ m = self[model]
177
+ raise "Can't find model #{model}" unless m
178
+ url = "/_ingest/pipeline/#{name.gsub(/\s+/, " ").gsub(/\s+/, "_")}"
179
+ field_map_to_temp = field_map.transform_values { |actual_target| "#{actual_target}_temp" }
180
+ temp_to_field_map = field_map.values.each_with_object({}) do |actual_target, h|
181
+ h["#{actual_target}_temp"] = actual_target
182
+ end
183
+
184
+ payload = {
185
+ description: description,
186
+ processors: [
187
+ {
188
+ text_embedding: {
189
+ model_id: m.id,
190
+ field_map: field_map_to_temp
191
+ }
192
+ }
193
+ ]
194
+ }
195
+
196
+ temp_to_field_map.each_pair do |tmp, real|
197
+ payload[:processors] << {
198
+ copy: {
199
+ source_field: "#{tmp}.knn",
200
+ target_field: real,
201
+ ignore_missing: true,
202
+ remove_source: true
203
+ }
204
+ }
205
+ end
206
+ @os.http.put(url, body: payload)
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenSearch
4
+ module Sugar
5
+ # Current gem version.
6
+ VERSION = "1.0.0"
7
+ end
8
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenSearch
4
+ module Sugar
5
+ end
6
+ end
7
+
8
+ require "opensearch"
9
+
10
+ require_relative "sugar/version"
11
+ require_relative "sugar/index"
12
+ require_relative "sugar/client"
13
+
14
+ module OpenSearch
15
+ # Top-level namespace for the opensearch-sugar gem.
16
+ #
17
+ # Provides a thin, object-oriented wrapper around the {https://github.com/opensearch-project/opensearch-ruby opensearch-ruby}
18
+ # client. The main entry point is {OpenSearch::Sugar.client}, which returns a
19
+ # fully configured {OpenSearch::Sugar::Client}.
20
+ #
21
+ # @see OpenSearch::Sugar::Client
22
+ # @see OpenSearch::Sugar::Index
23
+ # @see OpenSearch::Sugar::Models
24
+ module Sugar
25
+ # Base error class for all opensearch-sugar exceptions.
26
+ #
27
+ # Raised when operations such as settings or mappings updates fail.
28
+ # Rescuing this class catches all gem-specific errors without catching
29
+ # unrelated OpenSearch transport-layer exceptions.
30
+ #
31
+ # @example
32
+ # begin
33
+ # client.update_settings(bad_settings, "my_index")
34
+ # rescue OpenSearch::Sugar::Error => e
35
+ # puts "Settings update failed: #{e.message}"
36
+ # end
37
+ class Error < StandardError; end
38
+
39
+ # Convenience factory that returns a new {OpenSearch::Sugar::Client}.
40
+ #
41
+ # Accepts the same keyword arguments as {OpenSearch::Sugar::Client#initialize}.
42
+ #
43
+ # @param kwargs [Hash] Keyword arguments forwarded to {OpenSearch::Sugar::Client#initialize}
44
+ # @return [OpenSearch::Sugar::Client]
45
+ # @example
46
+ # client = OpenSearch::Sugar.client(host: "https://localhost:9200")
47
+ def self.client(**kwargs)
48
+ OpenSearch::Sugar::Client.new(**kwargs)
49
+ end
50
+
51
+ # Alias for {.client}. Allows idiomatic +OpenSearch::Sugar.new(...)+ construction.
52
+ #
53
+ # @param kwargs [Hash] Keyword arguments forwarded to {OpenSearch::Sugar::Client#initialize}
54
+ # @return [OpenSearch::Sugar::Client]
55
+ # @example
56
+ # client = OpenSearch::Sugar.new
57
+ def self.new(**kwargs)
58
+ client(**kwargs)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,361 @@
1
+ # Delegated Method Calls in HOWTO.md
2
+
3
+ *(Documentation written by GitHub Copilot, powered by Claude Sonnet 4.5)*
4
+
5
+ This document identifies all method calls in HOWTO.md that rely on delegation to the underlying `OpenSearch::Client` rather than using OpenSearch::Sugar's own implemented methods.
6
+
7
+ ## OpenSearch::Sugar Implemented Methods
8
+
9
+ ### Client Methods (OpenSearch::Sugar::Client)
10
+ - `set_log_level(logger:, level:)`
11
+ - `has_index?(name)`
12
+ - `index_names`
13
+ - `[](index_name)` - index accessor
14
+ - `open_or_create(index_name)`
15
+ - `update_settings(settings, index_name)`
16
+ - `update_mappings(mappings, index_name)`
17
+ - `models` - accessor for Models instance
18
+ - `raw_client` - accessor for underlying client
19
+
20
+ ### Index Methods (OpenSearch::Sugar::Index)
21
+ - `update_settings(settings)`
22
+ - `settings`
23
+ - `update_mappings(mappings)`
24
+ - `mappings`
25
+ - `delete!`
26
+ - `count`
27
+ - `aliases`
28
+ - `create_alias(alias_name)`
29
+ - `all_available_analyzers` / `analyzers`
30
+ - `analyze_text(analyzer:, text:)`
31
+ - `analyze_text_field(field:, text:)`
32
+ - `delete_by_id(id)`
33
+ - `clear!`
34
+
35
+ ---
36
+
37
+ ## Delegated Method Calls in HOWTO.md
38
+
39
+ ### Connection and Configuration Section
40
+
41
+ **Line 94: `client.cluster.health`**
42
+ ```ruby
43
+ response = client.cluster.health
44
+ ```
45
+ - **Delegated to:** `OpenSearch::Client#cluster`
46
+ - **OpenSearch API:** [Cluster Health API](https://opensearch.org/docs/latest/api-reference/cluster-api/cluster-health/)
47
+ - **Purpose:** Get cluster health status
48
+
49
+ ---
50
+
51
+ ### Index Management Section
52
+
53
+ **Line 175: `client.indices.delete(index: 'my_index')`**
54
+ ```ruby
55
+ client.indices.delete(index: 'my_index')
56
+ ```
57
+ - **Delegated to:** `OpenSearch::Client#indices.delete`
58
+ - **OpenSearch API:** [Delete Index API](https://opensearch.org/docs/latest/api-reference/index-apis/delete-index/)
59
+ - **Note:** Sugar alternative exists: `index.delete!`
60
+
61
+ ---
62
+
63
+ ### Document Operations Section
64
+
65
+ **Line 196-203: `client.index(...)`**
66
+ ```ruby
67
+ # Add with explicit ID
68
+ client.index(
69
+ index: 'my_index',
70
+ id: 'doc123',
71
+ body: {
72
+ title: 'My Document',
73
+ content: 'Document content here'
74
+ }
75
+ )
76
+
77
+ # Add with auto-generated ID
78
+ response = client.index(
79
+ index: 'my_index',
80
+ body: { title: 'My Document' }
81
+ )
82
+ ```
83
+ - **Delegated to:** `OpenSearch::Client#index`
84
+ - **OpenSearch API:** [Index Document API](https://opensearch.org/docs/latest/api-reference/document-apis/index-document/)
85
+ - **Purpose:** Index a single document
86
+
87
+ **Line 220: `client.delete(index: 'my_index', id: 'doc123')`**
88
+ ```ruby
89
+ client.delete(index: 'my_index', id: 'doc123')
90
+ ```
91
+ - **Delegated to:** `OpenSearch::Client#delete`
92
+ - **OpenSearch API:** [Delete Document API](https://opensearch.org/docs/latest/api-reference/document-apis/delete-document/)
93
+ - **Note:** Sugar alternative exists: `index.delete_by_id('doc123')`
94
+
95
+ **Line 237-242: `client.get(...)`**
96
+ ```ruby
97
+ response = client.get(
98
+ index: 'my_index',
99
+ id: 'doc123'
100
+ )
101
+
102
+ document = response['_source']
103
+ ```
104
+ - **Delegated to:** `OpenSearch::Client#get`
105
+ - **OpenSearch API:** [Get Document API](https://opensearch.org/docs/latest/api-reference/document-apis/get-documents/)
106
+ - **Purpose:** Retrieve a document by ID
107
+
108
+ **Line 250-271: `client.update(...)`**
109
+ ```ruby
110
+ # Partial update
111
+ client.update(
112
+ index: 'my_index',
113
+ id: 'doc123',
114
+ body: {
115
+ doc: {
116
+ title: 'Updated Title'
117
+ }
118
+ }
119
+ )
120
+
121
+ # Full replacement
122
+ client.index(
123
+ index: 'my_index',
124
+ id: 'doc123',
125
+ body: {
126
+ title: 'New Title',
127
+ content: 'New content'
128
+ }
129
+ )
130
+ ```
131
+ - **Delegated to:** `OpenSearch::Client#update`
132
+ - **OpenSearch API:** [Update Document API](https://opensearch.org/docs/latest/api-reference/document-apis/update-document/)
133
+ - **Purpose:** Update an existing document
134
+
135
+ **Line 283: `client.bulk(body: operations)`**
136
+ ```ruby
137
+ response = client.bulk(body: operations)
138
+ ```
139
+ - **Delegated to:** `OpenSearch::Client#bulk`
140
+ - **OpenSearch API:** [Bulk API](https://opensearch.org/docs/latest/api-reference/document-apis/bulk/)
141
+ - **Purpose:** Perform multiple index/delete operations in a single request
142
+
143
+ **Line 301-304: `client.indices.refresh(...)`**
144
+ ```ruby
145
+ # Make documents immediately searchable
146
+ client.indices.refresh(index: 'my_index')
147
+
148
+ # Refresh all indexes
149
+ client.indices.refresh
150
+ ```
151
+ - **Delegated to:** `OpenSearch::Client#indices.refresh`
152
+ - **OpenSearch API:** [Refresh API](https://opensearch.org/docs/latest/api-reference/index-apis/refresh/)
153
+ - **Purpose:** Make recent changes searchable
154
+
155
+ ---
156
+
157
+ ### Search and Analysis Section
158
+
159
+ **Line 320-334: `client.search(...)`**
160
+ ```ruby
161
+ response = client.search(
162
+ index: 'my_index',
163
+ body: {
164
+ query: {
165
+ match: {
166
+ title: 'search terms'
167
+ }
168
+ }
169
+ }
170
+ )
171
+
172
+ hits = response['hits']['hits']
173
+ ```
174
+ - **Delegated to:** `OpenSearch::Client#search`
175
+ - **OpenSearch API:** [Search API](https://opensearch.org/docs/latest/api-reference/search/)
176
+ - **Purpose:** Execute a search query
177
+
178
+ **Line 391-406: `client.search(...)` with aggregations**
179
+ ```ruby
180
+ response = client.search(
181
+ index: 'products',
182
+ body: {
183
+ size: 0,
184
+ aggs: {
185
+ categories: {
186
+ terms: {
187
+ field: 'category.keyword',
188
+ size: 10
189
+ }
190
+ }
191
+ }
192
+ }
193
+ )
194
+ ```
195
+ - **Delegated to:** `OpenSearch::Client#search`
196
+ - **OpenSearch API:** [Search API with Aggregations](https://opensearch.org/docs/latest/aggregations/)
197
+ - **Purpose:** Perform aggregations
198
+
199
+ **Line 416-426: `client.search(...)` with multi_match**
200
+ ```ruby
201
+ response = client.search(
202
+ index: 'my_index',
203
+ body: {
204
+ query: {
205
+ multi_match: {
206
+ query: 'search terms',
207
+ fields: ['title^3', 'description^2', 'content'],
208
+ type: 'best_fields'
209
+ }
210
+ }
211
+ }
212
+ )
213
+ ```
214
+ - **Delegated to:** `OpenSearch::Client#search`
215
+ - **OpenSearch API:** [Multi-Match Query](https://opensearch.org/docs/latest/query-dsl/full-text/multi-match/)
216
+ - **Purpose:** Search across multiple fields
217
+
218
+ ---
219
+
220
+ ### Aliases Section
221
+
222
+ **Line 547-550: `client.indices.put_alias(...)`**
223
+ ```ruby
224
+ client.indices.put_alias(
225
+ index: 'my_index',
226
+ name: 'my_alias'
227
+ )
228
+ ```
229
+ - **Delegated to:** `OpenSearch::Client#indices.put_alias`
230
+ - **OpenSearch API:** [Create Alias API](https://opensearch.org/docs/latest/api-reference/alias/)
231
+ - **Note:** Sugar alternative exists: `index.create_alias('my_alias')`
232
+
233
+ **Line 564-567: `client.indices.delete_alias(...)`**
234
+ ```ruby
235
+ client.indices.delete_alias(
236
+ index: 'my_index',
237
+ name: 'my_alias'
238
+ )
239
+ ```
240
+ - **Delegated to:** `OpenSearch::Client#indices.delete_alias`
241
+ - **OpenSearch API:** [Delete Alias API](https://opensearch.org/docs/latest/api-reference/alias/)
242
+ - **Purpose:** Remove an alias
243
+
244
+ **Line 574-582: `client.indices.update_aliases(...)`**
245
+ ```ruby
246
+ client.indices.update_aliases(
247
+ body: {
248
+ actions: [
249
+ { remove: { index: 'old_index', alias: 'my_alias' } },
250
+ { add: { index: 'new_index', alias: 'my_alias' } }
251
+ ]
252
+ }
253
+ )
254
+ ```
255
+ - **Delegated to:** `OpenSearch::Client#indices.update_aliases`
256
+ - **OpenSearch API:** [Update Aliases API](https://opensearch.org/docs/latest/api-reference/alias/)
257
+ - **Purpose:** Atomic alias operations (swap aliases)
258
+
259
+ ---
260
+
261
+ ### ML Models Section
262
+
263
+ **Line 652-660: `client.index(...)` with pipeline**
264
+ ```ruby
265
+ client.index(
266
+ index: 'my_index',
267
+ pipeline: 'text_embedding',
268
+ body: {
269
+ text: 'This is my document text',
270
+ title: 'Document Title'
271
+ }
272
+ )
273
+ ```
274
+ - **Delegated to:** `OpenSearch::Client#index`
275
+ - **OpenSearch API:** [Index with Pipeline](https://opensearch.org/docs/latest/ingest-pipelines/)
276
+ - **Purpose:** Index document using an ingest pipeline
277
+
278
+ ---
279
+
280
+ ### Error Handling Section
281
+
282
+ **Line 722: `client.bulk(body: operations)`**
283
+ ```ruby
284
+ response = client.bulk(body: operations)
285
+ ```
286
+ - **Delegated to:** `OpenSearch::Client#bulk`
287
+ - **OpenSearch API:** [Bulk API](https://opensearch.org/docs/latest/api-reference/document-apis/bulk/)
288
+ - **Purpose:** Bulk operations error handling example
289
+
290
+ **Line 769-774: `client.index(...)` in retry example**
291
+ ```ruby
292
+ client.index(
293
+ index: 'my_index',
294
+ id: 'doc123',
295
+ body: { title: 'My Document' }
296
+ )
297
+ ```
298
+ - **Delegated to:** `OpenSearch::Client#index`
299
+ - **OpenSearch API:** [Index Document API](https://opensearch.org/docs/latest/api-reference/document-apis/index-document/)
300
+ - **Purpose:** Retry pattern example
301
+
302
+ ---
303
+
304
+ ## Summary Statistics
305
+
306
+ ### Total Delegated Method Calls: 19 unique examples
307
+
308
+ ### Breakdown by Category:
309
+ - **Connection & Configuration:** 1
310
+ - **Index Management:** 1
311
+ - **Document Operations:** 7
312
+ - **Search & Analysis:** 3
313
+ - **Aliases:** 3
314
+ - **ML Models:** 1
315
+ - **Error Handling:** 2 (duplicates of above)
316
+
317
+ ### Most Common Delegated Methods:
318
+ 1. `client.index(...)` - 5 occurrences (indexing documents)
319
+ 2. `client.search(...)` - 3 occurrences (searching)
320
+ 3. `client.bulk(...)` - 2 occurrences (bulk operations)
321
+ 4. `client.indices.*` - 6 occurrences (various index operations)
322
+ 5. `client.cluster.*` - 1 occurrence (cluster health)
323
+ 6. `client.get(...)` - 1 occurrence (get document)
324
+ 7. `client.update(...)` - 1 occurrence (update document)
325
+ 8. `client.delete(...)` - 1 occurrence (delete document)
326
+
327
+ ### Methods with Sugar Alternatives Available:
328
+ - `client.indices.delete(index: 'my_index')` → `index.delete!`
329
+ - `client.delete(index: 'my_index', id: 'doc123')` → `index.delete_by_id('doc123')`
330
+ - `client.indices.put_alias(...)` → `index.create_alias('my_alias')`
331
+
332
+ ### Methods Without Sugar Alternatives (Must Use Delegation):
333
+ - `client.index(...)` - Document indexing
334
+ - `client.get(...)` - Get document by ID
335
+ - `client.update(...)` - Update document
336
+ - `client.search(...)` - Search queries
337
+ - `client.bulk(...)` - Bulk operations
338
+ - `client.indices.refresh(...)` - Refresh index
339
+ - `client.indices.delete_alias(...)` - Delete alias
340
+ - `client.indices.update_aliases(...)` - Atomic alias updates
341
+ - `client.cluster.health` - Cluster health
342
+
343
+ ---
344
+
345
+ ## Recommendation
346
+
347
+ The documentation correctly demonstrates the delegation pattern by showing:
348
+ 1. **When Sugar methods exist** - it generally uses them (like `index.count`, `index.delete!`, `index.create_alias`)
349
+ 2. **When Sugar methods don't exist** - it demonstrates delegation (like `client.search`, `client.index`, `client.bulk`)
350
+ 3. **Both options when available** - showing users they have choices
351
+
352
+ This aligns with OpenSearch::Sugar's design philosophy: "Use sugar where you want it, raw client where you need it."
353
+
354
+ The documentation could be improved by adding a note in each delegated example indicating:
355
+ ```ruby
356
+ # Using delegation - OpenSearch::Sugar::Client forwards this to OpenSearch::Client
357
+ client.search(...)
358
+ ```
359
+
360
+ This would make it clearer to users which methods are delegated vs. implemented.
361
+