Skip to content

Commit a2a05bb

Browse files
authored
Merge branch 'main' into update/sbt-scalafmt-2.5.6
2 parents af9d04e + 95061f8 commit a2a05bb

22 files changed

Lines changed: 223 additions & 67 deletions

File tree

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(task:*)",
5+
"Bash(sbt -client:*)",
6+
"Bash(git:*)",
7+
"Bash(gh release list:*)"
8+
]
9+
}
10+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
name: upgrade-elasticsearch
3+
description: Upgrade Elasticsearch end-to-end: bump versions, check Lucene, compile, unit test, open PR, monitor CI and fix failures.
4+
argument-hint: "[target version, e.g. 8.18.5]"
5+
---
6+
7+
Upgrade Elasticsearch to the version specified by the user. If no version is specified, run `gh release list --repo elastic/elasticsearch --limit 60` and pick the closest available version to the current one (i.e., the next patch or minor release — minimize the version jump). Follow these steps in order, fixing any issues before moving to the next step:
8+
9+
1. **Bump versions** in all six places:
10+
- `ElasticsearchVersion` in `build.sbt`
11+
- Image tag in `docker/Dockerfile`
12+
- Image tag and release download URL in `docs/pages/installation.md`
13+
- `version` file (format: `X.Y.Z.0`)
14+
- `elasticsearch` pin in `client-python/requirements.txt` — upgrade to the latest available version whose minor version does not exceed the new ES minor version. The Python client does **not** publish patch releases (e.g. there is no `8.18.6`), so check available versions first: `pip index versions elasticsearch` or check PyPI.
15+
- `Elastic4sVersion` in `build.sbt` — upgrade to the latest available `nl.gn0s1s:elastic4s-client-esjava` version whose minor version does not exceed the new ES minor version. Check available versions on Maven Central: `curl -s "https://repo1.maven.org/maven2/nl/gn0s1s/elastic4s-client-esjava_3/maven-metadata.xml" | grep '<version>'`
16+
17+
2. **Check if LuceneVersion needs updating.** ES ships with a bundled Lucene. If it changed, the old pinned version will show as "evicted" in the dependency tree:
18+
```
19+
sbt -client "elastiknn-plugin/dependencyTree" | grep lucene
20+
```
21+
If evicted, bump `LuceneVersion` in `build.sbt` to match.
22+
23+
3. **Verify compilation:** `task jvmCompile`. This compiles all modules including integration test sources (`compile; Test/compile`). Fix any issues before proceeding.
24+
25+
4. **Verify unit tests:** `task jvmUnitTest`. Fix any failures before proceeding.
26+
27+
Once all steps pass, open a PR with the title `Dependencies: upgrade Elasticsearch to <new version>` (e.g. `Dependencies: upgrade Elasticsearch to 8.18.5`).
28+
29+
5. **Monitor CI and fix failures.** After the PR is open, poll GitHub Actions until CI completes or a job fails:
30+
31+
```bash
32+
# Get the latest run ID for the PR branch
33+
gh run list --branch <branch-name> --limit 5
34+
35+
# Watch a specific run (blocks until done, then prints summary)
36+
gh run watch <run-id>
37+
38+
# If a job failed, get the logs
39+
gh run view <run-id> --log-failed
40+
```
41+
42+
For each failed job:
43+
- Read the failure output carefully to identify the root cause.
44+
- Fix the issue locally (edit code, push a new commit).
45+
- Wait for a new CI run to start on that push, then repeat the watch/log loop.
46+
- Continue until all jobs pass (green).
47+
48+
Do not close or abandon the PR — iterate until CI is green.

