diff --git a/api/src/main/java/run/halo/app/extension/index/KeyComparator.java b/api/src/main/java/run/halo/app/extension/index/KeyComparator.java new file mode 100644 index 0000000000..750bee3b1f --- /dev/null +++ b/api/src/main/java/run/halo/app/extension/index/KeyComparator.java @@ -0,0 +1,47 @@ +package run.halo.app.extension.index; + +import java.util.Comparator; +import org.springframework.lang.Nullable; + +public class KeyComparator implements Comparator { + public static final KeyComparator INSTANCE = new KeyComparator(); + + @Override + public int compare(@Nullable String a, @Nullable String b) { + if (a == null && b == null) { + return 0; + } else if (a == null) { + // null less than everything + return 1; + } else if (b == null) { + // null less than everything + return -1; + } + + int i = 0; + int j = 0; + while (i < a.length() && j < b.length()) { + if (Character.isDigit(a.charAt(i)) && Character.isDigit(b.charAt(j))) { + // handle number part + int num1 = 0; + int num2 = 0; + while (i < a.length() && Character.isDigit(a.charAt(i))) { + num1 = num1 * 10 + (a.charAt(i++) - '0'); + } + while (j < b.length() && Character.isDigit(b.charAt(j))) { + num2 = num2 * 10 + (b.charAt(j++) - '0'); + } + if (num1 != num2) { + return num1 - num2; + } + } else if (a.charAt(i) != b.charAt(j)) { + // handle non-number part + return a.charAt(i) - b.charAt(j); + } else { + i++; + j++; + } + } + return a.length() - b.length(); + } +} diff --git a/api/src/main/java/run/halo/app/extension/index/query/And.java b/api/src/main/java/run/halo/app/extension/index/query/And.java index 8a80b319f2..4719b881ec 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/And.java +++ b/api/src/main/java/run/halo/app/extension/index/query/And.java @@ -25,6 +25,8 @@ public NavigableSet matches(QueryIndexView indexView) { NavigableSet resultSet = null; for (Query query : childQueries) { NavigableSet currentResult = query.matches(indexView); + // Trim unneeded rows to shrink the dataset for the next query + indexView.removeByIdNotIn(currentResult); if (resultSet == null) { resultSet = Sets.newTreeSet(currentResult); } else { diff --git a/api/src/main/java/run/halo/app/extension/index/query/EqualQuery.java b/api/src/main/java/run/halo/app/extension/index/query/EqualQuery.java index 8816cd7bf1..6f23f901b9 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/EqualQuery.java +++ b/api/src/main/java/run/halo/app/extension/index/query/EqualQuery.java @@ -1,6 +1,7 @@ package run.halo.app.extension.index.query; import java.util.NavigableSet; +import org.springframework.util.Assert; public class EqualQuery extends SimpleQuery { @@ -10,6 +11,7 @@ public EqualQuery(String fieldName, String value) { public EqualQuery(String fieldName, String value, boolean isFieldRef) { super(fieldName, value, isFieldRef); + Assert.notNull(value, "Value must not be null, use IsNull or IsNotNull instead"); } @Override diff --git a/api/src/main/java/run/halo/app/extension/index/query/IsNotNull.java b/api/src/main/java/run/halo/app/extension/index/query/IsNotNull.java new file mode 100644 index 0000000000..114da9562f --- /dev/null +++ b/api/src/main/java/run/halo/app/extension/index/query/IsNotNull.java @@ -0,0 +1,15 @@ +package run.halo.app.extension.index.query; + +import java.util.NavigableSet; + +public class IsNotNull extends SimpleQuery { + + protected IsNotNull(String fieldName) { + super(fieldName, null); + } + + @Override + public NavigableSet matches(QueryIndexView indexView) { + return indexView.getAllIdsForField(fieldName); + } +} diff --git a/api/src/main/java/run/halo/app/extension/index/query/IsNull.java b/api/src/main/java/run/halo/app/extension/index/query/IsNull.java new file mode 100644 index 0000000000..5b09e8f8e2 --- /dev/null +++ b/api/src/main/java/run/halo/app/extension/index/query/IsNull.java @@ -0,0 +1,18 @@ +package run.halo.app.extension.index.query; + +import java.util.NavigableSet; + +public class IsNull extends SimpleQuery { + + protected IsNull(String fieldName) { + super(fieldName, null); + } + + @Override + public NavigableSet matches(QueryIndexView indexView) { + var allIds = indexView.getAllIds(); + var idsForField = indexView.getAllIdsForField(fieldName); + allIds.removeAll(idsForField); + return allIds; + } +} diff --git a/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java b/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java index 6b0aed4c21..baa5f2ed7a 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java +++ b/api/src/main/java/run/halo/app/extension/index/query/NotEqual.java @@ -2,6 +2,7 @@ import com.google.common.collect.Sets; import java.util.NavigableSet; +import org.springframework.util.Assert; public class NotEqual extends SimpleQuery { private final EqualQuery equalQuery; @@ -12,6 +13,7 @@ public NotEqual(String fieldName, String value) { public NotEqual(String fieldName, String value, boolean isFieldRef) { super(fieldName, value, isFieldRef); + Assert.notNull(value, "Value must not be null, use IsNull or IsNotNull instead"); this.equalQuery = new EqualQuery(fieldName, value, isFieldRef); } diff --git a/api/src/main/java/run/halo/app/extension/index/query/QueryFactory.java b/api/src/main/java/run/halo/app/extension/index/query/QueryFactory.java index 64da7e15d3..22b698246f 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/QueryFactory.java +++ b/api/src/main/java/run/halo/app/extension/index/query/QueryFactory.java @@ -20,7 +20,21 @@ public static Query all(String fieldName) { return new All(fieldName); } + public static Query isNull(String fieldName) { + return new IsNull(fieldName); + } + + public static Query isNotNull(String fieldName) { + return new IsNotNull(fieldName); + } + + /** + * Create a {@link NotEqual} for the given {@code fieldName} and {@code attributeValue}. + */ public static Query notEqual(String fieldName, String attributeValue) { + if (attributeValue == null) { + return new IsNotNull(fieldName); + } return new NotEqual(fieldName, attributeValue); } @@ -28,7 +42,13 @@ public static Query notEqualOtherField(String fieldName, String otherFieldName) return new NotEqual(fieldName, otherFieldName, true); } + /** + * Create a {@link EqualQuery} for the given {@code fieldName} and {@code attributeValue}. + */ public static Query equal(String fieldName, String attributeValue) { + if (attributeValue == null) { + return new IsNull(fieldName); + } return new EqualQuery(fieldName, attributeValue); } @@ -73,6 +93,9 @@ public static Query in(String fieldName, String... attributeValues) { return in(fieldName, Set.of(attributeValues)); } + /** + * Create an {@link InQuery} for the given {@code fieldName} and {@code values}. + */ public static Query in(String fieldName, Collection values) { Assert.notNull(values, "Values must not be null"); if (values.size() == 1) { @@ -85,6 +108,9 @@ public static Query in(String fieldName, Collection values) { return new InQuery(fieldName, valueSet); } + /** + * Create an {@link And} for the given {@link Query}s. + */ public static Query and(Collection queries) { Assert.notEmpty(queries, "Queries must not be empty"); if (queries.size() == 1) { @@ -98,6 +124,9 @@ public static And and(Query query1, Query query2) { return new And(queries); } + /** + * Create an {@link And} for the given {@link Query}s. + */ public static Query and(Query query1, Query query2, Query... additionalQueries) { var queries = new ArrayList(2 + additionalQueries.length); queries.add(query1); @@ -106,6 +135,9 @@ public static Query and(Query query1, Query query2, Query... additionalQueries) return new And(queries); } + /** + * Create an {@link And} for the given {@link Query}s. + */ public static Query and(Query query1, Query query2, Collection additionalQueries) { var queries = new ArrayList(2 + additionalQueries.size()); queries.add(query1); @@ -119,6 +151,9 @@ public static Query or(Query query1, Query query2) { return new Or(queries); } + /** + * Create an {@link Or} for the given {@link Query}s. + */ public static Query or(Query query1, Query query2, Query... additionalQueries) { var queries = new ArrayList(2 + additionalQueries.length); queries.add(query1); @@ -127,6 +162,9 @@ public static Query or(Query query1, Query query2, Query... additionalQueries) { return new Or(queries); } + /** + * Create an {@link Or} for the given {@link Query}s. + */ public static Query or(Query query1, Query query2, Collection additionalQueries) { var queries = new ArrayList(2 + additionalQueries.size()); queries.add(query1); diff --git a/api/src/main/java/run/halo/app/extension/index/query/QueryIndexView.java b/api/src/main/java/run/halo/app/extension/index/query/QueryIndexView.java index dcf6e02a92..18a8a8fc3d 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/QueryIndexView.java +++ b/api/src/main/java/run/halo/app/extension/index/query/QueryIndexView.java @@ -1,6 +1,8 @@ package run.halo.app.extension.index.query; +import java.util.List; import java.util.NavigableSet; +import org.springframework.data.domain.Sort; import run.halo.app.extension.Metadata; import run.halo.app.extension.index.IndexSpec; @@ -37,7 +39,7 @@ public interface QueryIndexView { NavigableSet getAllValuesForField(String fieldName); /** - * Gets all object ids for a given field name. + * Gets all object ids for a given field name without null cells. * * @param fieldName the field name * @return all indexed object ids for the given field name @@ -45,6 +47,13 @@ public interface QueryIndexView { */ NavigableSet getAllIdsForField(String fieldName); + /** + * Gets all object ids in this view. + * + * @return all object ids in this view + */ + NavigableSet getAllIds(); + NavigableSet findIdsForFieldValueEqual(String fieldName1, String fieldName2); NavigableSet findIdsForFieldValueGreaterThan(String fieldName1, String fieldName2, @@ -53,5 +62,7 @@ NavigableSet findIdsForFieldValueGreaterThan(String fieldName1, String f NavigableSet findIdsForFieldValueLessThan(String fieldName1, String fieldName2, boolean orEqual); - void removeAllFieldValuesByIdNotIn(NavigableSet ids); + void removeByIdNotIn(NavigableSet ids); + + List sortBy(Sort sort); } diff --git a/api/src/main/java/run/halo/app/extension/index/query/QueryIndexViewImpl.java b/api/src/main/java/run/halo/app/extension/index/query/QueryIndexViewImpl.java index 2b32f7c141..d85ce935ff 100644 --- a/api/src/main/java/run/halo/app/extension/index/query/QueryIndexViewImpl.java +++ b/api/src/main/java/run/halo/app/extension/index/query/QueryIndexViewImpl.java @@ -1,26 +1,35 @@ package run.halo.app.extension.index.query; -import com.google.common.collect.MultimapBuilder; -import com.google.common.collect.Multimaps; -import com.google.common.collect.SetMultimap; +import com.google.common.collect.HashBasedTable; import com.google.common.collect.Sets; +import com.google.common.collect.Table; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.NavigableSet; +import java.util.Optional; +import java.util.Set; import java.util.TreeSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import run.halo.app.extension.index.KeyComparator; /** - * A default implementation for {@link QueryIndexView}. + * A default implementation for {@link run.halo.app.extension.index.query.QueryIndexView}. * * @author guqing * @since 2.12.0 */ public class QueryIndexViewImpl implements QueryIndexView { private final Lock lock = new ReentrantLock(); - private final Map> orderedMatches; + private final Set fieldNames; + private final Table> orderedMatches; /** * Creates a new {@link QueryIndexViewImpl} for the given {@link Map} of index entries. @@ -28,10 +37,21 @@ public class QueryIndexViewImpl implements QueryIndexView { * @param indexEntries index entries from indexer to create the view for. */ public QueryIndexViewImpl(Map>> indexEntries) { - this.orderedMatches = new HashMap<>(); + this.fieldNames = new HashSet<>(); + this.orderedMatches = HashBasedTable.create(); for (var entry : indexEntries.entrySet()) { - // do not use stream collect here as it is slower - this.orderedMatches.put(entry.getKey(), createSetMultiMap(entry.getValue())); + String fieldName = entry.getKey(); + this.fieldNames.add(fieldName); + for (var fieldEntry : entry.getValue()) { + var id = fieldEntry.getValue(); + var fieldValue = fieldEntry.getKey(); + var columnValue = this.orderedMatches.get(id, fieldName); + if (columnValue == null) { + columnValue = Sets.newTreeSet(); + this.orderedMatches.put(id, fieldName, columnValue); + } + columnValue.add(fieldValue); + } } } @@ -40,8 +60,13 @@ public NavigableSet getIdsForFieldValue(String fieldName, String fieldVa lock.lock(); try { checkFieldNameIndexed(fieldName); - SetMultimap fieldMap = orderedMatches.get(fieldName); - return fieldMap != null ? new TreeSet<>(fieldMap.get(fieldValue)) : emptySet(); + var result = new TreeSet(); + for (var cell : orderedMatches.cellSet()) { + if (cell.getColumnKey().equals(fieldName) && cell.getValue().contains(fieldValue)) { + result.add(cell.getRowKey()); + } + } + return result; } finally { lock.unlock(); } @@ -52,8 +77,13 @@ public NavigableSet getAllValuesForField(String fieldName) { lock.lock(); try { checkFieldNameIndexed(fieldName); - SetMultimap fieldMap = orderedMatches.get(fieldName); - return fieldMap != null ? new TreeSet<>(fieldMap.keySet()) : emptySet(); + var result = Sets.newTreeSet(); + for (var cell : orderedMatches.cellSet()) { + if (cell.getColumnKey().equals(fieldName)) { + result.addAll(cell.getValue()); + } + } + return result; } finally { lock.unlock(); } @@ -64,8 +94,24 @@ public NavigableSet getAllIdsForField(String fieldName) { lock.lock(); try { checkFieldNameIndexed(fieldName); - SetMultimap fieldMap = orderedMatches.get(fieldName); - return fieldMap != null ? new TreeSet<>(fieldMap.values()) : emptySet(); + NavigableSet ids = new TreeSet<>(); + // iterate over the table and collect all IDs associated with the given field name + for (var cell : orderedMatches.cellSet()) { + if (cell.getColumnKey().equals(fieldName)) { + ids.add(cell.getRowKey()); + } + } + return ids; + } finally { + lock.unlock(); + } + } + + @Override + public NavigableSet getAllIds() { + lock.lock(); + try { + return new TreeSet<>(orderedMatches.rowKeySet()); } finally { lock.unlock(); } @@ -77,17 +123,25 @@ public NavigableSet findIdsForFieldValueEqual(String fieldName1, String try { checkFieldNameIndexed(fieldName1); checkFieldNameIndexed(fieldName2); - var index1 = orderedMatches.get(fieldName1); - var index2 = orderedMatches.get(fieldName2); - var idFieldValuesForIndex2Map = - Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build()); - var result = Sets.newTreeSet(); - for (Map.Entry entryForIndex1 : index1.entries()) { - var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue()); - for (String item : fieldValues) { - if (entryForIndex1.getKey().equals(item)) { - result.add(entryForIndex1.getValue()); + NavigableSet result = new TreeSet<>(); + + // obtain all values for both fields and their corresponding IDs + var field1ValuesToIds = getColumnValuesToIdsMap(fieldName1); + var field2ValuesToIds = getColumnValuesToIdsMap(fieldName2); + + // iterate over each value of the first field + for (Map.Entry> entry : field1ValuesToIds.entrySet()) { + String fieldValue = entry.getKey(); + NavigableSet idsForFieldValue = entry.getValue(); + + // if the second field contains the same value, add all matching IDs + if (field2ValuesToIds.containsKey(fieldValue)) { + NavigableSet matchingIds = field2ValuesToIds.get(fieldValue); + for (String id : idsForFieldValue) { + if (matchingIds.contains(id)) { + result.add(id); + } } } } @@ -97,6 +151,29 @@ public NavigableSet findIdsForFieldValueEqual(String fieldName1, String } } + private Map> getColumnValuesToIdsMap(String fieldName) { + var valuesToIdsMap = new HashMap>(); + for (var cell : orderedMatches.cellSet()) { + if (cell.getColumnKey().equals(fieldName)) { + var celValues = cell.getValue(); + if (CollectionUtils.isEmpty(celValues)) { + continue; + } + if (celValues.size() != 1) { + throw new IllegalArgumentException( + "Unsupported multi cell values to join with other field for: " + fieldName + + " with values: " + celValues); + } + String fieldValue = cell.getValue().first(); + if (!valuesToIdsMap.containsKey(fieldValue)) { + valuesToIdsMap.put(fieldValue, new TreeSet<>()); + } + valuesToIdsMap.get(fieldValue).add(cell.getRowKey()); + } + } + return valuesToIdsMap; + } + @Override public NavigableSet findIdsForFieldValueGreaterThan(String fieldName1, String fieldName2, boolean orEqual) { @@ -105,19 +182,28 @@ public NavigableSet findIdsForFieldValueGreaterThan(String fieldName1, checkFieldNameIndexed(fieldName1); checkFieldNameIndexed(fieldName2); - var index1 = orderedMatches.get(fieldName1); - var index2 = orderedMatches.get(fieldName2); + NavigableSet result = new TreeSet<>(); - var idFieldValuesForIndex2Map = - Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build()); + // obtain all values for both fields and their corresponding IDs + var field1ValuesToIds = getColumnValuesToIdsMap(fieldName1); + var field2ValuesToIds = getColumnValuesToIdsMap(fieldName2); - var result = Sets.newTreeSet(); - for (Map.Entry entryForIndex1 : index1.entries()) { - var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue()); - for (String item : fieldValues) { - int compare = entryForIndex1.getKey().compareTo(item); - if (orEqual ? compare >= 0 : compare > 0) { - result.add(entryForIndex1.getValue()); + // iterate over each value of the first field + for (var entryField1 : field1ValuesToIds.entrySet()) { + String fieldValue1 = entryField1.getKey(); + + // iterate over each value of the second field + for (var entryField2 : field2ValuesToIds.entrySet()) { + String fieldValue2 = entryField2.getKey(); + + int comparison = fieldValue1.compareTo(fieldValue2); + if (orEqual ? comparison >= 0 : comparison > 0) { + // if the second field contains the same value, add all matching IDs + for (String id : entryField1.getValue()) { + if (field2ValuesToIds.get(fieldValue2).contains(id)) { + result.add(id); + } + } } } } @@ -134,19 +220,29 @@ public NavigableSet findIdsForFieldValueLessThan(String fieldName1, Stri try { checkFieldNameIndexed(fieldName1); checkFieldNameIndexed(fieldName2); - SetMultimap index1 = orderedMatches.get(fieldName1); - SetMultimap index2 = orderedMatches.get(fieldName2); - var idFieldValuesForIndex2Map = - Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build()); + NavigableSet result = new TreeSet<>(); - var result = Sets.newTreeSet(); - for (Map.Entry entryForIndex1 : index1.entries()) { - var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue()); - for (String item : fieldValues) { - int compare = entryForIndex1.getKey().compareTo(item); - if (orEqual ? compare <= 0 : compare < 0) { - result.add(entryForIndex1.getValue()); + // obtain all values for both fields and their corresponding IDs + var field1ValuesToIds = getColumnValuesToIdsMap(fieldName1); + var field2ValuesToIds = getColumnValuesToIdsMap(fieldName2); + + // iterate over each value of the first field + for (var entryField1 : field1ValuesToIds.entrySet()) { + String fieldValue1 = entryField1.getKey(); + + // iterate over each value of the second field + for (var entryField2 : field2ValuesToIds.entrySet()) { + String fieldValue2 = entryField2.getKey(); + + int comparison = fieldValue1.compareTo(fieldValue2); + if (orEqual ? comparison <= 0 : comparison < 0) { + // if the second field contains the same value, add all matching IDs + for (String id : entryField1.getValue()) { + if (field2ValuesToIds.get(fieldValue2).contains(id)) { + result.add(id); + } + } } } } @@ -157,38 +253,100 @@ public NavigableSet findIdsForFieldValueLessThan(String fieldName1, Stri } @Override - public void removeAllFieldValuesByIdNotIn(NavigableSet ids) { + public void removeByIdNotIn(NavigableSet ids) { lock.lock(); try { - for (var fieldNameValuesEntry : orderedMatches.entrySet()) { - SetMultimap indicates = fieldNameValuesEntry.getValue(); - indicates.entries().removeIf(entry -> !ids.contains(entry.getValue())); + Set idsToRemove = new HashSet<>(); + // check each row key if it is not in the given ids set + for (String rowKey : orderedMatches.rowKeySet()) { + if (!ids.contains(rowKey)) { + idsToRemove.add(rowKey); + } + } + + // remove all rows that are not in the given ids set + for (String idToRemove : idsToRemove) { + orderedMatches.row(idToRemove).clear(); } } finally { lock.unlock(); } } - private void checkFieldNameIndexed(String fieldName) { - if (!orderedMatches.containsKey(fieldName)) { - throw new IllegalArgumentException("Field name " + fieldName - + " is not indexed, please ensure it added to the index spec before querying"); + @Override + public List sortBy(Sort sort) { + lock.lock(); + try { + for (Sort.Order order : sort) { + String fieldName = order.getProperty(); + checkFieldNameIndexed(fieldName); + } + + // obtain all row keys (IDs) + Set allRowKeys = orderedMatches.rowKeySet(); + + // convert row keys to list for sorting + List sortedRowKeys = new ArrayList<>(allRowKeys); + if (sort.isUnsorted()) { + return sortedRowKeys; + } + + // sort row keys according to sort criteria in a Sort object + sortedRowKeys.sort((id1, id2) -> { + for (Sort.Order order : sort) { + String fieldName = order.getProperty(); + + // compare the values of the two rows on the field + int comparison = compareRowValue(id1, id2, fieldName, order.isAscending()); + if (comparison != 0) { + return comparison; + } + } + // if all sort criteria are equal, return 0 + return 0; + }); + + return sortedRowKeys; + } finally { + lock.unlock(); } } - private TreeSet emptySet() { - return new TreeSet<>(); + private int compareRowValue(String id1, String id2, String fieldName, boolean isAscending) { + var value1 = getSingleFieldValueForSort(id1, fieldName); + var value2 = getSingleFieldValueForSort(id2, fieldName); + // nulls are less than everything whatever the sort order is + // do not simply the following code for null check,it's different from KeyComparator + if (value1 == null && value2 == null) { + return 0; + } else if (value1 == null) { + return 1; + } else if (value2 == null) { + return -1; + } + return isAscending ? KeyComparator.INSTANCE.compare(value1, value2) + : KeyComparator.INSTANCE.compare(value2, value1); } - private SetMultimap createSetMultiMap( - Collection> entries) { + @Nullable + String getSingleFieldValueForSort(String rowKey, String fieldName) { + return Optional.ofNullable(orderedMatches.get(rowKey, fieldName)) + .filter(values -> !CollectionUtils.isEmpty(values)) + .map(values -> { + if (values.size() != 1) { + throw new IllegalArgumentException( + "Unsupported multi field values to sort for: " + fieldName + + " with values: " + values); + } + return values.first(); + }) + .orElse(null); + } - SetMultimap multiMap = MultimapBuilder.hashKeys() - .hashSetValues() - .build(); - for (Map.Entry entry : entries) { - multiMap.put(entry.getKey(), entry.getValue()); + private void checkFieldNameIndexed(String fieldName) { + if (!fieldNames.contains(fieldName)) { + throw new IllegalArgumentException("Field name " + fieldName + + " is not indexed, please ensure it added to the index spec before querying"); } - return multiMap; } } diff --git a/api/src/test/java/run/halo/app/extension/index/KeyComparatorTest.java b/api/src/test/java/run/halo/app/extension/index/KeyComparatorTest.java new file mode 100644 index 0000000000..9092520b30 --- /dev/null +++ b/api/src/test/java/run/halo/app/extension/index/KeyComparatorTest.java @@ -0,0 +1,79 @@ +package run.halo.app.extension.index; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Comparator; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link KeyComparator}. + * + * @author guqing + * @since 2.12.0 + */ +class KeyComparatorTest { + + @Test + void keyComparator() { + var comparator = KeyComparator.INSTANCE; + String[] strings = {"103", "101", "102", "1011", "1013", "1021", "1022", "1012", "1023"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo( + new String[] {"101", "102", "103", "1011", "1012", "1013", "1021", "1022", "1023"}); + + Arrays.sort(strings, comparator.reversed()); + assertThat(strings).isEqualTo( + new String[] {"1023", "1022", "1021", "1013", "1012", "1011", "103", "102", "101"}); + + // but if we use natural order, the result is: + Arrays.sort(strings, Comparator.naturalOrder()); + assertThat(strings).isEqualTo( + new String[] {"101", "1011", "1012", "1013", "102", "1021", "1022", "1023", "103"}); + } + + @Test + void keyComparator2() { + var comparator = KeyComparator.INSTANCE; + String[] strings = + {"moment-101", "moment-102", "moment-103", "moment-1011", "moment-1013", "moment-1021", + "moment-1022", "moment-1012", "moment-1023"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo(new String[] {"moment-101", "moment-102", "moment-103", + "moment-1011", "moment-1012", "moment-1013", "moment-1021", "moment-1022", + "moment-1023"}); + + // date sort + strings = + new String[] {"2022-01-15", "2022-02-01", "2021-12-25", "2022-01-01", "2022-01-02"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo( + new String[] {"2021-12-25", "2022-01-01", "2022-01-02", "2022-01-15", "2022-02-01"}); + + // alphabet and number sort + strings = new String[] {"abc123", "abc45", "abc9", "abc100", "abc20"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo( + new String[] {"abc9", "abc20", "abc45", "abc100", "abc123"}); + + // test for pure alphabet sort + strings = new String[] {"xyz", "abc", "def", "abcde", "xyzabc"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo(new String[] {"abc", "abcde", "def", "xyz", "xyzabc"}); + + // test for empty string + strings = new String[] {"", "abc", "123", "xyz"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo(new String[] {"", "123", "abc", "xyz"}); + + // test for the same string + strings = new String[] {"abc", "abc", "abc", "abc"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo(new String[] {"abc", "abc", "abc", "abc"}); + + // test for null element + strings = new String[] {null, "abc", "123", "xyz"}; + Arrays.sort(strings, comparator); + assertThat(strings).isEqualTo(new String[] {"123", "abc", "xyz", null}); + } +} \ No newline at end of file diff --git a/api/src/test/java/run/halo/app/extension/index/query/AndTest.java b/api/src/test/java/run/halo/app/extension/index/query/AndTest.java index f7e7816371..5beef4ee0c 100644 --- a/api/src/test/java/run/halo/app/extension/index/query/AndTest.java +++ b/api/src/test/java/run/halo/app/extension/index/query/AndTest.java @@ -36,16 +36,18 @@ void testMatches() { Map.entry("17", "johnniang") ); var entries = Map.of("dept", deptEntry, "age", ageEntry); - var indexView = new QueryIndexViewImpl(entries); + var indexView = new QueryIndexViewImpl(entries); var query = and(equal("dept", "B"), equal("age", "18")); var resultSet = query.matches(indexView); assertThat(resultSet).containsExactly("zhangsan"); + indexView = new QueryIndexViewImpl(entries); query = and(equal("dept", "C"), equal("age", "18")); resultSet = query.matches(indexView); assertThat(resultSet).isEmpty(); + indexView = new QueryIndexViewImpl(entries); query = and( // guqing, halo, lisi, zhangsan or(equal("dept", "A"), equal("dept", "B")), @@ -55,6 +57,7 @@ void testMatches() { resultSet = query.matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder("guqing", "halo", "zhangsan"); + indexView = new QueryIndexViewImpl(entries); query = and( // guqing, halo, lisi, zhangsan or(equal("dept", "A"), equal("dept", "B")), @@ -64,6 +67,7 @@ void testMatches() { resultSet = query.matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder("guqing", "halo", "zhangsan"); + indexView = new QueryIndexViewImpl(entries); query = and( // guqing, halo, lisi, zhangsan or(equal("dept", "A"), equal("dept", "C")), @@ -76,7 +80,7 @@ void testMatches() { @Test void andMatch2() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var query = and(equal("lastName", "Fay"), and( equal("hireDate", "17"), diff --git a/api/src/test/java/run/halo/app/extension/index/query/EmployeeDataSet.java b/api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java similarity index 53% rename from api/src/test/java/run/halo/app/extension/index/query/EmployeeDataSet.java rename to api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java index bd2b205e38..c618e63e06 100644 --- a/api/src/test/java/run/halo/app/extension/index/query/EmployeeDataSet.java +++ b/api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Map; -public class EmployeeDataSet { +public class IndexViewDataSet { /** * Create a {@link QueryIndexView} for employee to test. @@ -95,4 +95,82 @@ public static QueryIndexView createEmployeeIndexView() { "departmentId", departmentIdEntry); return new QueryIndexViewImpl(entries); } + + /** + * Create a {@link QueryIndexView} for post to test. + * + * @return a {@link QueryIndexView} for post to test + */ + public static QueryIndexView createPostIndexViewWithNullCell() { + /* + * id title published publishTime owner + * 100 title1 true 2024-01-01T00:00:00 jack + * 101 title2 true 2024-01-02T00:00:00 rose + * 102 title3 false null smith + * 103 title4 false null peter + * 104 title5 false null john + * 105 title6 true 2024-01-05 00:00:00 tom + * 106 title7 true 2024-01-05 13:00:00 jerry + * 107 title8 true 2024-01-05 12:00:00 jerry + * 108 title9 false null jerry + */ + Collection> idEntry = List.of( + Map.entry("100", "100"), + Map.entry("101", "101"), + Map.entry("102", "102"), + Map.entry("103", "103"), + Map.entry("104", "104"), + Map.entry("105", "105"), + Map.entry("106", "106"), + Map.entry("107", "107"), + Map.entry("108", "108") + ); + Collection> titleEntry = List.of( + Map.entry("title1", "100"), + Map.entry("title2", "101"), + Map.entry("title3", "102"), + Map.entry("title4", "103"), + Map.entry("title5", "104"), + Map.entry("title6", "105"), + Map.entry("title7", "106"), + Map.entry("title8", "107"), + Map.entry("title9", "108") + ); + Collection> publishedEntry = List.of( + Map.entry("true", "100"), + Map.entry("true", "101"), + Map.entry("false", "102"), + Map.entry("false", "103"), + Map.entry("false", "104"), + Map.entry("true", "105"), + Map.entry("true", "106"), + Map.entry("true", "107"), + Map.entry("false", "108") + ); + Collection> publishTimeEntry = List.of( + Map.entry("2024-01-01T00:00:00", "100"), + Map.entry("2024-01-02T00:00:00", "101"), + Map.entry("2024-01-05 00:00:00", "105"), + Map.entry("2024-01-05 13:00:00", "106"), + Map.entry("2024-01-05 12:00:00", "107") + ); + + Collection> ownerEntry = List.of( + Map.entry("jack", "100"), + Map.entry("rose", "101"), + Map.entry("smith", "102"), + Map.entry("peter", "103"), + Map.entry("john", "104"), + Map.entry("tom", "105"), + Map.entry("jerry", "106"), + Map.entry("jerry", "107"), + Map.entry("jerry", "108") + ); + var entries = Map.of("id", idEntry, + "title", titleEntry, + "published", publishedEntry, + "publishTime", publishTimeEntry, + "owner", ownerEntry); + return new QueryIndexViewImpl(entries); + } } diff --git a/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java b/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java index 6c2b0e7b12..c69e44228a 100644 --- a/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java +++ b/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java @@ -18,19 +18,36 @@ */ class QueryFactoryTest { - @Test void allTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = all("firstName").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101", "102", "103", "104", "105" ); } + @Test + void isNullTest() { + var indexView = IndexViewDataSet.createPostIndexViewWithNullCell(); + var resultSet = QueryFactory.isNull("publishTime").matches(indexView); + assertThat(resultSet).containsExactlyInAnyOrder( + "102", "103", "104", "108" + ); + } + + @Test + void isNotNullTest() { + var indexView = IndexViewDataSet.createPostIndexViewWithNullCell(); + var resultSet = QueryFactory.isNotNull("publishTime").matches(indexView); + assertThat(resultSet).containsExactlyInAnyOrder( + "100", "101", "105", "106", "107" + ); + } + @Test void equalTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = equal("lastName", "Fay").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "104", "105" @@ -39,7 +56,7 @@ void equalTest() { @Test void equalOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = equalOtherField("managerId", "id").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "102", "103" @@ -48,7 +65,7 @@ void equalOtherFieldTest() { @Test void notEqualTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = notEqual("lastName", "Fay").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "101", "102", "103" @@ -57,7 +74,7 @@ void notEqualTest() { @Test void notEqualOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = notEqualOtherField("managerId", "id").matches(indexView); // 103 102 is equal assertThat(resultSet).containsExactlyInAnyOrder( @@ -67,7 +84,7 @@ void notEqualOtherFieldTest() { @Test void lessThanTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.lessThan("id", "103").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101", "102" @@ -76,7 +93,7 @@ void lessThanTest() { @Test void lessThanOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.lessThanOtherField("id", "managerId").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101" @@ -85,7 +102,7 @@ void lessThanOtherFieldTest() { @Test void lessThanOrEqualTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.lessThanOrEqual("id", "103").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101", "102", "103" @@ -94,7 +111,7 @@ void lessThanOrEqualTest() { @Test void lessThanOrEqualOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.lessThanOrEqualOtherField("id", "managerId").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( @@ -104,7 +121,7 @@ void lessThanOrEqualOtherFieldTest() { @Test void greaterThanTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.greaterThan("id", "103").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "104", "105" @@ -113,7 +130,7 @@ void greaterThanTest() { @Test void greaterThanOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.greaterThanOtherField("id", "managerId").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "104", "105" @@ -122,7 +139,7 @@ void greaterThanOtherFieldTest() { @Test void greaterThanOrEqualTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.greaterThanOrEqual("id", "103").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "103", "104", "105" @@ -131,7 +148,7 @@ void greaterThanOrEqualTest() { @Test void greaterThanOrEqualOtherFieldTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.greaterThanOrEqualOtherField("id", "managerId").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( @@ -141,7 +158,7 @@ void greaterThanOrEqualOtherFieldTest() { @Test void inTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.in("id", "103", "104").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "103", "104" @@ -150,7 +167,7 @@ void inTest() { @Test void inTest2() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.in("lastName", "Fay").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "100", "104", "105" @@ -159,13 +176,13 @@ void inTest2() { @Test void betweenTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = between("id", "103", "105").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "103", "104", "105" ); - indexView = EmployeeDataSet.createEmployeeIndexView(); + indexView = IndexViewDataSet.createEmployeeIndexView(); resultSet = between("salary", "2000", "2400").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "101", "102", "103" @@ -174,7 +191,7 @@ void betweenTest() { @Test void betweenLowerExclusive() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.betweenLowerExclusive("salary", "2000", "2400").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( @@ -184,7 +201,7 @@ void betweenLowerExclusive() { @Test void betweenUpperExclusive() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.betweenUpperExclusive("salary", "2000", "2400").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( @@ -194,7 +211,7 @@ void betweenUpperExclusive() { @Test void betweenExclusive() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.betweenExclusive("salary", "2000", "2400").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "102" @@ -203,7 +220,7 @@ void betweenExclusive() { @Test void startsWithTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.startsWith("firstName", "W").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "102" @@ -212,7 +229,7 @@ void startsWithTest() { @Test void endsWithTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.endsWith("firstName", "y").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "103" @@ -221,7 +238,7 @@ void endsWithTest() { @Test void containsTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = QueryFactory.contains("firstName", "i").matches(indexView); assertThat(resultSet).containsExactlyInAnyOrder( "102" @@ -231,4 +248,4 @@ void containsTest() { "104", "105" ); } -} \ No newline at end of file +} diff --git a/api/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java b/api/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java index 61d998966b..86f61db7c6 100644 --- a/api/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java +++ b/api/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java @@ -1,8 +1,16 @@ package run.halo.app.extension.index.query; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Sort; /** * Tests for {@link QueryIndexViewImpl}. @@ -12,9 +20,23 @@ */ class QueryIndexViewImplTest { + @Test + void getAllIdsForFieldTest() { + var indexView = IndexViewDataSet.createPostIndexViewWithNullCell(); + var resultSet = indexView.getAllIdsForField("title"); + assertThat(resultSet).containsExactlyInAnyOrder( + "100", "101", "102", "103", "104", "105", "106", "107", "108" + ); + + resultSet = indexView.getAllIdsForField("publishTime"); + assertThat(resultSet).containsExactlyInAnyOrder( + "100", "101", "105", "106", "107" + ); + } + @Test void findIdsForFieldValueEqualTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = indexView.findIdsForFieldValueEqual("managerId", "id"); assertThat(resultSet).containsExactlyInAnyOrder( "102", "103" @@ -23,13 +45,13 @@ void findIdsForFieldValueEqualTest() { @Test void findIdsForFieldValueGreaterThanTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", false); assertThat(resultSet).containsExactlyInAnyOrder( "104", "105" ); - indexView = EmployeeDataSet.createEmployeeIndexView(); + indexView = IndexViewDataSet.createEmployeeIndexView(); resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", true); assertThat(resultSet).containsExactlyInAnyOrder( "103", "102", "104", "105" @@ -38,13 +60,13 @@ void findIdsForFieldValueGreaterThanTest() { @Test void findIdsForFieldValueGreaterThanTest2() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", false); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101" ); - indexView = EmployeeDataSet.createEmployeeIndexView(); + indexView = IndexViewDataSet.createEmployeeIndexView(); resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", true); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101", "102", "103" @@ -53,13 +75,13 @@ void findIdsForFieldValueGreaterThanTest2() { @Test void findIdsForFieldValueLessThanTest() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", false); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101" ); - indexView = EmployeeDataSet.createEmployeeIndexView(); + indexView = IndexViewDataSet.createEmployeeIndexView(); resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", true); assertThat(resultSet).containsExactlyInAnyOrder( "100", "101", "102", "103" @@ -68,16 +90,129 @@ void findIdsForFieldValueLessThanTest() { @Test void findIdsForFieldValueLessThanTest2() { - var indexView = EmployeeDataSet.createEmployeeIndexView(); + var indexView = IndexViewDataSet.createEmployeeIndexView(); var resultSet = indexView.findIdsForFieldValueLessThan("managerId", "id", false); assertThat(resultSet).containsExactlyInAnyOrder( "104", "105" ); - indexView = EmployeeDataSet.createEmployeeIndexView(); + indexView = IndexViewDataSet.createEmployeeIndexView(); resultSet = indexView.findIdsForFieldValueLessThan("managerId", "id", true); assertThat(resultSet).containsExactlyInAnyOrder( "103", "102", "104", "105" ); } + + @Nested + class SortTest { + @Test + void testSortByUnsorted() { + Collection> entries = List.of( + Map.entry("Item1", "Item1"), + Map.entry("Item2", "Item2") + ); + var indexView = new QueryIndexViewImpl(Map.of("field1", entries)); + var sort = Sort.unsorted(); + + List sortedList = indexView.sortBy(sort); + assertThat(sortedList).isEqualTo(List.of("Item1", "Item2")); + } + + @Test + void testSortBySortedAscending() { + var indexEntries = new HashMap>>(); + indexEntries.put("field1", + List.of(Map.entry("key2", "Item2"), Map.entry("key1", "Item1"))); + var indexView = new QueryIndexViewImpl(indexEntries); + var sort = Sort.by(Sort.Order.asc("field1")); + + List sortedList = indexView.sortBy(sort); + + assertThat(sortedList).containsExactly("Item1", "Item2"); + } + + @Test + void testSortBySortedDescending() { + var indexEntries = new HashMap>>(); + indexEntries.put("field1", + List.of(Map.entry("key1", "Item1"), Map.entry("key2", "Item2"))); + var indexView = new QueryIndexViewImpl(indexEntries); + var sort = Sort.by(Sort.Order.desc("field1")); + + List sortedList = indexView.sortBy(sort); + + assertThat(sortedList).containsExactly("Item2", "Item1"); + } + + @Test + void testSortByMultipleFields() { + var indexEntries = new LinkedHashMap>>(); + indexEntries.put("field1", List.of(Map.entry("k3", "Item3"), Map.entry("k2", "Item2"))); + indexEntries.put("field2", List.of(Map.entry("k1", "Item1"), Map.entry("k3", "Item3"))); + var indexView = new QueryIndexViewImpl(indexEntries); + var sort = Sort.by(Sort.Order.asc("field1"), Sort.Order.desc("field2")); + + List sortedList = indexView.sortBy(sort); + + assertThat(sortedList).containsExactly("Item2", "Item3", "Item1"); + } + + @Test + void testSortByWithMissingFieldInMap() { + var indexEntries = new LinkedHashMap>>(); + var indexView = new QueryIndexViewImpl(indexEntries); + var sort = Sort.by(Sort.Order.asc("missingField")); + + assertThatThrownBy(() -> indexView.sortBy(sort)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Field name missingField is not indexed"); + } + + @Test + void testSortByMultipleFields2() { + var indexEntries = new LinkedHashMap>>(); + + var entry1 = List.of(Map.entry("John", "John"), + Map.entry("Bob", "Bob"), + Map.entry("Alice", "Alice") + ); + var entry2 = List.of(Map.entry("David", "David"), + Map.entry("Eva", "Eva"), + Map.entry("Frank", "Frank") + ); + var entry3 = List.of(Map.entry("George", "George"), + Map.entry("Helen", "Helen"), + Map.entry("Ivy", "Ivy") + ); + + indexEntries.put("field1", entry1); + indexEntries.put("field2", entry2); + indexEntries.put("field3", entry3); + + /* + *
+             * Row Key | field1 | field2 | field3
+             * -------|-------|-------|-------
+             * John   | John  |       |
+             * Bob    | Bob   |       |
+             * Alice  | Alice |       |
+             * David  |       | David |
+             * Eva    |       | Eva   |
+             * Frank  |       | Frank |
+             * George |       |       | George
+             * Helen  |       |       | Helen
+             * Ivy    |       |       | Ivy
+             * 
+ */ + var indexView = new QueryIndexViewImpl(indexEntries); + // "John", "Bob", "Alice", "David", "Eva", "Frank", "George", "Helen", "Ivy" + var sort = Sort.by(Sort.Order.desc("field1"), Sort.Order.asc("field2"), + Sort.Order.asc("field3")); + + List sortedList = indexView.sortBy(sort); + + assertThat(sortedList).containsSequence("John", "Bob", "Alice", "David", "Eva", "Frank", + "George", "Helen", "Ivy"); + } + } } diff --git a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java index 262b557a92..cc204c5bd7 100644 --- a/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java +++ b/application/src/main/java/run/halo/app/extension/index/IndexEntryImpl.java @@ -1,6 +1,5 @@ package run.halo.app.extension.index; -import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; import java.util.Collection; @@ -42,39 +41,6 @@ Comparator keyComparator() { return KeyComparator.INSTANCE.reversed(); } - static class KeyComparator implements Comparator { - public static final KeyComparator INSTANCE = new KeyComparator(); - - @Override - public int compare(String a, String b) { - int i = 0; - int j = 0; - while (i < a.length() && j < b.length()) { - if (Character.isDigit(a.charAt(i)) && Character.isDigit(b.charAt(j))) { - // handle number part - int num1 = 0; - int num2 = 0; - while (i < a.length() && Character.isDigit(a.charAt(i))) { - num1 = num1 * 10 + (a.charAt(i++) - '0'); - } - while (j < b.length() && Character.isDigit(b.charAt(j))) { - num2 = num2 * 10 + (b.charAt(j++) - '0'); - } - if (num1 != num2) { - return num1 - num2; - } - } else if (a.charAt(i) != b.charAt(j)) { - // handle non-number part - return a.charAt(i) - b.charAt(j); - } else { - i++; - j++; - } - } - return a.length() - b.length(); - } - } - @Override public void acquireReadLock() { this.rwl.readLock().lock(); @@ -151,7 +117,10 @@ public Collection> entries() { public Collection> immutableEntries() { readLock.lock(); try { - return ImmutableListMultimap.copyOf(indexKeyObjectNamesMap).entries(); + // Copy to a new list to avoid ConcurrentModificationException + return indexKeyObjectNamesMap.entries().stream() + .map(entry -> Map.entry(entry.getKey(), entry.getValue())) + .toList(); } finally { readLock.unlock(); } diff --git a/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java b/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java index 5dd092e97a..ff11f4ca63 100644 --- a/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java +++ b/application/src/main/java/run/halo/app/extension/index/IndexedQueryEngineImpl.java @@ -2,20 +2,17 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.PriorityQueue; import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Sort; -import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StopWatch; @@ -23,7 +20,7 @@ import run.halo.app.extension.ListOptions; import run.halo.app.extension.ListResult; import run.halo.app.extension.PageRequest; -import run.halo.app.extension.index.query.Query; +import run.halo.app.extension.index.query.All; import run.halo.app.extension.index.query.QueryIndexViewImpl; import run.halo.app.extension.router.selector.FieldSelector; import run.halo.app.extension.router.selector.LabelSelector; @@ -134,8 +131,9 @@ List doRetrieve(Indexer indexer, ListOptions options, Sort sort) { StopWatch stopWatch = new StopWatch(); stopWatch.start("build index entry map"); var fieldPathEntryMap = fieldPathIndexEntryMap(indexer); - stopWatch.stop(); var primaryEntry = getIndexEntry(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME, fieldPathEntryMap); + stopWatch.stop(); + // O(n) time complexity stopWatch.start("retrieve all metadata names"); var allMetadataNames = new ArrayList(); @@ -147,36 +145,38 @@ List doRetrieve(Indexer indexer, ListOptions options, Sort sort) { } stopWatch.stop(); + stopWatch.start("build index view"); + var indexViewMap = new HashMap>>(); + for (Map.Entry entry : fieldPathEntryMap.entrySet()) { + indexViewMap.put(entry.getKey(), entry.getValue().immutableEntries()); + } + // TODO optimize build indexView time + var indexView = new QueryIndexViewImpl(indexViewMap); + stopWatch.stop(); + stopWatch.start("retrieve matched metadata names"); var hasLabelSelector = hasLabelSelector(options.getLabelSelector()); final List matchedByLabels = hasLabelSelector ? retrieveForLabelMatchers(options.getLabelSelector().getMatchers(), fieldPathEntryMap, allMetadataNames) : allMetadataNames; + indexView.removeByIdNotIn(new TreeSet<>(matchedByLabels)); stopWatch.stop(); stopWatch.start("retrieve matched metadata names by fields"); final var hasFieldSelector = hasFieldSelector(options.getFieldSelector()); - var matchedByFields = hasFieldSelector - ? retrieveForFieldSelector(options.getFieldSelector().query(), fieldPathEntryMap) - : allMetadataNames; - stopWatch.stop(); - - stopWatch.start("merge result"); - List foundObjectKeys; - if (!hasLabelSelector && !hasFieldSelector) { - foundObjectKeys = allMetadataNames; - } else if (!hasLabelSelector) { - foundObjectKeys = matchedByFields; - } else { - foundObjectKeys = intersection(matchedByFields, matchedByLabels); + if (hasFieldSelector) { + var fieldSelector = options.getFieldSelector(); + var query = fieldSelector.query(); + var resultSet = query.matches(indexView); + indexView.removeByIdNotIn(resultSet); } stopWatch.stop(); stopWatch.start("sort result"); - ResultSorter resultSorter = new ResultSorter(fieldPathEntryMap, foundObjectKeys); - var result = resultSorter.sortBy(sort); + var result = indexView.sortBy(sort); stopWatch.stop(); + if (log.isTraceEnabled()) { log.trace("Retrieve result from indexer, {}", stopWatch.prettyPrint()); } @@ -188,109 +188,8 @@ boolean hasLabelSelector(LabelSelector labelSelector) { } boolean hasFieldSelector(FieldSelector fieldSelector) { - return fieldSelector != null && fieldSelector.query() != null; - } - - List retrieveForFieldSelector(Query query, Map fieldPathEntryMap) { - Map>> indexView = new HashMap<>(); - for (Map.Entry entry : fieldPathEntryMap.entrySet()) { - indexView.put(entry.getKey(), entry.getValue().immutableEntries()); - } - // TODO optimize build indexView time - var queryIndexView = new QueryIndexViewImpl(indexView); - var resultSet = query.matches(queryIndexView); - return new ArrayList<>(resultSet); - } - - /** - * Sort the given list by the given {@link Sort}. - */ - static class ResultSorter { - private final Map fieldPathEntryMap; - private final List list; - - public ResultSorter(Map fieldPathEntryMap, List list) { - this.fieldPathEntryMap = fieldPathEntryMap; - this.list = list; - } - - public List sortBy(@NonNull Sort sort) { - if (sort.isUnsorted()) { - return list; - } - var sortedLists = new ArrayList>(); - for (Sort.Order order : sort) { - var indexEntry = fieldPathEntryMap.get(order.getProperty()); - if (indexEntry == null) { - throwNotIndexedException(order.getProperty()); - } - var set = new HashSet<>(list); - var objectNames = new ArrayList(); - indexEntry.acquireReadLock(); - try { - for (var entry : indexEntry.entries()) { - var objectName = entry.getValue(); - if (set.contains(objectName)) { - objectNames.add(objectName); - } - } - } finally { - indexEntry.releaseReadLock(); - } - var indexOrder = indexEntry.getIndexDescriptor().getSpec().getOrder(); - var asc = IndexSpec.OrderType.ASC.equals(indexOrder); - if (asc != order.isAscending()) { - Collections.reverse(objectNames); - } - sortedLists.add(objectNames); - } - return mergeSortedLists(sortedLists); - } - - /** - *

Merge the given sorted lists into one sorted list.

- *

The time complexity is O(n * log(m)), n is the number of all elements in the - * sortedLists, m is the number of sortedLists.

- */ - private List mergeSortedLists(List> sortedLists) { - List result = new ArrayList<>(); - // Use a priority queue to store the current element of each list and its index in - // the list - PriorityQueue minHeap = new PriorityQueue<>( - Comparator.comparing(pair -> pair.value)); - - // Initialize the priority queue and add the first element of each list to the queue - for (int i = 0; i < sortedLists.size(); i++) { - if (!sortedLists.get(i).isEmpty()) { - minHeap.add(new Pair(i, 0, sortedLists.get(i).get(0))); - } - } - - while (!minHeap.isEmpty()) { - Pair current = minHeap.poll(); - result.add(current.value()); - - // Add the next element of this list to the priority queue - if (current.indexInList() + 1 < sortedLists.get(current.listIndex()).size()) { - var list = sortedLists.get(current.listIndex()); - minHeap.add(new Pair(current.listIndex(), - current.indexInList() + 1, - list.get(current.indexInList() + 1)) - ); - } - } - return result; - } - - /** - *

A pair of element and its position in the original list.

- *
-         * listIndex: column index.
-         * indexInList: element index in the list.
-         * value: element value.
-         * 
- */ - private record Pair(int listIndex, int indexInList, String value) { - } + return fieldSelector != null + && fieldSelector.query() != null + && !(fieldSelector.query() instanceof All); } } diff --git a/application/src/test/java/run/halo/app/extension/index/IndexEntryImplTest.java b/application/src/test/java/run/halo/app/extension/index/IndexEntryImplTest.java index 4cded8486c..479b9cf313 100644 --- a/application/src/test/java/run/halo/app/extension/index/IndexEntryImplTest.java +++ b/application/src/test/java/run/halo/app/extension/index/IndexEntryImplTest.java @@ -1,10 +1,7 @@ package run.halo.app.extension.index; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -108,66 +105,4 @@ void keyOrder() { Map.entry("slug-4", "fake-name-3")); assertThat(entry2.indexedKeys()).containsSequence("slug-1", "slug-2", "slug-3", "slug-4"); } - - @Test - void keyComparator() { - var comparator = IndexEntryImpl.KeyComparator.INSTANCE; - String[] strings = {"103", "101", "102", "1011", "1013", "1021", "1022", "1012", "1023"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo( - new String[] {"101", "102", "103", "1011", "1012", "1013", "1021", "1022", "1023"}); - - Arrays.sort(strings, comparator.reversed()); - assertThat(strings).isEqualTo( - new String[] {"1023", "1022", "1021", "1013", "1012", "1011", "103", "102", "101"}); - - // but if we use natural order, the result is: - Arrays.sort(strings, Comparator.naturalOrder()); - assertThat(strings).isEqualTo( - new String[] {"101", "1011", "1012", "1013", "102", "1021", "1022", "1023", "103"}); - } - - @Test - void keyComparator2() { - var comparator = IndexEntryImpl.KeyComparator.INSTANCE; - String[] strings = - {"moment-101", "moment-102", "moment-103", "moment-1011", "moment-1013", "moment-1021", - "moment-1022", "moment-1012", "moment-1023"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo(new String[] {"moment-101", "moment-102", "moment-103", - "moment-1011", "moment-1012", "moment-1013", "moment-1021", "moment-1022", - "moment-1023"}); - - // date sort - strings = - new String[] {"2022-01-15", "2022-02-01", "2021-12-25", "2022-01-01", "2022-01-02"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo( - new String[] {"2021-12-25", "2022-01-01", "2022-01-02", "2022-01-15", "2022-02-01"}); - - // alphabet and number sort - strings = new String[] {"abc123", "abc45", "abc9", "abc100", "abc20"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo( - new String[] {"abc9", "abc20", "abc45", "abc100", "abc123"}); - - // test for pure alphabet sort - strings = new String[] {"xyz", "abc", "def", "abcde", "xyzabc"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo(new String[] {"abc", "abcde", "def", "xyz", "xyzabc"}); - - // test for empty string - strings = new String[] {"", "abc", "123", "xyz"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo(new String[] {"", "123", "abc", "xyz"}); - - // test for the same string - strings = new String[] {"abc", "abc", "abc", "abc"}; - Arrays.sort(strings, comparator); - assertThat(strings).isEqualTo(new String[] {"abc", "abc", "abc", "abc"}); - - // test for null element - assertThatThrownBy(() -> Arrays.sort(new String[] {null, "abc", "123", "xyz"}, comparator)) - .isInstanceOf(NullPointerException.class); - } } diff --git a/application/src/test/java/run/halo/app/extension/index/IndexedQueryEngineImplTest.java b/application/src/test/java/run/halo/app/extension/index/IndexedQueryEngineImplTest.java index 7f99216d7d..753acbf298 100644 --- a/application/src/test/java/run/halo/app/extension/index/IndexedQueryEngineImplTest.java +++ b/application/src/test/java/run/halo/app/extension/index/IndexedQueryEngineImplTest.java @@ -5,7 +5,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -13,10 +12,8 @@ import static org.mockito.Mockito.when; import static run.halo.app.extension.index.query.QueryFactory.equal; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -249,237 +246,4 @@ void testRetrieveForLabelMatchersNoMatch() { static class DemoExtension extends AbstractExtension { } - - @Nested - @ExtendWith(MockitoExtension.class) - class ResultSorterTest { - - @Test - void testSortByUnsorted() { - var fieldPathEntryMap = Map.of(); - List list = new ArrayList<>(); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - Sort sort = Sort.unsorted(); - list.add("Item1"); - list.add("Item2"); - - List sortedList = sorter.sortBy(sort); - - assertThat(sortedList).containsExactly("Item1", "Item2"); - } - - @Test - void testSortBySortedAscending() { - final var fieldPathEntryMap = new HashMap(); - var entry = mock(IndexEntry.class); - when(entry.entries()).thenReturn(List.of( - Map.entry("key2", "Item2"), - Map.entry("key1", "Item1") - )); - lenient().when(entry.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field1") - .setOrder(IndexSpec.OrderType.DESC))); - fieldPathEntryMap.put("field1", entry); - - var list = new ArrayList<>(Arrays.asList("Item1", "Item2")); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - var sort = Sort.by(Sort.Order.asc("field1")); - List sortedList = sorter.sortBy(sort); - - assertThat(sortedList).containsExactly("Item1", "Item2"); - } - - @Test - void testSortBySortedDescending() { - final var fieldPathEntryMap = new HashMap(); - var entry = mock(IndexEntry.class); - when(entry.entries()).thenReturn(List.of( - Map.entry("key1", "Item1"), - Map.entry("key2", "Item2") - )); - var indexDescriptor = new IndexDescriptor(new IndexSpec() - .setName("field1") - .setOrder(IndexSpec.OrderType.ASC)); - when(entry.getIndexDescriptor()).thenReturn(indexDescriptor); - fieldPathEntryMap.put("field1", entry); - - var list = new ArrayList(); - list.add("Item1"); - list.add("Item2"); - Sort sort = Sort.by(Sort.Order.desc("field1")); - - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - List sortedList = sorter.sortBy(sort); - - assertThat(sortedList).containsSequence("Item2", "Item1"); - } - - @Test - void testSortByMultipleFields() { - final var fieldPathEntryMap = new LinkedHashMap(); - - var entry1 = mock(IndexEntry.class); - when(entry1.entries()).thenReturn(List.of( - Map.entry("k3", "Item3"), - Map.entry("k2", "Item2") - )); - when(entry1.getIndexDescriptor()).thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field1") - .setOrder(IndexSpec.OrderType.DESC))); - - var entry2 = mock(IndexEntry.class); - lenient().when(entry2.entries()).thenReturn(List.of( - Map.entry("k1", "Item1"), - Map.entry("k3", "Item3") - )); - lenient().when(entry2.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field2") - .setOrder(IndexSpec.OrderType.ASC))); - - fieldPathEntryMap.put("field1", entry1); - fieldPathEntryMap.put("field2", entry2); - - final Sort sort = Sort.by(Sort.Order.asc("field1"), - Sort.Order.desc("field2")); - - var list = new ArrayList<>(Arrays.asList("Item1", "Item2", "Item3")); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - List sortedList = sorter.sortBy(sort); - - assertThat(sortedList).containsSequence("Item2", "Item3", "Item1"); - } - - @Test - void testSortByMultipleFields2() { - final var fieldPathEntryMap = new LinkedHashMap(); - - var entry1 = mock(IndexEntry.class); - when(entry1.entries()).thenReturn(List.of( - Map.entry("John", "John"), - Map.entry("Bob", "Bob"), - Map.entry("Alice", "Alice") - )); - lenient().when(entry1.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field1") - .setOrder(IndexSpec.OrderType.DESC))); - - var entry2 = mock(IndexEntry.class); - when(entry2.entries()).thenReturn(List.of( - Map.entry("David", "David"), - Map.entry("Eva", "Eva"), - Map.entry("Frank", "Frank") - )); - lenient().when(entry2.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field2") - .setOrder(IndexSpec.OrderType.ASC))); - - var entry3 = mock(IndexEntry.class); - lenient().when(entry3.entries()).thenReturn(List.of( - Map.entry("George", "George"), - Map.entry("Helen", "Helen"), - Map.entry("Ivy", "Ivy") - )); - lenient().when(entry3.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field3") - .setOrder(IndexSpec.OrderType.ASC))); - - fieldPathEntryMap.put("field1", entry1); - fieldPathEntryMap.put("field2", entry2); - fieldPathEntryMap.put("field3", entry3); - - var list = new ArrayList<>( - Arrays.asList("Alice", "Bob", "Ivy", "Eva", "George")); - final Sort sort = Sort.by(Sort.Order.desc("field1"), - Sort.Order.asc("field2"), - Sort.Order.asc("field3")); - - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - List sortedList = sorter.sortBy(sort); - - assertThat(sortedList).containsSequence("Bob", "Alice", "Eva", "George", "Ivy"); - } - - @Test - void testSortByWithMissingFieldInMap() { - var fieldPathEntryMap = new LinkedHashMap(); - var list = new ArrayList<>(Arrays.asList("Item1", "Item2")); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - - Sort sort = Sort.by(Sort.Order.asc("missingField")); - assertThatThrownBy(() -> sorter.sortBy(sort)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "No index found for fieldPath: missingField, make sure you have created an " - + "index for this field."); - } - - @Test - void testSortByWithEmptyMap() { - var fieldPathEntryMap = new LinkedHashMap(); - var entry = mock(IndexEntry.class); - when(entry.entries()).thenReturn(List.of()); - lenient().when(entry.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field") - .setOrder(IndexSpec.OrderType.DESC))); - fieldPathEntryMap.put("field", entry); - - var list = new ArrayList<>(Arrays.asList("Item1", "Item2")); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - Sort sort = Sort.by(Sort.Order.asc("field")); - - List sortedList = sorter.sortBy(sort); - assertThat(sortedList).isEmpty(); - } - - @Test - void testSortByWithEmptyList() { - var fieldPathEntryMap = new LinkedHashMap(); - var entry = mock(IndexEntry.class); - when(entry.entries()).thenReturn(List.of( - Map.entry("John", "John"), - Map.entry("Bob", "Bob") - )); - lenient().when(entry.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field") - .setOrder(IndexSpec.OrderType.DESC))); - fieldPathEntryMap.put("field", entry); - - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, List.of()); - Sort sort = Sort.by(Sort.Order.asc("field")); - - List sortedList = sorter.sortBy(sort); - assertThat(sortedList).isEmpty(); - } - - @Test - void testSortByWithItemNotInIndex() { - var fieldPathEntryMap = new LinkedHashMap(); - var entry = mock(IndexEntry.class); - when(entry.entries()).thenReturn(List.of( - Map.entry("Item2", "Item2"), - Map.entry("Item1", "Item1") - )); - lenient().when(entry.getIndexDescriptor()) - .thenReturn(new IndexDescriptor(new IndexSpec() - .setName("field") - .setOrder(IndexSpec.OrderType.DESC))); - - fieldPathEntryMap.put("field", entry); - - // Item3 is not in the index - var list = new ArrayList<>(Arrays.asList("Item1", "Item3")); - var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list); - Sort sort = Sort.by(Sort.Order.asc("field")); - - List sortedList = sorter.sortBy(sort); - assertThat(sortedList).containsExactly("Item1"); - } - } -} \ No newline at end of file +}