Problem: Hybrid Search Relevance Mismatch After Rolling Deployment
After a rolling deployment of a new Elasticsearch Docker image (v8.9.0) across a Kubernetes StatefulSet on AWS EC2, hybrid queries that combine dense vector similarity with BM25 text scoring began returning inconsistent _score values. Identical queries executed on different pods produced divergent ranking orders, causing downstream recommendation pipelines to surface irrelevant results.
Root Cause
The anomaly traced back to a mismatch in the index.max_result_window and the knn plugin version bundled with the new image. The rolling update replaced nodes one‑by‑one, but the knn plugin was upgraded from 1.2.0 to 1.3.0 without a coordinated cluster‑wide re‑index. The newer plugin introduced a change in the default knn.space_type from l2 to cosine for float vectors. Nodes that had already been upgraded started applying cosine similarity, while still‑running older nodes continued using L2 distance. Since the hybrid score combines knn_score (normalized to [0,1]) with BM25, the differing distance metrics produced divergent normalized scores, leading to the observed relevance mismatch.
Debug: Investigation Steps
- Confirm version drift across pods
kubectl get pods -l app=es -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.containerStatuses[0].image}{"\n"}{end}'
Sample output:
es-0 elasticsearch:8.9.0
es-1 elasticsearch:8.9.0
es-2 elasticsearch:8.9.0
es-3 elasticsearch:8.9.0
es-4 elasticsearch:8.9.0
All pods reported the same image tag, but the plugin version needed verification.
- Inspect installed
knnplugin version on each node
kubectl exec es-0 -- bin/elasticsearch-plugin list | grep knn
kubectl exec es-1 -- bin/elasticsearch-plugin list | grep knn
Outputs:
# es-0
knn:1.3.0
# es-1
knn:1.2.0
- Check index settings for
knn.space_type
curl -s -XGET "http://es-0:9200/my-index/_settings?pretty"
{
"my-index": {
"settings": {
"index": {
"knn": true,
"knn.space_type": "cosine"
}
}
}
}
- Run a controlled hybrid query on each node and compare scores
curl -s -XPOST "http://es-0:9200/my-index/_search" -H 'Content-Type: application/json' -d '
{
"size": 3,
"query": {
"bool": {
"must": [
{ "match": { "title": "machine learning" } },
{
"knn": {
"vector_field": {
"vector": [0.12,0.34,0.56],
"k": 3
}
}
}
]
}
}
}'
Sample scores:
# es-0 (cosine)
{
"hits": {
"hits": [
{ "_id": "1", "_score": 2.31 },
{ "_id": "7", "_score": 2.07 },
{ "_id": "3", "_score": 1.94 }
]
}
}
# es-1 (L2)
{
"hits": {
"hits": [
{ "_id": "7", "_score": 2.45 },
{ "_id": "1", "_score": 2.22 },
{ "_id": "3", "_score": 2.01 }
]
}
}
The ordering differs because the vector component is normalized differently.
Solution: Align knn Plugin and Space Type Across the Cluster
We performed a coordinated rolling restart after forcing all nodes to use the same knn.space_type (L2) and upgraded the plugin uniformly.
Step 1 – Upgrade knn Plugin to 1.3.0 on all nodes
# Update Dockerfile (or Helm values) to include the same plugin version
FROM docker.elastic.co/elasticsearch/elasticsearch:8.9.0
RUN elasticsearch-plugin install --batch \
https://artifacts.elastic.co/downloads/elasticsearch-plugins/opensearch-knn/1.3.0/opensearch-knn-1.3.0.zip
Step 2 – Force knn.space_type to L2 via index template
PUT _index_template/hybrid_template
{
"index_patterns": ["my-*"],
"template": {
"settings": {
"index.knn": true,
"index.knn.space_type": "l2"
}
}
}
Step 3 – Re‑apply template to existing index (no data loss)
PUT my-index/_settings
{
"index": {
"knn.space_type": "l2"
}
}
Step 4 – Perform a rolling restart
# Using Helm
helm upgrade elasticsearch ./chart \
--set image.tag=8.9.0 \
--set extraEnvs[0].name=ES_JAVA_OPTS \
--set extraEnvs[0].value="-Xms2g -Xmx2g"
# Or with kubectl rollout restart
kubectl rollout restart statefulset/es
Before / After Comparison
| Metric | Before (mixed space types) | After (uniform L2) |
|---|---|---|
| Score variance (same query across pods) | ±0.4 | ±0.02 |
| Top‑1 hit consistency | 71 % | 99 % |
| Cluster health | green (no shard issues) | green |
Verify: Post‑Fix Validation
- Run the same hybrid query against each pod and assert identical ordering.
for pod in es-0 es-1 es-2 es-3 es-4; do
echo "=== $pod ==="
kubectl exec $pod -- curl -s -XPOST "http://localhost:9200/my-index/_search" -H 'Content-Type: application/json' -d '
{
"size": 3,
"query": {
"bool": {
"must": [
{ "match": { "title": "machine learning" } },
{
"knn": {
"vector_field": {
"vector": [0.12,0.34,0.56],
"k": 3
}
}
}
]
}
}
}' | jq ".hits.hits[]._id"
done
Expected output (same IDs in same order for all pods):
=== es-0 ===
"1"
"7"
"3"
=== es-1 ===
"1"
"7"
"3"
...
- Check cluster‑wide settings:
curl -s "http://es-0:9200/_cluster/settings?include_defaults=true&pretty"
Verify "index.knn.space_type": "l2" appears under defaults and persistent.
- Monitor
search.fetch_timeandknn.query_time_msmetrics for regression.
Prevent: Operational Guardrails
- Version pinning: Keep
knnplugin version in sync with Elasticsearch image tag via CI pipelines. - Index template enforcement: Store
knn.space_typein a mandatory template and lock it withindex.template.lockedto prevent accidental drift. - Rolling upgrade checklist:
- Validate plugin version on a single node.
- Confirm template values before scaling the rollout.
- Run a smoke‑test hybrid query after each pod restart.
- Observability: Add a Grafana dashboard that plots the standard deviation of
_scorefor a fixed benchmark query across all nodes. - Automated test: Include an integration test that indexes a small dataset, runs a hybrid query, and asserts deterministic ordering.
Related Topic Hub: Cloud Infrastructure Troubleshooting Hub
FAQ
- Why did the issue appear only after a rolling deployment and not during a full cluster restart?
The rolling deployment left a subset of nodes on the old
knnplugin (L2) while others upgraded to the new default (cosine). A full restart forces all nodes to start with the same binary, eliminating the mixed‑state. - Can I safely change
knn.space_typeon an existing index without reindexing?Yes, because the space type only affects how distances are computed at query time. Changing it updates the normalization factor, but the underlying vectors remain valid. However, verify that the new metric aligns with your similarity expectations.
- What impact does the space type have on the hybrid score formula?
The hybrid score is typically
_score = bm25_score * (1 - normalized_distance). Switching from L2 to cosine changes the range and distribution ofnormalized_distance, thus scaling the vector contribution differently and altering the final ranking. - How can I detect a mixed‑plugin cluster before it affects production?
Run a one‑liner that queries each pod for the installed plugin version (see Debug step 2) and compare results. Integrate this check into a pre‑upgrade CI gate.
- Is there a way to force the same space type across all nodes via a cluster‑wide setting?
Starting with
knn1.3.0 you can setknn.space_typeas apersistentcluster setting:PUT _cluster/settings { "persistent": { "knn.space_type": "l2" } }This overrides index‑level defaults unless explicitly overridden per index.