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.
- checksums.yaml +7 -0
- data/.agents/skills/diataxis/SKILL.md +142 -0
- data/.agents/skills/diataxis/references/examples.md +420 -0
- data/.agents/skills/diataxis/references/explanation-template.md +96 -0
- data/.agents/skills/diataxis/references/framework.md +400 -0
- data/.agents/skills/diataxis/references/how-to-guide-template.md +105 -0
- data/.agents/skills/diataxis/references/reference-template.md +110 -0
- data/.agents/skills/diataxis/references/tutorial-template.md +101 -0
- data/.agents/skills/diataxis/scripts/generate_index.py +139 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/AGENTS.md +120 -0
- data/CHANGELOG.md +5 -0
- data/Dockerfile.opensearch +4 -0
- data/Increase_Coverage.md +311 -0
- data/README.md +143 -0
- data/Rakefile +27 -0
- data/Steepfile +23 -0
- data/adrs/ADR-000-template.md +87 -0
- data/adrs/ADR-001-simpledelegator-for-client.md +138 -0
- data/adrs/ADR-002-facade-pattern-for-index.md +126 -0
- data/adrs/ADR-003-repository-pattern-for-models.md +148 -0
- data/adrs/ADR-004-integration-tests-no-mocking.md +91 -0
- data/adrs/ADR-005-exceptions-over-result-objects.md +107 -0
- data/adrs/ADR-006-ssl-on-by-default.md +95 -0
- data/adrs/ADR-007-selective-sugar-surface.md +118 -0
- data/adrs/ADR-008-integration-test-design.md +178 -0
- data/compose.yml +2 -0
- data/compose_opensearch.yml +31 -0
- data/docs/HOWTO.md +844 -0
- data/docs/REFERENCE.md +725 -0
- data/docs/TUTORIAL.md +327 -0
- data/docs/alias-api-design-notes.md +119 -0
- data/lib/opensearch/sugar/client.rb +300 -0
- data/lib/opensearch/sugar/index/include/utilities.rb +6 -0
- data/lib/opensearch/sugar/index.rb +339 -0
- data/lib/opensearch/sugar/models.rb +209 -0
- data/lib/opensearch/sugar/version.rb +8 -0
- data/lib/opensearch/sugar.rb +61 -0
- data/old_docs/DELEGATED_METHODS_ANALYSIS.md +361 -0
- data/old_docs/EXPLANATION.md +685 -0
- data/old_docs/README.md +155 -0
- data/old_docs/docs/CLI-PROPOSAL.md +257 -0
- data/old_docs/docs/HOWTO.md +798 -0
- data/old_docs/docs/REFERENCE.md +901 -0
- data/old_docs/docs/TUTORIAL.md +493 -0
- data/sig/opensearch/sugar.rbs +162 -0
- 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
|
+
|