CLAUDE.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build and Test Commands
6+
7+
### Task CLI
8+
9+
Use the `task` CLI — read Taskfile.yaml for all available tasks and to understand how they work.
10+
11+
### SBT
12+
13+
The underlying tool for most tasks is `sbt`, specifically `sbt -client` (thin client connecting to a persistent sbt server).
14+
Sometimes you need to use it directly rather than using
15+
16+
**Running a single test class:**
17+
```bash
18+
sbt -client "elastiknn-plugin/testOnly com.klibisz.elastiknn.query.SomeSpec"
19+
```
20+
21+
### Integration Testing
22+
23+
Integration tests require a Docker cluster (see `dockerRunTestingCluster` / `dockerStopTestingCluster` tasks in Taskfile.yaml).
24+
25+
### Java Version
26+
27+
The project uses Java 21 (see `.tool-versions`). Manage via asdf. Building with a newer JDK will embed that version in `plugin-descriptor.properties`, causing the plugin install to fail in the Docker container (which runs ES's bundled JDK).
28+
29+
## Project Structure
30+
31+
The project has seven SBT subprojects (Scala 3.3.6):
32+
33+
| Module | Role |
34+
|--------|------|
35+
| `elastiknn-api4s` | API types: (`Vec`, `Mapping`, `NearestNeighborsQuery`, etc.) XContent serialization |
36+
| `elastiknn-models` | LSH model implementations and vector similarity math (Java + Panama SIMD) |
37+
| `elastiknn-lucene` | Custom Lucene queries (`MatchHashesAndScoreQuery`) and field types |
38+
| `elastiknn-plugin` | ES plugin: mappers, query builders, score functions |
39+
| `elastiknn-plugin-integration-tests` | Full end-to-end tests against a live ES cluster |
40+
| `elastiknn-client-elastic4s` | Scala client library using elastic4s |
41+
| `elastiknn-jmh-benchmarks` | JMH microbenchmarks |
42+
43+
Dependency order: `api4s``models``lucene``plugin``integration-tests`
44+
45+
## Key Version Variables (build.sbt)
46+
47+
```scala
48+
ElasticsearchVersion // must match docker/Dockerfile and version file
49+
LuceneVersion // must match the Lucene bundled in the ES release
50+
ElastiknnVersion // read from the `version` file (e.g. 8.18.4.0)
51+
```
52+
53+
When upgrading ES, update all four: `build.sbt`, `docker/Dockerfile`, `docs/pages/installation.md`, and `version`. Also check whether the bundled Lucene version changed (`sbt elastiknn-plugin/dependencyTree | grep lucene`).
54+
55+
## Dependency Upgrades
56+
57+
Use `/upgrade-elasticsearch` to perform an Elasticsearch upgrade end-to-end.
58+
59+
Other JVM dependencies are mostly handled by Scala Steward via automated PRs (see git log for the pattern).
60+
61+
## SIMD / Panama Vector API
62+
63+
`PanamaFloatVectorOps` uses `jdk.incubator.vector` for SIMD-accelerated dot product, cosine, L1, and L2 computations. It is enabled at runtime via the `elastiknn.jdk-incubator-vector.enabled` setting. The sbt server and ES container both need `--add-modules jdk.incubator.vector` (already configured in `.sbtopts` and `build.sbt`).
64+
65+
SIMD `reduceLanes(ADD)` can produce slightly different floating-point results before vs. after JIT compilation — relevant when writing determinism tests.
66+
67+
## CI
68+
69+
`.github/workflows/ci.yaml` runs: lint → compile → assemble → unit tests → start Docker cluster → integration tests. Scala compiler is strict (`CiMode`) when `CI=true`; locally it uses `DevMode` (relaxed).

build.sbt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import ElasticsearchPluginPlugin.autoImport.*
22
import org.typelevel.sbt.tpolecat.{CiMode, DevMode}
33
import org.typelevel.scalacoptions.*
44

5-
Global / scalaVersion := "3.3.6"
5+
Global / scalaVersion := "3.3.7"
66

77
Global / scalacOptions += "-explain"
88

99
lazy val CirceVersion = "0.14.14"
10-
lazy val ElasticsearchVersion = "8.18.3"
11-
lazy val Elastic4sVersion = "8.19.0"
10+
lazy val ElasticsearchVersion = "9.3.3"
11+
lazy val Elastic4sVersion = "9.3.0"
1212
lazy val ElastiknnVersion = IO.read(file("version")).strip()
13-
lazy val LuceneVersion = "9.12.0"
13+
lazy val LuceneVersion = "10.3.2"
1414

1515
// Setting this to simplify local development.
1616
// https://github.com/typelevel/sbt-tpolecat/tree/v0.5.1?tab=readme-ov-file#modes
@@ -22,7 +22,7 @@ lazy val TestSettings = Seq(
2222
Test / parallelExecution := false,
2323
Test / logBuffered := false,
2424
Test / testOptions += Tests.Argument("-oD"),
25-
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test,
25+
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.20" % Test,
2626
// https://github.com/typelevel/sbt-tpolecat/tree/v0.5.1?tab=readme-ov-file#scalatest-warnings
2727
Test / tpolecatExcludeOptions += ScalacOptions.warnNonUnitStatement
2828
)
@@ -127,11 +127,11 @@ lazy val `elastiknn-plugin` = project
127127
elasticsearchPluginRunSettings += "elastiknn.jdk-incubator-vector.enabled=true",
128128
elasticsearchPluginEsJavaOpts += "--add-modules jdk.incubator.vector",
129129
libraryDependencies ++= Seq(
130-
"com.google.guava" % "guava" % "33.4.8-jre",
130+
"com.google.guava" % "guava" % "33.5.0-jre",
131131
"com.google.guava" % "failureaccess" % "1.0.3",
132132
"org.scalanlp" %% "breeze" % "2.1.0" % Test,
133133
"io.circe" %% "circe-parser" % CirceVersion % Test,
134-
"ch.qos.logback" % "logback-classic" % "1.5.18" % Test
134+
"ch.qos.logback" % "logback-classic" % "1.5.32" % Test
135135
),
136136
TestSettings
137137
)

