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,798 @@
1
+ # How-to Guides
2
+
3
+ *(Documentation written by GitHub Copilot, powered by Claude Sonnet 4.5)*
4
+
5
+ This document provides practical recipes for solving specific problems with OpenSearch::Sugar. Each guide is focused on accomplishing a particular task.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Connection and Configuration](#connection-and-configuration)
10
+ - [Index Management](#index-management)
11
+ - [Document Operations](#document-operations)
12
+ - [Search and Analysis](#search-and-analysis)
13
+ - [Settings and Mappings](#settings-and-mappings)
14
+ - [Aliases](#aliases)
15
+ - [ML Models](#ml-models)
16
+ - [Error Handling](#error-handling)
17
+
18
+ ---
19
+
20
+ ## Connection and Configuration
21
+
22
+ ### How to Connect with Environment Variables
23
+
24
+ ```ruby
25
+ # Set environment variables first:
26
+ # OPENSEARCH_URL=https://localhost:9200
27
+ # OPENSEARCH_USER=admin
28
+ # OPENSEARCH_PASSWORD=secret
29
+
30
+ require 'opensearch/sugar'
31
+
32
+ # Client automatically uses environment variables
33
+ client = OpenSearch::Sugar.new
34
+ ```
35
+
36
+ ### How to Connect with Explicit Credentials
37
+
38
+ ```ruby
39
+ client = OpenSearch::Sugar.new(
40
+ host: 'https://search.example.com:9200',
41
+ user: 'myuser',
42
+ password: 'mypassword'
43
+ )
44
+ ```
45
+
46
+ ### How to Configure Connection Options
47
+
48
+ ```ruby
49
+ client = OpenSearch::Sugar.new(
50
+ host: 'https://localhost:9200',
51
+ user: 'admin',
52
+ password: 'admin',
53
+ retry_on_failure: 3,
54
+ request_timeout: 10,
55
+ log: true,
56
+ trace: false,
57
+ transport_options: {
58
+ ssl: {
59
+ verify: true,
60
+ ca_file: '/path/to/ca.pem'
61
+ }
62
+ }
63
+ )
64
+ ```
65
+
66
+ ### How to Disable SSL Verification (Development Only)
67
+
68
+ ```ruby
69
+ client = OpenSearch::Sugar.new(
70
+ host: 'https://localhost:9200',
71
+ transport_options: {
72
+ ssl: { verify: false }
73
+ }
74
+ )
75
+ ```
76
+
77
+ **Warning:** Only use this in development! Production should use proper SSL.
78
+
79
+ ### How to Set Cluster Log Level
80
+
81
+ ```ruby
82
+ # Set all loggers to warn level
83
+ client.set_log_level(logger: 'logger._root', level: 'warn')
84
+
85
+ # Set specific logger
86
+ client.set_log_level(logger: 'index.search.slowlog', level: 'debug')
87
+ ```
88
+
89
+ See [OpenSearch Logging Documentation](https://opensearch.org/docs/latest/install-and-configure/configuring-opensearch/logs/)
90
+
91
+ ### How to Access the Raw Client
92
+
93
+ ```ruby
94
+ # OpenSearch::Sugar::Client delegates to the raw client
95
+ # So you can call any opensearch-ruby method directly:
96
+ response = client.cluster.health
97
+
98
+ # Or access explicitly:
99
+ raw = client.raw_client
100
+ response = raw.cluster.health
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Index Management
106
+
107
+ ### How to Create an Index
108
+
109
+ ```ruby
110
+ # Create a new index with k-NN enabled (default)
111
+ index = OpenSearch::Sugar::Index.create(
112
+ client: client,
113
+ name: 'my_index',
114
+ knn: true
115
+ )
116
+
117
+ # Create without k-NN
118
+ index = OpenSearch::Sugar::Index.create(
119
+ client: client,
120
+ name: 'my_index',
121
+ knn: false
122
+ )
123
+ ```
124
+
125
+ **When this fails:**
126
+ - Index already exists → Use `open_or_create_index` or delete the existing index first
127
+
128
+ ### How to Open an Existing Index
129
+
130
+ ```ruby
131
+ # Open an existing index
132
+ index = client['my_index']
133
+
134
+ # Or using the explicit method
135
+ index = OpenSearch::Sugar::Index.open(client: client, name: 'my_index')
136
+ ```
137
+
138
+ **When this fails:**
139
+ - Index doesn't exist → Use `create` or `open_or_create_index`
140
+
141
+ ### How to Open or Create an Index
142
+
143
+ ```ruby
144
+ # Opens if exists, creates if doesn't
145
+ index = client.open_or_create_index('my_index')
146
+ ```
147
+
148
+ **Use this when:**
149
+ - You're not sure if the index exists
150
+ - You want idempotent setup scripts
151
+
152
+ ### How to Check if an Index Exists
153
+
154
+ ```ruby
155
+ if client.has_index?('my_index')
156
+ puts "Index exists!"
157
+ else
158
+ puts "Index not found"
159
+ end
160
+ ```
161
+
162
+ ### How to List All Indexes
163
+
164
+ ```ruby
165
+ # Get array of index names
166
+ names = client.index_names
167
+ puts "Indexes: #{names.join(', ')}"
168
+ ```
169
+
170
+ ### How to Delete an Index
171
+
172
+ ```ruby
173
+ index = client['my_index']
174
+ index.delete!
175
+
176
+ # Or directly
177
+ client.indices.delete(index: 'my_index')
178
+ ```
179
+
180
+ **Warning:** This permanently deletes the index and all its data!
181
+
182
+ ### How to Count Documents in an Index
183
+
184
+ ```ruby
185
+ index = client['my_index']
186
+ count = index.count
187
+ puts "Total documents: #{count}"
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Document Operations
193
+
194
+ ### How to Add a Single Document
195
+
196
+ ```ruby
197
+ # Add with explicit ID
198
+ client.index(
199
+ index: 'my_index',
200
+ id: 'doc123',
201
+ body: {
202
+ title: 'My Document',
203
+ content: 'Document content here'
204
+ }
205
+ )
206
+
207
+ # Add with auto-generated ID
208
+ response = client.index(
209
+ index: 'my_index',
210
+ body: { title: 'My Document' }
211
+ )
212
+ puts "Generated ID: #{response['_id']}"
213
+ ```
214
+
215
+ ### How to Delete a Document by ID
216
+
217
+ ```ruby
218
+ index = client['my_index']
219
+ index.delete_by_id('doc123')
220
+
221
+ # Or using raw client
222
+ client.delete(index: 'my_index', id: 'doc123')
223
+ ```
224
+
225
+ ### How to Delete All Documents from an Index
226
+
227
+ ```ruby
228
+ index = client['my_index']
229
+ deleted_count = index.clear!
230
+ puts "Deleted #{deleted_count} documents"
231
+ ```
232
+
233
+ **Note:** This keeps the index structure but removes all documents.
234
+
235
+ ### How to Get a Document by ID
236
+
237
+ ```ruby
238
+ # Using raw client (OpenSearch::Sugar delegates all methods)
239
+ response = client.get(
240
+ index: 'my_index',
241
+ id: 'doc123'
242
+ )
243
+
244
+ document = response['_source']
245
+ puts document
246
+ ```
247
+
248
+ ### How to Update a Document
249
+
250
+ ```ruby
251
+ # Partial update
252
+ client.update(
253
+ index: 'my_index',
254
+ id: 'doc123',
255
+ body: {
256
+ doc: {
257
+ title: 'Updated Title'
258
+ }
259
+ }
260
+ )
261
+
262
+ # Full replacement
263
+ client.index(
264
+ index: 'my_index',
265
+ id: 'doc123',
266
+ body: {
267
+ title: 'New Title',
268
+ content: 'New content'
269
+ }
270
+ )
271
+ ```
272
+
273
+ ### How to Bulk Index Documents
274
+
275
+ ```ruby
276
+ # Prepare bulk operations
277
+ operations = []
278
+
279
+ documents.each do |doc|
280
+ operations << { index: { _index: 'my_index', _id: doc[:id] } }
281
+ operations << doc
282
+ end
283
+
284
+ # Execute bulk request
285
+ response = client.bulk(body: operations)
286
+
287
+ # Check for errors
288
+ if response['errors']
289
+ response['items'].each do |item|
290
+ if item['index']['error']
291
+ puts "Error indexing #{item['index']['_id']}: #{item['index']['error']['reason']}"
292
+ end
293
+ end
294
+ else
295
+ puts "Successfully indexed #{operations.size / 2} documents"
296
+ end
297
+ ```
298
+
299
+ ### How to Refresh an Index
300
+
301
+ ```ruby
302
+ index = client['my_index']
303
+
304
+ # Make documents immediately searchable
305
+ index.refresh
306
+
307
+ # Or using the raw client for a different index or all indexes
308
+ client.indices.refresh(index: 'other_index')
309
+ client.indices.refresh
310
+ ```
311
+
312
+ **When to use:**
313
+ - After indexing documents in tests
314
+ - When you need immediate search results
315
+ - Not recommended for production (automatic refresh is fine)
316
+
317
+ ---
318
+
319
+ ## Search and Analysis
320
+
321
+ ### How to Search Documents
322
+
323
+ ```ruby
324
+ # Simple match query
325
+ response = client.search(
326
+ index: 'my_index',
327
+ body: {
328
+ query: {
329
+ match: {
330
+ title: 'search terms'
331
+ }
332
+ }
333
+ }
334
+ )
335
+
336
+ # Get results
337
+ hits = response['hits']['hits']
338
+ hits.each do |hit|
339
+ doc = hit['_source']
340
+ score = hit['_score']
341
+ puts "#{doc['title']} (score: #{score})"
342
+ end
343
+ ```
344
+
345
+ ### How to Analyze Text with an Analyzer
346
+
347
+ ```ruby
348
+ index = client['my_index']
349
+
350
+ # Analyze text using a custom analyzer defined on the index
351
+ tokens = index.analyze_text(
352
+ analyzer: 'my_custom_analyzer',
353
+ text: 'The quick brown fox jumps'
354
+ )
355
+
356
+ puts tokens.join(', ')
357
+ # Output: quick, brown, fox, jumps
358
+ ```
359
+
360
+ **Note:** `analyze_text` works with analyzers explicitly defined in the index settings.
361
+ Use the raw `client.indices.analyze` API to test built-in analyzers (e.g., `standard`)
362
+ without defining them in the index.
363
+
364
+ ### How to Analyze Text Using a Field's Analyzer
365
+
366
+ ```ruby
367
+ index = client['my_index']
368
+
369
+ # Analyze using the analyzer configured for a field
370
+ tokens = index.analyze_text_field(
371
+ field: 'title',
372
+ text: 'The quick brown fox'
373
+ )
374
+ ```
375
+
376
+ **When this fails:**
377
+ - Field doesn't exist → Check field name in mappings
378
+ - No analyzer specified → Field must have an analyzer configured
379
+
380
+ ### How to List Available Analyzers
381
+
382
+ ```ruby
383
+ index = client['my_index']
384
+
385
+ # Get all analyzers (index + cluster level)
386
+ analyzers = index.analyzers
387
+ puts "Available analyzers: #{analyzers.join(', ')}"
388
+ ```
389
+
390
+ ### How to Perform Aggregations
391
+
392
+ ```ruby
393
+ # Count by category
394
+ response = client.search(
395
+ index: 'products',
396
+ body: {
397
+ size: 0, # Don't return documents
398
+ aggs: {
399
+ categories: {
400
+ terms: {
401
+ field: 'category.keyword',
402
+ size: 10
403
+ }
404
+ }
405
+ }
406
+ }
407
+ )
408
+
409
+ # Get aggregation results
410
+ buckets = response['aggregations']['categories']['buckets']
411
+ buckets.each do |bucket|
412
+ puts "#{bucket['key']}: #{bucket['doc_count']}"
413
+ end
414
+ ```
415
+
416
+ ### How to Perform a Multi-field Search
417
+
418
+ ```ruby
419
+ response = client.search(
420
+ index: 'my_index',
421
+ body: {
422
+ query: {
423
+ multi_match: {
424
+ query: 'search terms',
425
+ fields: ['title^3', 'description^2', 'content'],
426
+ type: 'best_fields'
427
+ }
428
+ }
429
+ }
430
+ )
431
+ ```
432
+
433
+ **Field boosting:**
434
+ - `^3` = 3x weight
435
+ - `^2` = 2x weight
436
+ - No boost = 1x weight
437
+
438
+ ---
439
+
440
+ ## Settings and Mappings
441
+
442
+ ### How to Update Index Settings
443
+
444
+ ```ruby
445
+ index = client['my_index']
446
+
447
+ settings = {
448
+ settings: {
449
+ number_of_replicas: 2,
450
+ refresh_interval: '5s',
451
+ analysis: {
452
+ analyzer: {
453
+ my_analyzer: {
454
+ type: 'custom',
455
+ tokenizer: 'standard',
456
+ filter: ['lowercase', 'stop']
457
+ }
458
+ }
459
+ }
460
+ }
461
+ }
462
+
463
+ index.update_settings(settings)
464
+ puts "Settings updated!"
465
+ ```
466
+
467
+ **The gem automatically:**
468
+ - Closes the index before updating
469
+ - Applies the settings
470
+ - Reopens the index
471
+
472
+ ### How to Get Index Settings
473
+
474
+ ```ruby
475
+ index = client['my_index']
476
+ settings = index.settings
477
+
478
+ # Access specific settings
479
+ analyzer_config = settings.dig('my_index', 'settings', 'index', 'analysis', 'analyzer')
480
+ ```
481
+
482
+ ### How to Update Index Mappings
483
+
484
+ ```ruby
485
+ index = client['my_index']
486
+
487
+ mappings = {
488
+ mappings: {
489
+ properties: {
490
+ title: { type: 'text', analyzer: 'standard' },
491
+ category: { type: 'keyword' },
492
+ price: { type: 'float' },
493
+ created_at: { type: 'date' }
494
+ }
495
+ }
496
+ }
497
+
498
+ index.update_mappings(mappings)
499
+ ```
500
+
501
+ ### How to Get Index Mappings
502
+
503
+ ```ruby
504
+ index = client['my_index']
505
+ mappings = index.mappings
506
+
507
+ # Access field mappings
508
+ properties = mappings.dig('my_index', 'mappings', 'properties')
509
+ ```
510
+
511
+ ### How to Create a Custom Analyzer
512
+
513
+ ```ruby
514
+ settings = {
515
+ settings: {
516
+ analysis: {
517
+ # Define custom analyzer
518
+ analyzer: {
519
+ email_analyzer: {
520
+ type: 'custom',
521
+ tokenizer: 'uax_url_email',
522
+ filter: ['lowercase']
523
+ }
524
+ },
525
+ # Or use a built-in analyzer with custom config
526
+ custom_standard: {
527
+ type: 'standard',
528
+ max_token_length: 255,
529
+ stopwords: ['the', 'is', 'and']
530
+ }
531
+ }
532
+ }
533
+ }
534
+
535
+ index.update_settings(settings)
536
+ ```
537
+
538
+ ---
539
+
540
+ ## Aliases
541
+
542
+ ### How to Create an Alias
543
+
544
+ ```ruby
545
+ index = client['my_index']
546
+ index.create_alias('my_alias')
547
+
548
+ # Or using raw client
549
+ client.indices.put_alias(
550
+ index: 'my_index',
551
+ name: 'my_alias'
552
+ )
553
+ ```
554
+
555
+ ### How to List Index Aliases
556
+
557
+ ```ruby
558
+ index = client['my_index']
559
+ aliases = index.aliases
560
+ puts "Aliases: #{aliases.join(', ')}"
561
+ ```
562
+
563
+ ### How to Delete an Alias
564
+
565
+ ```ruby
566
+ client.indices.delete_alias(
567
+ index: 'my_index',
568
+ name: 'my_alias'
569
+ )
570
+ ```
571
+
572
+ ### How to Switch an Alias to a New Index
573
+
574
+ ```ruby
575
+ # Atomic alias switch (for zero-downtime reindexing)
576
+ client.indices.update_aliases(
577
+ body: {
578
+ actions: [
579
+ { remove: { index: 'old_index', alias: 'my_alias' } },
580
+ { add: { index: 'new_index', alias: 'my_alias' } }
581
+ ]
582
+ }
583
+ )
584
+ ```
585
+
586
+ **Use case:** Rolling index updates without downtime
587
+
588
+ ---
589
+
590
+ ## ML Models
591
+
592
+ ### How to Register and Deploy a Model
593
+
594
+ ```ruby
595
+ models = client.models
596
+
597
+ model = models.register(
598
+ name: 'huggingface/sentence-transformers/all-MiniLM-L12-v2',
599
+ version: '1.0.1',
600
+ format: 'TORCH_SCRIPT'
601
+ )
602
+
603
+ puts "Model deployed: #{model.name}"
604
+ ```
605
+
606
+ **Note:** Registration includes deployment. The method waits for deployment to complete.
607
+
608
+ ### How to List All Models
609
+
610
+ ```ruby
611
+ models = client.models
612
+ all_models = models.list
613
+
614
+ all_models.each do |model|
615
+ puts "#{model.name} v#{model.version} (ID: #{model.id})"
616
+ end
617
+ ```
618
+
619
+ ### How to Find a Model
620
+
621
+ ```ruby
622
+ models = client.models
623
+
624
+ # By exact name
625
+ model = models['all-MiniLM-L12-v2']
626
+
627
+ # By ID
628
+ model = models['abc123']
629
+
630
+ # By partial name (case-insensitive regex)
631
+ model = models['minilm'] # Finds latest version
632
+ ```
633
+
634
+ ### How to Create an Embedding Pipeline
635
+
636
+ ```ruby
637
+ models = client.models
638
+
639
+ models.create_pipeline(
640
+ name: 'text_embedding',
641
+ model: 'all-MiniLM-L12-v2',
642
+ description: 'Generate embeddings for text fields',
643
+ field_map: {
644
+ 'text' => 'text_embedding',
645
+ 'title' => 'title_embedding'
646
+ }
647
+ )
648
+ ```
649
+
650
+ ### How to Use a Pipeline for Indexing
651
+
652
+ ```ruby
653
+ # Index with pipeline
654
+ client.index(
655
+ index: 'my_index',
656
+ pipeline: 'text_embedding',
657
+ body: {
658
+ text: 'This is my document text',
659
+ title: 'Document Title'
660
+ }
661
+ )
662
+
663
+ # The pipeline automatically adds embedding fields
664
+ ```
665
+
666
+ ### How to Undeploy a Model
667
+
668
+ ```ruby
669
+ models = client.models
670
+ models.undeploy!('all-MiniLM-L12-v2')
671
+ ```
672
+
673
+ ### How to Delete a Model
674
+
675
+ ```ruby
676
+ models = client.models
677
+ models.delete!('all-MiniLM-L12-v2')
678
+ # This automatically undeploys first
679
+ ```
680
+
681
+ ---
682
+
683
+
684
+ ## Error Handling
685
+
686
+ ### How to Handle Connection Errors
687
+
688
+ ```ruby
689
+ require 'opensearch/sugar'
690
+
691
+ begin
692
+ client = OpenSearch::Sugar.new(
693
+ host: 'https://wrong-host:9200',
694
+ user: 'admin',
695
+ password: 'wrong'
696
+ )
697
+ puts "Connected successfully"
698
+ rescue OpenSearch::Transport::Transport::Error => e
699
+ puts "Connection failed: #{e.message}"
700
+ end
701
+ ```
702
+
703
+ ### How to Handle Index Not Found
704
+
705
+ ```ruby
706
+ begin
707
+ index = OpenSearch::Sugar::Index.open(
708
+ client: client,
709
+ name: 'nonexistent_index'
710
+ )
711
+ rescue ArgumentError => e
712
+ puts "Error: #{e.message}"
713
+ # Create the index instead
714
+ index = OpenSearch::Sugar::Index.create(
715
+ client: client,
716
+ name: 'nonexistent_index'
717
+ )
718
+ end
719
+ ```
720
+
721
+ ### How to Handle Bulk Operation Errors
722
+
723
+ ```ruby
724
+ response = client.bulk(body: operations)
725
+
726
+ if response['errors']
727
+ errors = []
728
+ response['items'].each do |item|
729
+ if error = item.dig('index', 'error')
730
+ errors << {
731
+ id: item['index']['_id'],
732
+ reason: error['reason'],
733
+ type: error['type']
734
+ }
735
+ end
736
+ end
737
+
738
+ puts "Bulk operation had #{errors.size} errors:"
739
+ errors.each do |err|
740
+ puts " - #{err[:id]}: #{err[:reason]}"
741
+ end
742
+ else
743
+ puts "Bulk operation successful"
744
+ end
745
+ ```
746
+
747
+ ### How to Handle Analyzer Not Found
748
+
749
+ ```ruby
750
+ index = client['my_index']
751
+
752
+ begin
753
+ tokens = index.analyze_text(
754
+ analyzer: 'nonexistent_analyzer',
755
+ text: 'some text'
756
+ )
757
+ rescue ArgumentError => e
758
+ puts "Error: #{e.message}"
759
+ # List available analyzers
760
+ puts "Available: #{index.analyzers.join(', ')}"
761
+ end
762
+ ```
763
+
764
+ ### How to Retry Failed Operations
765
+
766
+ ```ruby
767
+ max_retries = 3
768
+ retry_count = 0
769
+
770
+ begin
771
+ client.index(
772
+ index: 'my_index',
773
+ id: 'doc123',
774
+ body: { title: 'My Document' }
775
+ )
776
+ rescue OpenSearch::Transport::Transport::Error => e
777
+ retry_count += 1
778
+ if retry_count < max_retries
779
+ puts "Retry #{retry_count}/#{max_retries}"
780
+ sleep(2 ** retry_count) # Exponential backoff
781
+ retry
782
+ else
783
+ puts "Failed after #{max_retries} retries: #{e.message}"
784
+ raise
785
+ end
786
+ end
787
+ ```
788
+
789
+ ---
790
+
791
+ ## Additional Resources
792
+
793
+ - [OpenSearch Documentation](https://opensearch.org/docs/latest/)
794
+ - [OpenSearch API Reference](https://opensearch.org/docs/latest/api-reference/)
795
+ - [Tutorial](TUTORIAL.md) - Learning-oriented guide
796
+ - [Reference](REFERENCE.md) - Complete API documentation
797
+ - [Explanation](EXPLANATION.md) - Understanding concepts
798
+