retrieveAll(GroupVersionKind type, ListOptions options, Sort sort);
}
diff --git a/application/src/main/java/run/halo/app/extension/index/Indexer.java b/api/src/main/java/run/halo/app/extension/index/Indexer.java
similarity index 84%
rename from application/src/main/java/run/halo/app/extension/index/Indexer.java
rename to api/src/main/java/run/halo/app/extension/index/Indexer.java
index 78702f3c7..e92f3360d 100644
--- a/application/src/main/java/run/halo/app/extension/index/Indexer.java
+++ b/api/src/main/java/run/halo/app/extension/index/Indexer.java
@@ -2,6 +2,7 @@ package run.halo.app.extension.index;
import java.util.Iterator;
import java.util.function.Function;
+import org.springframework.lang.NonNull;
import run.halo.app.extension.Extension;
/**
@@ -20,7 +21,7 @@ public interface Indexer {
/**
* Index the specified {@link Extension} by {@link IndexDescriptor}s.
* First, the {@link Indexer} will index the {@link Extension} by the
- * {@link IndexDescriptor}s and record the index entries to {@link IndexerTransaction} and
+ * {@link IndexDescriptor}s and record the index entries to {@code IndexerTransaction} and
* commit the transaction, if any error occurs, the transaction will be rollback to keep the
* {@link Indexer} consistent.
*
@@ -33,7 +34,7 @@ public interface Indexer {
* Update indexes for the specified {@link Extension} by {@link IndexDescriptor}s.
* First, the {@link Indexer} will remove the index entries of the {@link Extension} by
* the old {@link IndexDescriptor}s and reindex the {@link Extension} to generate change logs
- * to {@link IndexerTransaction} and commit the transaction, if any error occurs, the
+ * to {@code IndexerTransaction} and commit the transaction, if any error occurs, the
* transaction will be rollback to keep the {@link Indexer} consistent.
*
* @param extension the {@link Extension} to be updated
@@ -73,11 +74,21 @@ public interface Indexer {
*/
void removeIndexRecords(Function matchFn);
+ /**
+ * Get the {@link IndexEntry} by index name if found and ready.
+ *
+ * @param name an index name
+ * @return the {@link IndexEntry} if found
+ * @throws IllegalArgumentException if the index name is not found or the index is not ready
+ */
+ @NonNull
+ IndexEntry getIndexEntry(String name);
+
/**
* Gets an iterator over all the ready {@link IndexEntry}s, in no particular order.
*
* @return an iterator over all the ready {@link IndexEntry}s
- * @link {@link IndexDescriptor#isReady()}
+ * @see IndexDescriptor#isReady()
*/
Iterator readyIndexesIterator();
@@ -85,7 +96,11 @@ public interface Indexer {
* Gets an iterator over all the {@link IndexEntry}s, in no particular order.
*
* @return an iterator over all the {@link IndexEntry}s
- * @link {@link IndexDescriptor#isReady()}
+ * @see IndexDescriptor#isReady()
*/
Iterator allIndexesIterator();
+
+ void acquireReadLock();
+
+ void releaseReadLock();
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/All.java b/api/src/main/java/run/halo/app/extension/index/query/All.java
index 541d73020..46b47b413 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/All.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/All.java
@@ -10,6 +10,11 @@ public class All extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
- return indexView.getAllIdsForField(fieldName);
+ return indexView.getIdsForField(fieldName);
+ }
+
+ @Override
+ public String toString() {
+ return fieldName + " != null";
}
}
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 8a80b319f..b47e8f0c8 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
@@ -3,6 +3,7 @@ package run.halo.app.extension.index.query;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.NavigableSet;
+import java.util.stream.Collectors;
public class And extends LogicalQuery {
@@ -33,4 +34,10 @@ public class And extends LogicalQuery {
}
return resultSet == null ? Sets.newTreeSet() : resultSet;
}
+
+ @Override
+ public String toString() {
+ return "(" + childQueries.stream().map(Query::toString)
+ .collect(Collectors.joining(" AND ")) + ")";
+ }
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/Between.java b/api/src/main/java/run/halo/app/extension/index/query/Between.java
index 829770b5d..e512a3f00 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/Between.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/Between.java
@@ -1,6 +1,5 @@
package run.halo.app.extension.index.query;
-import com.google.common.collect.Sets;
import java.util.NavigableSet;
public class Between extends SimpleQuery {
@@ -19,17 +18,14 @@ public class Between extends SimpleQuery {
this.upperInclusive = upperInclusive;
}
-
@Override
public NavigableSet matches(QueryIndexView indexView) {
- NavigableSet allValues = indexView.getAllValuesForField(fieldName);
- // get all values in the specified range
- var subSet = allValues.subSet(lowerValue, lowerInclusive, upperValue, upperInclusive);
+ return indexView.between(fieldName, lowerValue, lowerInclusive, upperValue, upperInclusive);
+ }
- var resultSet = Sets.newTreeSet();
- for (String val : subSet) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
- }
- return resultSet;
+ @Override
+ public String toString() {
+ return fieldName + " BETWEEN " + (lowerInclusive ? "[" : "(") + lowerValue + ", "
+ + upperValue + (upperInclusive ? "]" : ")");
}
}
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 6f23f901b..0ce826adc 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
@@ -17,16 +17,16 @@ public class EqualQuery extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
if (isFieldRef) {
- return resultSetForRefValue(indexView);
+ return indexView.findMatchingIdsWithEqualValues(fieldName, value);
}
- return resultSetForExactValue(indexView);
+ return indexView.findIds(fieldName, value);
}
- private NavigableSet resultSetForRefValue(QueryIndexView indexView) {
- return indexView.findIdsForFieldValueEqual(fieldName, value);
- }
-
- private NavigableSet resultSetForExactValue(QueryIndexView indexView) {
- return indexView.getIdsForFieldValue(fieldName, value);
+ @Override
+ public String toString() {
+ if (isFieldRef) {
+ return fieldName + " = " + value;
+ }
+ return fieldName + " = '" + value + "'";
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/GreaterThanQuery.java b/api/src/main/java/run/halo/app/extension/index/query/GreaterThanQuery.java
index b8670ee4d..101286d16 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/GreaterThanQuery.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/GreaterThanQuery.java
@@ -1,6 +1,5 @@
package run.halo.app.extension.index.query;
-import com.google.common.collect.Sets;
import java.util.NavigableSet;
public class GreaterThanQuery extends SimpleQuery {
@@ -18,24 +17,15 @@ public class GreaterThanQuery extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
if (isFieldRef) {
- return resultSetForRefValue(indexView);
+ return indexView.findMatchingIdsWithGreaterValues(fieldName, value, orEqual);
}
- return resultSetForExtractValue(indexView);
+ return indexView.findIdsGreaterThan(fieldName, value, orEqual);
}
- private NavigableSet resultSetForRefValue(QueryIndexView indexView) {
- return indexView.findIdsForFieldValueGreaterThan(fieldName, value, orEqual);
- }
-
- private NavigableSet resultSetForExtractValue(QueryIndexView indexView) {
- var resultSet = Sets.newTreeSet();
- var allValues = indexView.getAllValuesForField(fieldName);
- NavigableSet tailSet =
- orEqual ? allValues.tailSet(value, true) : allValues.tailSet(value, false);
-
- for (String val : tailSet) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
- }
- return resultSet;
+ @Override
+ public String toString() {
+ return fieldName
+ + (orEqual ? " >= " : " > ")
+ + (isFieldRef ? value : "'" + value + "'");
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/InQuery.java b/api/src/main/java/run/halo/app/extension/index/query/InQuery.java
index 5c743190e..b9aa02683 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/InQuery.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/InQuery.java
@@ -1,8 +1,9 @@
package run.halo.app.extension.index.query;
-import com.google.common.collect.Sets;
import java.util.NavigableSet;
import java.util.Set;
+import java.util.stream.Collectors;
+import run.halo.app.extension.index.IndexEntryOperatorImpl;
public class InQuery extends SimpleQuery {
private final Set values;
@@ -14,10 +15,15 @@ public class InQuery extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
- NavigableSet resultSet = Sets.newTreeSet();
- for (String val : values) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
- }
- return resultSet;
+ var indexEntry = indexView.getIndexEntry(fieldName);
+ var operator = new IndexEntryOperatorImpl(indexEntry);
+ return operator.findIn(values);
+ }
+
+ @Override
+ public String toString() {
+ return fieldName + " IN (" + values.stream()
+ .map(value -> "'" + value + "'")
+ .collect(Collectors.joining(", ")) + ")";
}
}
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
index 114da9562..1c66133d5 100644
--- 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
@@ -10,6 +10,11 @@ public class IsNotNull extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
- return indexView.getAllIdsForField(fieldName);
+ return indexView.getIdsForField(fieldName);
+ }
+
+ @Override
+ public String toString() {
+ return fieldName + " IS NOT NULL";
}
}
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
index 5b09e8f8e..d74040c9f 100644
--- 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
@@ -10,9 +10,19 @@ public class IsNull extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
- var allIds = indexView.getAllIds();
- var idsForField = indexView.getAllIdsForField(fieldName);
- allIds.removeAll(idsForField);
- return allIds;
+ indexView.acquireReadLock();
+ try {
+ var allIds = indexView.getAllIds();
+ var idsForNonNullValue = indexView.getIdsForField(fieldName);
+ allIds.removeAll(idsForNonNullValue);
+ return allIds;
+ } finally {
+ indexView.releaseReadLock();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return fieldName + " IS NULL";
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/LessThanQuery.java b/api/src/main/java/run/halo/app/extension/index/query/LessThanQuery.java
index 4f882cb64..d7168e8ae 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/LessThanQuery.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/LessThanQuery.java
@@ -1,6 +1,5 @@
package run.halo.app.extension.index.query;
-import com.google.common.collect.Sets;
import java.util.NavigableSet;
public class LessThanQuery extends SimpleQuery {
@@ -18,24 +17,15 @@ public class LessThanQuery extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
if (isFieldRef) {
- return resultSetForRefValue(indexView);
+ return indexView.findMatchingIdsWithSmallerValues(fieldName, value, orEqual);
}
- return resultSetForExactValue(indexView);
+ return indexView.findIdsLessThan(fieldName, value, orEqual);
}
- private NavigableSet resultSetForRefValue(QueryIndexView indexView) {
- return indexView.findIdsForFieldValueLessThan(fieldName, value, orEqual);
- }
-
- private NavigableSet resultSetForExactValue(QueryIndexView indexView) {
- var resultSet = Sets.newTreeSet();
- var allValues = indexView.getAllValuesForField(fieldName);
- var headSet = orEqual ? allValues.headSet(value, true)
- : allValues.headSet(value, false);
-
- for (String val : headSet) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
- }
- return resultSet;
+ @Override
+ public String toString() {
+ return fieldName
+ + (orEqual ? " <= " : " < ")
+ + (isFieldRef ? value : "'" + value + "'");
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/Not.java b/api/src/main/java/run/halo/app/extension/index/query/Not.java
index 908507ff9..98d0e29fc 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/Not.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/Not.java
@@ -24,4 +24,9 @@ public class Not extends LogicalQuery {
allIds.removeAll(negatedResult);
return allIds;
}
+
+ @Override
+ public String toString() {
+ return "NOT (" + negatedQuery + ")";
+ }
}
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 baa5f2ed7..3ffa33ff0 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
@@ -1,6 +1,5 @@
package run.halo.app.extension.index.query;
-import com.google.common.collect.Sets;
import java.util.NavigableSet;
import org.springframework.util.Assert;
@@ -19,15 +18,19 @@ public class NotEqual extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
- var names = equalQuery.matches(indexView);
- var allNames = indexView.getAllIdsForField(fieldName);
-
- var resultSet = Sets.newTreeSet();
- for (String name : allNames) {
- if (!names.contains(name)) {
- resultSet.add(name);
- }
+ indexView.acquireReadLock();
+ try {
+ NavigableSet equalNames = equalQuery.matches(indexView);
+ NavigableSet allNames = indexView.getIdsForField(fieldName);
+ allNames.removeAll(equalNames);
+ return allNames;
+ } finally {
+ indexView.releaseReadLock();
}
- return resultSet;
+ }
+
+ @Override
+ public String toString() {
+ return fieldName + " != " + (isFieldRef ? value : "'" + value + "'");
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/Or.java b/api/src/main/java/run/halo/app/extension/index/query/Or.java
index ec79270ca..c8579c54f 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/Or.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/Or.java
@@ -3,6 +3,7 @@ package run.halo.app.extension.index.query;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.NavigableSet;
+import java.util.stream.Collectors;
public class Or extends LogicalQuery {
@@ -18,4 +19,10 @@ public class Or extends LogicalQuery {
}
return resultSet;
}
+
+ @Override
+ public String toString() {
+ return "(" + childQueries.stream().map(Query::toString)
+ .collect(Collectors.joining(" OR ")) + ")";
+ }
}
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 18a8a8fc3..eb6a91542 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
@@ -4,6 +4,7 @@ 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.IndexEntry;
import run.halo.app.extension.index.IndexSpec;
/**
@@ -19,6 +20,7 @@ import run.halo.app.extension.index.IndexSpec;
* @since 2.12.0
*/
public interface QueryIndexView {
+
/**
* Gets all object ids for a given field name and field value.
*
@@ -27,16 +29,7 @@ public interface QueryIndexView {
* @return all indexed object ids associated with the given field name and field value
* @throws IllegalArgumentException if the field name is not indexed
*/
- NavigableSet getIdsForFieldValue(String fieldName, String fieldValue);
-
- /**
- * Gets all field values for a given field name.
- *
- * @param fieldName the field name
- * @return all field values for the given field name
- * @throws IllegalArgumentException if the field name is not indexed
- */
- NavigableSet getAllValuesForField(String fieldName);
+ NavigableSet findIds(String fieldName, String fieldValue);
/**
* Gets all object ids for a given field name without null cells.
@@ -45,7 +38,7 @@ public interface QueryIndexView {
* @return all indexed object ids for the given field name
* @throws IllegalArgumentException if the field name is not indexed
*/
- NavigableSet getAllIdsForField(String fieldName);
+ NavigableSet getIdsForField(String fieldName);
/**
* Gets all object ids in this view.
@@ -54,15 +47,97 @@ public interface QueryIndexView {
*/
NavigableSet getAllIds();
- NavigableSet findIdsForFieldValueEqual(String fieldName1, String fieldName2);
+ /**
+ * Finds and returns a set of unique identifiers (metadata.name) for entries that have
+ * matching values across two fields and where the values are equal.
+ * For example:
+ *
+ * metadata.name | field1 | field2
+ * ------------- | ------ | ------
+ * foo | 1 | 1
+ * bar | 2 | 3
+ * baz | 3 | 3
+ *
+ * findMatchingIdsWithEqualValues("field1", "field2")
would return ["foo","baz"]
+ *
+ * @see #findMatchingIdsWithGreaterValues(String, String, boolean)
+ * @see #findMatchingIdsWithSmallerValues(String, String, boolean)
+ */
+ NavigableSet findMatchingIdsWithEqualValues(String fieldName1, String fieldName2);
- NavigableSet findIdsForFieldValueGreaterThan(String fieldName1, String fieldName2,
+ /**
+ * Finds and returns a set of unique identifiers (metadata.name) for entries that have
+ * matching values across two fields, but where the value associated with fieldName1 is
+ * greater than the value associated with fieldName2.
+ * For example:
+ *
+ * metadata.name | field1 | field2
+ * ------------- | ------ | ------
+ * foo | 1 | 1
+ * bar | 2 | 3
+ * baz | 3 | 3
+ * qux | 4 | 2
+ *
+ * findMatchingIdsWithGreaterValues("field1", "field2")
would return ["qux"]
+ * findMatchingIdsWithGreaterValues("field2", "field1")
would return ["bar"]
+ * findMatchingIdsWithGreaterValues("field1", "field2", true)
would return
+ * ["foo","baz","qux"]
+ *
+ * @param fieldName1 The field name whose values are compared as the larger values.
+ * @param fieldName2 The field name whose values are compared as the smaller values.
+ * @param orEqual whether to include equal values
+ * @return A result set of ids where the entries in fieldName1 have greater values than those
+ * in fieldName2 for entries that have the same id across both fields
+ */
+ NavigableSet findMatchingIdsWithGreaterValues(String fieldName1, String fieldName2,
boolean orEqual);
- NavigableSet findIdsForFieldValueLessThan(String fieldName1, String fieldName2,
+ NavigableSet findIdsGreaterThan(String fieldName, String fieldValue, boolean orEqual);
+
+ /**
+ * Finds and returns a set of unique identifiers (metadata.name) for entries that have
+ * matching values across two fields, but where the value associated with fieldName1 is
+ * less than the value associated with fieldName2.
+ * For example:
+ *
+ * metadata.name | field1 | field2
+ * ------------- | ------ | ------
+ * foo | 1 | 1
+ * bar | 2 | 3
+ * baz | 3 | 3
+ * qux | 4 | 2
+ *
+ * findMatchingIdsWithSmallerValues("field1", "field2")
would return ["bar"]
+ * findMatchingIdsWithSmallerValues("field2", "field1")
would return ["qux"]
+ * findMatchingIdsWithSmallerValues("field1", "field2", true)
would return
+ * ["foo","bar","baz"]
+ *
+ * @param fieldName1 The field name whose values are compared as the smaller values.
+ * @param fieldName2 The field name whose values are compared as the larger values.
+ * @param orEqual whether to include equal values
+ * @return A result set of ids where the entries in fieldName1 have smaller values than those
+ * in fieldName2 for entries that have the same id across both fields
+ */
+ NavigableSet findMatchingIdsWithSmallerValues(String fieldName1, String fieldName2,
boolean orEqual);
- void removeByIdNotIn(NavigableSet ids);
+ NavigableSet findIdsLessThan(String fieldName, String fieldValue, boolean orEqual);
- List sortBy(Sort sort);
+ NavigableSet between(String fieldName, String lowerValue, boolean lowerInclusive,
+ String upperValue, boolean upperInclusive);
+
+ List sortBy(NavigableSet resultSet, Sort sort);
+
+ IndexEntry getIndexEntry(String fieldName);
+
+ /**
+ * Acquire a read lock on the indexer.
+ * if you need to operate on more than one {@code IndexEntry} at the same time, you need to
+ * lock first.
+ *
+ * @see #getIndexEntry(String)
+ */
+ void acquireReadLock();
+
+ void releaseReadLock();
}
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 d85ce935f..e5456a9b7 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,352 +1,230 @@
package run.halo.app.extension.index.query;
-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.Comparator;
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 java.util.function.BiPredicate;
import org.springframework.data.domain.Sort;
-import org.springframework.lang.Nullable;
-import org.springframework.util.CollectionUtils;
+import run.halo.app.extension.index.IndexEntry;
+import run.halo.app.extension.index.IndexEntryOperator;
+import run.halo.app.extension.index.IndexEntryOperatorImpl;
+import run.halo.app.extension.index.Indexer;
import run.halo.app.extension.index.KeyComparator;
/**
* A default implementation for {@link run.halo.app.extension.index.query.QueryIndexView}.
*
* @author guqing
- * @since 2.12.0
+ * @since 2.17.0
*/
public class QueryIndexViewImpl implements QueryIndexView {
- private final Lock lock = new ReentrantLock();
- private final Set fieldNames;
- private final Table> orderedMatches;
+
+ public static final String PRIMARY_INDEX_NAME = "metadata.name";
+
+ private final Indexer indexer;
/**
- * Creates a new {@link QueryIndexViewImpl} for the given {@link Map} of index entries.
+ * Construct a new {@link QueryIndexViewImpl} with the given {@link Indexer}.
*
- * @param indexEntries index entries from indexer to create the view for.
+ * @throws IllegalArgumentException if the primary index does not exist
*/
- public QueryIndexViewImpl(Map>> indexEntries) {
- this.fieldNames = new HashSet<>();
- this.orderedMatches = HashBasedTable.create();
- for (var entry : indexEntries.entrySet()) {
- 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);
- }
- }
+ public QueryIndexViewImpl(Indexer indexer) {
+ // check if primary index exists
+ indexer.getIndexEntry(PRIMARY_INDEX_NAME);
+ this.indexer = indexer;
}
@Override
- public NavigableSet getIdsForFieldValue(String fieldName, String fieldValue) {
- lock.lock();
- try {
- checkFieldNameIndexed(fieldName);
- 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();
- }
+ public NavigableSet findIds(String fieldName, String fieldValue) {
+ var operator = getEntryOperator(fieldName);
+ return operator.find(fieldValue);
}
@Override
- public NavigableSet getAllValuesForField(String fieldName) {
- lock.lock();
- try {
- checkFieldNameIndexed(fieldName);
- var result = Sets.newTreeSet();
- for (var cell : orderedMatches.cellSet()) {
- if (cell.getColumnKey().equals(fieldName)) {
- result.addAll(cell.getValue());
- }
- }
- return result;
- } finally {
- lock.unlock();
- }
- }
-
- @Override
- public NavigableSet getAllIdsForField(String fieldName) {
- lock.lock();
- try {
- checkFieldNameIndexed(fieldName);
- 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();
- }
+ public NavigableSet getIdsForField(String fieldName) {
+ var operator = getEntryOperator(fieldName);
+ return new TreeSet<>(operator.getValues());
}
@Override
public NavigableSet getAllIds() {
- lock.lock();
- try {
- return new TreeSet<>(orderedMatches.rowKeySet());
- } finally {
- lock.unlock();
- }
+ return new TreeSet<>(allIds());
}
@Override
- public NavigableSet findIdsForFieldValueEqual(String fieldName1, String fieldName2) {
- lock.lock();
+ public NavigableSet findMatchingIdsWithEqualValues(String fieldName1,
+ String fieldName2) {
+ indexer.acquireReadLock();
try {
- checkFieldNameIndexed(fieldName1);
- checkFieldNameIndexed(fieldName2);
-
- 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);
- }
- }
- }
- }
- return result;
- } finally {
- lock.unlock();
- }
- }
-
- 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) {
- lock.lock();
- try {
- checkFieldNameIndexed(fieldName1);
- checkFieldNameIndexed(fieldName2);
-
- 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 (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);
- }
- }
- }
- }
- }
- return result;
- } finally {
- lock.unlock();
- }
- }
-
- @Override
- public NavigableSet findIdsForFieldValueLessThan(String fieldName1, String fieldName2,
- boolean orEqual) {
- lock.lock();
- try {
- checkFieldNameIndexed(fieldName1);
- checkFieldNameIndexed(fieldName2);
-
- 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 (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);
- }
- }
- }
- }
- }
- return result;
- } finally {
- lock.unlock();
- }
- }
-
- @Override
- public void removeByIdNotIn(NavigableSet ids) {
- lock.lock();
- try {
- 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();
- }
- }
-
- @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 findIdsWithKeyComparator(fieldName1, fieldName2, (k1, k2) -> {
+ var compare = KeyComparator.INSTANCE.compare(k1, k2);
+ return compare == 0;
});
-
- return sortedRowKeys;
} finally {
- lock.unlock();
+ indexer.releaseReadLock();
}
}
- 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;
+ @Override
+ public NavigableSet findMatchingIdsWithGreaterValues(String fieldName1,
+ String fieldName2, boolean orEqual) {
+ indexer.acquireReadLock();
+ try {
+ return findIdsWithKeyComparator(fieldName1, fieldName2, (k1, k2) -> {
+ var compare = KeyComparator.INSTANCE.compare(k1, k2);
+ return orEqual ? compare <= 0 : compare < 0;
+ });
+ } finally {
+ indexer.releaseReadLock();
}
- return isAscending ? KeyComparator.INSTANCE.compare(value1, value2)
- : KeyComparator.INSTANCE.compare(value2, value1);
}
- @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);
+ @Override
+ public NavigableSet findIdsGreaterThan(String fieldName, String fieldValue,
+ boolean orEqual) {
+ var operator = getEntryOperator(fieldName);
+ return operator.greaterThan(fieldValue, orEqual);
+ }
+
+ @Override
+ public NavigableSet findMatchingIdsWithSmallerValues(String fieldName1,
+ String fieldName2, boolean orEqual) {
+ indexer.acquireReadLock();
+ try {
+ return findIdsWithKeyComparator(fieldName1, fieldName2, (k1, k2) -> {
+ var compare = KeyComparator.INSTANCE.compare(k1, k2);
+ return orEqual ? compare >= 0 : compare > 0;
+ });
+ } finally {
+ indexer.releaseReadLock();
+ }
+ }
+
+ @Override
+ public NavigableSet findIdsLessThan(String fieldName, String fieldValue,
+ boolean orEqual) {
+ var operator = getEntryOperator(fieldName);
+ return operator.lessThan(fieldValue, orEqual);
+ }
+
+ @Override
+ public NavigableSet between(String fieldName, String lowerValue, boolean lowerInclusive,
+ String upperValue, boolean upperInclusive) {
+ var operator = getEntryOperator(fieldName);
+ return operator.range(lowerValue, upperValue, lowerInclusive, upperInclusive);
+ }
+
+ @Override
+ public List sortBy(NavigableSet ids, Sort sort) {
+ if (sort.isUnsorted()) {
+ return new ArrayList<>(ids);
+ }
+ indexer.acquireReadLock();
+ try {
+ var combinedComparator = sort.stream()
+ .map(this::comparatorFrom)
+ .reduce(Comparator::thenComparing)
+ .orElseThrow();
+ return ids.stream()
+ .sorted(combinedComparator)
+ .toList();
+ } finally {
+ indexer.releaseReadLock();
+ }
+ }
+
+ Comparator comparatorFrom(Sort.Order order) {
+ var indexEntry = getIndexEntry(order.getProperty());
+ var idPositionMap = indexEntry.getIdPositionMap();
+ var isDesc = order.isDescending();
+ // This sort algorithm works leveraging on that the idPositionMap is a map of id -> position
+ // if the id is not in the map, it means that it is not indexed, and it will be placed at
+ // the end
+ return (a, b) -> {
+ var indexOfA = idPositionMap.get(a);
+ var indexOfB = idPositionMap.get(b);
+
+ var isAIndexed = indexOfA != null;
+ var isBIndexed = indexOfB != null;
+
+ if (!isAIndexed && !isBIndexed) {
+ return 0;
+ }
+ // un-indexed item are always at the end
+ if (!isAIndexed) {
+ return isDesc ? -1 : 1;
+ }
+ if (!isBIndexed) {
+ return isDesc ? 1 : -1;
+ }
+ return isDesc ? Integer.compare(indexOfB, indexOfA)
+ : Integer.compare(indexOfA, indexOfB);
+ };
+ }
+
+ @Override
+ public IndexEntry getIndexEntry(String fieldName) {
+ return indexer.getIndexEntry(fieldName);
+ }
+
+ @Override
+ public void acquireReadLock() {
+ indexer.acquireReadLock();
+ }
+
+ @Override
+ public void releaseReadLock() {
+ indexer.releaseReadLock();
+ }
+
+ private IndexEntryOperator getEntryOperator(String fieldName) {
+ var indexEntry = getIndexEntry(fieldName);
+ return createIndexEntryOperator(indexEntry);
+ }
+
+ private IndexEntryOperator createIndexEntryOperator(IndexEntry entry) {
+ return new IndexEntryOperatorImpl(entry);
+ }
+
+ private Set allIds() {
+ var indexEntry = getIndexEntry(PRIMARY_INDEX_NAME);
+ return createIndexEntryOperator(indexEntry).getValues();
+ }
+
+ /**
+ * Must lock the indexer before calling this method.
+ */
+ private NavigableSet findIdsWithKeyComparator(String fieldName1, String fieldName2,
+ BiPredicate keyComparator) {
+ // get entries from indexer for fieldName1
+ var entriesA = getIndexEntry(fieldName1).entries();
+
+ Map> keyMap = new HashMap<>();
+ for (Map.Entry entry : entriesA) {
+ keyMap.computeIfAbsent(entry.getValue(), v -> new ArrayList<>()).add(entry.getKey());
+ }
+
+ NavigableSet result = new TreeSet<>();
+
+ // get entries from indexer for fieldName2
+ var entriesB = getIndexEntry(fieldName2).entries();
+ for (Map.Entry entry : entriesB) {
+ List matchedKeys = keyMap.get(entry.getValue());
+ if (matchedKeys != null) {
+ for (String key : matchedKeys) {
+ if (keyComparator.test(entry.getKey(), key)) {
+ result.add(entry.getValue());
+ // found one match, no need to continue
+ break;
+ }
}
- return values.first();
- })
- .orElse(null);
- }
-
- 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 result;
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/StringContains.java b/api/src/main/java/run/halo/app/extension/index/query/StringContains.java
index 069ba90bb..f6e7dc5b8 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/StringContains.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/StringContains.java
@@ -1,6 +1,7 @@
package run.halo.app.extension.index.query;
import com.google.common.collect.Sets;
+import java.util.Map;
import java.util.NavigableSet;
import org.apache.commons.lang3.StringUtils;
@@ -12,12 +13,24 @@ public class StringContains extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
var resultSet = Sets.newTreeSet();
- var fieldValues = indexView.getAllValuesForField(fieldName);
- for (String val : fieldValues) {
- if (StringUtils.containsIgnoreCase(val, value)) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
+ var indexEntry = indexView.getIndexEntry(fieldName);
+
+ indexEntry.acquireReadLock();
+ try {
+ for (Map.Entry entry : indexEntry.entries()) {
+ var fieldValue = entry.getKey();
+ if (StringUtils.containsIgnoreCase(fieldValue, value)) {
+ resultSet.add(entry.getValue());
+ }
}
+ return resultSet;
+ } finally {
+ indexEntry.releaseReadLock();
}
- return resultSet;
+ }
+
+ @Override
+ public String toString() {
+ return "contains(" + fieldName + ", '" + value + "')";
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/StringEndsWith.java b/api/src/main/java/run/halo/app/extension/index/query/StringEndsWith.java
index b6e2bed00..51853be7a 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/StringEndsWith.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/StringEndsWith.java
@@ -1,7 +1,9 @@
package run.halo.app.extension.index.query;
import com.google.common.collect.Sets;
+import java.util.Map;
import java.util.NavigableSet;
+import org.apache.commons.lang3.StringUtils;
public class StringEndsWith extends SimpleQuery {
public StringEndsWith(String fieldName, String value) {
@@ -11,12 +13,24 @@ public class StringEndsWith extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
var resultSet = Sets.newTreeSet();
- var fieldValues = indexView.getAllValuesForField(fieldName);
- for (String val : fieldValues) {
- if (val.endsWith(value)) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
+ var indexEntry = indexView.getIndexEntry(fieldName);
+
+ indexEntry.acquireReadLock();
+ try {
+ for (Map.Entry entry : indexEntry.entries()) {
+ var fieldValue = entry.getKey();
+ if (StringUtils.endsWith(fieldValue, value)) {
+ resultSet.add(entry.getValue());
+ }
}
+ return resultSet;
+ } finally {
+ indexEntry.releaseReadLock();
}
- return resultSet;
+ }
+
+ @Override
+ public String toString() {
+ return "endsWith(" + fieldName + ", '" + value + "')";
}
}
diff --git a/api/src/main/java/run/halo/app/extension/index/query/StringStartsWith.java b/api/src/main/java/run/halo/app/extension/index/query/StringStartsWith.java
index 5d0fd5a46..bad07d806 100644
--- a/api/src/main/java/run/halo/app/extension/index/query/StringStartsWith.java
+++ b/api/src/main/java/run/halo/app/extension/index/query/StringStartsWith.java
@@ -1,7 +1,9 @@
package run.halo.app.extension.index.query;
import com.google.common.collect.Sets;
+import java.util.Map;
import java.util.NavigableSet;
+import org.apache.commons.lang3.StringUtils;
public class StringStartsWith extends SimpleQuery {
public StringStartsWith(String fieldName, String value) {
@@ -11,13 +13,24 @@ public class StringStartsWith extends SimpleQuery {
@Override
public NavigableSet matches(QueryIndexView indexView) {
var resultSet = Sets.newTreeSet();
- var allValues = indexView.getAllValuesForField(fieldName);
+ var indexEntry = indexView.getIndexEntry(fieldName);
- for (String val : allValues) {
- if (val.startsWith(value)) {
- resultSet.addAll(indexView.getIdsForFieldValue(fieldName, val));
+ indexEntry.acquireReadLock();
+ try {
+ for (Map.Entry entry : indexEntry.entries()) {
+ var fieldValue = entry.getKey();
+ if (StringUtils.startsWith(fieldValue, value)) {
+ resultSet.add(entry.getValue());
+ }
}
+ return resultSet;
+ } finally {
+ indexEntry.releaseReadLock();
}
- return resultSet;
+ }
+
+ @Override
+ public String toString() {
+ return "startsWith(" + fieldName + ", '" + value + "')";
}
}
diff --git a/api/src/test/java/run/halo/app/extension/controller/RequestSynchronizerTest.java b/api/src/test/java/run/halo/app/extension/controller/RequestSynchronizerTest.java
index dc6234eec..b8368c65f 100644
--- a/api/src/test/java/run/halo/app/extension/controller/RequestSynchronizerTest.java
+++ b/api/src/test/java/run/halo/app/extension/controller/RequestSynchronizerTest.java
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Sort;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionMatcher;
import run.halo.app.extension.FakeExtension;
@@ -53,7 +54,7 @@ class RequestSynchronizerTest {
@Test
void shouldStartCorrectlyWhenSyncingAllOnStart() {
var type = GroupVersionKind.fromExtension(FakeExtension.class);
- when(indexedQueryEngine.retrieveAll(eq(type), isA(ListOptions.class)))
+ when(indexedQueryEngine.retrieveAll(eq(type), isA(ListOptions.class), any(Sort.class)))
.thenReturn(List.of("fake-01", "fake-02"));
synchronizer.start();
@@ -62,7 +63,7 @@ class RequestSynchronizerTest {
assertFalse(synchronizer.isDisposed());
verify(indexedQueryEngine, times(1)).retrieveAll(eq(type),
- isA(ListOptions.class));
+ isA(ListOptions.class), isA(Sort.class));
verify(watcher, times(2)).onAdd(isA(Reconciler.Request.class));
verify(client, times(1)).watch(same(watcher));
}
diff --git a/api/src/test/java/run/halo/app/extension/index/query/InQueryTest.java b/api/src/test/java/run/halo/app/extension/index/query/InQueryTest.java
new file mode 100644
index 000000000..c7e8d961b
--- /dev/null
+++ b/api/src/test/java/run/halo/app/extension/index/query/InQueryTest.java
@@ -0,0 +1,24 @@
+package run.halo.app.extension.index.query;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.LinkedHashSet;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link InQuery}.
+ *
+ * @author guqing
+ * @since 2.17.0
+ */
+class InQueryTest {
+
+ @Test
+ void testToString() {
+ var values = new LinkedHashSet();
+ values.add("Alice");
+ values.add("Bob");
+ var inQuery = new InQuery("name", values);
+ assertThat(inQuery.toString()).isEqualTo("name IN ('Alice', 'Bob')");
+ }
+}
\ No newline at end of file
diff --git a/api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java b/api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java
deleted file mode 100644
index c618e63e0..000000000
--- a/api/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package run.halo.app.extension.index.query;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-public class IndexViewDataSet {
-
- /**
- * Create a {@link QueryIndexView} for employee to test.
- *
- * @return a {@link QueryIndexView} for employee to test
- */
- public static QueryIndexView createEmployeeIndexView() {
- /*
- * id firstName lastName email hireDate salary managerId departmentId
- * 100 Pat Fay p 17 2600 101 50
- * 101 Lee Day l 17 2400 102 40
- * 102 William Jay w 19 2200 102 50
- * 103 Mary Day p 17 2000 103 50
- * 104 John Fay j 17 1800 103 50
- * 105 Gon Fay p 18 1900 101 40
- */
- 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")
- );
- Collection> firstNameEntry = List.of(
- Map.entry("Pat", "100"),
- Map.entry("Lee", "101"),
- Map.entry("William", "102"),
- Map.entry("Mary", "103"),
- Map.entry("John", "104"),
- Map.entry("Gon", "105")
- );
- Collection> lastNameEntry = List.of(
- Map.entry("Fay", "100"),
- Map.entry("Day", "101"),
- Map.entry("Jay", "102"),
- Map.entry("Day", "103"),
- Map.entry("Fay", "104"),
- Map.entry("Fay", "105")
- );
- Collection> emailEntry = List.of(
- Map.entry("p", "100"),
- Map.entry("l", "101"),
- Map.entry("w", "102"),
- Map.entry("p", "103"),
- Map.entry("j", "104"),
- Map.entry("p", "105")
- );
- Collection> hireDateEntry = List.of(
- Map.entry("17", "100"),
- Map.entry("17", "101"),
- Map.entry("19", "102"),
- Map.entry("17", "103"),
- Map.entry("17", "104"),
- Map.entry("18", "105")
- );
- Collection> salaryEntry = List.of(
- Map.entry("2600", "100"),
- Map.entry("2400", "101"),
- Map.entry("2200", "102"),
- Map.entry("2000", "103"),
- Map.entry("1800", "104"),
- Map.entry("1900", "105")
- );
- Collection> managerIdEntry = List.of(
- Map.entry("101", "100"),
- Map.entry("102", "101"),
- Map.entry("102", "102"),
- Map.entry("103", "103"),
- Map.entry("103", "104"),
- Map.entry("101", "105")
- );
- Collection> departmentIdEntry = List.of(
- Map.entry("50", "100"),
- Map.entry("40", "101"),
- Map.entry("50", "102"),
- Map.entry("50", "103"),
- Map.entry("50", "104"),
- Map.entry("40", "105")
- );
- var entries = Map.of("id", idEntry,
- "firstName", firstNameEntry,
- "lastName", lastNameEntry,
- "email", emailEntry,
- "hireDate", hireDateEntry,
- "salary", salaryEntry,
- "managerId", managerIdEntry,
- "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/IsNotNullTest.java b/api/src/test/java/run/halo/app/extension/index/query/IsNotNullTest.java
new file mode 100644
index 000000000..be6b708f9
--- /dev/null
+++ b/api/src/test/java/run/halo/app/extension/index/query/IsNotNullTest.java
@@ -0,0 +1,20 @@
+package run.halo.app.extension.index.query;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link IsNotNull}.
+ *
+ * @author guqing
+ * @since 2.17.0
+ */
+class IsNotNullTest {
+
+ @Test
+ void testToString() {
+ var isNotNull = new IsNotNull("name");
+ assertThat(isNotNull.toString()).isEqualTo("name IS NOT NULL");
+ }
+}
\ 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
deleted file mode 100644
index 86f61db7c..000000000
--- a/api/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java
+++ /dev/null
@@ -1,218 +0,0 @@
-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}.
- *
- * @author guqing
- * @since 2.12.0
- */
-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 = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = indexView.findIdsForFieldValueEqual("managerId", "id");
- assertThat(resultSet).containsExactlyInAnyOrder(
- "102", "103"
- );
- }
-
- @Test
- void findIdsForFieldValueGreaterThanTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", false);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "104", "105"
- );
-
- indexView = IndexViewDataSet.createEmployeeIndexView();
- resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", true);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "103", "102", "104", "105"
- );
- }
-
- @Test
- void findIdsForFieldValueGreaterThanTest2() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", false);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "100", "101"
- );
-
- indexView = IndexViewDataSet.createEmployeeIndexView();
- resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", true);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "100", "101", "102", "103"
- );
- }
-
- @Test
- void findIdsForFieldValueLessThanTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", false);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "100", "101"
- );
-
- indexView = IndexViewDataSet.createEmployeeIndexView();
- resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", true);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "100", "101", "102", "103"
- );
- }
-
- @Test
- void findIdsForFieldValueLessThanTest2() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = indexView.findIdsForFieldValueLessThan("managerId", "id", false);
- assertThat(resultSet).containsExactlyInAnyOrder(
- "104", "105"
- );
-
- 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/api/src/test/java/run/halo/app/extension/index/query/StringContainsTest.java b/api/src/test/java/run/halo/app/extension/index/query/StringContainsTest.java
new file mode 100644
index 000000000..f55cdd680
--- /dev/null
+++ b/api/src/test/java/run/halo/app/extension/index/query/StringContainsTest.java
@@ -0,0 +1,20 @@
+package run.halo.app.extension.index.query;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link StringContains}.
+ *
+ * @author guqing
+ * @since 2.17.0
+ */
+class StringContainsTest {
+
+ @Test
+ void testToString() {
+ var stringContains = new StringContains("name", "Alice");
+ assertThat(stringContains.toString()).isEqualTo("contains(name, 'Alice')");
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java
index a634fc42b..ad6077492 100644
--- a/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java
+++ b/application/src/main/java/run/halo/app/extension/ReactiveExtensionClientImpl.java
@@ -112,8 +112,30 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
@Override
public Flux listAll(Class type, ListOptions options, Sort sort) {
- return listBy(type, options, PageRequestImpl.ofSize(0).withSort(sort))
- .flatMapIterable(ListResult::getItems);
+ var scheme = schemeManager.get(type);
+ return Mono.fromSupplier(
+ () -> indexedQueryEngine.retrieveAll(scheme.groupVersionKind(), options, sort))
+ .doOnSuccess(objectKeys -> {
+ if (log.isDebugEnabled()) {
+ if (objectKeys.size() > 500) {
+ log.warn("The number of objects retrieved by listAll is too large ({}) "
+ + "and it is recommended to use paging query.",
+ objectKeys.size());
+ }
+ }
+ })
+ .flatMapMany(objectKeys -> {
+ var storeNames = objectKeys.stream()
+ .map(objectKey -> ExtensionStoreUtil.buildStoreName(scheme, objectKey))
+ .toList();
+ final long startTimeMs = System.currentTimeMillis();
+ return client.listByNames(storeNames)
+ .map(extensionStore -> converter.convertFrom(type, extensionStore))
+ .doOnNext(s -> {
+ log.debug("Successfully retrieved all by names from db for {} in {}ms",
+ scheme.groupVersionKind(), System.currentTimeMillis() - startTimeMs);
+ });
+ });
}
@Override
@@ -130,7 +152,7 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
final long startTimeMs = System.currentTimeMillis();
return client.listByNames(storeNames)
.map(extensionStore -> converter.convertFrom(type, extensionStore))
- .doFinally(s -> {
+ .doOnNext(s -> {
log.debug("Successfully retrieved by names from db for {} in {}ms",
scheme.groupVersionKind(), System.currentTimeMillis() - startTimeMs);
})
diff --git a/application/src/main/java/run/halo/app/extension/index/DefaultIndexer.java b/application/src/main/java/run/halo/app/extension/index/DefaultIndexer.java
index bc4030401..10c570978 100644
--- a/application/src/main/java/run/halo/app/extension/index/DefaultIndexer.java
+++ b/application/src/main/java/run/halo/app/extension/index/DefaultIndexer.java
@@ -11,6 +11,7 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
+import org.springframework.lang.NonNull;
import run.halo.app.extension.Extension;
/**
@@ -172,6 +173,28 @@ public class DefaultIndexer implements Indexer {
}
}
+ @Override
+ @NonNull
+ public IndexEntry getIndexEntry(String name) {
+ readLock.lock();
+ try {
+ var indexDescriptor = findIndexByName(name);
+ if (indexDescriptor == null) {
+ throw new IllegalArgumentException(
+ "No index found for fieldPath [" + name + "], "
+ + "make sure you have created an index for this field.");
+ }
+ if (!indexDescriptor.isReady()) {
+ throw new IllegalStateException(
+ "Index [" + name + "] is not ready, "
+ + "Please wait for more time or check the index status.");
+ }
+ return indexEntries.get(indexDescriptor);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
@Override
public Iterator readyIndexesIterator() {
readLock.lock();
@@ -197,4 +220,14 @@ public class DefaultIndexer implements Indexer {
readLock.unlock();
}
}
+
+ @Override
+ public void acquireReadLock() {
+ readLock.lock();
+ }
+
+ @Override
+ public void releaseReadLock() {
+ readLock.unlock();
+ }
}
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 cc204c5bd..b677d6e2a 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
@@ -4,9 +4,11 @@ import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import java.util.Collection;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
+import java.util.NavigableSet;
+import java.util.TreeSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -94,10 +96,13 @@ public class IndexEntryImpl implements IndexEntry {
}
@Override
- public Set indexedKeys() {
+ public NavigableSet indexedKeys() {
readLock.lock();
try {
- return indexKeyObjectNamesMap.keySet();
+ var keys = indexKeyObjectNamesMap.keySet();
+ var resultSet = new TreeSet<>(keyComparator());
+ resultSet.addAll(keys);
+ return resultSet;
} finally {
readLock.unlock();
}
@@ -114,20 +119,32 @@ public class IndexEntryImpl implements IndexEntry {
}
@Override
- public Collection> immutableEntries() {
+ public Map getIdPositionMap() {
readLock.lock();
try {
- // Copy to a new list to avoid ConcurrentModificationException
- return indexKeyObjectNamesMap.entries().stream()
- .map(entry -> Map.entry(entry.getKey(), entry.getValue()))
- .toList();
+ // asMap is sorted by key
+ var keyObjectMap = getKeyObjectMap();
+ int i = 0;
+ var idPositionMap = new HashMap();
+ for (var valueIdsEntry : keyObjectMap.entrySet()) {
+ var ids = valueIdsEntry.getValue();
+ for (String id : ids) {
+ idPositionMap.put(id, i);
+ }
+ i++;
+ }
+ return idPositionMap;
} finally {
readLock.unlock();
}
}
+ protected Map> getKeyObjectMap() {
+ return indexKeyObjectNamesMap.asMap();
+ }
+
@Override
- public List getByIndexKey(String indexKey) {
+ public List getObjectNamesBy(String indexKey) {
readLock.lock();
try {
return indexKeyObjectNamesMap.get(indexKey);
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 1e30d05d8..809cea401 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
@@ -1,15 +1,14 @@
package run.halo.app.extension.index;
-import java.util.ArrayList;
-import java.util.Collection;
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.NavigableSet;
import java.util.Set;
import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@@ -23,6 +22,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.QueryFactory;
+import run.halo.app.extension.index.query.QueryIndexView;
import run.halo.app.extension.index.query.QueryIndexViewImpl;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.extension.router.selector.LabelSelector;
@@ -41,155 +41,108 @@ public class IndexedQueryEngineImpl implements IndexedQueryEngine {
private final IndexerFactory indexerFactory;
- private static Map fieldPathIndexEntryMap(Indexer indexer) {
- // O(n) time complexity
- Map indexEntryMap = new HashMap<>();
- var iterator = indexer.readyIndexesIterator();
- while (iterator.hasNext()) {
- var indexEntry = iterator.next();
- var descriptor = indexEntry.getIndexDescriptor();
- var indexedFieldPath = descriptor.getSpec().getName();
- indexEntryMap.put(indexedFieldPath, indexEntry);
- }
- return indexEntryMap;
- }
-
- static IndexEntry getIndexEntry(String fieldPath, Map fieldPathEntryMap) {
- if (!fieldPathEntryMap.containsKey(fieldPath)) {
- throwNotIndexedException(fieldPath);
- }
- return fieldPathEntryMap.get(fieldPath);
- }
-
@Override
public ListResult retrieve(GroupVersionKind type, ListOptions options,
PageRequest page) {
- var indexer = indexerFactory.getIndexer(type);
- var allMatchedResult = doRetrieve(indexer, options, page.getSort());
+ var allMatchedResult = doRetrieve(type, options, page.getSort());
var list = ListResult.subList(allMatchedResult, page.getPageNumber(), page.getPageSize());
return new ListResult<>(page.getPageNumber(), page.getPageSize(),
allMatchedResult.size(), list);
}
@Override
- public List retrieveAll(GroupVersionKind type, ListOptions options) {
- var indexer = indexerFactory.getIndexer(type);
- return doRetrieve(indexer, options, Sort.unsorted());
+ public List retrieveAll(GroupVersionKind type, ListOptions options, Sort sort) {
+ return doRetrieve(type, options, sort);
}
- static List intersection(List list1, List list2) {
- Set set = new LinkedHashSet<>(list1);
- List intersection = new ArrayList<>();
- for (T item : list2) {
- if (set.contains(item) && !intersection.contains(item)) {
- intersection.add(item);
- }
- }
- return intersection;
- }
-
- static void throwNotIndexedException(String fieldPath) {
- throw new IllegalArgumentException(
- "No index found for fieldPath: " + fieldPath
- + ", make sure you have created an index for this field.");
- }
-
- List retrieveForLabelMatchers(List labelMatchers,
- Map fieldPathEntryMap, List allMetadataNames) {
- var indexEntry = getIndexEntry(LabelIndexSpecUtils.LABEL_PATH, fieldPathEntryMap);
- // O(m) time complexity, m is the number of labelMatchers
- var labelKeysToQuery = labelMatchers.stream()
- .sorted(Comparator.comparing(SelectorMatcher::getKey))
- .map(SelectorMatcher::getKey)
- .collect(Collectors.toSet());
-
- Map> objectNameLabelsMap = new HashMap<>();
- indexEntry.acquireReadLock();
- try {
- indexEntry.entries().forEach(entry -> {
- // key is labelKey=labelValue, value is objectName
- var labelPair = LabelIndexSpecUtils.labelKeyValuePair(entry.getKey());
- if (!labelKeysToQuery.contains(labelPair.getFirst())) {
- return;
- }
- objectNameLabelsMap.computeIfAbsent(entry.getValue(), k -> new HashMap<>())
- .put(labelPair.getFirst(), labelPair.getSecond());
- });
- } finally {
- indexEntry.releaseReadLock();
- }
- // O(p * m) time complexity, p is the number of allMetadataNames
- return allMetadataNames.stream()
- .filter(objectName -> {
- var labels = objectNameLabelsMap.getOrDefault(objectName, Map.of());
+ NavigableSet retrieveForLabelMatchers(Indexer indexer,
+ List labelMatchers) {
+ var objectLabelMap = ObjectLabelMap.buildFrom(indexer, labelMatchers);
+ // O(k×m) time complexity, k is the number of keys, m is the number of labelMatchers
+ return objectLabelMap.objectIdLabelsMap()
+ .entrySet()
+ .stream()
+ .filter(entry -> {
+ var labels = entry.getValue();
// object match all labels will be returned
return labelMatchers.stream()
.allMatch(matcher -> matcher.test(labels.get(matcher.getKey())));
})
- .toList();
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toCollection(TreeSet::new));
}
- List doRetrieve(Indexer indexer, ListOptions options, Sort sort) {
- StopWatch stopWatch = new StopWatch();
- stopWatch.start("build index entry map");
- var fieldPathEntryMap = fieldPathIndexEntryMap(indexer);
- var primaryEntry = getIndexEntry(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME, fieldPathEntryMap);
- stopWatch.stop();
-
- // O(n) time complexity
- stopWatch.start("retrieve all metadata names");
- var allMetadataNames = new ArrayList();
- primaryEntry.acquireReadLock();
- try {
- allMetadataNames.addAll(primaryEntry.indexedKeys());
- } finally {
- primaryEntry.releaseReadLock();
- }
- stopWatch.stop();
-
- stopWatch.start("build index view");
- var fieldNamesUsedInQuery = getFieldNamesUsedInListOptions(options, sort);
- var indexViewMap = new HashMap>>();
- for (Map.Entry entry : fieldPathEntryMap.entrySet()) {
- if (!fieldNamesUsedInQuery.contains(entry.getKey())) {
- continue;
- }
- 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");
- if (hasLabelSelector(options.getLabelSelector())) {
- var matchedByLabels = retrieveForLabelMatchers(options.getLabelSelector().getMatchers(),
- fieldPathEntryMap, allMetadataNames);
- if (allMetadataNames.size() != matchedByLabels.size()) {
- indexView.removeByIdNotIn(new TreeSet<>(matchedByLabels));
- }
- }
- stopWatch.stop();
-
- stopWatch.start("retrieve matched metadata names by fields");
+ NavigableSet evaluateSelectorsForIndex(Indexer indexer, QueryIndexView indexView,
+ ListOptions options) {
+ final var hasLabelSelector = hasLabelSelector(options.getLabelSelector());
final var hasFieldSelector = hasFieldSelector(options.getFieldSelector());
- if (hasFieldSelector) {
- var fieldSelector = options.getFieldSelector();
- var query = fieldSelector.query();
- var resultSet = query.matches(indexView);
- indexView.removeByIdNotIn(resultSet);
+
+ if (!hasLabelSelector && !hasFieldSelector) {
+ return QueryFactory.all().matches(indexView);
}
+
+ // only label selector
+ if (hasLabelSelector && !hasFieldSelector) {
+ return retrieveForLabelMatchers(indexer, options.getLabelSelector().getMatchers());
+ }
+
+ // only field selector
+ if (!hasLabelSelector) {
+ var fieldSelector = options.getFieldSelector();
+ return fieldSelector.query().matches(indexView);
+ }
+
+ // both label and field selector
+ var fieldSelector = options.getFieldSelector();
+ var forField = fieldSelector.query().matches(indexView);
+ var forLabel =
+ retrieveForLabelMatchers(indexer, options.getLabelSelector().getMatchers());
+
+ // determine the optimal retainAll direction based on the size of the collection
+ var resultSet = (forField.size() <= forLabel.size()) ? forField : forLabel;
+ resultSet.retainAll((resultSet == forField) ? forLabel : forField);
+ return resultSet;
+ }
+
+ List doRetrieve(GroupVersionKind type, ListOptions options, Sort sort) {
+ var indexer = indexerFactory.getIndexer(type);
+
+ StopWatch stopWatch = new StopWatch(type.toString());
+
+ stopWatch.start("Check index status to ensure all indexes are ready");
+ var fieldNamesUsedInQuery = getFieldNamesUsedInListOptions(options, sort);
+ checkIndexForNames(indexer, fieldNamesUsedInQuery);
stopWatch.stop();
- stopWatch.start("sort result");
- var result = indexView.sortBy(sort);
+ var indexView = new QueryIndexViewImpl(indexer);
+
+ stopWatch.start("Evaluate selectors for index");
+ var resultSet = evaluateSelectorsForIndex(indexer, indexView, options);
+ stopWatch.stop();
+
+ stopWatch.start("Sort result set by sort order");
+ var result = indexView.sortBy(resultSet, sort);
stopWatch.stop();
if (log.isTraceEnabled()) {
- log.trace("Retrieve result from indexer, {}", stopWatch.prettyPrint());
+ log.trace("Retrieve result from indexer by query [{}],\n {}", options,
+ stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
}
return result;
}
+ void checkIndexForNames(Indexer indexer, Set indexNames) {
+ indexer.acquireReadLock();
+ try {
+ for (String indexName : indexNames) {
+ // get index entry will throw exception if index not found
+ indexer.getIndexEntry(indexName);
+ }
+ } finally {
+ indexer.releaseReadLock();
+ }
+ }
+
@NonNull
private Set getFieldNamesUsedInListOptions(ListOptions options, Sort sort) {
var fieldNamesUsedInQuery = new HashSet();
@@ -213,4 +166,46 @@ public class IndexedQueryEngineImpl implements IndexedQueryEngine {
boolean hasFieldSelector(FieldSelector fieldSelector) {
return fieldSelector != null;
}
+
+ record ObjectLabelMap(Map> objectIdLabelsMap) {
+
+ public static ObjectLabelMap buildFrom(Indexer indexer,
+ List labelMatchers) {
+ indexer.acquireReadLock();
+ try {
+ final var objectNameLabelsMap = new HashMap>();
+ final var labelIndexEntry = indexer.getIndexEntry(LabelIndexSpecUtils.LABEL_PATH);
+ // O(m) time complexity, m is the number of labelMatchers
+ final var labelKeysToQuery = labelMatchers.stream()
+ .sorted(Comparator.comparing(SelectorMatcher::getKey))
+ .map(SelectorMatcher::getKey)
+ .collect(Collectors.toSet());
+
+ labelIndexEntry.entries().forEach(entry -> {
+ // key is labelKey=labelValue, value is objectName
+ var labelPair = LabelIndexSpecUtils.labelKeyValuePair(entry.getKey());
+ if (!labelKeysToQuery.contains(labelPair.getFirst())) {
+ return;
+ }
+ objectNameLabelsMap.computeIfAbsent(entry.getValue(), k -> new HashMap<>())
+ .put(labelPair.getFirst(), labelPair.getSecond());
+ });
+
+ var nameIndexOperator = new IndexEntryOperatorImpl(
+ indexer.getIndexEntry(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME)
+ );
+ var allIndexedObjectNames = nameIndexOperator.getValues();
+
+ // remove all object names that exist labels,O(n) time complexity
+ allIndexedObjectNames.removeAll(objectNameLabelsMap.keySet());
+ // add absent object names to object labels map
+ for (String name : allIndexedObjectNames) {
+ objectNameLabelsMap.put(name, new HashMap<>());
+ }
+ return new ObjectLabelMap(objectNameLabelsMap);
+ } finally {
+ indexer.releaseReadLock();
+ }
+ }
+ }
}
diff --git a/application/src/test/java/run/halo/app/content/PostQueryTest.java b/application/src/test/java/run/halo/app/content/PostQueryTest.java
deleted file mode 100644
index d8b50bfc4..000000000
--- a/application/src/test/java/run/halo/app/content/PostQueryTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package run.halo.app.content;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.mock.web.reactive.function.server.MockServerRequest;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.server.ServerWebExchange;
-import run.halo.app.extension.index.query.QueryIndexViewImpl;
-
-/**
- * Tests for {@link PostQuery}.
- *
- * @author guqing
- * @since 2.6.0
- */
-@ExtendWith(MockitoExtension.class)
-class PostQueryTest {
-
- @Test
- void userScopedQueryTest() {
- MultiValueMap multiValueMap = new LinkedMultiValueMap<>();
- MockServerRequest request = MockServerRequest.builder()
- .queryParams(multiValueMap)
- .exchange(mock(ServerWebExchange.class))
- .build();
-
- PostQuery postQuery = new PostQuery(request, "faker");
-
- var listOptions = postQuery.toListOptions();
- assertThat(listOptions).isNotNull();
- assertThat(listOptions.getFieldSelector()).isNotNull();
- var nameEntry =
- (Collection>) List.of(Map.entry("metadata.name", "faker"));
- var entry = (Collection>) List.of(Map.entry("faker", "faker"));
- var indexView =
- new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry));
- assertThat(listOptions.getFieldSelector().query().matches(indexView))
- .containsExactly("faker");
-
- entry = List.of(Map.entry("another-faker", "user1"));
- indexView = new QueryIndexViewImpl(Map.of("spec.owner", entry, "metadata.name", nameEntry));
- assertThat(listOptions.getFieldSelector().query().matches(indexView)).isEmpty();
- }
-}
diff --git a/application/src/test/java/run/halo/app/extension/index/DefaultIndexerTest.java b/application/src/test/java/run/halo/app/extension/index/DefaultIndexerTest.java
index ca6ebf133..a1d6010b1 100644
--- a/application/src/test/java/run/halo/app/extension/index/DefaultIndexerTest.java
+++ b/application/src/test/java/run/halo/app/extension/index/DefaultIndexerTest.java
@@ -24,6 +24,28 @@ import run.halo.app.infra.exception.DuplicateNameException;
@ExtendWith(MockitoExtension.class)
class DefaultIndexerTest {
+ private static FakeExtension createFakeExtension() {
+ var fake = new FakeExtension();
+ fake.setMetadata(new Metadata());
+ fake.getMetadata().setName("fake-extension");
+ fake.setEmail("fake-email");
+ return fake;
+ }
+
+ private static IndexSpec getNameIndexSpec() {
+ return getIndexSpec("metadata.name", true,
+ IndexAttributeFactory.simpleAttribute(FakeExtension.class,
+ e -> e.getMetadata().getName()));
+ }
+
+ private static IndexSpec getIndexSpec(String name, boolean unique, IndexAttribute attribute) {
+ return new IndexSpec()
+ .setName(name)
+ .setOrder(IndexSpec.OrderType.ASC)
+ .setUnique(unique)
+ .setIndexFunc(attribute);
+ }
+
@Test
void constructor() {
var spec = getNameIndexSpec();
@@ -48,20 +70,29 @@ class DefaultIndexerTest {
.hasMessage("Index entry not found for: metadata.name");
}
+ @Test
+ void getIndexEntryTest() {
+ var spec = getNameIndexSpec();
+ var descriptor = new IndexDescriptor(spec);
+ descriptor.setReady(true);
+ var indexContainer = new IndexEntryContainer();
+ indexContainer.add(new IndexEntryImpl(descriptor));
+
+ var defaultIndexer = new DefaultIndexer(List.of(descriptor), indexContainer);
+ assertThatThrownBy(() -> defaultIndexer.getIndexEntry("not-exist"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("No index found for fieldPath [not-exist], "
+ + "make sure you have created an index for this field.");
+
+ assertThat(defaultIndexer.getIndexEntry("metadata.name")).isNotNull();
+ }
+
@Test
void getObjectKey() {
var fake = createFakeExtension();
assertThat(DefaultIndexer.getObjectKey(fake)).isEqualTo("fake-extension");
}
- private static FakeExtension createFakeExtension() {
- var fake = new FakeExtension();
- fake.setMetadata(new Metadata());
- fake.getMetadata().setName("fake-extension");
- fake.setEmail("fake-email");
- return fake;
- }
-
@Test
void indexRecord() {
var nameIndex = getNameIndexSpec();
@@ -81,12 +112,6 @@ class DefaultIndexerTest {
assertThat(entries).contains(Map.entry("fake-extension", "fake-extension"));
}
- private static IndexSpec getNameIndexSpec() {
- return getIndexSpec("metadata.name", true,
- IndexAttributeFactory.simpleAttribute(FakeExtension.class,
- e -> e.getMetadata().getName()));
- }
-
@Test
void indexRecordWithExceptionShouldRollback() {
var indexContainer = new IndexEntryContainer();
@@ -242,14 +267,6 @@ class DefaultIndexerTest {
assertThat(iterator.hasNext()).isFalse();
}
- private static IndexSpec getIndexSpec(String name, boolean unique, IndexAttribute attribute) {
- return new IndexSpec()
- .setName(name)
- .setOrder(IndexSpec.OrderType.ASC)
- .setUnique(unique)
- .setIndexFunc(attribute);
- }
-
@Data
@EqualsAndHashCode(callSuper = true)
@GVK(group = "test", version = "v1", kind = "FakeExtension", plural = "fakeextensions",
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 479b9cf31..fbadfedff 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,12 +1,19 @@
package run.halo.app.extension.index;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static run.halo.app.extension.index.query.IndexViewDataSet.createCommentIndexView;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
+import run.halo.app.extension.index.query.IndexViewDataSet;
+import run.halo.app.extension.index.query.QueryIndexView;
/**
* Tests for {@link IndexEntryImpl}.
@@ -58,7 +65,7 @@ class IndexEntryImplTest {
}
@Test
- void getByIndexKey() {
+ void getObjectIdsTest() {
var spec =
PrimaryKeySpecUtils.primaryKeyIndexSpec(IndexEntryContainerTest.FakeExtension.class);
var descriptor = new IndexDescriptor(spec);
@@ -67,7 +74,7 @@ class IndexEntryImplTest {
assertThat(entry.indexedKeys()).containsExactly("slug-1", "slug-2");
assertThat(entry.entries()).hasSize(2);
- assertThat(entry.getByIndexKey("slug-1")).isEqualTo(List.of("fake-name-1"));
+ assertThat(entry.getObjectNamesBy("slug-1")).isEqualTo(List.of("fake-name-1"));
}
@Test
@@ -91,7 +98,6 @@ class IndexEntryImplTest {
assertThat(entry.indexedKeys()).containsSequence("slug-4", "slug-3", "slug-2", "slug-1");
-
spec.setOrder(IndexSpec.OrderType.ASC);
var descriptor2 = new IndexDescriptor(spec);
var entry2 = new IndexEntryImpl(descriptor2);
@@ -105,4 +111,40 @@ class IndexEntryImplTest {
Map.entry("slug-4", "fake-name-3"));
assertThat(entry2.indexedKeys()).containsSequence("slug-1", "slug-2", "slug-3", "slug-4");
}
+
+ @Test
+ void getIdPositionMapTest() {
+ var indexView = createCommentIndexView();
+ var topIndexEntry = prepareForPositionMapTest(indexView, "spec.top");
+ var topIndexEntryFromView = indexView.getIndexEntry("spec.top");
+ assertThat(topIndexEntry.getIdPositionMap())
+ .isEqualTo(topIndexEntryFromView.getIdPositionMap());
+
+ var creationTimeIndexEntry = prepareForPositionMapTest(indexView, "spec.creationTime");
+ var creationTimeIndexEntryFromView = indexView.getIndexEntry("spec.creationTime");
+ assertThat(creationTimeIndexEntry.getIdPositionMap())
+ .isEqualTo(creationTimeIndexEntryFromView.getIdPositionMap());
+
+ var priorityIndexEntry = prepareForPositionMapTest(indexView, "spec.priority");
+ var priorityIndexEntryFromView = indexView.getIndexEntry("spec.priority");
+ assertThat(priorityIndexEntry.getIdPositionMap())
+ .isEqualTo(priorityIndexEntryFromView.getIdPositionMap());
+ }
+
+ IndexEntry prepareForPositionMapTest(QueryIndexView indexView, String property) {
+ var indexSpec = mock(IndexSpec.class);
+ var descriptor = mock(IndexDescriptor.class);
+ when(descriptor.getSpec()).thenReturn(indexSpec);
+ var indexEntry = new IndexEntryImpl(descriptor);
+
+ var indexEntryFromView = indexView.getIndexEntry(property);
+ var sortedEntries = IndexViewDataSet.sortEntries(indexEntryFromView.entries());
+
+ var spyIndexEntry = spy(indexEntry);
+
+ doReturn(IndexViewDataSet.toKeyObjectMap(sortedEntries))
+ .when(spyIndexEntry).getKeyObjectMap();
+
+ return spyIndexEntry;
+ }
}
diff --git a/application/src/test/java/run/halo/app/extension/index/IndexEntryOperatorImplTest.java b/application/src/test/java/run/halo/app/extension/index/IndexEntryOperatorImplTest.java
new file mode 100644
index 000000000..15d0f15d4
--- /dev/null
+++ b/application/src/test/java/run/halo/app/extension/index/IndexEntryOperatorImplTest.java
@@ -0,0 +1,177 @@
+package run.halo.app.extension.index;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import run.halo.app.extension.index.query.IndexViewDataSet;
+
+/**
+ * Tests for {@link IndexEntryOperatorImpl}.
+ *
+ * @author guqing
+ * @since 2.17.0
+ */
+@ExtendWith(MockitoExtension.class)
+class IndexEntryOperatorImplTest {
+
+ @Mock
+ private IndexEntry indexEntry;
+
+ @InjectMocks
+ private IndexEntryOperatorImpl indexEntryOperator;
+
+ private LinkedHashMap> createIndexedMapAndPile() {
+ var entries = new ArrayList>();
+ entries.add(Map.entry("apple", "A"));
+ entries.add(Map.entry("banana", "B"));
+ entries.add(Map.entry("cherry", "C"));
+ entries.add(Map.entry("date", "D"));
+ entries.add(Map.entry("egg", "E"));
+ entries.add(Map.entry("f", "F"));
+
+ var indexedMap = IndexViewDataSet.toKeyObjectMap(entries);
+ lenient().when(indexEntry.indexedKeys()).thenReturn(new TreeSet<>(indexedMap.keySet()));
+ lenient().when(indexEntry.getObjectNamesBy(anyString())).thenAnswer(invocation -> {
+ var key = (String) invocation.getArgument(0);
+ return indexedMap.get(key);
+ });
+ lenient().when(indexEntry.entries()).thenReturn(entries);
+ return indexedMap;
+ }
+
+ @Test
+ void lessThan() {
+ final var indexedMap = createIndexedMapAndPile();
+
+ var result = indexEntryOperator.lessThan("banana", false);
+ assertThat(result).containsExactly("A");
+
+ result = indexEntryOperator.lessThan("banana", true);
+ assertThat(result).containsExactly("A", "B");
+
+ result = indexEntryOperator.lessThan("cherry", true);
+ assertThat(result).containsExactly("A", "B", "C");
+
+ // does not exist key
+ result = indexEntryOperator.lessThan("z", false);
+ var objectIds = indexedMap.values().stream()
+ .flatMap(Collection::stream)
+ .toArray(String[]::new);
+ assertThat(result).contains(objectIds);
+
+ result = indexEntryOperator.lessThan("a", false);
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void greaterThan() {
+ createIndexedMapAndPile();
+
+ var result = indexEntryOperator.greaterThan("banana", false);
+ assertThat(result).containsExactly("C", "D", "E", "F");
+
+ result = indexEntryOperator.greaterThan("banana", true);
+ assertThat(result).containsExactly("B", "C", "D", "E", "F");
+
+ result = indexEntryOperator.greaterThan("cherry", true);
+ assertThat(result).containsExactly("C", "D", "E", "F");
+
+ result = indexEntryOperator.greaterThan("cherry", false);
+ assertThat(result).containsExactly("D", "E", "F");
+
+ // does not exist key
+ result = indexEntryOperator.greaterThan("z", false);
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void greaterThanForNumberString() {
+ var entries = List.of(
+ Map.entry("100", "1"),
+ Map.entry("101", "2"),
+ Map.entry("102", "3"),
+ Map.entry("103", "4"),
+ Map.entry("110", "5"),
+ Map.entry("111", "6"),
+ Map.entry("112", "7"),
+ Map.entry("120", "8")
+ );
+ var indexedMap = IndexViewDataSet.toKeyObjectMap(entries);
+ when(indexEntry.indexedKeys()).thenReturn(new TreeSet<>(indexedMap.keySet()));
+ lenient().when(indexEntry.getObjectNamesBy(anyString())).thenAnswer(invocation -> {
+ var key = (String) invocation.getArgument(0);
+ return indexedMap.get(key);
+ });
+ when(indexEntry.entries()).thenReturn(entries);
+
+ var result = indexEntryOperator.greaterThan("102", false);
+ assertThat(result).containsExactly("4", "5", "6", "7", "8");
+
+ result = indexEntryOperator.greaterThan("110", false);
+ assertThat(result).containsExactly("6", "7", "8");
+ }
+
+ @Test
+ void range() {
+ createIndexedMapAndPile();
+
+ var result = indexEntryOperator.range("banana", "date", true, false);
+ assertThat(result).containsExactly("B", "C");
+
+ result = indexEntryOperator.range("banana", "date", false, false);
+ assertThat(result).containsExactly("C");
+
+ result = indexEntryOperator.range("banana", "date", true, true);
+ assertThat(result).containsExactly("B", "C", "D");
+
+ result = indexEntryOperator.range("apple", "egg", false, true);
+ assertThat(result).containsExactly("B", "C", "D", "E");
+
+ // end not exist
+ result = indexEntryOperator.range("d", "z", false, false);
+ assertThat(result).containsExactly("D", "E", "F");
+
+ // start key > end key
+ assertThatThrownBy(() -> indexEntryOperator.range("z", "f", false, false))
+ .isInstanceOf(IllegalArgumentException.class);
+
+ // both not exist
+ result = indexEntryOperator.range("z", "zz", false, false);
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void findTest() {
+ createIndexedMapAndPile();
+
+ var result = indexEntryOperator.find("banana");
+ assertThat(result).containsExactly("B");
+
+ result = indexEntryOperator.find("date");
+ assertThat(result).containsExactly("D");
+
+ result = indexEntryOperator.find("z");
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void findInTest() {
+ createIndexedMapAndPile();
+ var result = indexEntryOperator.findIn(List.of("banana", "date"));
+ assertThat(result).containsExactly("B", "D");
+ }
+}
\ No newline at end of file
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 753acbf29..ce7d19dba 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
@@ -1,7 +1,6 @@
package run.halo.app.extension.index;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@@ -10,13 +9,12 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static run.halo.app.extension.index.query.IndexViewDataSet.pileForIndexer;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.junit.jupiter.api.Nested;
@@ -51,16 +49,6 @@ class IndexedQueryEngineImplTest {
@InjectMocks
private IndexedQueryEngineImpl indexedQueryEngine;
- @Test
- void getIndexEntry() {
- Map indexMap = new HashMap<>();
- assertThatThrownBy(() -> IndexedQueryEngineImpl.getIndexEntry("field1", indexMap))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage(
- "No index found for fieldPath: field1, make sure you have created an index for "
- + "this field.");
- }
-
@Test
void retrieve() {
var spyIndexedQueryEngine = spy(indexedQueryEngine);
@@ -69,8 +57,6 @@ class IndexedQueryEngineImplTest {
var gvk = GroupVersionKind.fromExtension(DemoExtension.class);
- when(indexerFactory.getIndexer(eq(gvk))).thenReturn(mock(Indexer.class));
-
var pageRequest = mock(PageRequest.class);
when(pageRequest.getPageNumber()).thenReturn(1);
when(pageRequest.getPageSize()).thenReturn(2);
@@ -80,8 +66,7 @@ class IndexedQueryEngineImplTest {
assertThat(result.getItems()).containsExactly("object1", "object2");
assertThat(result.getTotal()).isEqualTo(3);
- verify(spyIndexedQueryEngine).doRetrieve(any(), any(), eq(Sort.unsorted()));
- verify(indexerFactory).getIndexer(eq(gvk));
+ verify(spyIndexedQueryEngine).doRetrieve(eq(gvk), any(), eq(Sort.unsorted()));
verify(pageRequest, times(2)).getPageNumber();
verify(pageRequest, times(2)).getPageSize();
verify(pageRequest).getSort();
@@ -95,85 +80,53 @@ class IndexedQueryEngineImplTest {
var gvk = GroupVersionKind.fromExtension(DemoExtension.class);
- when(indexerFactory.getIndexer(eq(gvk))).thenReturn(mock(Indexer.class));
-
- var result = spyIndexedQueryEngine.retrieveAll(gvk, new ListOptions());
+ var result = spyIndexedQueryEngine.retrieveAll(gvk, new ListOptions(), Sort.unsorted());
assertThat(result).isEmpty();
- verify(spyIndexedQueryEngine).doRetrieve(any(), any(), eq(Sort.unsorted()));
- verify(indexerFactory).getIndexer(eq(gvk));
+ verify(spyIndexedQueryEngine).doRetrieve(eq(gvk), any(), eq(Sort.unsorted()));
}
@Test
void doRetrieve() {
var indexer = mock(Indexer.class);
- var labelEntry = mock(IndexEntry.class);
- var fieldSlugEntry = mock(IndexEntry.class);
- var nameEntry = mock(IndexEntry.class);
- when(indexer.readyIndexesIterator()).thenReturn(
- List.of(labelEntry, nameEntry, fieldSlugEntry).iterator());
- when(nameEntry.getIndexDescriptor())
- .thenReturn(new IndexDescriptor(
- PrimaryKeySpecUtils.primaryKeyIndexSpec(DemoExtension.class)));
- when(nameEntry.indexedKeys()).thenReturn(Set.of("object1", "object2", "object3"));
+ var gvk = GroupVersionKind.fromExtension(DemoExtension.class);
- when(fieldSlugEntry.getIndexDescriptor())
- .thenReturn(new IndexDescriptor(new IndexSpec()
- .setName("slug")
- .setOrder(IndexSpec.OrderType.ASC)));
- when((fieldSlugEntry.immutableEntries())).thenReturn(
- List.of(Map.entry("slug1", "object1"), Map.entry("slug2", "object2")));
+ when(indexerFactory.getIndexer(eq(gvk))).thenReturn(indexer);
- when(labelEntry.getIndexDescriptor())
- .thenReturn(
- new IndexDescriptor(LabelIndexSpecUtils.labelIndexSpec(DemoExtension.class)));
- when(labelEntry.entries()).thenReturn(List.of(
+ pileForIndexer(indexer, PrimaryKeySpecUtils.PRIMARY_INDEX_NAME, List.of(
+ Map.entry("object1", "object1"),
+ Map.entry("object2", "object2"),
+ Map.entry("object3", "object3")
+ ));
+
+ pileForIndexer(indexer, LabelIndexSpecUtils.LABEL_PATH, List.of(
Map.entry("key1=value1", "object1"),
Map.entry("key2=value2", "object1"),
Map.entry("key1=value1", "object2"),
Map.entry("key2=value2", "object2"),
Map.entry("key1=value1", "object3")
));
+
+ pileForIndexer(indexer, "slug", List.of(
+ Map.entry("slug1", "object1"),
+ Map.entry("slug2", "object2")
+ ));
+
var listOptions = new ListOptions();
listOptions.setLabelSelector(LabelSelector.builder()
.eq("key1", "value1").build());
listOptions.setFieldSelector(FieldSelector.of(equal("slug", "slug1")));
- var result = indexedQueryEngine.doRetrieve(indexer, listOptions, Sort.unsorted());
+
+ var result = indexedQueryEngine.doRetrieve(gvk, listOptions, Sort.unsorted());
assertThat(result).containsExactly("object1");
}
- @Test
- void intersection() {
- var list1 = Arrays.asList(1, 2, 3, 4);
- var list2 = Arrays.asList(3, 4, 5, 6);
- var expected = Arrays.asList(3, 4);
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
+ @Data
+ @EqualsAndHashCode(callSuper = true)
+ @GVK(group = "test", version = "v1", kind = "demo", plural = "demos", singular = "demo")
+ static class DemoExtension extends AbstractExtension {
- list1 = Arrays.asList(1, 2, 3);
- list2 = Arrays.asList(4, 5, 6);
- expected = List.of();
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
-
- list1 = List.of();
- list2 = Arrays.asList(1, 2, 3);
- expected = List.of();
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
-
- list1 = Arrays.asList(1, 2, 3);
- list2 = List.of();
- expected = List.of();
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
-
- list1 = List.of();
- list2 = List.of();
- expected = List.of();
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
-
- list1 = Arrays.asList(1, 2, 2, 3);
- list2 = Arrays.asList(2, 3, 3, 4);
- expected = Arrays.asList(2, 3);
- assertThat(IndexedQueryEngineImpl.intersection(list1, list2)).isEqualTo(expected);
}
@Nested
@@ -186,10 +139,6 @@ class IndexedQueryEngineImplTest {
void testRetrieveForLabelMatchers() {
// Setup mocks
IndexEntry indexEntryMock = mock(IndexEntry.class);
- Map fieldPathEntryMap =
- Map.of(LabelIndexSpecUtils.LABEL_PATH, indexEntryMock);
- List allMetadataNames = Arrays.asList("object1", "object2", "object3");
-
// Setup mock behavior
when(indexEntryMock.entries())
.thenReturn(List.of(Map.entry("key1=value1", "object1"),
@@ -203,23 +152,22 @@ class IndexedQueryEngineImplTest {
List labelMatchers = Arrays.asList(matcher1, matcher2);
- // Expected results
- List expected = Arrays.asList("object1", "object2");
-
+ var indexer = mock(Indexer.class);
+ when(indexer.getIndexEntry(eq(LabelIndexSpecUtils.LABEL_PATH)))
+ .thenReturn(indexEntryMock);
+ var nameIndexEntry = mock(IndexEntry.class);
+ when(indexer.getIndexEntry(eq(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME)))
+ .thenReturn(nameIndexEntry);
+ when(nameIndexEntry.entries()).thenReturn(List.of(Map.entry("object1", "object1"),
+ Map.entry("object2", "object2"), Map.entry("object3", "object3")));
// Test
- assertThat(indexedQueryEngine.retrieveForLabelMatchers(labelMatchers, fieldPathEntryMap,
- allMetadataNames))
- .isEqualTo(expected);
+ assertThat(indexedQueryEngine.retrieveForLabelMatchers(indexer, labelMatchers))
+ .containsSequence("object1", "object2");
}
@Test
void testRetrieveForLabelMatchersNoMatch() {
- // Setup mocks
IndexEntry indexEntryMock = mock(IndexEntry.class);
- Map fieldPathEntryMap =
- Map.of(LabelIndexSpecUtils.LABEL_PATH, indexEntryMock);
- List allMetadataNames = Arrays.asList("object1", "object2", "object3");
-
// Setup mock behavior
when(indexEntryMock.entries())
.thenReturn(List.of(Map.entry("key1=value1", "object1"),
@@ -230,20 +178,17 @@ class IndexedQueryEngineImplTest {
var matcher1 = EqualityMatcher.equal("key3", "value3");
List labelMatchers = List.of(matcher1);
- // Expected results
- List expected = List.of();
-
+ var indexer = mock(Indexer.class);
+ when(indexer.getIndexEntry(eq(LabelIndexSpecUtils.LABEL_PATH)))
+ .thenReturn(indexEntryMock);
+ var nameIndexEntry = mock(IndexEntry.class);
+ when(indexer.getIndexEntry(eq(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME)))
+ .thenReturn(nameIndexEntry);
+ when(nameIndexEntry.entries()).thenReturn(List.of(Map.entry("object1", "object1"),
+ Map.entry("object2", "object2"), Map.entry("object3", "object3")));
// Test
- assertThat(indexedQueryEngine.retrieveForLabelMatchers(labelMatchers, fieldPathEntryMap,
- allMetadataNames))
- .isEqualTo(expected);
+ assertThat(
+ indexedQueryEngine.retrieveForLabelMatchers(indexer, labelMatchers)).isEmpty();
}
}
-
- @Data
- @EqualsAndHashCode(callSuper = true)
- @GVK(group = "test", version = "v1", kind = "demo", plural = "demos", singular = "demo")
- static class DemoExtension extends AbstractExtension {
-
- }
}
diff --git a/api/src/test/java/run/halo/app/extension/index/query/AndTest.java b/application/src/test/java/run/halo/app/extension/index/query/AndTest.java
similarity index 81%
rename from api/src/test/java/run/halo/app/extension/index/query/AndTest.java
rename to application/src/test/java/run/halo/app/extension/index/query/AndTest.java
index d96fcf070..0c29a8570 100644
--- a/api/src/test/java/run/halo/app/extension/index/query/AndTest.java
+++ b/application/src/test/java/run/halo/app/extension/index/query/AndTest.java
@@ -1,15 +1,17 @@
package run.halo.app.extension.index.query;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static run.halo.app.extension.index.query.IndexViewDataSet.pileForIndexer;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.greaterThan;
import static run.halo.app.extension.index.query.QueryFactory.or;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
+import run.halo.app.extension.index.Indexer;
/**
* Tests for the {@link And} query.
@@ -21,33 +23,45 @@ public class AndTest {
@Test
void testMatches() {
- Collection> deptEntry = List.of(Map.entry("A", "guqing"),
+ var deptEntry = List.of(Map.entry("A", "guqing"),
Map.entry("A", "halo"),
Map.entry("B", "lisi"),
Map.entry("B", "zhangsan"),
Map.entry("C", "ryanwang"),
Map.entry("C", "johnniang")
);
- Collection> ageEntry = List.of(Map.entry("19", "halo"),
+ var ageEntry = List.of(Map.entry("19", "halo"),
Map.entry("19", "guqing"),
Map.entry("18", "zhangsan"),
Map.entry("17", "lisi"),
Map.entry("17", "ryanwang"),
Map.entry("17", "johnniang")
);
- var entries = Map.of("dept", deptEntry, "age", ageEntry);
- var indexView = new QueryIndexViewImpl(entries);
+ var indexer = mock(Indexer.class);
+
+ pileForIndexer(indexer, QueryIndexViewImpl.PRIMARY_INDEX_NAME, List.of(
+ Map.entry("guqing", "guqing"),
+ Map.entry("halo", "halo"),
+ Map.entry("lisi", "lisi"),
+ Map.entry("zhangsan", "zhangsan"),
+ Map.entry("ryanwang", "ryanwang"),
+ Map.entry("johnniang", "johnniang")
+ ));
+
+ pileForIndexer(indexer, "dept", deptEntry);
+
+ pileForIndexer(indexer, "age", ageEntry);
+
+ var indexView = new QueryIndexViewImpl(indexer);
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")),
@@ -57,7 +71,6 @@ public class AndTest {
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")),
@@ -67,7 +80,6 @@ public class AndTest {
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")),
diff --git a/application/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java b/application/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java
new file mode 100644
index 000000000..5768ea825
--- /dev/null
+++ b/application/src/test/java/run/halo/app/extension/index/query/IndexViewDataSet.java
@@ -0,0 +1,340 @@
+package run.halo.app.extension.index.query;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import run.halo.app.extension.index.IndexEntry;
+import run.halo.app.extension.index.Indexer;
+import run.halo.app.extension.index.KeyComparator;
+
+public class IndexViewDataSet {
+
+ /**
+ * Create a {@link QueryIndexView} for employee to test.
+ *
+ * | id | firstName | lastName | email | hireDate | salary | managerId | departmentId |
+ * |----|-----------|----------|-------|----------|--------|-----------|--------------|
+ * | 100| Pat | Fay | p | 17 | 2600 | 101 | 50 |
+ * | 101| Lee | Day | l | 17 | 2400 | 102 | 40 |
+ * | 102| William | Jay | w | 19 | 2200 | 102 | 50 |
+ * | 103| Mary | Day | p | 17 | 2000 | 103 | 50 |
+ * | 104| John | Fay | j | 17 | 1800 | 103 | 50 |
+ * | 105| Gon | Fay | p | 18 | 1900 | 101 | 40 |
+ *
+ *
+ * @return a {@link QueryIndexView} for employee to test
+ */
+ public static QueryIndexView createEmployeeIndexView() {
+ final var 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")
+ );
+ final var firstNameEntry = List.of(
+ Map.entry("Pat", "100"),
+ Map.entry("Lee", "101"),
+ Map.entry("William", "102"),
+ Map.entry("Mary", "103"),
+ Map.entry("John", "104"),
+ Map.entry("Gon", "105")
+ );
+ final var lastNameEntry = List.of(
+ Map.entry("Fay", "100"),
+ Map.entry("Day", "101"),
+ Map.entry("Jay", "102"),
+ Map.entry("Day", "103"),
+ Map.entry("Fay", "104"),
+ Map.entry("Fay", "105")
+ );
+ final var emailEntry = List.of(
+ Map.entry("p", "100"),
+ Map.entry("l", "101"),
+ Map.entry("w", "102"),
+ Map.entry("p", "103"),
+ Map.entry("j", "104"),
+ Map.entry("p", "105")
+ );
+ final var hireDateEntry = List.of(
+ Map.entry("17", "100"),
+ Map.entry("17", "101"),
+ Map.entry("19", "102"),
+ Map.entry("17", "103"),
+ Map.entry("17", "104"),
+ Map.entry("18", "105")
+ );
+ final var salaryEntry = List.of(
+ Map.entry("2600", "100"),
+ Map.entry("2400", "101"),
+ Map.entry("2200", "102"),
+ Map.entry("2000", "103"),
+ Map.entry("1800", "104"),
+ Map.entry("1900", "105")
+ );
+ final var managerIdEntry = List.of(
+ Map.entry("101", "100"),
+ Map.entry("102", "101"),
+ Map.entry("102", "102"),
+ Map.entry("103", "103"),
+ Map.entry("103", "104"),
+ Map.entry("101", "105")
+ );
+ final var departmentIdEntry = List.of(
+ Map.entry("50", "100"),
+ Map.entry("40", "101"),
+ Map.entry("50", "102"),
+ Map.entry("50", "103"),
+ Map.entry("50", "104"),
+ Map.entry("40", "105")
+ );
+
+ var indexer = mock(Indexer.class);
+
+ pileForIndexer(indexer, QueryIndexViewImpl.PRIMARY_INDEX_NAME, idEntry);
+ pileForIndexer(indexer, "firstName", firstNameEntry);
+ pileForIndexer(indexer, "lastName", lastNameEntry);
+ pileForIndexer(indexer, "email", emailEntry);
+ pileForIndexer(indexer, "hireDate", hireDateEntry);
+ pileForIndexer(indexer, "salary", salaryEntry);
+ pileForIndexer(indexer, "managerId", managerIdEntry);
+ pileForIndexer(indexer, "departmentId", departmentIdEntry);
+
+ return new QueryIndexViewImpl(indexer);
+ }
+
+ /**
+ * Create a {@link QueryIndexView} for post to test.
+ *
+ * | 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 |
+ *
+ *
+ * @return a {@link QueryIndexView} for post to test
+ */
+ public static QueryIndexView createPostIndexViewWithNullCell() {
+ final var 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")
+ );
+ final var 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")
+ );
+ final var 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")
+ );
+ final var 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")
+ );
+
+ final var 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 indexer = mock(Indexer.class);
+ pileForIndexer(indexer, QueryIndexViewImpl.PRIMARY_INDEX_NAME, idEntry);
+ pileForIndexer(indexer, "title", titleEntry);
+ pileForIndexer(indexer, "published", publishedEntry);
+ pileForIndexer(indexer, "publishTime", publishTimeEntry);
+ pileForIndexer(indexer, "owner", ownerEntry);
+
+ return new QueryIndexViewImpl(indexer);
+ }
+
+ /**
+ * Creates a fake comment index view for below data.
+ *
+ * | Name | Top | Priority | Creation Time |
+ * | ---- | ----- | -------- | -------------------------------- |
+ * | 1 | True | 0 | 2024-06-05 02:58:15.633165+00:00 |
+ * | 2 | True | 1 | 2024-06-05 02:58:16.633165+00:00 |
+ * | 4 | True | 2 | 2024-06-05 02:58:18.633165+00:00 |
+ * | 3 | True | 2 | 2024-06-05 02:58:17.633165+00:00 |
+ * | 5 | True | 3 | 2024-06-05 02:58:18.633165+00:00 |
+ * | 6 | True | 3 | 2024-06-05 02:58:18.633165+00:00 |
+ * | 10 | False | 0 | 2024-06-05 02:58:17.633165+00:00 |
+ * | 9 | False | 0 | 2024-06-05 02:58:17.633165+00:00 |
+ * | 8 | False | 0 | 2024-06-05 02:58:16.633165+00:00 |
+ * | 7 | False | 0 | 2024-06-05 02:58:15.633165+00:00 |
+ * | 11 | False | 0 | 2024-06-05 02:58:14.633165+00:00 |
+ * | 12 | False | 1 | 2024-06-05 02:58:14.633165+00:00 |
+ * | 14 | False | 3 | 2024-06-05 02:58:17.633165+00:00 |
+ * | 13 | False | 3 | 2024-06-05 02:58:14.633165+00:00 |
+ *
+ */
+ public static QueryIndexView createCommentIndexView() {
+ final var idEntry = List.of(
+ Map.entry("1", "1"),
+ Map.entry("2", "2"),
+ Map.entry("3", "3"),
+ Map.entry("4", "4"),
+ Map.entry("5", "5"),
+ Map.entry("6", "6"),
+ Map.entry("7", "7"),
+ Map.entry("8", "8"),
+ Map.entry("9", "9"),
+ Map.entry("10", "10"),
+ Map.entry("11", "11"),
+ Map.entry("12", "12"),
+ Map.entry("13", "13"),
+ Map.entry("14", "14")
+ );
+ final var creationTimeEntry = List.of(
+ Map.entry("2024-06-05 02:58:15.633165", "1"),
+ Map.entry("2024-06-05 02:58:16.633165", "2"),
+ Map.entry("2024-06-05 02:58:17.633165", "3"),
+ Map.entry("2024-06-05 02:58:18.633165", "4"),
+ Map.entry("2024-06-05 02:58:18.633165", "5"),
+ Map.entry("2024-06-05 02:58:18.633165", "6"),
+ Map.entry("2024-06-05 02:58:15.633165", "7"),
+ Map.entry("2024-06-05 02:58:16.633165", "8"),
+ Map.entry("2024-06-05 02:58:17.633165", "9"),
+ Map.entry("2024-06-05 02:58:17.633165", "10"),
+ Map.entry("2024-06-05 02:58:14.633165", "11"),
+ Map.entry("2024-06-05 02:58:14.633165", "12"),
+ Map.entry("2024-06-05 02:58:14.633165", "13"),
+ Map.entry("2024-06-05 02:58:17.633165", "14")
+ );
+ final var topEntry = List.of(
+ Map.entry("true", "1"),
+ Map.entry("true", "2"),
+ Map.entry("true", "3"),
+ Map.entry("true", "4"),
+ Map.entry("true", "5"),
+ Map.entry("true", "6"),
+ Map.entry("false", "7"),
+ Map.entry("false", "8"),
+ Map.entry("false", "9"),
+ Map.entry("false", "10"),
+ Map.entry("false", "11"),
+ Map.entry("false", "12"),
+ Map.entry("false", "13"),
+ Map.entry("false", "14")
+ );
+ final var priorityEntry = List.of(
+ Map.entry("0", "1"),
+ Map.entry("1", "2"),
+ Map.entry("2", "3"),
+ Map.entry("2", "4"),
+ Map.entry("3", "5"),
+ Map.entry("3", "6"),
+ Map.entry("0", "7"),
+ Map.entry("0", "8"),
+ Map.entry("0", "9"),
+ Map.entry("0", "10"),
+ Map.entry("0", "11"),
+ Map.entry("1", "12"),
+ Map.entry("3", "13"),
+ Map.entry("3", "14")
+ );
+
+ var indexer = mock(Indexer.class);
+ pileForIndexer(indexer, QueryIndexViewImpl.PRIMARY_INDEX_NAME, idEntry);
+ pileForIndexer(indexer, "spec.creationTime", creationTimeEntry);
+ pileForIndexer(indexer, "spec.top", topEntry);
+ pileForIndexer(indexer, "spec.priority", priorityEntry);
+
+ return new QueryIndexViewImpl(indexer);
+ }
+
+ public static void pileForIndexer(Indexer indexer, String propertyName,
+ Collection> entries) {
+ var indexEntry = mock(IndexEntry.class);
+ lenient().when(indexer.getIndexEntry(propertyName)).thenReturn(indexEntry);
+ var sortedEntries = sortEntries(entries);
+
+ lenient().when(indexEntry.entries()).thenReturn(sortedEntries);
+ lenient().when(indexEntry.getIdPositionMap()).thenReturn(idPositionMap(sortedEntries));
+
+ var indexedMap = toKeyObjectMap(sortedEntries);
+ lenient().when(indexEntry.indexedKeys()).thenReturn(new TreeSet<>(indexedMap.keySet()));
+ lenient().when(indexEntry.getObjectNamesBy(anyString())).thenAnswer(invocation -> {
+ var key = (String) invocation.getArgument(0);
+ return indexedMap.get(key);
+ });
+ lenient().when(indexEntry.entries()).thenReturn(entries);
+ }
+
+ public static List> sortEntries(
+ Collection> entries) {
+ return entries.stream()
+ .sorted((a, b) -> KeyComparator.INSTANCE.compare(a.getKey(), b.getKey()))
+ .toList();
+ }
+
+ public static Map idPositionMap(
+ Collection> sortedEntries) {
+ var asMap = toKeyObjectMap(sortedEntries);
+ int i = 0;
+ var idPositionMap = new HashMap();
+ for (var valueIdsEntry : asMap.entrySet()) {
+ var ids = valueIdsEntry.getValue();
+ for (String id : ids) {
+ idPositionMap.put(id, i);
+ }
+ i++;
+ }
+ return idPositionMap;
+ }
+
+ public static LinkedHashMap> toKeyObjectMap(
+ Collection> sortedEntries) {
+ return sortedEntries.stream()
+ .collect(Collectors.groupingBy(Map.Entry::getKey,
+ LinkedHashMap::new,
+ Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
+ }
+}
diff --git a/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java b/application/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java
similarity index 75%
rename from api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java
rename to application/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java
index e7b8918d7..8b10ce029 100644
--- a/api/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java
+++ b/application/src/test/java/run/halo/app/extension/index/query/QueryFactoryTest.java
@@ -1,6 +1,7 @@
package run.halo.app.extension.index.query;
import static org.assertj.core.api.Assertions.assertThat;
+import static run.halo.app.extension.index.query.IndexViewDataSet.createEmployeeIndexView;
import static run.halo.app.extension.index.query.QueryFactory.all;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.between;
@@ -35,9 +36,11 @@ import org.junit.jupiter.api.Test;
*/
class QueryFactoryTest {
+ private final String id = QueryIndexViewImpl.PRIMARY_INDEX_NAME;
+
@Test
void allTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = all("firstName").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101", "102", "103", "104", "105"
@@ -64,7 +67,7 @@ class QueryFactoryTest {
@Test
void equalTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = equal("lastName", "Fay").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "104", "105"
@@ -73,8 +76,8 @@ class QueryFactoryTest {
@Test
void equalOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = equalOtherField("managerId", "id").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = equalOtherField("managerId", id).matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"102", "103"
);
@@ -82,7 +85,7 @@ class QueryFactoryTest {
@Test
void notEqualTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = notEqual("lastName", "Fay").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"101", "102", "103"
@@ -91,8 +94,8 @@ class QueryFactoryTest {
@Test
void notEqualOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = notEqualOtherField("managerId", "id").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = notEqualOtherField("managerId", id).matches(indexView);
// 103 102 is equal
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101", "104", "105"
@@ -101,8 +104,8 @@ class QueryFactoryTest {
@Test
void lessThanTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = lessThan("id", "103").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = lessThan(id, "103").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101", "102"
);
@@ -110,8 +113,8 @@ class QueryFactoryTest {
@Test
void lessThanOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = lessThanOtherField("id", "managerId").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = lessThanOtherField(id, "managerId").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101"
);
@@ -119,8 +122,8 @@ class QueryFactoryTest {
@Test
void lessThanOrEqualTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = lessThanOrEqual("id", "103").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = lessThanOrEqual(id, "103").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101", "102", "103"
);
@@ -128,9 +131,9 @@ class QueryFactoryTest {
@Test
void lessThanOrEqualOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet =
- lessThanOrEqualOtherField("id", "managerId").matches(indexView);
+ lessThanOrEqualOtherField(id, "managerId").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "101", "102", "103"
);
@@ -138,8 +141,8 @@ class QueryFactoryTest {
@Test
void greaterThanTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = greaterThan("id", "103").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = greaterThan(id, "103").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"104", "105"
);
@@ -147,8 +150,8 @@ class QueryFactoryTest {
@Test
void greaterThanOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = greaterThanOtherField("id", "managerId").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = greaterThanOtherField(id, "managerId").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"104", "105"
);
@@ -156,8 +159,8 @@ class QueryFactoryTest {
@Test
void greaterThanOrEqualTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = greaterThanOrEqual("id", "103").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = greaterThanOrEqual(id, "103").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"103", "104", "105"
);
@@ -165,9 +168,9 @@ class QueryFactoryTest {
@Test
void greaterThanOrEqualOtherFieldTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet =
- greaterThanOrEqualOtherField("id", "managerId").matches(indexView);
+ greaterThanOrEqualOtherField(id, "managerId").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"102", "103", "104", "105"
);
@@ -175,8 +178,8 @@ class QueryFactoryTest {
@Test
void inTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = in("id", "103", "104").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = in(id, "103", "104").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"103", "104"
);
@@ -184,7 +187,7 @@ class QueryFactoryTest {
@Test
void inTest2() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = in("lastName", "Fay").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"100", "104", "105"
@@ -193,13 +196,13 @@ class QueryFactoryTest {
@Test
void betweenTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
- var resultSet = between("id", "103", "105").matches(indexView);
+ var indexView = createEmployeeIndexView();
+ var resultSet = between(id, "103", "105").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"103", "104", "105"
);
- indexView = IndexViewDataSet.createEmployeeIndexView();
+ indexView = createEmployeeIndexView();
resultSet = between("salary", "2000", "2400").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"101", "102", "103"
@@ -208,7 +211,7 @@ class QueryFactoryTest {
@Test
void betweenLowerExclusive() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet =
QueryFactory.betweenLowerExclusive("salary", "2000", "2400").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
@@ -218,7 +221,7 @@ class QueryFactoryTest {
@Test
void betweenUpperExclusive() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet =
QueryFactory.betweenUpperExclusive("salary", "2000", "2400").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
@@ -228,7 +231,7 @@ class QueryFactoryTest {
@Test
void betweenExclusive() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = QueryFactory.betweenExclusive("salary", "2000", "2400").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"102"
@@ -237,7 +240,7 @@ class QueryFactoryTest {
@Test
void startsWithTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = startsWith("firstName", "W").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"102"
@@ -246,7 +249,7 @@ class QueryFactoryTest {
@Test
void endsWithTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = endsWith("firstName", "y").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"103"
@@ -255,7 +258,7 @@ class QueryFactoryTest {
@Test
void containsTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet = contains("firstName", "i").matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
"102"
@@ -268,7 +271,7 @@ class QueryFactoryTest {
@Test
void notTest() {
- var indexView = IndexViewDataSet.createEmployeeIndexView();
+ var indexView = createEmployeeIndexView();
var resultSet =
QueryFactory.not(QueryFactory.contains("firstName", "i")).matches(indexView);
assertThat(resultSet).containsExactlyInAnyOrder(
@@ -286,10 +289,10 @@ class QueryFactoryTest {
// and composite query
query = and(
and(equal("firstName", "W"), equal("lastName", "Fay")),
- or(equalOtherField("id", "userId"), lessThan("age", "123"))
+ or(equalOtherField(id, "userId"), lessThan("age", "123"))
);
fieldNames = getFieldNamesUsedInQuery(query);
- assertThat(fieldNames).containsExactlyInAnyOrder("firstName", "lastName", "id", "userId",
+ assertThat(fieldNames).containsExactlyInAnyOrder("firstName", "lastName", id, "userId",
"age");
// or composite query
diff --git a/application/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java b/application/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java
new file mode 100644
index 000000000..b74629570
--- /dev/null
+++ b/application/src/test/java/run/halo/app/extension/index/query/QueryIndexViewImplTest.java
@@ -0,0 +1,301 @@
+package run.halo.app.extension.index.query;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static run.halo.app.extension.index.query.IndexViewDataSet.createCommentIndexView;
+import static run.halo.app.extension.index.query.IndexViewDataSet.createEmployeeIndexView;
+import static run.halo.app.extension.index.query.IndexViewDataSet.createPostIndexViewWithNullCell;
+import static run.halo.app.extension.index.query.IndexViewDataSet.pileForIndexer;
+import static run.halo.app.extension.index.query.QueryIndexViewImpl.PRIMARY_INDEX_NAME;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Sort;
+import run.halo.app.extension.index.IndexEntry;
+import run.halo.app.extension.index.Indexer;
+
+/**
+ * Tests for {@link QueryIndexViewImpl}.
+ *
+ * @author guqing
+ * @since 2.17.0
+ */
+class QueryIndexViewImplTest {
+ final String id = PRIMARY_INDEX_NAME;
+
+ @Test
+ void getAllIdsForFieldTest() {
+ var indexView = createPostIndexViewWithNullCell();
+ var resultSet = indexView.getIdsForField("title");
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101", "102", "103", "104", "105", "106", "107", "108"
+ );
+
+ resultSet = indexView.getIdsForField("publishTime");
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101", "105", "106", "107"
+ );
+ }
+
+ @Test
+ void findIdsForValueEqualTest() {
+ var indexView = createEmployeeIndexView();
+ var resultSet = indexView.findMatchingIdsWithEqualValues("managerId", id);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "102", "103"
+ );
+ }
+
+ @Test
+ void findIdsForFieldValueGreaterThanTest() {
+ var indexView = createEmployeeIndexView();
+ var resultSet = indexView.findMatchingIdsWithGreaterValues(id, "managerId", false);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "104", "105"
+ );
+
+ indexView = createEmployeeIndexView();
+ resultSet = indexView.findMatchingIdsWithGreaterValues(id, "managerId", true);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "103", "102", "104", "105"
+ );
+ }
+
+ @Test
+ void findIdsForFieldValueGreaterThanTest2() {
+ var indexView = createEmployeeIndexView();
+ var resultSet = indexView.findMatchingIdsWithGreaterValues("managerId", id, false);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101"
+ );
+
+ indexView = createEmployeeIndexView();
+ resultSet = indexView.findMatchingIdsWithGreaterValues("managerId", id, true);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101", "102", "103"
+ );
+ }
+
+ @Test
+ void findIdsForFieldValueLessThanTest() {
+ var indexView = createEmployeeIndexView();
+ var resultSet = indexView.findMatchingIdsWithSmallerValues(id, "managerId", false);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101"
+ );
+
+ indexView = createEmployeeIndexView();
+ resultSet = indexView.findMatchingIdsWithSmallerValues(id, "managerId", true);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "100", "101", "102", "103"
+ );
+ }
+
+ @Test
+ void findIdsForFieldValueLessThanTest2() {
+ var indexView = createEmployeeIndexView();
+ var resultSet = indexView.findMatchingIdsWithSmallerValues("managerId", id, false);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "104", "105"
+ );
+
+ indexView = createEmployeeIndexView();
+ resultSet = indexView.findMatchingIdsWithSmallerValues("managerId", id, true);
+ assertThat(resultSet).containsExactlyInAnyOrder(
+ "103", "102", "104", "105"
+ );
+ }
+
+ @Nested
+ @ExtendWith(MockitoExtension.class)
+ class SortTest {
+ @Mock
+ private Indexer indexer;
+
+ @Test
+ void testSortByUnsorted() {
+ var idEntry = mock(IndexEntry.class);
+ when(indexer.getIndexEntry(PRIMARY_INDEX_NAME))
+ .thenReturn(idEntry);
+ var indexView = new QueryIndexViewImpl(indexer);
+
+ var sort = Sort.unsorted();
+
+ var resultSet = new TreeSet<>(List.of("Item1", "Item2"));
+ List sortedList = indexView.sortBy(resultSet, sort);
+ assertThat(sortedList).isEqualTo(List.of("Item1", "Item2"));
+ }
+
+ @Test
+ void testSortBySortedAscending() {
+ pileForIndexer(indexer, "field1",
+ List.of(Map.entry("key2", "Item2"), Map.entry("key1", "Item1")));
+
+ pileForIndexer(indexer, PRIMARY_INDEX_NAME,
+ List.of(Map.entry("Item1", "Item1"), Map.entry("Item2", "Item2")));
+
+ var indexView = new QueryIndexViewImpl(indexer);
+
+ var sort = Sort.by(Sort.Order.asc("field1"));
+
+ List sortedList = indexView.sortBy(indexView.getAllIds(), sort);
+
+ assertThat(sortedList).containsSequence("Item1", "Item2");
+ }
+
+ @Test
+ void testSortBySortedDescending() {
+ pileForIndexer(indexer, "field1",
+ List.of(Map.entry("key1", "Item1"), Map.entry("key2", "Item2")));
+
+ pileForIndexer(indexer, PRIMARY_INDEX_NAME,
+ List.of(Map.entry("Item1", "Item1"), Map.entry("Item2", "Item2")));
+
+ var indexView = new QueryIndexViewImpl(indexer);
+
+ var sort = Sort.by(Sort.Order.desc("field1"));
+
+ var resultSet = new TreeSet<>(List.of("Item1", "Item2"));
+ List sortedList = indexView.sortBy(resultSet, sort);
+
+ assertThat(sortedList).containsExactly("Item2", "Item1");
+ }
+
+ @Test
+ void testSortByMultipleFields() {
+ pileForIndexer(indexer, "field1",
+ List.of(Map.entry("k3", "Item3"), Map.entry("k2", "Item2")));
+
+ pileForIndexer(indexer, "field2",
+ List.of(Map.entry("k1", "Item1"), Map.entry("k3", "Item3")));
+
+ pileForIndexer(indexer, id,
+ List.of(Map.entry("Item1", "Item1"), Map.entry("Item2", "Item2"),
+ Map.entry("Item3", "Item3")));
+
+ var indexView = new QueryIndexViewImpl(indexer);
+
+ var sort = Sort.by(Sort.Order.asc("field1"), Sort.Order.desc("field2"));
+
+ var resultSet = new TreeSet<>(List.of("Item1", "Item2", "Item3"));
+ List sortedList = indexView.sortBy(resultSet, sort);
+
+ assertThat(sortedList).containsExactly("Item2", "Item3", "Item1");
+ }
+
+ @Test
+ void testSortByMultipleFields2() {
+ pileForIndexer(indexer, id, List.of());
+
+ pileForIndexer(indexer, "field1", List.of(Map.entry("John", "John"),
+ Map.entry("Bob", "Bob"),
+ Map.entry("Alice", "Alice")
+ ));
+ pileForIndexer(indexer, "field2", List.of(Map.entry("David", "David"),
+ Map.entry("Eva", "Eva"),
+ Map.entry("Frank", "Frank")
+ ));
+ pileForIndexer(indexer, "field3", List.of(Map.entry("George", "George"),
+ Map.entry("Helen", "Helen"),
+ Map.entry("Ivy", "Ivy")
+ ));
+
+ /*
+ *
+ * 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(indexer);
+ var sort = Sort.by(Sort.Order.desc("field1"), Sort.Order.asc("field2"),
+ Sort.Order.asc("field3"));
+
+ var resultSet = new TreeSet<>(
+ List.of("Bob", "John", "Eva", "Alice", "Ivy", "David", "Frank", "Helen", "George"));
+ List sortedList = indexView.sortBy(resultSet, sort);
+
+ assertThat(sortedList).containsSequence("David", "Eva", "Frank", "George", "Helen",
+ "Ivy", "John", "Bob", "Alice");
+ }
+
+ /**
+ * Result for the following data.
+ *
+ * | id | firstName | lastName | email | hireDate | salary | managerId | departmentId |
+ * |----|-----------|----------|-------|----------|--------|-----------|--------------|
+ * | 100| Pat | Fay | p | 17 | 2600 | 101 | 50 |
+ * | 101| Lee | Day | l | 17 | 2400 | 102 | 40 |
+ * | 103| Mary | Day | p | 17 | 2000 | 103 | 50 |
+ * | 104| John | Fay | j | 17 | 1800 | 103 | 50 |
+ * | 105| Gon | Fay | p | 18 | 1900 | 101 | 40 |
+ * | 102| William | Jay | w | 19 | 2200 | 102 | 50 |
+ *
+ */
+ @Test
+ void sortByMultipleFieldsWithFirstSame() {
+ var indexView = createEmployeeIndexView();
+ var ids = indexView.getAllIds();
+ var result = indexView.sortBy(ids, Sort.by(Sort.Order.asc("hireDate"),
+ Sort.Order.asc("lastName"))
+ );
+ assertThat(result).containsSequence("101", "103", "100", "104", "105", "102");
+ }
+
+ /**
+ * Result for the following data.
+ *
+ * | id | title | published | publishTime | owner |
+ * |-----|--------|-----------|---------------------|-------|
+ * | 100 | title1 | true | 2024-01-01T00:00:00 | jack |
+ * | 101 | title2 | true | 2024-01-02T00:00:00 | rose |
+ * | 105 | title6 | true | 2024-01-05 00:00:00 | tom |
+ * | 107 | title8 | true | 2024-01-05 12:00:00 | jerry |
+ * | 106 | title7 | true | 2024-01-05 13:00:00 | jerry |
+ * | 108 | title9 | false | null | jerry |
+ * | 104 | title5 | false | null | john |
+ * | 103 | title4 | false | null | peter |
+ * | 102 | title3 | false | null | smith |
+ *
+ */
+ @Test
+ void sortByMultipleFieldsForPostDataSet() {
+ var indexView = createPostIndexViewWithNullCell();
+ var ids = indexView.getAllIds();
+ var result = indexView.sortBy(ids, Sort.by(Sort.Order.asc("publishTime"),
+ Sort.Order.desc("title"))
+ );
+ assertThat(result).containsSequence("100", "101", "105", "107", "106", "108", "104",
+ "103", "102");
+ }
+
+ @Test
+ void sortByMultipleFieldsForCommentDataSet() {
+ var indexView = createCommentIndexView();
+ var ids = indexView.getAllIds();
+ var sort = Sort.by(Sort.Order.desc("spec.top"),
+ Sort.Order.asc("spec.priority"),
+ Sort.Order.desc("spec.creationTime"),
+ Sort.Order.asc("metadata.name")
+ );
+ var result = indexView.sortBy(ids, sort);
+ assertThat(result).containsSequence("1", "2", "4", "3", "5", "6", "9", "10", "8", "7",
+ "11", "12", "14", "13");
+ }
+ }
+}