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
data/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# opensearch-sugar
|
|
2
|
+
|
|
3
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
A Ruby gem that wraps the [opensearch-ruby](https://github.com/opensearch-project/opensearch-ruby)
|
|
7
|
+
client with an object-oriented shell -- collections (indexes), documents, ML models, etc --
|
|
8
|
+
instead of having everything runn straight off the client. I find it an easier way
|
|
9
|
+
to think about and code interactions with OpenSearch.
|
|
10
|
+
|
|
11
|
+
The gem was a finished product before I had access to LLMs. All the _original_ code
|
|
12
|
+
is hand-crafted like a Seattle beer, but a variety of improvements and much
|
|
13
|
+
of the documentation (and rbs) is AI-generated.
|
|
14
|
+
|
|
15
|
+
Of course, AI may have negated the need for nice interfaces and
|
|
16
|
+
human-centered program design. So...YMMV in terms of usefulness.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Quick example
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require "opensearch/sugar"
|
|
23
|
+
|
|
24
|
+
client = OpenSearch::Sugar::Client.new # Pass credentials, or default uses ENV variables
|
|
25
|
+
|
|
26
|
+
index = client.open_or_create_index("products")
|
|
27
|
+
|
|
28
|
+
index.update_mappings(
|
|
29
|
+
mappings: {
|
|
30
|
+
properties: {
|
|
31
|
+
title: { type: "text" },
|
|
32
|
+
category: { type: "keyword" },
|
|
33
|
+
price: { type: "float" }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
index.index_document({ title: "Dune", category: "fiction", price: 12.99 }, "isbn-0441013597")
|
|
39
|
+
index.refresh # make the document immediately searchable
|
|
40
|
+
|
|
41
|
+
results = client.search(
|
|
42
|
+
index: "products",
|
|
43
|
+
body: { query: { match: { title: "dune" } } }
|
|
44
|
+
)
|
|
45
|
+
puts results["hits"]["hits"].first.dig("_source", "title")
|
|
46
|
+
#=> "Dune"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The underlying Client object is attatched to every object created through the API,
|
|
50
|
+
and is a paper-thin shell on a client from the official ruby gem.
|
|
51
|
+
|
|
52
|
+
See the AI-written [Tutorial](docs/TUTORIAL.md) for a full walkthrough.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# Gemfile
|
|
58
|
+
gem "opensearch-sugar"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bundle install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Connection details are read from environment variables:
|
|
68
|
+
|
|
69
|
+
| Variable | Used for | Default |
|
|
70
|
+
|----------|----------|---------|
|
|
71
|
+
| `OPENSEARCH_URL` | Cluster URL | `https://localhost:9000` |
|
|
72
|
+
| `OPENSEARCH_HOST` | Cluster URL (lower priority) | — |
|
|
73
|
+
| `OPENSEARCH_USER` | Basic auth user | `"admin"` |
|
|
74
|
+
| `OPENSEARCH_PASSWORD` | Basic auth password | — |
|
|
75
|
+
| `OPENSEARCH_INITIAL_ADMIN_PASSWORD` | Basic auth password (lower priority) | — |
|
|
76
|
+
|
|
77
|
+
Any keyword argument accepted by `OpenSearch::Client.new` can be passed to
|
|
78
|
+
`OpenSearch::Sugar::Client.new` and will override the defaults.
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
Provided by various AI models, although I've done some editing. Those things are faily good at this, even though
|
|
83
|
+
they don't so much have a unique voice (or, rather, they all have the same unique voice)
|
|
84
|
+
|
|
85
|
+
- **[Tutorial](docs/TUTORIAL.md)** — step-by-step walkthrough building a searchable book catalog from scratch
|
|
86
|
+
- **[How-to Guides](docs/HOWTO.md)** — practical recipes for connection options, document CRUD, search, aliases, ML models, error handling, and more
|
|
87
|
+
- **[API Reference](docs/REFERENCE.md)** — complete method reference for `Client`, `Index`, and `Models`
|
|
88
|
+
|
|
89
|
+
## Tests and development
|
|
90
|
+
|
|
91
|
+
I'v never seen the point of writing unit tests for this sort of thing. I mean, a few, but anything
|
|
92
|
+
that interacts with a server and needs to verify that The Right Thing Happened benefits from
|
|
93
|
+
talking to the real thing. Plus I don't have to maintain the mocks. Something like
|
|
94
|
+
[VCR](https://github.com/vcr/vcr) is appealing; maybe soon.
|
|
95
|
+
|
|
96
|
+
### To run the tests:
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
Start a local OpenSearch cluster:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
docker compose up -d
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
OpenSearch takes ~30 seconds to be ready. Wait until the following command succeeds
|
|
106
|
+
before running the test suite:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
❯ curl -sk https://localhost:29200 -u 'admin:Dw2F%3E*!m&psx64' | grep -q tagline && echo "Ready"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Run the specs:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
bundle exec rspec
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To generate a coverage report:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
bundle exec rake coverage
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
or:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
COVERAGE=true bundle exec rspec
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Coverage reports are written to `coverage/index.html`.
|
|
131
|
+
|
|
132
|
+
To see full HTTP request/response logs during a run:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
OPENSEARCH_LOG=true bundle exec rspec
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Contributing
|
|
139
|
+
|
|
140
|
+
Bug reports and pull requests are welcome at
|
|
141
|
+
<https://github.com/mlibrary/opensearch-sugar>. Please open an issue before
|
|
142
|
+
submitting large changes.
|
|
143
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "yard"
|
|
6
|
+
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
+
|
|
9
|
+
require "standard/rake"
|
|
10
|
+
|
|
11
|
+
YARD::Rake::YardocTask.new(:yard) do |t|
|
|
12
|
+
t.files = ["lib/**/*.rb"]
|
|
13
|
+
t.options = ["--output-dir", "doc/", "--markup", "markdown"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Run Steep type checker"
|
|
17
|
+
task :steep do
|
|
18
|
+
sh "bundle exec steep check"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "Run specs with coverage report"
|
|
22
|
+
task :coverage do
|
|
23
|
+
ENV["COVERAGE"] = "true"
|
|
24
|
+
Rake::Task["spec"].invoke
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
task default: %i[spec standard]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
D = Steep::Diagnostic
|
|
2
|
+
|
|
3
|
+
target :lib do
|
|
4
|
+
signature "sig"
|
|
5
|
+
|
|
6
|
+
check "lib"
|
|
7
|
+
|
|
8
|
+
library "logger"
|
|
9
|
+
library "delegate"
|
|
10
|
+
library "json"
|
|
11
|
+
|
|
12
|
+
configure_code_diagnostics(D::Ruby.default) do |hash|
|
|
13
|
+
# OpenSearch::Sugar::Client extends SimpleDelegator; delegated methods (e.g. `indices`,
|
|
14
|
+
# `cluster`) are resolved dynamically at runtime and cannot be statically typed.
|
|
15
|
+
hash[D::Ruby::NoMethod] = :information
|
|
16
|
+
|
|
17
|
+
# opensearch-ruby and other third-party gems ship no RBS; suppress noise from
|
|
18
|
+
# unresolvable constants and untyped collection literals that arise at their boundaries.
|
|
19
|
+
hash[D::Ruby::UnknownConstant] = :information
|
|
20
|
+
hash[D::Ruby::UnannotatedEmptyCollection] = :information
|
|
21
|
+
hash[D::Ruby::ArgumentTypeMismatch] = :information
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# ADR-NNN: Title
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
<!-- One of: Proposed | Accepted | Deprecated | Superseded by [ADR-NNN](ADR-NNN-title.md) -->
|
|
6
|
+
|
|
7
|
+
## Date
|
|
8
|
+
|
|
9
|
+
YYYY-MM-DD
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
<!--
|
|
14
|
+
Describe the problem, the forces at play, and the background needed to understand
|
|
15
|
+
the decision. Include:
|
|
16
|
+
- What situation requires a decision
|
|
17
|
+
- Prior art or existing approaches (e.g., how a reference implementation handles this)
|
|
18
|
+
- Constraints imposed by earlier decisions
|
|
19
|
+
- Options that were on the table (summarized here; detailed in Alternatives Considered)
|
|
20
|
+
|
|
21
|
+
This section answers: "Why are we making a decision at all?"
|
|
22
|
+
-->
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
<!--
|
|
27
|
+
State the decision clearly and unambiguously. Use present tense ("We use...", not
|
|
28
|
+
"We will use..." or "We used...").
|
|
29
|
+
|
|
30
|
+
Include code samples (real Kotlin or clearly-marked pseudocode) demonstrating the
|
|
31
|
+
chosen approach at the API level. Samples should show both the definition and the
|
|
32
|
+
call site where relevant.
|
|
33
|
+
-->
|
|
34
|
+
|
|
35
|
+
## Consequences
|
|
36
|
+
|
|
37
|
+
### Positive
|
|
38
|
+
|
|
39
|
+
<!-- Benefits, improvements, and capabilities unlocked by this decision. -->
|
|
40
|
+
|
|
41
|
+
### Negative
|
|
42
|
+
|
|
43
|
+
<!-- Costs, limitations, and risks introduced. Be honest. -->
|
|
44
|
+
|
|
45
|
+
### Neutral
|
|
46
|
+
|
|
47
|
+
<!-- Things that are neither good nor bad but worth recording — behavioral changes,
|
|
48
|
+
constraints on future decisions, things that may surprise readers, etc. -->
|
|
49
|
+
|
|
50
|
+
## Alternatives Considered
|
|
51
|
+
|
|
52
|
+
<!--
|
|
53
|
+
For each rejected alternative:
|
|
54
|
+
- Name or brief description
|
|
55
|
+
- Why it was rejected
|
|
56
|
+
|
|
57
|
+
If the Context section already covers alternatives in sufficient detail, this
|
|
58
|
+
section may be omitted.
|
|
59
|
+
-->
|
|
60
|
+
|
|
61
|
+
## Diagram
|
|
62
|
+
|
|
63
|
+
<!--
|
|
64
|
+
Optional. Include a Mermaid diagram when the decision has a structural, flow, or
|
|
65
|
+
sequence component worth visualizing. Use flowchart, sequenceDiagram, or classDiagram
|
|
66
|
+
as appropriate.
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
flowchart LR
|
|
70
|
+
A --> B
|
|
71
|
+
```
|
|
72
|
+
-->
|
|
73
|
+
|
|
74
|
+
## Open Questions
|
|
75
|
+
|
|
76
|
+
<!--
|
|
77
|
+
Optional. Deferred sub-decisions that follow directly from this decision but are
|
|
78
|
+
not yet settled. Each item should reference the constraint or choice that makes
|
|
79
|
+
it dependent on future work.
|
|
80
|
+
-->
|
|
81
|
+
|
|
82
|
+
## Documentation Requirements
|
|
83
|
+
|
|
84
|
+
<!--
|
|
85
|
+
Optional. Explicit callouts for what must be documented in the public API,
|
|
86
|
+
README, or contributor guide as a direct result of this decision.
|
|
87
|
+
-->
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# ADR-001: Use SimpleDelegator to Wrap OpenSearch::Client
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Date
|
|
8
|
+
|
|
9
|
+
2026-04-28
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`OpenSearch::Sugar::Client` needs to provide convenient, higher-level methods (e.g., `has_index?`,
|
|
14
|
+
`open_or_create_index`, index management helpers) while also giving callers access to the full
|
|
15
|
+
`OpenSearch::Client` API. The OpenSearch Ruby client exposes a large, versioned surface area —
|
|
16
|
+
cluster operations, document APIs, index APIs, ML Commons, and more — that would be impractical
|
|
17
|
+
to wrap exhaustively and expensive to keep in sync as the upstream client evolves.
|
|
18
|
+
|
|
19
|
+
Options considered:
|
|
20
|
+
|
|
21
|
+
- **Inheritance** — subclass `OpenSearch::Client` directly
|
|
22
|
+
- **Manual delegation** — write `def_delegator` calls for every method needed
|
|
23
|
+
- **`SimpleDelegator` (Forwardable)** — wrap the client via Ruby's built-in delegation mechanism
|
|
24
|
+
- **Composition with `method_missing`** — forward unknown calls via `method_missing`/`respond_to_missing?`
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
We use `SimpleDelegator` from Ruby's standard library to wrap an `OpenSearch::Client` instance.
|
|
29
|
+
`Client` inherits from `SimpleDelegator`, calls `__setobj__` with the raw client at initialization,
|
|
30
|
+
and adds Sugar methods on top.
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class OpenSearch::Sugar::Client < SimpleDelegator
|
|
34
|
+
def initialize(**kwargs)
|
|
35
|
+
@raw_client = OpenSearch::Client.new(**kwargs)
|
|
36
|
+
super(@raw_client)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Sugar methods
|
|
40
|
+
def has_index?(name)
|
|
41
|
+
indices.exists?(index: name)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def open_or_create_index(name, ...)
|
|
45
|
+
# ...
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Call site — Sugar method
|
|
50
|
+
client.has_index?("my_index")
|
|
51
|
+
|
|
52
|
+
# Call site — delegated to raw OpenSearch::Client transparently
|
|
53
|
+
client.search(index: "my_index", body: { query: { match_all: {} } })
|
|
54
|
+
client.cluster.health
|
|
55
|
+
client.bulk(body: operations)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The raw client remains accessible via `__getobj__` (or an explicit `raw_client` accessor) when
|
|
59
|
+
callers need it explicitly.
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
### Positive
|
|
64
|
+
|
|
65
|
+
- **Zero maintenance burden for the full API**: every current and future `OpenSearch::Client`
|
|
66
|
+
method is automatically available without any wrapper code.
|
|
67
|
+
- **No abstraction trap**: callers are never forced to find a Sugar equivalent; they can drop
|
|
68
|
+
through to the raw client at any time.
|
|
69
|
+
- **Official documentation applies directly**: users can read opensearch-ruby docs and apply
|
|
70
|
+
them without translation.
|
|
71
|
+
- **Transparent migration**: code already written against `OpenSearch::Client` works without
|
|
72
|
+
modification when the object is a `Sugar::Client`.
|
|
73
|
+
|
|
74
|
+
### Negative
|
|
75
|
+
|
|
76
|
+
- **`is_a?` / `instance_of?` surprises**: `SimpleDelegator` instances do not report as instances
|
|
77
|
+
of the wrapped class. Callers relying on `client.is_a?(OpenSearch::Client)` will get `false`.
|
|
78
|
+
- **Method resolution ambiguity**: if Sugar adds a method with the same name as a raw-client
|
|
79
|
+
method, the Sugar method wins silently. This requires discipline when naming Sugar helpers.
|
|
80
|
+
- **`SimpleDelegator` is less common than plain composition**: contributors unfamiliar with
|
|
81
|
+
Ruby's delegator classes may find the inheritance unusual.
|
|
82
|
+
|
|
83
|
+
### Neutral
|
|
84
|
+
|
|
85
|
+
- `__getobj__` / `__setobj__` are available for testing and introspection but are not part of
|
|
86
|
+
the public Sugar API.
|
|
87
|
+
- Adding Sugar methods that conflict with upstream client methods is a breaking change; names
|
|
88
|
+
must be chosen carefully.
|
|
89
|
+
|
|
90
|
+
## Alternatives Considered
|
|
91
|
+
|
|
92
|
+
**Inheritance from `OpenSearch::Client`**
|
|
93
|
+
Rejected because `OpenSearch::Client` is not designed for subclassing; its initializer and
|
|
94
|
+
internal wiring make safe subclassing fragile and likely to break across upstream releases.
|
|
95
|
+
|
|
96
|
+
**Manual delegation with `Forwardable`**
|
|
97
|
+
Rejected because it requires enumerating every method to forward. Any new method added to the
|
|
98
|
+
upstream client would be invisible until manually added here — exactly the maintenance trap we
|
|
99
|
+
want to avoid.
|
|
100
|
+
|
|
101
|
+
**`method_missing` / `respond_to_missing?`**
|
|
102
|
+
Rejected because `SimpleDelegator` already implements this pattern correctly and is battle-tested
|
|
103
|
+
in Ruby's standard library. Rolling our own would duplicate that work with higher risk.
|
|
104
|
+
|
|
105
|
+
## Diagram
|
|
106
|
+
|
|
107
|
+
```mermaid
|
|
108
|
+
classDiagram
|
|
109
|
+
class SimpleDelegator {
|
|
110
|
+
+__getobj__()
|
|
111
|
+
+__setobj__(obj)
|
|
112
|
+
}
|
|
113
|
+
class `OpenSearch::Sugar::Client` {
|
|
114
|
+
-@raw_client : OpenSearch::Client
|
|
115
|
+
+has_index?(name) bool
|
|
116
|
+
+open_or_create_index(name) Index
|
|
117
|
+
+index_names() Array
|
|
118
|
+
+models() Models
|
|
119
|
+
}
|
|
120
|
+
class `OpenSearch::Client` {
|
|
121
|
+
+indices IndicesNamespace
|
|
122
|
+
+cluster ClusterNamespace
|
|
123
|
+
+search(...) Hash
|
|
124
|
+
+bulk(...) Hash
|
|
125
|
+
+index(...) Hash
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
SimpleDelegator <|-- `OpenSearch::Sugar::Client`
|
|
129
|
+
`OpenSearch::Sugar::Client` --> `OpenSearch::Client` : wraps via __setobj__
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Documentation Requirements
|
|
133
|
+
|
|
134
|
+
- The public README and HOWTO must explain that `Sugar::Client` exposes the full
|
|
135
|
+
`OpenSearch::Client` API via delegation, and show side-by-side examples of Sugar methods
|
|
136
|
+
vs. delegated calls.
|
|
137
|
+
- The EXPLANATION doc should describe the `SimpleDelegator` pattern and its implications for
|
|
138
|
+
`is_a?` checks.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# ADR-002: Facade Pattern for OpenSearch::Sugar::Index
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Date
|
|
8
|
+
|
|
9
|
+
2026-04-28
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Several OpenSearch index operations require multiple coordinated API calls to complete safely.
|
|
14
|
+
The clearest example is updating certain index settings (e.g., analyzer configuration): OpenSearch
|
|
15
|
+
requires the index to be closed before applying the settings and then reopened afterward. If any
|
|
16
|
+
step fails partway through, the index can be left in an inconsistent state.
|
|
17
|
+
|
|
18
|
+
Other examples of multi-step sequences include:
|
|
19
|
+
|
|
20
|
+
- Applying settings or mappings that require a close/reopen cycle
|
|
21
|
+
- Introspecting settings or mappings (requires knowing which sub-keys to traverse)
|
|
22
|
+
- Managing aliases (creating, listing)
|
|
23
|
+
- Text analysis testing (`analyze` API)
|
|
24
|
+
|
|
25
|
+
Without a wrapper, callers must remember and correctly sequence every step every time. This is
|
|
26
|
+
error-prone and produces repeated, scattered boilerplate across application code.
|
|
27
|
+
|
|
28
|
+
Options considered:
|
|
29
|
+
|
|
30
|
+
- **Expose raw client methods only** — callers assemble sequences themselves
|
|
31
|
+
- **Module of helper functions** — stateless utility methods that accept an index name
|
|
32
|
+
- **`Index` class as a Facade** — an object that encapsulates the index name, holds a client
|
|
33
|
+
reference, and exposes single-method operations for each multi-step sequence
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
`OpenSearch::Sugar::Index` acts as a Facade over the raw index APIs. It holds the index name
|
|
38
|
+
and a reference to the `Sugar::Client`, and exposes single-method operations that internally
|
|
39
|
+
perform all necessary steps with consistent error handling.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Behind the scenes: close → put_settings → open
|
|
43
|
+
index.update_settings(
|
|
44
|
+
settings: {
|
|
45
|
+
analysis: { analyzer: { my_analyzer: { type: "standard" } } }
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Behind the scenes: get_settings, traverse nested keys, return clean hash
|
|
50
|
+
index.settings
|
|
51
|
+
|
|
52
|
+
# Behind the scenes: close → open (if index exists); create (if not)
|
|
53
|
+
index = client.open_or_create_index("my_index")
|
|
54
|
+
|
|
55
|
+
# Single call for text analysis
|
|
56
|
+
tokens = index.test_analyzer_by_name(analyzer: "my_analyzer", text: "Hello World")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each method handles errors uniformly and raises `OpenSearch::Sugar::Error` (or a subclass) on
|
|
60
|
+
failure, rather than leaking raw transport exceptions from intermediate steps.
|
|
61
|
+
|
|
62
|
+
## Consequences
|
|
63
|
+
|
|
64
|
+
### Positive
|
|
65
|
+
|
|
66
|
+
- **Error-prone sequences become single calls**: callers cannot accidentally omit a step or
|
|
67
|
+
leave an index closed after a settings update.
|
|
68
|
+
- **Consistent error handling**: all multi-step operations funnel through the same error
|
|
69
|
+
boundary; callers handle one exception type.
|
|
70
|
+
- **Encapsulation**: the index name is carried by the object, eliminating repetitive
|
|
71
|
+
`index: name` keyword arguments scattered throughout call sites.
|
|
72
|
+
- **Easier to test**: sequence logic is isolated in one class, making it straightforward to
|
|
73
|
+
verify the correct sequence is called.
|
|
74
|
+
|
|
75
|
+
### Negative
|
|
76
|
+
|
|
77
|
+
- **Increased abstraction**: callers working with the raw client directly may find it confusing
|
|
78
|
+
that `Index` and `Client` both offer some overlapping capabilities (e.g., `update_settings`
|
|
79
|
+
exists on both, with `Index#update_settings` being the preferred single-object entry point).
|
|
80
|
+
- **Performance transparency**: the automatic close/reopen cycle for settings updates is hidden
|
|
81
|
+
from callers. A caller unaware of this could be surprised by the two extra API calls and the
|
|
82
|
+
brief unavailability of the index.
|
|
83
|
+
|
|
84
|
+
### Neutral
|
|
85
|
+
|
|
86
|
+
- `Index` objects are lightweight value-like wrappers; they do not cache state from OpenSearch
|
|
87
|
+
and always fetch fresh data on each call.
|
|
88
|
+
- The Facade does not prevent callers from accessing the raw client via `index.client` (or
|
|
89
|
+
through the delegated `Sugar::Client`) when they need finer control.
|
|
90
|
+
|
|
91
|
+
## Alternatives Considered
|
|
92
|
+
|
|
93
|
+
**Raw client methods only**
|
|
94
|
+
Rejected because it pushes multi-step orchestration into every call site. The close/reopen
|
|
95
|
+
sequence for settings updates is particularly easy to get wrong (e.g., forgetting to reopen
|
|
96
|
+
after a failure).
|
|
97
|
+
|
|
98
|
+
**Module of stateless helper functions**
|
|
99
|
+
Rejected because it requires passing the index name (and client) on every call, which is
|
|
100
|
+
repetitive. It also makes it harder to build a coherent, discoverable API — callers would
|
|
101
|
+
need to know which helpers to compose for each task.
|
|
102
|
+
|
|
103
|
+
## Diagram
|
|
104
|
+
|
|
105
|
+
```mermaid
|
|
106
|
+
sequenceDiagram
|
|
107
|
+
participant Caller
|
|
108
|
+
participant Index as Sugar::Index
|
|
109
|
+
participant OS as OpenSearch::Client
|
|
110
|
+
|
|
111
|
+
Caller->>Index: update_settings(settings)
|
|
112
|
+
Index->>OS: indices.close(index: name)
|
|
113
|
+
OS-->>Index: ok
|
|
114
|
+
Index->>OS: indices.put_settings(index: name, body: settings)
|
|
115
|
+
OS-->>Index: ok
|
|
116
|
+
Index->>OS: indices.open(index: name)
|
|
117
|
+
OS-->>Index: ok
|
|
118
|
+
Index-->>Caller: (returns)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Documentation Requirements
|
|
122
|
+
|
|
123
|
+
- HOWTO must note that `update_settings` for analyzer-related settings automatically performs
|
|
124
|
+
the close/reopen cycle and that the index is briefly unavailable during the operation.
|
|
125
|
+
- EXPLANATION should describe the Facade pattern and explain why certain operations require
|
|
126
|
+
closing the index.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ADR-003: Repository Pattern for OpenSearch::Sugar::Models
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Date
|
|
8
|
+
|
|
9
|
+
2026-04-28
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
OpenSearch ML Commons stores models with internal IDs that are opaque UUIDs assigned at
|
|
14
|
+
registration time. Callers typically know a model by its human-readable name or a short
|
|
15
|
+
nickname, not its UUID. Retrieving, deploying, and deleting a model through the raw client
|
|
16
|
+
requires:
|
|
17
|
+
|
|
18
|
+
1. Listing all models (paginated)
|
|
19
|
+
2. Scanning results to match by name or ID
|
|
20
|
+
3. Using the resolved UUID in subsequent calls
|
|
21
|
+
|
|
22
|
+
This lookup-then-act sequence must be repeated at every call site that needs to work with a
|
|
23
|
+
model by name. Additionally, the ML model lifecycle has strict ordering requirements:
|
|
24
|
+
|
|
25
|
+
- A model must be registered and fully deployed before it can be used
|
|
26
|
+
- A model must be undeployed before it can be deleted
|
|
27
|
+
|
|
28
|
+
Without a dedicated abstraction, callers are responsible for both the lookup logic and the
|
|
29
|
+
lifecycle sequencing — producing fragile, repetitive code.
|
|
30
|
+
|
|
31
|
+
Options considered:
|
|
32
|
+
|
|
33
|
+
- **Raw client calls at each call site** — callers handle lookup and lifecycle themselves
|
|
34
|
+
- **Module of helper functions** — stateless helpers that accept a client and model identifier
|
|
35
|
+
- **`Models` class as a Repository** — an object bound to a client that treats ML models as
|
|
36
|
+
first-class resources and unifies lookup, lifecycle management, and pipeline construction
|
|
37
|
+
|
|
38
|
+
## Decision
|
|
39
|
+
|
|
40
|
+
`OpenSearch::Sugar::Models` implements the Repository pattern. It is accessed via
|
|
41
|
+
`client.models` and provides a unified interface for all ML model operations. Lookup accepts
|
|
42
|
+
a name, UUID, or partial name match via `[]`:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
models = client.models
|
|
46
|
+
|
|
47
|
+
# Register and wait for deployment (blocks until deployed)
|
|
48
|
+
model = models.register(
|
|
49
|
+
name: "huggingface/sentence-transformers/all-MiniLM-L12-v2",
|
|
50
|
+
version: "1.0.1"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Look up by name, UUID, or partial match
|
|
54
|
+
model = models["all-MiniLM-L12-v2"]
|
|
55
|
+
model = models["3a4b5c6d-..."] # UUID
|
|
56
|
+
|
|
57
|
+
# List all registered models
|
|
58
|
+
all_models = models.list
|
|
59
|
+
|
|
60
|
+
# Delete safely (undeploys first)
|
|
61
|
+
models.delete!(model)
|
|
62
|
+
|
|
63
|
+
# Build an ingest pipeline for embeddings
|
|
64
|
+
models.create_pipeline(
|
|
65
|
+
name: "embedding_pipeline",
|
|
66
|
+
model: model.name,
|
|
67
|
+
field_map: { "text" => "text_embedding" }
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The `Models` class hides the paginated list API, name/ID resolution, and the undeploy-before-delete
|
|
72
|
+
requirement behind these single-purpose methods.
|
|
73
|
+
|
|
74
|
+
## Consequences
|
|
75
|
+
|
|
76
|
+
### Positive
|
|
77
|
+
|
|
78
|
+
- **Unified lookup**: callers do not need to know whether they have a name or UUID; `[]` resolves
|
|
79
|
+
either, hiding the paginated scan.
|
|
80
|
+
- **Lifecycle safety**: `delete!` always undeploys before deleting; callers cannot accidentally
|
|
81
|
+
delete a deployed model and produce an error.
|
|
82
|
+
- **`register` blocks until deployed**: removes the need for callers to poll deployment status
|
|
83
|
+
themselves.
|
|
84
|
+
- **Discoverable API**: all model operations are co-located on one object, making the surface
|
|
85
|
+
easy to explore.
|
|
86
|
+
|
|
87
|
+
### Negative
|
|
88
|
+
|
|
89
|
+
- **Blocking `register`**: waiting synchronously for model deployment may be unacceptable in
|
|
90
|
+
latency-sensitive contexts (e.g., a web request). Callers in those contexts must run
|
|
91
|
+
registration in a background job.
|
|
92
|
+
- **Partial-match ambiguity**: `[]` with a partial name string will match the first result;
|
|
93
|
+
if multiple models share a partial name, the result is non-deterministic. Callers must use
|
|
94
|
+
sufficiently specific identifiers.
|
|
95
|
+
- **Pagination hidden**: `list` returns all models by iterating pages internally. For clusters
|
|
96
|
+
with very large model catalogs this could be slow; there is no lazy/streaming alternative.
|
|
97
|
+
|
|
98
|
+
### Neutral
|
|
99
|
+
|
|
100
|
+
- `Models` does not cache; each `list` call hits OpenSearch. Callers that need repeated
|
|
101
|
+
lookups in a tight loop should retrieve and store the model object themselves.
|
|
102
|
+
- The pipeline construction method (`create_pipeline`) is included here because pipelines are
|
|
103
|
+
tightly coupled to a specific model ID. This is a design convenience, not a strict Repository
|
|
104
|
+
concern.
|
|
105
|
+
|
|
106
|
+
## Alternatives Considered
|
|
107
|
+
|
|
108
|
+
**Raw client calls at each call site**
|
|
109
|
+
Rejected because the paginated lookup and lifecycle sequencing would be duplicated everywhere
|
|
110
|
+
models are used. The undeploy-before-delete requirement is easy to forget and results in
|
|
111
|
+
confusing OpenSearch errors.
|
|
112
|
+
|
|
113
|
+
**Module of stateless helper functions**
|
|
114
|
+
Rejected for the same reasons as in ADR-002: requires passing the client on every call,
|
|
115
|
+
produces a flat, less discoverable API, and makes it harder to enforce invariants like
|
|
116
|
+
blocking until deployment is complete.
|
|
117
|
+
|
|
118
|
+
## Diagram
|
|
119
|
+
|
|
120
|
+
```mermaid
|
|
121
|
+
sequenceDiagram
|
|
122
|
+
participant Caller
|
|
123
|
+
participant Models as Sugar::Models
|
|
124
|
+
participant OS as OpenSearch ML Commons
|
|
125
|
+
|
|
126
|
+
Caller->>Models: register(name:, version:)
|
|
127
|
+
Models->>OS: ml.register_model(...)
|
|
128
|
+
OS-->>Models: task_id
|
|
129
|
+
loop Poll until deployed
|
|
130
|
+
Models->>OS: ml.get_task(task_id)
|
|
131
|
+
OS-->>Models: status
|
|
132
|
+
end
|
|
133
|
+
Models-->>Caller: Model object
|
|
134
|
+
|
|
135
|
+
Caller->>Models: delete!(model)
|
|
136
|
+
Models->>OS: ml.undeploy_model(model_id)
|
|
137
|
+
OS-->>Models: ok
|
|
138
|
+
Models->>OS: ml.delete_model(model_id)
|
|
139
|
+
OS-->>Models: ok
|
|
140
|
+
Models-->>Caller: (returns)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Documentation Requirements
|
|
144
|
+
|
|
145
|
+
- HOWTO must warn that `register` blocks and is not appropriate for use in a hot path.
|
|
146
|
+
- HOWTO must document the partial-match behavior of `[]` and recommend using explicit names
|
|
147
|
+
or UUIDs where uniqueness matters.
|
|
148
|
+
- REFERENCE must document the `Models` public API with parameter types and return values.
|