|
36 | 36 |
|
37 | 37 | import org.apache.lucene.search.Sort; |
38 | 38 | import org.apache.lucene.search.SortField; |
| 39 | +import org.apache.lucene.search.SortedNumericSelector; |
39 | 40 | import org.apache.lucene.search.SortedNumericSortField; |
40 | 41 | import org.apache.lucene.search.SortedSetSortField; |
| 42 | +import org.opensearch.action.search.SearchResponse; |
| 43 | +import org.opensearch.common.collect.Tuple; |
41 | 44 | import org.opensearch.common.settings.Settings; |
42 | 45 | import org.opensearch.core.xcontent.XContentBuilder; |
| 46 | +import org.opensearch.index.query.QueryBuilders; |
| 47 | +import org.opensearch.search.sort.SortOrder; |
43 | 48 | import org.opensearch.test.ParameterizedStaticSettingsOpenSearchIntegTestCase; |
44 | 49 |
|
45 | 50 | import java.io.IOException; |
| 51 | +import java.util.ArrayList; |
46 | 52 | import java.util.Arrays; |
47 | 53 | import java.util.Collection; |
| 54 | +import java.util.Collections; |
| 55 | +import java.util.HashMap; |
| 56 | +import java.util.List; |
| 57 | +import java.util.Map; |
| 58 | +import java.util.UUID; |
48 | 59 |
|
49 | 60 | import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; |
50 | 61 | import static org.opensearch.search.SearchService.CLUSTER_CONCURRENT_SEGMENT_SEARCH_SETTING; |
51 | 62 | import static org.hamcrest.Matchers.containsString; |
52 | 63 |
|
53 | 64 | public class IndexSortIT extends ParameterizedStaticSettingsOpenSearchIntegTestCase { |
54 | 65 | private static final XContentBuilder TEST_MAPPING = createTestMapping(); |
| 66 | + private static final XContentBuilder NESTED_TEST_MAPPING = createNestedTestMapping(); |
55 | 67 |
|
56 | 68 | public IndexSortIT(Settings staticSettings) { |
57 | 69 | super(staticSettings); |
@@ -95,6 +107,49 @@ private static XContentBuilder createTestMapping() { |
95 | 107 | } |
96 | 108 | } |
97 | 109 |
|
| 110 | + private static XContentBuilder createNestedTestMapping() { |
| 111 | + try { |
| 112 | + return jsonBuilder().startObject() |
| 113 | + .startObject("properties") |
| 114 | + .startObject("foo") |
| 115 | + .field("type", "integer") |
| 116 | + .endObject() |
| 117 | + .startObject("foo1") |
| 118 | + .field("type", "keyword") |
| 119 | + .endObject() |
| 120 | + .startObject("contacts") |
| 121 | + .field("type", "nested") |
| 122 | + .startObject("properties") |
| 123 | + .startObject("name") |
| 124 | + .field("type", "keyword") |
| 125 | + .endObject() |
| 126 | + .startObject("age") |
| 127 | + .field("type", "integer") |
| 128 | + .endObject() |
| 129 | + .endObject() |
| 130 | + .endObject() |
| 131 | + .endObject() |
| 132 | + .endObject(); |
| 133 | + } catch (IOException e) { |
| 134 | + throw new IllegalStateException(e); |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + private static void addNestedDocuments(String id, int foo, String foo1, String name, int age) throws IOException { |
| 139 | + XContentBuilder sourceBuilder = jsonBuilder().startObject() |
| 140 | + .field("foo", foo) |
| 141 | + .field("foo1", foo1) |
| 142 | + .startArray("contacts") |
| 143 | + .startObject() |
| 144 | + .field("name", name) |
| 145 | + .field("age", age) |
| 146 | + .endObject() |
| 147 | + .endArray() |
| 148 | + .endObject(); |
| 149 | + |
| 150 | + client().prepareIndex("nested-test-index").setId(id).setSource(sourceBuilder).get(); |
| 151 | + } |
| 152 | + |
98 | 153 | public void testIndexSort() { |
99 | 154 | SortField dateSort = new SortedNumericSortField("date", SortField.Type.LONG, false); |
100 | 155 | dateSort.setMissingValue(Long.MAX_VALUE); |
@@ -146,4 +201,141 @@ public void testInvalidIndexSort() { |
146 | 201 | ); |
147 | 202 | assertThat(exc.getMessage(), containsString("docvalues not found for index sort field:[keyword]")); |
148 | 203 | } |
| 204 | + |
| 205 | + public void testIndexSortOnNestedField() throws IOException { |
| 206 | + boolean ascending = randomBoolean(); |
| 207 | + SortedNumericSelector.Type selector = ascending ? SortedNumericSelector.Type.MIN : SortedNumericSelector.Type.MAX; |
| 208 | + SortField regularSort = new SortedNumericSortField("foo", SortField.Type.INT, !ascending, selector); |
| 209 | + regularSort.setMissingValue(ascending ? Integer.MAX_VALUE : Integer.MIN_VALUE); |
| 210 | + |
| 211 | + Sort indexSort = new Sort(regularSort); |
| 212 | + |
| 213 | + prepareCreate("nested-test-index").setSettings( |
| 214 | + Settings.builder() |
| 215 | + .put(indexSettings()) |
| 216 | + .put("index.number_of_shards", "1") |
| 217 | + .put("index.number_of_replicas", "0") |
| 218 | + .putList("index.sort.field", "foo") |
| 219 | + .putList("index.sort.order", ascending ? "asc" : "desc") |
| 220 | + ).setMapping(NESTED_TEST_MAPPING).get(); |
| 221 | + |
| 222 | + int numDocs = randomIntBetween(10, 30); |
| 223 | + List<Integer> fooValues = new ArrayList<>(numDocs); |
| 224 | + List<String> ids = new ArrayList<>(numDocs); |
| 225 | + |
| 226 | + for (int i = 0; i < numDocs; i++) { |
| 227 | + String id = String.valueOf(i); |
| 228 | + int fooValue = randomIntBetween(1, 100); |
| 229 | + String name = UUID.randomUUID().toString().replace("-", "").substring(0, 5); |
| 230 | + |
| 231 | + addNestedDocuments(id, fooValue, "", name, fooValue); |
| 232 | + fooValues.add(fooValue); |
| 233 | + ids.add(id); |
| 234 | + } |
| 235 | + |
| 236 | + flushAndRefresh("nested-test-index"); |
| 237 | + ensureGreen("nested-test-index"); |
| 238 | + |
| 239 | + assertSortedSegments("nested-test-index", indexSort); |
| 240 | + |
| 241 | + SearchResponse response = client().prepareSearch("nested-test-index") |
| 242 | + .addSort("foo", ascending ? SortOrder.ASC : SortOrder.DESC) |
| 243 | + .setQuery(QueryBuilders.matchAllQuery()) |
| 244 | + .setSize(numDocs) |
| 245 | + .get(); |
| 246 | + |
| 247 | + assertEquals(numDocs, response.getHits().getTotalHits().value()); |
| 248 | + |
| 249 | + Map<Integer, String> valueToId = new HashMap<>(); |
| 250 | + for (int i = 0; i < numDocs; i++) { |
| 251 | + valueToId.put(fooValues.get(i), ids.get(i)); |
| 252 | + } |
| 253 | + |
| 254 | + List<Integer> sortedValues = new ArrayList<>(fooValues); |
| 255 | + if (ascending) { |
| 256 | + Collections.sort(sortedValues); |
| 257 | + } else { |
| 258 | + sortedValues.sort(Collections.reverseOrder()); |
| 259 | + } |
| 260 | + |
| 261 | + for (int i = 0; i < numDocs; i++) { |
| 262 | + int expectedValue = sortedValues.get(i); |
| 263 | + assertEquals(expectedValue, response.getHits().getAt(i).getSourceAsMap().get("foo")); |
| 264 | + } |
| 265 | + } |
| 266 | + |
| 267 | + public void testIndexSortWithNestedField_MultiField() throws IOException { |
| 268 | + boolean ascendingPrimary = randomBoolean(); |
| 269 | + boolean ascendingSecondary = randomBoolean(); |
| 270 | + prepareCreate("nested-test-index").setSettings( |
| 271 | + Settings.builder() |
| 272 | + .put(indexSettings()) |
| 273 | + .put("index.number_of_shards", "1") |
| 274 | + .put("index.number_of_replicas", "0") |
| 275 | + .putList("index.sort.field", "foo", "foo1") |
| 276 | + .putList("index.sort.order", ascendingPrimary ? "asc" : "desc", ascendingSecondary ? "asc" : "desc") |
| 277 | + ).setMapping(NESTED_TEST_MAPPING).get(); |
| 278 | + |
| 279 | + int numDocs = randomIntBetween(10, 30); |
| 280 | + List<Tuple<Integer, String>> docValues = new ArrayList<>(numDocs); |
| 281 | + List<String> ids = new ArrayList<>(numDocs); |
| 282 | + |
| 283 | + int duplicateValue = randomIntBetween(30, 50); |
| 284 | + int numDuplicates = randomIntBetween(3, 5); |
| 285 | + |
| 286 | + for (int i = 0; i < numDocs; i++) { |
| 287 | + String id = String.valueOf(i); |
| 288 | + int fooValue; |
| 289 | + if (i < numDuplicates) { |
| 290 | + fooValue = duplicateValue; |
| 291 | + } else { |
| 292 | + fooValue = randomIntBetween(1, 100); |
| 293 | + } |
| 294 | + String name = UUID.randomUUID().toString().replace("-", "").substring(0, 5); |
| 295 | + addNestedDocuments(id, fooValue, name, name, fooValue); |
| 296 | + docValues.add(new Tuple<>(fooValue, name)); |
| 297 | + ids.add(id); |
| 298 | + } |
| 299 | + |
| 300 | + flushAndRefresh("nested-test-index"); |
| 301 | + ensureGreen("nested-test-index"); |
| 302 | + SearchResponse response = client().prepareSearch("nested-test-index") |
| 303 | + .addSort("foo", ascendingPrimary ? SortOrder.ASC : SortOrder.DESC) |
| 304 | + .addSort("foo1", ascendingSecondary ? SortOrder.ASC : SortOrder.DESC) |
| 305 | + .setQuery(QueryBuilders.matchAllQuery()) |
| 306 | + .setSize(numDocs) |
| 307 | + .get(); |
| 308 | + |
| 309 | + assertEquals(numDocs, response.getHits().getTotalHits().value()); |
| 310 | + |
| 311 | + List<Tuple<Integer, String>> sortedValues = new ArrayList<>(docValues); |
| 312 | + sortedValues.sort((a, b) -> { |
| 313 | + int primaryCompare = ascendingPrimary ? Integer.compare(a.v1(), b.v1()) : Integer.compare(b.v1(), a.v1()); |
| 314 | + if (primaryCompare != 0) { |
| 315 | + return primaryCompare; |
| 316 | + } |
| 317 | + return ascendingSecondary ? a.v2().compareTo(b.v2()) : b.v2().compareTo(a.v2()); |
| 318 | + }); |
| 319 | + |
| 320 | + for (int i = 0; i < numDocs; i++) { |
| 321 | + assertEquals(sortedValues.get(i).v1(), response.getHits().getAt(i).getSourceAsMap().get("foo")); |
| 322 | + assertEquals(sortedValues.get(i).v2(), response.getHits().getAt(i).getSourceAsMap().get("foo1")); |
| 323 | + } |
| 324 | + } |
| 325 | + |
| 326 | + public void testIndexSortWithSortFieldInsideDocBlock() { |
| 327 | + IllegalArgumentException exception = expectThrows( |
| 328 | + IllegalArgumentException.class, |
| 329 | + () -> prepareCreate("nested-sort-test").setSettings( |
| 330 | + Settings.builder() |
| 331 | + .put(indexSettings()) |
| 332 | + .put("index.number_of_shards", "1") |
| 333 | + .put("index.number_of_replicas", "0") |
| 334 | + .putList("index.sort.field", "contacts.age") |
| 335 | + .putList("index.sort.order", "desc") |
| 336 | + ).setMapping(NESTED_TEST_MAPPING).get() |
| 337 | + ); |
| 338 | + |
| 339 | + assertThat(exception.getMessage(), containsString("index sorting on nested fields is not supported")); |
| 340 | + } |
149 | 341 | } |
0 commit comments