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,685 @@
1
+ # Understanding OpenSearch::Sugar
2
+
3
+ *(Documentation written by GitHub Copilot, powered by Claude Sonnet 4.5)*
4
+
5
+ This document provides conceptual explanations and discussions about OpenSearch::Sugar's design, architecture, and key concepts. Rather than showing you what to do, it helps you understand why things work the way they do.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Design Philosophy](#design-philosophy)
10
+ - [Architecture and Patterns](#architecture-and-patterns)
11
+ - [When to Use OpenSearch::Sugar](#when-to-use-opensearchsugar)
12
+ - [Understanding Text Analysis](#understanding-text-analysis)
13
+ - [Index Management Concepts](#index-management-concepts)
14
+ - [ML Models and Embeddings](#ml-models-and-embeddings)
15
+ - [Performance Considerations](#performance-considerations)
16
+ - [Security and Best Practices](#security-and-best-practices)
17
+
18
+ ---
19
+
20
+ ## Design Philosophy
21
+
22
+ ### The "Sugar" Metaphor
23
+
24
+ OpenSearch::Sugar is intentionally named to reflect its purpose: it adds sweetness (convenience) on top of the official OpenSearch Ruby client without hiding or replacing the underlying functionality. Just as you can enjoy coffee with or without sugar, you can use OpenSearch with or without this wrapper.
25
+
26
+ ### Core Principles
27
+
28
+ #### 1. Convention Over Configuration
29
+
30
+ OpenSearch::Sugar provides sensible defaults that work out of the box:
31
+
32
+ ```ruby
33
+ # Just works - uses environment variables for connection
34
+ client = OpenSearch::Sugar.new
35
+
36
+ # Opens or creates - handles existence checks automatically
37
+ index = client.open_or_create('my_index')
38
+ ```
39
+
40
+ The gem assumes common use cases and handles the boilerplate, but everything can be customized when needed.
41
+
42
+ #### 2. Progressive Disclosure
43
+
44
+ Simple things should be simple, complex things should be possible:
45
+
46
+ ```ruby
47
+ # Simple: Get document count
48
+ count = index.count
49
+
50
+ # Complex: Use full OpenSearch query DSL
51
+ results = client.search(
52
+ index: 'my_index',
53
+ body: {
54
+ query: { ... complex query ... },
55
+ aggs: { ... complex aggregations ... }
56
+ }
57
+ )
58
+ ```
59
+
60
+ You don't need to learn the entire OpenSearch API to do basic tasks, but you can access the full API when needed.
61
+
62
+ #### 3. Transparency Through Delegation
63
+
64
+ OpenSearch::Sugar::Client uses Ruby's `SimpleDelegator` to wrap `OpenSearch::Client`. This design choice has important implications:
65
+
66
+ - **No hidden functionality**: Every method available in the official client is available here
67
+ - **No magic**: You can read the opensearch-ruby documentation and apply it directly
68
+ - **Easy migration**: Code written with opensearch-ruby works with OpenSearch::Sugar
69
+ - **Best of both worlds**: Use sugar methods for common tasks, raw client for everything else
70
+
71
+ ```ruby
72
+ # Sugar method
73
+ client.has_index?('my_index')
74
+
75
+ # Delegated to raw client
76
+ client.indices.exists?(index: 'my_index')
77
+
78
+ # Both work! Use whichever feels more natural
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Architecture and Patterns
84
+
85
+ ### The Delegation Pattern
86
+
87
+ The `Client` class inherits from `SimpleDelegator`, which forwards all method calls to the wrapped object:
88
+
89
+ ```ruby
90
+ class Client < SimpleDelegator
91
+ def initialize(**kwargs)
92
+ @raw_client = OpenSearch::Client.new(**kwargs)
93
+ __setobj__(@raw_client) # Set up delegation
94
+ # Now all raw_client methods are available
95
+ end
96
+ end
97
+ ```
98
+
99
+ **Why this matters:**
100
+ - You're never "trapped" by the abstraction
101
+ - The gem doesn't need to keep up with every OpenSearch API change
102
+ - Documentation from the official client applies directly
103
+
104
+ ### The Facade Pattern
105
+
106
+ The `Index` class acts as a facade, simplifying complex sequences of operations:
107
+
108
+ ```ruby
109
+ def update_settings(settings)
110
+ # Behind the scenes:
111
+ # 1. Close the index
112
+ # 2. Apply settings
113
+ # 3. Reopen the index
114
+ # 4. Handle errors gracefully
115
+ client.update_settings(settings, name)
116
+ end
117
+ ```
118
+
119
+ **Benefits:**
120
+ - Error-prone sequences become single method calls
121
+ - Consistent error handling
122
+ - Easier testing and maintenance
123
+
124
+ ### The Repository Pattern
125
+
126
+ The `Models` class acts as a repository for ML models:
127
+
128
+ ```ruby
129
+ models = client.models
130
+
131
+ # Find by various identifiers
132
+ model = models['name']
133
+ model = models['id']
134
+ model = models['partial_name_match']
135
+
136
+ # List all
137
+ all_models = models.list
138
+ ```
139
+
140
+ **Why this works well:**
141
+ - ML models are treated as first-class resources
142
+ - Complex queries (by name, ID, or nickname) are unified
143
+ - The interface hides OpenSearch's internal model storage structure
144
+
145
+ ---
146
+
147
+ ## When to Use OpenSearch::Sugar
148
+
149
+ ### Perfect Use Cases
150
+
151
+ **1. Application Development**
152
+ - Building Ruby applications that use OpenSearch
153
+ - Creating search features in Rails apps
154
+ - Developing internal tools and scripts
155
+
156
+ **2. Index Management**
157
+ - Creating and configuring indexes
158
+ - Managing settings and mappings
159
+ - Working with aliases and index lifecycle
160
+
161
+ **3. ML Integration**
162
+ - Deploying and managing ML models
163
+ - Creating embedding pipelines
164
+ - Vector search applications
165
+
166
+ **4. Prototyping and Exploration**
167
+ - Quickly testing OpenSearch features
168
+ - Learning OpenSearch concepts
169
+ - Building proof-of-concepts
170
+
171
+ ### When to Use the Raw Client Instead
172
+
173
+ **1. Complex Queries**
174
+ - Advanced search DSL with nested aggregations
175
+ - Specialized query types not wrapped by sugar methods
176
+ - Performance-critical search operations where you need fine control
177
+
178
+ **2. Bulk Operations at Scale**
179
+ - Processing millions of documents
180
+ - Custom bulk error handling
181
+ - Streaming data ingestion
182
+
183
+ **3. Low-Level Operations**
184
+ - Cluster management
185
+ - Shard allocation
186
+ - Snapshot and restore
187
+
188
+ **The good news:** You don't have to choose! Use sugar methods where they help, drop to the raw client where they don't:
189
+
190
+ ```ruby
191
+ # Mix and match freely
192
+ index = client.open_or_create('my_index') # Sugar
193
+ count = index.count # Sugar
194
+
195
+ response = client.search( # Raw client
196
+ index: index.name,
197
+ body: { query: { ... } }
198
+ )
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Understanding Text Analysis
204
+
205
+ ### Why Text Analysis Matters
206
+
207
+ Text analysis is the process of converting text into searchable tokens. It's one of the most important concepts in OpenSearch:
208
+
209
+ ```ruby
210
+ # Original text
211
+ "The Quick Brown Fox"
212
+
213
+ # After standard analyzer
214
+ ["the", "quick", "brown", "fox"]
215
+ ```
216
+
217
+ Without proper analysis, searches won't work as expected.
218
+
219
+ ### The Analysis Pipeline
220
+
221
+ Every field goes through these stages:
222
+
223
+ 1. **Character filters** - Modify the text (e.g., strip HTML)
224
+ 2. **Tokenizer** - Split text into tokens
225
+ 3. **Token filters** - Modify tokens (lowercase, stemming, stop words)
226
+
227
+ ```ruby
228
+ settings = {
229
+ settings: {
230
+ analysis: {
231
+ analyzer: {
232
+ my_analyzer: {
233
+ type: 'custom',
234
+ char_filter: ['html_strip'], # 1. Remove HTML
235
+ tokenizer: 'standard', # 2. Split on whitespace/punctuation
236
+ filter: ['lowercase', 'stop'] # 3. Lowercase + remove stop words
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### Index-Time vs Search-Time Analysis
245
+
246
+ A common pattern is using different analyzers for indexing and searching:
247
+
248
+ ```ruby
249
+ mappings = {
250
+ mappings: {
251
+ properties: {
252
+ title: {
253
+ type: 'text',
254
+ analyzer: 'strict_analyzer', # Index-time: aggressive filtering
255
+ search_analyzer: 'lenient_analyzer' # Search-time: keep more terms
256
+ }
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ **Why?**
263
+ - **Index-time**: Be strict, remove noise, normalize heavily
264
+ - **Search-time**: Be lenient, match user's exact input
265
+
266
+ ### Testing Your Analyzers
267
+
268
+ OpenSearch::Sugar makes it easy to test analysis:
269
+
270
+ ```ruby
271
+ tokens = index.analyze_text(
272
+ analyzer: 'my_analyzer',
273
+ text: 'Sample text'
274
+ )
275
+
276
+ # See exactly what gets indexed!
277
+ puts tokens
278
+ ```
279
+
280
+ **Always test your analyzers** before indexing production data.
281
+
282
+ ---
283
+
284
+ ## Index Management Concepts
285
+
286
+ ### The Index Lifecycle
287
+
288
+ Indexes in OpenSearch typically follow this lifecycle:
289
+
290
+ 1. **Create** - Define structure
291
+ 2. **Configure** - Set analyzers, shards, replicas
292
+ 3. **Map** - Define field types
293
+ 4. **Populate** - Add documents
294
+ 5. **Query** - Search and retrieve
295
+ 6. **Maintain** - Update settings, reindex
296
+ 7. **Archive/Delete** - Remove when no longer needed
297
+
298
+ OpenSearch::Sugar provides methods for each stage.
299
+
300
+ ### Settings vs Mappings
301
+
302
+ **Settings** control how the index behaves:
303
+ - Number of shards and replicas
304
+ - Refresh intervals
305
+ - Analysis configuration (analyzers, tokenizers, filters)
306
+
307
+ **Mappings** define the document structure:
308
+ - Field names and types
309
+ - Which analyzer each field uses
310
+ - Whether fields are indexed, stored, or both
311
+
312
+ ```ruby
313
+ # Settings: HOW the index works
314
+ index.update_settings(
315
+ settings: {
316
+ number_of_replicas: 2,
317
+ analysis: { ... }
318
+ }
319
+ )
320
+
321
+ # Mappings: WHAT the documents contain
322
+ index.update_mappings(
323
+ mappings: {
324
+ properties: {
325
+ title: { type: 'text' },
326
+ date: { type: 'date' }
327
+ }
328
+ }
329
+ )
330
+ ```
331
+
332
+ ### Why Indexes Need to Close for Updates
333
+
334
+ Some settings and mappings can't be changed on an open index because:
335
+ - They affect how data is stored on disk
336
+ - Changing them would require rewriting existing data
337
+ - OpenSearch needs to ensure consistency
338
+
339
+ OpenSearch::Sugar handles this automatically:
340
+
341
+ ```ruby
342
+ # Behind the scenes:
343
+ # indices.close(index: 'my_index')
344
+ # indices.put_settings(...)
345
+ # indices.open(index: 'my_index')
346
+
347
+ index.update_settings(settings)
348
+ ```
349
+
350
+ ### The Alias Pattern
351
+
352
+ Aliases are pointers to indexes. They enable:
353
+
354
+ 1. **Zero-downtime reindexing**
355
+ 2. **Multiple indexes in one query**
356
+ 3. **Versioned indexes**
357
+
358
+ ```ruby
359
+ # Create new index with updated mappings
360
+ new_index = client.create('products_v2')
361
+ new_index.update_mappings(improved_mappings)
362
+
363
+ # Copy data from old to new
364
+ # ... reindex operation ...
365
+
366
+ # Atomic switch
367
+ client.indices.update_aliases(
368
+ body: {
369
+ actions: [
370
+ { remove: { index: 'products_v1', alias: 'products' } },
371
+ { add: { index: 'products_v2', alias: 'products' } }
372
+ ]
373
+ }
374
+ )
375
+
376
+ # Applications using 'products' alias are unaffected!
377
+ ```
378
+
379
+ ---
380
+
381
+ ## ML Models and Embeddings
382
+
383
+ ### What Are Embeddings?
384
+
385
+ Embeddings are numerical representations of text (or other data) that capture semantic meaning:
386
+
387
+ ```
388
+ "cat" → [0.2, 0.8, 0.1, ...]
389
+ "kitten" → [0.3, 0.7, 0.2, ...] # Similar to "cat"
390
+ "car" → [0.9, 0.1, 0.3, ...] # Different from "cat"
391
+ ```
392
+
393
+ This enables:
394
+ - Semantic search (find documents by meaning, not just keywords)
395
+ - Recommendations (find similar documents)
396
+ - Classification and clustering
397
+
398
+ ### The ML Model Workflow
399
+
400
+ OpenSearch::Sugar simplifies the ML workflow:
401
+
402
+ ```ruby
403
+ # 1. Register and deploy a model
404
+ model = client.models.register(
405
+ name: 'huggingface/sentence-transformers/all-MiniLM-L12-v2',
406
+ version: '1.0.1'
407
+ )
408
+
409
+ # 2. Create an ingest pipeline
410
+ client.models.create_pipeline(
411
+ name: 'embedding_pipeline',
412
+ model: model.name,
413
+ field_map: { 'text' => 'text_embedding' }
414
+ )
415
+
416
+ # 3. Index documents with the pipeline
417
+ client.index(
418
+ index: 'my_index',
419
+ pipeline: 'embedding_pipeline',
420
+ body: { text: 'My document text' }
421
+ )
422
+ # The pipeline automatically adds 'text_embedding' field
423
+
424
+ # 4. Search using k-NN
425
+ results = client.search(
426
+ index: 'my_index',
427
+ body: {
428
+ query: {
429
+ knn: {
430
+ text_embedding: {
431
+ vector: query_embedding,
432
+ k: 10
433
+ }
434
+ }
435
+ }
436
+ }
437
+ )
438
+ ```
439
+
440
+ ### Why Models Are Complex
441
+
442
+ ML models in OpenSearch:
443
+ - Must be registered before use
444
+ - Take time to deploy
445
+ - Have specific format requirements
446
+ - Need to be undeployed before deletion
447
+
448
+ OpenSearch::Sugar handles these complexities:
449
+
450
+ ```ruby
451
+ # Simple interface
452
+ model = models.register(...) # Waits for deployment
453
+ models.delete!(model) # Undeploys first, then deletes
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Performance Considerations
459
+
460
+ ### Indexing Performance
461
+
462
+ **Bulk operations are essential** for high-throughput indexing:
463
+
464
+ ```ruby
465
+ # Slow: Individual requests
466
+ documents.each do |doc|
467
+ client.index(index: 'my_index', body: doc)
468
+ end
469
+
470
+ # Fast: Bulk request
471
+ operations = documents.flat_map do |doc|
472
+ [
473
+ { index: { _index: 'my_index', _id: doc[:id] } },
474
+ doc
475
+ ]
476
+ end
477
+ client.bulk(body: operations)
478
+ ```
479
+
480
+ **Why?**
481
+ - Network overhead: 1 request vs. 1000 requests
482
+ - OpenSearch optimization: Processes batches more efficiently
483
+ - Refresh timing: Fewer refresh operations
484
+
485
+ ### Refresh Interval
486
+
487
+ By default, OpenSearch makes documents searchable every second:
488
+
489
+ ```ruby
490
+ # For bulk indexing, disable automatic refresh
491
+ index.update_settings(
492
+ settings: { refresh_interval: '-1' }
493
+ )
494
+
495
+ # Index lots of documents
496
+ # ...
497
+
498
+ # Manual refresh when done
499
+ client.indices.refresh(index: index.name)
500
+
501
+ # Re-enable automatic refresh
502
+ index.update_settings(
503
+ settings: { refresh_interval: '1s' }
504
+ )
505
+ ```
506
+
507
+ **Tradeoff:**
508
+ - Fast indexing but delayed search visibility
509
+ - vs.
510
+ - Slower indexing but immediate search visibility
511
+
512
+ ### Replica Strategy
513
+
514
+ Replicas improve read performance but slow down indexing:
515
+
516
+ ```ruby
517
+ # For initial bulk load
518
+ index.update_settings(settings: { number_of_replicas: 0 })
519
+
520
+ # Index documents
521
+ # ...
522
+
523
+ # Add replicas after indexing
524
+ index.update_settings(settings: { number_of_replicas: 2 })
525
+ ```
526
+
527
+ ### Search Performance
528
+
529
+ **Use filters when possible:**
530
+
531
+ ```ruby
532
+ # Faster: filter (cacheable)
533
+ {
534
+ query: {
535
+ bool: {
536
+ filter: [
537
+ { term: { status: 'published' } }
538
+ ]
539
+ }
540
+ }
541
+ }
542
+
543
+ # Slower: query (scoring required)
544
+ {
545
+ query: {
546
+ match: { status: 'published' }
547
+ }
548
+ }
549
+ ```
550
+
551
+ Filters don't calculate relevance scores and can be cached.
552
+
553
+ ---
554
+
555
+ ## Security and Best Practices
556
+
557
+ ### Connection Security
558
+
559
+ **Development:**
560
+ ```ruby
561
+ client = OpenSearch::Sugar.new(
562
+ host: 'https://localhost:9200',
563
+ transport_options: {
564
+ ssl: { verify: false } # OK for dev
565
+ }
566
+ )
567
+ ```
568
+
569
+ **Production:**
570
+ ```ruby
571
+ client = OpenSearch::Sugar.new(
572
+ host: 'https://search.production.com:9200',
573
+ user: ENV['OPENSEARCH_USER'],
574
+ password: ENV['OPENSEARCH_PASSWORD'],
575
+ transport_options: {
576
+ ssl: {
577
+ verify: true,
578
+ ca_file: '/path/to/ca.pem'
579
+ }
580
+ }
581
+ )
582
+ ```
583
+
584
+ **Never:**
585
+ - Hardcode credentials in source code
586
+ - Disable SSL verification in production
587
+ - Use default passwords
588
+ - Expose OpenSearch directly to the internet
589
+
590
+ ### Environment Variables
591
+
592
+ Use environment variables for configuration:
593
+
594
+ ```ruby
595
+ # .env file
596
+ OPENSEARCH_URL=https://localhost:9200
597
+ OPENSEARCH_USER=admin
598
+ OPENSEARCH_PASSWORD=secret_password
599
+
600
+ # Load with dotenv gem
601
+ require 'dotenv/load'
602
+
603
+ # Client automatically uses these
604
+ client = OpenSearch::Sugar.new
605
+ ```
606
+
607
+ **Benefits:**
608
+ - Different settings per environment
609
+ - No secrets in version control
610
+ - Easy configuration management
611
+
612
+ ### Index Design Best Practices
613
+
614
+ **1. Use appropriate field types:**
615
+ ```ruby
616
+ {
617
+ title: { type: 'text' }, # Full-text search
618
+ status: { type: 'keyword' }, # Exact match, aggregations
619
+ price: { type: 'float' }, # Numeric range queries
620
+ created_at: { type: 'date' } # Date range queries
621
+ }
622
+ ```
623
+
624
+ **2. Include keyword subfields for text:**
625
+ ```ruby
626
+ {
627
+ title: {
628
+ type: 'text',
629
+ fields: {
630
+ keyword: { type: 'keyword' }
631
+ }
632
+ }
633
+ }
634
+
635
+ # Now you can:
636
+ # - Search: match on 'title'
637
+ # - Sort/aggregate: use 'title.keyword'
638
+ ```
639
+
640
+ **3. Design for your query patterns:**
641
+ - If you only filter, use keyword type
642
+ - If you search text, use text type with appropriate analyzer
643
+ - If you do both, use text with keyword subfield
644
+
645
+ ### Error Handling
646
+
647
+ Always handle errors in production:
648
+
649
+ ```ruby
650
+ begin
651
+ index = client.open_or_create('my_index')
652
+ index.count
653
+ rescue OpenSearch::Transport::Transport::Error => e
654
+ logger.error "OpenSearch error: #{e.message}"
655
+ # Handle error appropriately
656
+ rescue ArgumentError => e
657
+ logger.error "Invalid argument: #{e.message}"
658
+ # Handle error appropriately
659
+ end
660
+ ```
661
+
662
+ **Don't:**
663
+ - Silently ignore errors
664
+ - Use broad rescue clauses
665
+ - Let OpenSearch exceptions crash your application
666
+
667
+ ---
668
+
669
+ ## Conclusion
670
+
671
+ OpenSearch::Sugar is designed to make working with OpenSearch more enjoyable and productive while never hiding the power of the underlying system. Understanding these concepts will help you make better decisions about:
672
+
673
+ - When to use sugar methods vs. raw client methods
674
+ - How to design efficient indexes
675
+ - How to optimize for your specific use case
676
+ - How to build maintainable, secure applications
677
+
678
+ ## Further Reading
679
+
680
+ - **[Tutorial](TUTORIAL.md)** - Hands-on learning experience
681
+ - **[How-to Guides](HOWTO.md)** - Solve specific problems
682
+ - **[Reference](REFERENCE.md)** - Complete API documentation
683
+ - **[OpenSearch Documentation](https://opensearch.org/docs/latest/)** - Official OpenSearch docs
684
+ - **[OpenSearch Best Practices](https://opensearch.org/docs/latest/tuning-your-cluster/)** - Performance tuning
685
+