Compatibility
Octane targets OpenSearch 3.x. Not every component spans the same part of it, and the reason is worth understanding before you plan an upgrade — it decides which artifacts you have to rebuild.
What each component supports
| Component | Declared range | Meaning | Rebuild needed when |
|---|---|---|---|
octane-license | [3.3.0,3.9.0) | 3.3.0 through 3.8.x | OpenSearch 3.9 |
octane-content | [3.3.0,3.9.0) | 3.3.0 through 3.8.x | OpenSearch 3.9 |
octane-accelerator | [3.3.0,3.9.0) | 3.3.0 through 3.8.x | OpenSearch 3.9 |
| autoscale controller | n/a | any 3.x over REST | never — it is not installed in the cluster |
| advisor | n/a | any 3.x over REST | never — it is not installed in the cluster |
A node reads that range from the plugin's descriptor at startup and refuses to start if it does not match. This is a hard check, not a warning.
The advisor has no version constraint at all. It reads a cluster over REST and installs nothing, so it runs against 3.0, 3.1 and 3.2 — releases where none of the plugins can be installed. If you are evaluating Octane on a cluster older than 3.3, the advisor is the part you can still use.
One artifact per component covers 3.3 through 3.8. That includes the accelerator, which used to be pinned to a single minor; see Why the accelerator is different for how it spans Lucene generations without a rebuild.
Why the floor is 3.3.0
Two separate limits set it, and neither is about anything Octane calls.
The version parser has to be able to read the range at all. Bracket ranges are parsed from OpenSearch 3.2.0 onward. On 3.0 and 3.1 there is no bracket parser, so a plugin declaring one is rejected outright.
The range must not name 4.0.0. This is the subtle one, and it is why carets are unusable low in
the line. On OpenSearch 3.0.0 through 3.3.2, Version.fromString("4.0.0") returns a
LegacyESVersion whose numeric id (4000099) sits below every masked OpenSearch 3.x id — 3.3.0 is
137247827. A caret range expands to an implicit < 4.0.0 ceiling, so running.before(4.0.0) is
never true and ^3.0.0 is satisfied by nothing at all, including 3.0.0 itself. The node fails with
Plugin [octane-license] was built for OpenSearch version ^3.0.0 but version 3.0.0 is running,
which reads like our defect and is not. It was fixed as a side effect of removing LegacyESVersion
(opensearch-project/OpenSearch#19793),
first released in 3.4.0.
A bracket whose bounds both sit inside 3.x never names 4.0.0 and is therefore immune. Measured
against each release's own opensearch-core, [3.3.0,3.9.0) evaluates true on 3.3.0, 3.4.0, 3.5.0,
3.6.0, 3.7.0 and 3.8.0. Writing [3.3.0,4.0.0) instead reintroduces the bug exactly — on 3.3.0 it
throws Lower bound must be less than or equal to upper bound at construction, before any version is
compared. The build rejects a 4.x ceiling for that reason.
So the parser floor is 3.2 and the tested floor is 3.3, which is the oldest minor our back-compatibility suite actually boots a node on. Reaching 3.0–3.1 would cost one artifact per minor — the limit there is the version parser, not Lucene and not our API use.
Earlier builds declared
^3.0.0, then^3.4.0, and the table here has twice described a span the artifacts did not actually cover. The ranges above are each measured against a running node of every version named.
Why the accelerator is different
The accelerator is not more fragile by accident. It implements Lucene codec SPIs — it extends
FlatVectorsWriter and FlatVectorsReader and imports Lucene's versioned codec packages. Those are
internal extension points, and their signatures move between Lucene minors:
OpenSearch 3.0.0 ──▶ Lucene 10.1 FlatVectorsWriter(...) lucene101
OpenSearch 3.8.0 ──▶ Lucene 10.5 FlatVectorsWriter(...)' lucene104
▲
└── different abstract methods,
different constructor, different package
No single compilation can satisfy both, and a runtime version check cannot rescue it: between these
releases the superclass's own constructor signature changed, and a super(...) call must be the
first statement in a constructor, so there is no point at which a check could run.
One artifact still can, by carrying more than one compilation. The accelerator compiles the version-bound code once per SPI shape — a generation — and ships them side by side in the same jar. At run time it resolves the Lucene it is actually on and loads only that generation. The others are never loaded, so classes that could not possibly link on this node are never asked to.
octane-accelerator-1.1.0.0.zip
├── generation 103 compiled against Lucene 10.3 → OpenSearch 3.3 – 3.7
└── generation 105 compiled against Lucene 10.5 → OpenSearch 3.8
▲
└── selected at run time; the other is never loaded
There is a second, independent axis: the codec name written into segments. Lucene104Codec does
not exist before Lucene 10.4, so nodes on 10.3 write LuceniaAccelerator103 and nodes on 10.4+ write
LuceniaAccelerator104. Both names stay registered forever, which is what makes a segment written on
3.4 still open on 3.8. See what happens to data you have already
written.
What that costs is bounded and declared. Every version-bound construction lives behind one class,
io.lucenia.accelerator.lucene.LuceneSpi, and a build gate (luceneSurface) fails if any file
outside a short allowlist touches versioned Lucene API — in both directions: an undeclared file
fails, and so does a declared one that no longer needs to be there. Supporting a new Lucene means
adding a generation, not searching the module for what moved. A back-compatibility level boots a real
node at the bottom of the declared range and exercises both the vector and the multi-way-tree paths
on it, because "it compiled" has already proved insufficient here at least once — see how the range
is actually verified.
The other plugins touch only OpenSearch's stable plugin APIs, so their reach is set by the version parser rather than by Lucene — which is why all four ranges now agree.
How the range is actually verified
A declared range is only ever evaluated by the old node deciding whether to accept the plugin, so nothing running on a current release can test it. It is invisible to unit tests, to integration tests and to the REST smoke suite; it surfaces only as an install refusal on someone's older cluster. That is why it gets a level of its own.
The back-compatibility level boots a real node at the bottom of the declared range, installs all three plugins in dependency order, and runs against it:
| What runs there | Covers |
|---|---|
:accelerator:bwcTest | quantised vector write + search, and the multi-way tree via geo_shape |
:content:bwcYamlTest | the whole REST suite — extract, compliance, redaction, fetch governance, ontology import, reprojection, license gates |
A node refuses to start when any installed plugin's range does not match its version, so the cluster coming up is itself the assertion — that is what catches a stale claim.
OpenSearch publishes old minors for linux only, so Gradle can start that node in CI but not on a macOS laptop, where the level skips loudly. The Docker images are published for every platform, and these are REST tests, so a container works just as well:
./scripts/bwc-local.sh # the floor version
./scripts/bwc-local.sh 3.5.0 # any other version in the range
Use it before pushing anything that touches a version boundary. It runs in well under a minute, and the alternative — discovering the same defect from a CI log — is how five consecutive commits on one branch went red.
Why a range at all
By default, OpenSearch plugins pin an exact version. A plugin built for 3.8.0 will not load on 3.8.1 — the node refuses to start. Across a fleet that turns every patch upgrade into a coordinated plugin rebuild and redeploy.
OpenSearch parses the descriptor's version through a semantic-version range, so a plugin may declare one instead:
| Operator | Example | Satisfied by |
|---|---|---|
| exact | 3.8.0 | 3.8.0 only |
tilde ~ | ~3.8.0 | 3.8.0, 3.8.1, 3.8.7 — not 3.9.0 |
caret ^ | ^3.4.0 | 3.4.0 through 3.x — not evaluated correctly below 3.4, because it implies a 4.0.0 ceiling |
| bracket | [3.3.0,3.9.0) | 3.3.0 through 3.8.x — parsed from 3.2 onward; immune to the 4.0.0 problem as long as both bounds stay inside 3.x |
Octane declares a bracket range on every plugin, so neither a patch nor a minor upgrade within 3.3–3.8 requires a new build of anything.
Upgrading OpenSearch
Patch upgrade (3.8.0 → 3.8.4): nothing to do. Every artifact keeps working.
Minor upgrade inside the range (3.4 → 3.5, 3.5 → 3.6, … up to 3.8): nothing to do. The same artifacts keep working, which is what makes a rolling upgrade possible — during one, nodes of two different minors run the same plugin zip at the same time, and the accelerator selects the generation matching the Lucene on each node independently. Segments written by either are readable by both.
Minor upgrade past the ceiling (3.8.x → 3.9.0): rebuild and redeploy. Have the new build in hand before you upgrade — a node with a plugin outside its declared range does not start. The ceiling is exclusive on purpose: a minor that has not been tested is refused rather than trusted.
Major upgrade (3.x → 4.0): everything needs a rebuild.
Upgrading downward — installing on a node older than 3.3.0 — is refused at install time, and on 3.0 and 3.1 the refusal is a parser error rather than a version message, because those releases cannot read a bracket range at all.
What happens to data you have already written
Everything above is about whether a plugin loads. This is the separate question of whether the indices you already have stay readable, and it is the one that actually matters.
An accelerator upgrade does not rewrite, migrate or strand accelerated segments. Every segment
records the name of the codec that wrote it, and reopening resolves that name back through Lucene's
SPI. Our codec names carry the Lucene generation they wrap — LuceniaAccelerator104 wraps
Lucene104Codec — and a build targeting a newer Lucene adds a generation rather than editing the
existing one, leaving the old name registered and read-only. Old segments therefore keep resolving to
exactly the code that wrote them, and merges move data onto the current generation over time without
anyone reindexing.
This is the same mechanism Lucene uses on itself: the current default codec lives in lucene-core
and every previous default is retained in lucene-backward-codecs. Note that Lucene's own
back-compatibility guarantee covers Lucene's codecs, not third-party ones — a plugin gets the
mechanism, not the outcome, and has to retain its own readers deliberately.
The one change that would break it is swapping the Lucene codec underneath an existing name. Stored fields, term vectors, norms, field infos, segment info, compound and live docs are decoded by that delegate and are not self-describing, so a segment would be handed to a decoder that did not write it. Two things guard against it: a regression test pinning each codec name to its generation, and a checked-in index written by an earlier build that the suite reopens on every run.
Removing the accelerator from a node holding accelerated indices still makes them unreadable — that is unchanged, and is a property of the codec being recorded per segment. Reindex before removing the plugin, or keep it installed and disable it per index.
Building for another release
One property controls what the build compiles and tests against:
./gradlew check -Popensearch_version=3.4.0
The default is 3.0.0 — below the declared floor of 3.3.0, and deliberately so. Compiling against
the oldest 3.x there is makes the compiler enforce an API floor: a call to something that only exists
in a later 3.x becomes a build failure here rather than a NoSuchMethodError on a customer's node.
Compiling lower than we ship costs nothing and catches more, and it is why the floor can move down
again if the parser limit is ever lifted, without recompiling against anything new.
The accelerator builds against a newer release, because it must see the Lucene its newest generation targets:
./gradlew :accelerator:assemble -Paccelerator_opensearch_version=3.9.0
Both live in gradle.properties, which is the only place any version is named.
Verifying what an artifact claims
The range is written into the plugin descriptor inside the zip. To check a build before you ship it:
unzip -p octane-content-1.1.0.0.zip plugin-descriptor.properties | grep opensearch.version
# opensearch.version=[3.3.0,3.9.0)
Managed OpenSearch
Amazon OpenSearch Service has supported custom plugins since November 2024, so "you cannot install plugins on a managed service" is out of date. It is still true that Octane's plugins cannot be installed there, for two independent reasons.
The extension points are not supported. AWS permits five:
| Extension point | Octane uses it |
|---|---|
AnalysisPlugin | no |
SearchPlugin | accelerator — the lucenia_knn query |
MapperPlugin | accelerator — the lucenia_vector field type |
ScriptPlugin | no |
SearchPipelinePlugin | content — the retrieval_grounding processor |
Not on that list, and required by every Octane plugin:
EnginePlugin— the accelerator's codec. This is the whole plugin; the query and field type are surface over segments the codec writes. Without it there is nothing to accelerate.IngestPlugin— every content processor exceptretrieval_grounding.ActionPlugin— the license REST API, which is how a trial is started at all.
The engine version does not match. Custom plugins require a descriptor naming a 2.x version
with a patch of zero (2.15.0, 2.17.0). Octane targets 3.x and declares a semantic-version
range, which that format does not admit.
There are also domain prerequisites — node-to-node encryption, encryption at rest, enforced HTTPS,
and a TLS policy of Policy-Min-TLS-1-2-PFS-2023-10 — plus an Amazon Inspector vulnerability scan on
upload, and a blue/green deployment on every install and uninstall.
What does work on a managed domain is the autoscale controller. It
installs nothing: it runs beside the cluster and speaks REST. In recommend mode it needs no special
permissions at all. Actuation is a different question — a managed domain is resized through the
provider's API, and Octane ships no scale target for one
(scale targets), so the recommendation is
yours to apply.
Check the current list before relying on this; AWS has expanded it once already.
Java
JDK 21, matching OpenSearch 3.x. The descriptor records java.version=21 and the node enforces it.