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,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
|
+
|