client-python/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
elasticsearch==8.18.1
1+
elasticsearch==9.3.0
22
tqdm==4.61.1
33
scipy==1.10.1

docker/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
FROM docker.elastic.co/elasticsearch/elasticsearch:8.18.3
1+
FROM docker.elastic.co/elasticsearch/elasticsearch:9.3.3
22
COPY elastiknn-plugin/target/elastiknn*.zip .
3-
RUN elasticsearch-plugin install -b file:$(ls elastiknn*zip | sort | tail -n1)
3+
RUN PLUGIN=$(ls elastiknn*.zip | sort | tail -n1) && elasticsearch-plugin install -b "file://$PWD/$PLUGIN"

docs/pages/installation.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ Elasticsearch requires a new version of Elastiknn for every version of Elasticse
2121
Because of this, Elastiknn's versioning scheme includes the Elasticsearch version, followed by an incrementing version denoting changes to Elastiknn.
2222
For example, Elastiknn versions `8.4.2.0` and `8.4.2.1` were the first and second releases of Elastiknn corresponding to Elasticsearch version 8.4.2.
2323

24-
Elastiknn's main branch and the documentation site stay up-to-date with the latest Elasticsearch version, currently 8.x.
24+
Elastiknn's main branch and the documentation site stay up-to-date with the latest Elasticsearch version, currently 9.x.
2525
We maintain a second branch, [elasticsearch-7x](https://github.com/alexklibisz/elastiknn/tree/elasticsearch-7x), for Elasticsearch 7.x releases.
2626

27-
### Elasticsearch 8.x
27+
### Elasticsearch 9.x
2828

2929
|:--|:--|
3030
|Plugin Release| [![Plugin Release Status][Badge-Plugin-Release]][Link-Plugin-Release]|
@@ -42,8 +42,8 @@ Make a Dockerfile like below.
4242
The image version (`elasticsearch:A.B.C`) must match the plugin's version (e.g. `A.B.C.x/elastiknn-A.B.C.x`).
4343

4444
```docker
45-
FROM docker.elastic.co/elasticsearch/elasticsearch:8.18.3
46-
RUN elasticsearch-plugin install --batch https://github.com/alexklibisz/elastiknn/releases/download/8.18.3.0/elastiknn-8.18.3.0.zip
45+
FROM docker.elastic.co/elasticsearch/elasticsearch:9.3.3
46+
RUN elasticsearch-plugin install --batch https://github.com/alexklibisz/elastiknn/releases/download/9.3.3.0/elastiknn-9.3.3.0.zip
4747
```
4848

4949
Build and run the Dockerfile. If you have any issues please refer to the [official docs.](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)

elastiknn-client-elastic4s/src/main/scala/com/klibisz/elastiknn/client/ElastiknnClient.scala

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
package com.klibisz.elastiknn.client
22

3-
import com.fasterxml.jackson.module.scala.JavaTypeable
43
import com.klibisz.elastiknn.api._
54
import com.sksamuel.elastic4s.ElasticDsl._
65
import com.sksamuel.elastic4s._
76
import com.sksamuel.elastic4s.http.JavaClient
87
import com.sksamuel.elastic4s.requests.bulk.{BulkResponse, BulkResponseItem}
98
import com.sksamuel.elastic4s.requests.indexes.{CreateIndexResponse, PutMappingResponse}
109
import com.sksamuel.elastic4s.requests.searches.{SearchRequest, SearchResponse}
10+
import cats.instances.future.catsStdInstancesForFuture
1111
import org.apache.http.HttpHost
1212
import org.elasticsearch.client.RestClient
1313

1414
import scala.annotation.tailrec
15+
import scala.concurrent.{Await, ExecutionContext, Future}
16+
import scala.concurrent.duration._
1517
import scala.jdk.CollectionConverters._
16-
import scala.concurrent.{ExecutionContext, Future}
1718

1819
trait ElastiknnClient[F[_]] extends AutoCloseable {
1920

2021
/** Underlying client from the elastic4s library.
2122
*/
22-
val elasticClient: ElasticClient
23+
val elasticClient: ElasticClient[F]
2324

2425
/** Abstract method for executing a request.
2526
*/
26-
def execute[T, U](request: T)(using handler: Handler[T, U], javaTypeable: JavaTypeable[U]): F[Response[U]]
27+
def execute[T, U](request: T)(using handler: Handler[T, U]): F[Response[U]]
2728

2829
/** Execute the given request.
2930
*/
30-
final def apply[T, U](request: T)(using handler: Handler[T, U], javaTypeable: JavaTypeable[U]): F[Response[U]] = execute(request)
31+
final def apply[T, U](request: T)(using handler: Handler[T, U]): F[Response[U]] = execute(request)
3132

3233
/** See ElastiknnRequests.putMapping().
3334
*/
@@ -112,13 +113,12 @@ object ElastiknnClient {
112113
* [[ElastiknnFutureClient]]
113114
*/
114115
def futureClient(restClient: RestClient, strictFailure: Boolean)(using ec: ExecutionContext): ElastiknnFutureClient = {
115-
val jc: JavaClient = new JavaClient(restClient)
116+
val jc: JavaClient = JavaClient.fromRestClient(restClient)
116117
new ElastiknnFutureClient {
117-
given executor: Executor[Future] = Executor.FutureExecutor(ec)
118-
given functor: Functor[Future] = Functor.FutureFunctor(ec)
119-
val elasticClient: ElasticClient = ElasticClient(jc)
120-
override def execute[T, U](req: T)(using handler: Handler[T, U], javaTypeable: JavaTypeable[U]): Future[Response[U]] = {
121-
val future: Future[Response[U]] = elasticClient.execute(req)
118+
given cats.Functor[Future] = catsStdInstancesForFuture
119+
val elasticClient: ElasticClient[Future] = ElasticClient(jc)
120+
override def execute[T, U](req: T)(using handler: Handler[T, U]): Future[Response[U]] = {
121+
val future: Future[Response[U]] = elasticClient.execute(req)(using handler, CommonRequestOptions.defaults)
122122
if (strictFailure) future.flatMap { res =>
123123
checkResponse(req, res) match {
124124
case Left(ex) => Future.failed(ex)
@@ -129,7 +129,7 @@ object ElastiknnClient {
129129
}
130130
override def toString: String =
131131
s"${ElastiknnClient.getClass.getSimpleName} connected to ${restClient.getNodes.asScala.toList.mkString(",")}"
132-
override def close(): Unit = elasticClient.close()
132+
override def close(): Unit = Await.result(elasticClient.close(), 30.seconds)
133133
}
134134
}
135135

elastiknn-lucene/src/main/java/org/apache/lucene/search/MatchHashesAndScoreQuery.java

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -97,34 +97,44 @@ public Explanation explain(LeafReaderContext context, int doc) throws IOExceptio
9797
}
9898

9999
@Override
100-
public Scorer scorer(LeafReaderContext context) throws IOException {
101-
ScoreFunction scoreFunction = scoreFunctionBuilder.apply(context);
102-
LeafReader reader = context.reader();
103-
HitCounter counter = countHits(reader);
104-
DocIdSetIterator disi = counter.docIdSetIterator(candidates);
105-
106-
return new Scorer(this) {
100+
public ScorerSupplier scorerSupplier(LeafReaderContext context) throws IOException {
101+
return new ScorerSupplier() {
107102
@Override
108-
public DocIdSetIterator iterator() {
109-
return disi;
110-
}
103+
public Scorer get(long leadCost) throws IOException {
104+
ScoreFunction scoreFunction = scoreFunctionBuilder.apply(context);
105+
LeafReader reader = context.reader();
106+
HitCounter counter = countHits(reader);
107+
DocIdSetIterator disi = counter.docIdSetIterator(candidates);
108+
109+
return new Scorer() {
110+
@Override
111+
public DocIdSetIterator iterator() {
112+
return disi;
113+
}
111114

112-
@Override
113-
public float getMaxScore(int upTo) {
114-
return Float.MAX_VALUE;
115-
}
115+
@Override
116+
public float getMaxScore(int upTo) {
117+
return Float.MAX_VALUE;
118+
}
116119

117-
@Override
118-
public float score() {
119-
int docID = docID();
120-
// TODO: how does it get to this state? This error did come up once in some local testing.
121-
if (docID == DocIdSetIterator.NO_MORE_DOCS) return 0f;
122-
else return (float) scoreFunction.score(docID, counter.get(docID));
120+
@Override
121+
public float score() {
122+
int docID = docID();
123+
// TODO: how does it get to this state? This error did come up once in some local testing.
124+
if (docID == DocIdSetIterator.NO_MORE_DOCS) return 0f;
125+
else return (float) scoreFunction.score(docID, counter.get(docID));
126+
}
127+
128+
@Override
129+
public int docID() {
130+
return disi.docID();
131+
}
132+
};
123133
}
124134

125135
@Override
126-
public int docID() {
127-
return disi.docID();
136+
public long cost() {
137+
return candidates;
128138
}
129139
};
130140
}

elastiknn-plugin-integration-tests/src/test/scala/com/klibisz/elastiknn/ElasticAsyncClient.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ trait ElasticAsyncClient {
2222

2323
protected lazy val eknn: ElastiknnClient[Future] = ElastiknnClient.futureClient(httpHost.getHostName, httpHost.getPort)
2424

25-
protected lazy val client: ElasticClient = eknn.elasticClient
25+
protected lazy val client: ElasticClient[Future] = eknn.elasticClient
2626

2727
}

0 commit comments

Comments
 (0)