Skip to content

Commit 4e95e69

Browse files
authored
Fix negative scores returned from multi_match query with cross_fields (#13829) (#13983)
1 parent 32bd119 commit 4e95e69

File tree

8 files changed

+84
-1
lines changed

8 files changed

+84
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4545
- Fix get field mapping API returns 404 error in mixed cluster with multiple versions ([#13624](https://github.com/opensearch-project/OpenSearch/pull/13624))
4646
- Allow clearing `remote_store.compatibility_mode` setting ([#13646](https://github.com/opensearch-project/OpenSearch/pull/13646))
4747
- Painless: ensure type "UnmodifiableMap" for params ([#13885](https://github.com/opensearch-project/OpenSearch/pull/13885))
48+
- Don't return negative scores from `multi_match` query with `cross_fields` type ([#13829](https://github.com/opensearch-project/OpenSearch/pull/13829))
4849
- Pass parent filter to inner hit query ([#13903](https://github.com/opensearch-project/OpenSearch/pull/13903))
4950
- Fix NPE on restore searchable snapshot ([#13911](https://github.com/opensearch-project/OpenSearch/pull/13911))
5051

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"Cross fields do not return negative scores":
2+
- skip:
3+
version: " - 2.99.99"
4+
reason: "This fix is in 2.15. Until we do the BWC dance, we need to skip all pre-3.0, though."
5+
- do:
6+
index:
7+
index: test
8+
id: 1
9+
body: { "color" : "orange red yellow" }
10+
- do:
11+
index:
12+
index: test
13+
id: 2
14+
body: { "color": "orange red purple", "shape": "red square" }
15+
- do:
16+
index:
17+
index: test
18+
id: 3
19+
body: { "color" : "orange red yellow purple" }
20+
- do:
21+
indices.refresh: { }
22+
- do:
23+
search:
24+
index: test
25+
body:
26+
query:
27+
multi_match:
28+
query: "red"
29+
type: "cross_fields"
30+
fields: [ "color", "shape^100"]
31+
tie_breaker: 0.1
32+
explain: true
33+
- match: { hits.total.value: 3 }
34+
- match: { hits.hits.0._id: "2" }
35+
- gt: { hits.hits.2._score: 0.0 }

server/src/main/java/org/apache/lucene/queries/BlendedTermQuery.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ protected void blend(final TermStates[] contexts, int maxDoc, IndexReader reader
120120
}
121121
int max = 0;
122122
long minSumTTF = Long.MAX_VALUE;
123+
int[] docCounts = new int[contexts.length];
123124
for (int i = 0; i < contexts.length; i++) {
124125
TermStates ctx = contexts[i];
125126
int df = ctx.docFreq();
@@ -133,6 +134,7 @@ protected void blend(final TermStates[] contexts, int maxDoc, IndexReader reader
133134
// we need to find out the minimum sumTTF to adjust the statistics
134135
// otherwise the statistics don't match
135136
minSumTTF = Math.min(minSumTTF, reader.getSumTotalTermFreq(terms[i].field()));
137+
docCounts[i] = reader.getDocCount(terms[i].field());
136138
}
137139
}
138140
if (maxDoc > minSumTTF) {
@@ -175,7 +177,11 @@ protected int compare(int i, int j) {
175177
if (prev > current) {
176178
actualDf++;
177179
}
178-
contexts[i] = ctx = adjustDF(reader.getContext(), ctx, Math.min(maxDoc, actualDf));
180+
// Per field, we want to guarantee that the adjusted df does not exceed the number of docs with the field.
181+
// That is, in the IDF formula (log(1 + (N - n + 0.5) / (n + 0.5))), we need to make sure that n (the
182+
// adjusted df) is never bigger than N (the number of docs with the field).
183+
int fieldMaxDoc = Math.min(maxDoc, docCounts[i]);
184+
contexts[i] = ctx = adjustDF(reader.getContext(), ctx, Math.min(fieldMaxDoc, actualDf));
179185
prev = current;
180186
sumTTF += ctx.totalTermFreq();
181187
}

test/framework/src/main/java/org/opensearch/test/rest/yaml/section/Assertion.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import java.io.IOException;
3838
import java.util.Map;
3939

40+
import static org.junit.Assert.fail;
41+
4042
/**
4143
* Base class for executable sections that hold assertions
4244
*/
@@ -79,6 +81,41 @@ protected final Object getActualValue(ClientYamlTestExecutionContext executionCo
7981
return executionContext.response(field);
8082
}
8183

84+
static Object convertActualValue(Object actualValue, Object expectedValue) {
85+
if (actualValue == null || expectedValue.getClass().isAssignableFrom(actualValue.getClass())) {
86+
return actualValue;
87+
}
88+
if (actualValue instanceof Number && expectedValue instanceof Number) {
89+
if (expectedValue instanceof Float) {
90+
return Float.parseFloat(actualValue.toString());
91+
} else if (expectedValue instanceof Double) {
92+
return Double.parseDouble(actualValue.toString());
93+
} else if (expectedValue instanceof Integer) {
94+
return Integer.parseInt(actualValue.toString());
95+
} else if (expectedValue instanceof Long) {
96+
return Long.parseLong(actualValue.toString());
97+
}
98+
}
99+
// Force a class cast exception here, so developers can flesh out the above logic as needed.
100+
try {
101+
expectedValue.getClass().cast(actualValue);
102+
} catch (ClassCastException e) {
103+
fail(
104+
"Type mismatch: Expected value ("
105+
+ expectedValue
106+
+ ") has type "
107+
+ expectedValue.getClass()
108+
+ ". "
109+
+ "Actual value ("
110+
+ actualValue
111+
+ ") has type "
112+
+ actualValue.getClass()
113+
+ "."
114+
);
115+
}
116+
return actualValue;
117+
}
118+
82119
@Override
83120
public XContentLocation getLocation() {
84121
return location;

test/framework/src/main/java/org/opensearch/test/rest/yaml/section/GreaterThanAssertion.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public GreaterThanAssertion(XContentLocation location, String field, Object expe
7171
@Override
7272
protected void doAssert(Object actualValue, Object expectedValue) {
7373
logger.trace("assert that [{}] is greater than [{}] (field: [{}])", actualValue, expectedValue, getField());
74+
actualValue = convertActualValue(actualValue, expectedValue);
7475
assertThat(
7576
"value of [" + getField() + "] is not comparable (got [" + safeClass(actualValue) + "])",
7677
actualValue,

test/framework/src/main/java/org/opensearch/test/rest/yaml/section/GreaterThanEqualToAssertion.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public GreaterThanEqualToAssertion(XContentLocation location, String field, Obje
7272
@Override
7373
protected void doAssert(Object actualValue, Object expectedValue) {
7474
logger.trace("assert that [{}] is greater than or equal to [{}] (field: [{}])", actualValue, expectedValue, getField());
75+
actualValue = convertActualValue(actualValue, expectedValue);
7576
assertThat(
7677
"value of [" + getField() + "] is not comparable (got [" + safeClass(actualValue) + "])",
7778
actualValue,

test/framework/src/main/java/org/opensearch/test/rest/yaml/section/LessThanAssertion.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public LessThanAssertion(XContentLocation location, String field, Object expecte
7272
@Override
7373
protected void doAssert(Object actualValue, Object expectedValue) {
7474
logger.trace("assert that [{}] is less than [{}] (field: [{}])", actualValue, expectedValue, getField());
75+
actualValue = convertActualValue(actualValue, expectedValue);
7576
assertThat(
7677
"value of [" + getField() + "] is not comparable (got [" + safeClass(actualValue) + "])",
7778
actualValue,

test/framework/src/main/java/org/opensearch/test/rest/yaml/section/LessThanOrEqualToAssertion.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public LessThanOrEqualToAssertion(XContentLocation location, String field, Objec
7272
@Override
7373
protected void doAssert(Object actualValue, Object expectedValue) {
7474
logger.trace("assert that [{}] is less than or equal to [{}] (field: [{}])", actualValue, expectedValue, getField());
75+
actualValue = convertActualValue(actualValue, expectedValue);
7576
assertThat(
7677
"value of [" + getField() + "] is not comparable (got [" + safeClass(actualValue) + "])",
7778
actualValue,

0 commit comments

Comments
 (0)