mirror of https://github.com/halo-dev/halo
refactor: optimize the implementation of indexed query engine through query index view (#5233)
#### What type of PR is this? /kind improvement /area core /milestone 2.12.x #### What this PR does / why we need it: 通过重构 QueryIndexView 的实现方式来优化 IndexedQueryEngine 的逻辑并简化排序过程 how to test it? 单元测试通过即可,此 PR 的修改都是基于单元测试的基础上对原代码做的重构 #### Does this PR introduce a user-facing change? ```release-note None ```pull/5245/head
parent
8523a67e06
commit
57fb644173
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.extension.index;
|
||||
|
||||
import java.util.Comparator;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
public class KeyComparator implements Comparator<String> {
|
||||
public static final KeyComparator INSTANCE = new KeyComparator();
|
||||
|
||||
@Override
|
||||
public int compare(@Nullable String a, @Nullable String b) {
|
||||
if (a == null && b == null) {
|
||||
return 0;
|
||||
} else if (a == null) {
|
||||
// null less than everything
|
||||
return 1;
|
||||
} else if (b == null) {
|
||||
// null less than everything
|
||||
return -1;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (i < a.length() && j < b.length()) {
|
||||
if (Character.isDigit(a.charAt(i)) && Character.isDigit(b.charAt(j))) {
|
||||
// handle number part
|
||||
int num1 = 0;
|
||||
int num2 = 0;
|
||||
while (i < a.length() && Character.isDigit(a.charAt(i))) {
|
||||
num1 = num1 * 10 + (a.charAt(i++) - '0');
|
||||
}
|
||||
while (j < b.length() && Character.isDigit(b.charAt(j))) {
|
||||
num2 = num2 * 10 + (b.charAt(j++) - '0');
|
||||
}
|
||||
if (num1 != num2) {
|
||||
return num1 - num2;
|
||||
}
|
||||
} else if (a.charAt(i) != b.charAt(j)) {
|
||||
// handle non-number part
|
||||
return a.charAt(i) - b.charAt(j);
|
||||
} else {
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return a.length() - b.length();
|
||||
}
|
||||
}
|
|
@ -25,6 +25,8 @@ public class And extends LogicalQuery {
|
|||
NavigableSet<String> resultSet = null;
|
||||
for (Query query : childQueries) {
|
||||
NavigableSet<String> currentResult = query.matches(indexView);
|
||||
// Trim unneeded rows to shrink the dataset for the next query
|
||||
indexView.removeByIdNotIn(currentResult);
|
||||
if (resultSet == null) {
|
||||
resultSet = Sets.newTreeSet(currentResult);
|
||||
} else {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import java.util.NavigableSet;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class EqualQuery extends SimpleQuery {
|
||||
|
||||
|
@ -10,6 +11,7 @@ public class EqualQuery extends SimpleQuery {
|
|||
|
||||
public EqualQuery(String fieldName, String value, boolean isFieldRef) {
|
||||
super(fieldName, value, isFieldRef);
|
||||
Assert.notNull(value, "Value must not be null, use IsNull or IsNotNull instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import java.util.NavigableSet;
|
||||
|
||||
public class IsNotNull extends SimpleQuery {
|
||||
|
||||
protected IsNotNull(String fieldName) {
|
||||
super(fieldName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<String> matches(QueryIndexView indexView) {
|
||||
return indexView.getAllIdsForField(fieldName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import java.util.NavigableSet;
|
||||
|
||||
public class IsNull extends SimpleQuery {
|
||||
|
||||
protected IsNull(String fieldName) {
|
||||
super(fieldName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<String> matches(QueryIndexView indexView) {
|
||||
var allIds = indexView.getAllIds();
|
||||
var idsForField = indexView.getAllIdsForField(fieldName);
|
||||
allIds.removeAll(idsForField);
|
||||
return allIds;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package run.halo.app.extension.index.query;
|
|||
|
||||
import com.google.common.collect.Sets;
|
||||
import java.util.NavigableSet;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class NotEqual extends SimpleQuery {
|
||||
private final EqualQuery equalQuery;
|
||||
|
@ -12,6 +13,7 @@ public class NotEqual extends SimpleQuery {
|
|||
|
||||
public NotEqual(String fieldName, String value, boolean isFieldRef) {
|
||||
super(fieldName, value, isFieldRef);
|
||||
Assert.notNull(value, "Value must not be null, use IsNull or IsNotNull instead");
|
||||
this.equalQuery = new EqualQuery(fieldName, value, isFieldRef);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,21 @@ public class QueryFactory {
|
|||
return new All(fieldName);
|
||||
}
|
||||
|
||||
public static Query isNull(String fieldName) {
|
||||
return new IsNull(fieldName);
|
||||
}
|
||||
|
||||
public static Query isNotNull(String fieldName) {
|
||||
return new IsNotNull(fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link NotEqual} for the given {@code fieldName} and {@code attributeValue}.
|
||||
*/
|
||||
public static Query notEqual(String fieldName, String attributeValue) {
|
||||
if (attributeValue == null) {
|
||||
return new IsNotNull(fieldName);
|
||||
}
|
||||
return new NotEqual(fieldName, attributeValue);
|
||||
}
|
||||
|
||||
|
@ -28,7 +42,13 @@ public class QueryFactory {
|
|||
return new NotEqual(fieldName, otherFieldName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link EqualQuery} for the given {@code fieldName} and {@code attributeValue}.
|
||||
*/
|
||||
public static Query equal(String fieldName, String attributeValue) {
|
||||
if (attributeValue == null) {
|
||||
return new IsNull(fieldName);
|
||||
}
|
||||
return new EqualQuery(fieldName, attributeValue);
|
||||
}
|
||||
|
||||
|
@ -73,6 +93,9 @@ public class QueryFactory {
|
|||
return in(fieldName, Set.of(attributeValues));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link InQuery} for the given {@code fieldName} and {@code values}.
|
||||
*/
|
||||
public static Query in(String fieldName, Collection<String> values) {
|
||||
Assert.notNull(values, "Values must not be null");
|
||||
if (values.size() == 1) {
|
||||
|
@ -85,6 +108,9 @@ public class QueryFactory {
|
|||
return new InQuery(fieldName, valueSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link And} for the given {@link Query}s.
|
||||
*/
|
||||
public static Query and(Collection<Query> queries) {
|
||||
Assert.notEmpty(queries, "Queries must not be empty");
|
||||
if (queries.size() == 1) {
|
||||
|
@ -98,6 +124,9 @@ public class QueryFactory {
|
|||
return new And(queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link And} for the given {@link Query}s.
|
||||
*/
|
||||
public static Query and(Query query1, Query query2, Query... additionalQueries) {
|
||||
var queries = new ArrayList<Query>(2 + additionalQueries.length);
|
||||
queries.add(query1);
|
||||
|
@ -106,6 +135,9 @@ public class QueryFactory {
|
|||
return new And(queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link And} for the given {@link Query}s.
|
||||
*/
|
||||
public static Query and(Query query1, Query query2, Collection<Query> additionalQueries) {
|
||||
var queries = new ArrayList<Query>(2 + additionalQueries.size());
|
||||
queries.add(query1);
|
||||
|
@ -119,6 +151,9 @@ public class QueryFactory {
|
|||
return new Or(queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link Or} for the given {@link Query}s.
|
||||
*/
|
||||
public static Query or(Query query1, Query query2, Query... additionalQueries) {
|
||||
var queries = new ArrayList<Query>(2 + additionalQueries.length);
|
||||
queries.add(query1);
|
||||
|
@ -127,6 +162,9 @@ public class QueryFactory {
|
|||
return new Or(queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link Or} for the given {@link Query}s.
|
||||
*/
|
||||
public static Query or(Query query1, Query query2, Collection<Query> additionalQueries) {
|
||||
var queries = new ArrayList<Query>(2 + additionalQueries.size());
|
||||
queries.add(query1);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NavigableSet;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.index.IndexSpec;
|
||||
|
||||
|
@ -37,7 +39,7 @@ public interface QueryIndexView {
|
|||
NavigableSet<String> getAllValuesForField(String fieldName);
|
||||
|
||||
/**
|
||||
* Gets all object ids for a given field name.
|
||||
* Gets all object ids for a given field name without null cells.
|
||||
*
|
||||
* @param fieldName the field name
|
||||
* @return all indexed object ids for the given field name
|
||||
|
@ -45,6 +47,13 @@ public interface QueryIndexView {
|
|||
*/
|
||||
NavigableSet<String> getAllIdsForField(String fieldName);
|
||||
|
||||
/**
|
||||
* Gets all object ids in this view.
|
||||
*
|
||||
* @return all object ids in this view
|
||||
*/
|
||||
NavigableSet<String> getAllIds();
|
||||
|
||||
NavigableSet<String> findIdsForFieldValueEqual(String fieldName1, String fieldName2);
|
||||
|
||||
NavigableSet<String> findIdsForFieldValueGreaterThan(String fieldName1, String fieldName2,
|
||||
|
@ -53,5 +62,7 @@ public interface QueryIndexView {
|
|||
NavigableSet<String> findIdsForFieldValueLessThan(String fieldName1, String fieldName2,
|
||||
boolean orEqual);
|
||||
|
||||
void removeAllFieldValuesByIdNotIn(NavigableSet<String> ids);
|
||||
void removeByIdNotIn(NavigableSet<String> ids);
|
||||
|
||||
List<String> sortBy(Sort sort);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,35 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.SetMultimap;
|
||||
import com.google.common.collect.HashBasedTable;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Table;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.extension.index.KeyComparator;
|
||||
|
||||
/**
|
||||
* A default implementation for {@link QueryIndexView}.
|
||||
* A default implementation for {@link run.halo.app.extension.index.query.QueryIndexView}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.12.0
|
||||
*/
|
||||
public class QueryIndexViewImpl implements QueryIndexView {
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Map<String, SetMultimap<String, String>> orderedMatches;
|
||||
private final Set<String> fieldNames;
|
||||
private final Table<String, String, NavigableSet<String>> orderedMatches;
|
||||
|
||||
/**
|
||||
* Creates a new {@link QueryIndexViewImpl} for the given {@link Map} of index entries.
|
||||
|
@ -28,10 +37,21 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
* @param indexEntries index entries from indexer to create the view for.
|
||||
*/
|
||||
public QueryIndexViewImpl(Map<String, Collection<Map.Entry<String, String>>> indexEntries) {
|
||||
this.orderedMatches = new HashMap<>();
|
||||
this.fieldNames = new HashSet<>();
|
||||
this.orderedMatches = HashBasedTable.create();
|
||||
for (var entry : indexEntries.entrySet()) {
|
||||
// do not use stream collect here as it is slower
|
||||
this.orderedMatches.put(entry.getKey(), createSetMultiMap(entry.getValue()));
|
||||
String fieldName = entry.getKey();
|
||||
this.fieldNames.add(fieldName);
|
||||
for (var fieldEntry : entry.getValue()) {
|
||||
var id = fieldEntry.getValue();
|
||||
var fieldValue = fieldEntry.getKey();
|
||||
var columnValue = this.orderedMatches.get(id, fieldName);
|
||||
if (columnValue == null) {
|
||||
columnValue = Sets.newTreeSet();
|
||||
this.orderedMatches.put(id, fieldName, columnValue);
|
||||
}
|
||||
columnValue.add(fieldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,8 +60,13 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
lock.lock();
|
||||
try {
|
||||
checkFieldNameIndexed(fieldName);
|
||||
SetMultimap<String, String> fieldMap = orderedMatches.get(fieldName);
|
||||
return fieldMap != null ? new TreeSet<>(fieldMap.get(fieldValue)) : emptySet();
|
||||
var result = new TreeSet<String>();
|
||||
for (var cell : orderedMatches.cellSet()) {
|
||||
if (cell.getColumnKey().equals(fieldName) && cell.getValue().contains(fieldValue)) {
|
||||
result.add(cell.getRowKey());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@ -52,8 +77,13 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
lock.lock();
|
||||
try {
|
||||
checkFieldNameIndexed(fieldName);
|
||||
SetMultimap<String, String> fieldMap = orderedMatches.get(fieldName);
|
||||
return fieldMap != null ? new TreeSet<>(fieldMap.keySet()) : emptySet();
|
||||
var result = Sets.<String>newTreeSet();
|
||||
for (var cell : orderedMatches.cellSet()) {
|
||||
if (cell.getColumnKey().equals(fieldName)) {
|
||||
result.addAll(cell.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@ -64,8 +94,24 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
lock.lock();
|
||||
try {
|
||||
checkFieldNameIndexed(fieldName);
|
||||
SetMultimap<String, String> fieldMap = orderedMatches.get(fieldName);
|
||||
return fieldMap != null ? new TreeSet<>(fieldMap.values()) : emptySet();
|
||||
NavigableSet<String> ids = new TreeSet<>();
|
||||
// iterate over the table and collect all IDs associated with the given field name
|
||||
for (var cell : orderedMatches.cellSet()) {
|
||||
if (cell.getColumnKey().equals(fieldName)) {
|
||||
ids.add(cell.getRowKey());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NavigableSet<String> getAllIds() {
|
||||
lock.lock();
|
||||
try {
|
||||
return new TreeSet<>(orderedMatches.rowKeySet());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@ -77,17 +123,25 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
try {
|
||||
checkFieldNameIndexed(fieldName1);
|
||||
checkFieldNameIndexed(fieldName2);
|
||||
var index1 = orderedMatches.get(fieldName1);
|
||||
var index2 = orderedMatches.get(fieldName2);
|
||||
|
||||
var idFieldValuesForIndex2Map =
|
||||
Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build());
|
||||
var result = Sets.<String>newTreeSet();
|
||||
for (Map.Entry<String, String> entryForIndex1 : index1.entries()) {
|
||||
var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue());
|
||||
for (String item : fieldValues) {
|
||||
if (entryForIndex1.getKey().equals(item)) {
|
||||
result.add(entryForIndex1.getValue());
|
||||
NavigableSet<String> 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<String, NavigableSet<String>> entry : field1ValuesToIds.entrySet()) {
|
||||
String fieldValue = entry.getKey();
|
||||
NavigableSet<String> idsForFieldValue = entry.getValue();
|
||||
|
||||
// if the second field contains the same value, add all matching IDs
|
||||
if (field2ValuesToIds.containsKey(fieldValue)) {
|
||||
NavigableSet<String> matchingIds = field2ValuesToIds.get(fieldValue);
|
||||
for (String id : idsForFieldValue) {
|
||||
if (matchingIds.contains(id)) {
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +151,29 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
}
|
||||
}
|
||||
|
||||
private Map<String, NavigableSet<String>> getColumnValuesToIdsMap(String fieldName) {
|
||||
var valuesToIdsMap = new HashMap<String, NavigableSet<String>>();
|
||||
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<String> findIdsForFieldValueGreaterThan(String fieldName1,
|
||||
String fieldName2, boolean orEqual) {
|
||||
|
@ -105,19 +182,28 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
checkFieldNameIndexed(fieldName1);
|
||||
checkFieldNameIndexed(fieldName2);
|
||||
|
||||
var index1 = orderedMatches.get(fieldName1);
|
||||
var index2 = orderedMatches.get(fieldName2);
|
||||
NavigableSet<String> result = new TreeSet<>();
|
||||
|
||||
var idFieldValuesForIndex2Map =
|
||||
Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build());
|
||||
// obtain all values for both fields and their corresponding IDs
|
||||
var field1ValuesToIds = getColumnValuesToIdsMap(fieldName1);
|
||||
var field2ValuesToIds = getColumnValuesToIdsMap(fieldName2);
|
||||
|
||||
var result = Sets.<String>newTreeSet();
|
||||
for (Map.Entry<String, String> entryForIndex1 : index1.entries()) {
|
||||
var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue());
|
||||
for (String item : fieldValues) {
|
||||
int compare = entryForIndex1.getKey().compareTo(item);
|
||||
if (orEqual ? compare >= 0 : compare > 0) {
|
||||
result.add(entryForIndex1.getValue());
|
||||
// iterate over each value of the first field
|
||||
for (var entryField1 : field1ValuesToIds.entrySet()) {
|
||||
String fieldValue1 = entryField1.getKey();
|
||||
|
||||
// iterate over each value of the second field
|
||||
for (var entryField2 : field2ValuesToIds.entrySet()) {
|
||||
String fieldValue2 = entryField2.getKey();
|
||||
|
||||
int comparison = fieldValue1.compareTo(fieldValue2);
|
||||
if (orEqual ? comparison >= 0 : comparison > 0) {
|
||||
// if the second field contains the same value, add all matching IDs
|
||||
for (String id : entryField1.getValue()) {
|
||||
if (field2ValuesToIds.get(fieldValue2).contains(id)) {
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,19 +220,29 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
try {
|
||||
checkFieldNameIndexed(fieldName1);
|
||||
checkFieldNameIndexed(fieldName2);
|
||||
SetMultimap<String, String> index1 = orderedMatches.get(fieldName1);
|
||||
SetMultimap<String, String> index2 = orderedMatches.get(fieldName2);
|
||||
|
||||
var idFieldValuesForIndex2Map =
|
||||
Multimaps.invertFrom(index2, MultimapBuilder.treeKeys().treeSetValues().build());
|
||||
NavigableSet<String> result = new TreeSet<>();
|
||||
|
||||
var result = Sets.<String>newTreeSet();
|
||||
for (Map.Entry<String, String> entryForIndex1 : index1.entries()) {
|
||||
var fieldValues = idFieldValuesForIndex2Map.get(entryForIndex1.getValue());
|
||||
for (String item : fieldValues) {
|
||||
int compare = entryForIndex1.getKey().compareTo(item);
|
||||
if (orEqual ? compare <= 0 : compare < 0) {
|
||||
result.add(entryForIndex1.getValue());
|
||||
// obtain all values for both fields and their corresponding IDs
|
||||
var field1ValuesToIds = getColumnValuesToIdsMap(fieldName1);
|
||||
var field2ValuesToIds = getColumnValuesToIdsMap(fieldName2);
|
||||
|
||||
// iterate over each value of the first field
|
||||
for (var entryField1 : field1ValuesToIds.entrySet()) {
|
||||
String fieldValue1 = entryField1.getKey();
|
||||
|
||||
// iterate over each value of the second field
|
||||
for (var entryField2 : field2ValuesToIds.entrySet()) {
|
||||
String fieldValue2 = entryField2.getKey();
|
||||
|
||||
int comparison = fieldValue1.compareTo(fieldValue2);
|
||||
if (orEqual ? comparison <= 0 : comparison < 0) {
|
||||
// if the second field contains the same value, add all matching IDs
|
||||
for (String id : entryField1.getValue()) {
|
||||
if (field2ValuesToIds.get(fieldValue2).contains(id)) {
|
||||
result.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -157,38 +253,100 @@ public class QueryIndexViewImpl implements QueryIndexView {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void removeAllFieldValuesByIdNotIn(NavigableSet<String> ids) {
|
||||
public void removeByIdNotIn(NavigableSet<String> ids) {
|
||||
lock.lock();
|
||||
try {
|
||||
for (var fieldNameValuesEntry : orderedMatches.entrySet()) {
|
||||
SetMultimap<String, String> indicates = fieldNameValuesEntry.getValue();
|
||||
indicates.entries().removeIf(entry -> !ids.contains(entry.getValue()));
|
||||
Set<String> 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<String> sortBy(Sort sort) {
|
||||
lock.lock();
|
||||
try {
|
||||
for (Sort.Order order : sort) {
|
||||
String fieldName = order.getProperty();
|
||||
checkFieldNameIndexed(fieldName);
|
||||
}
|
||||
|
||||
// obtain all row keys (IDs)
|
||||
Set<String> allRowKeys = orderedMatches.rowKeySet();
|
||||
|
||||
// convert row keys to list for sorting
|
||||
List<String> sortedRowKeys = new ArrayList<>(allRowKeys);
|
||||
if (sort.isUnsorted()) {
|
||||
return sortedRowKeys;
|
||||
}
|
||||
|
||||
// sort row keys according to sort criteria in a Sort object
|
||||
sortedRowKeys.sort((id1, id2) -> {
|
||||
for (Sort.Order order : sort) {
|
||||
String fieldName = order.getProperty();
|
||||
|
||||
// compare the values of the two rows on the field
|
||||
int comparison = compareRowValue(id1, id2, fieldName, order.isAscending());
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
// if all sort criteria are equal, return 0
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sortedRowKeys;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private int compareRowValue(String id1, String id2, String fieldName, boolean isAscending) {
|
||||
var value1 = getSingleFieldValueForSort(id1, fieldName);
|
||||
var value2 = getSingleFieldValueForSort(id2, fieldName);
|
||||
// nulls are less than everything whatever the sort order is
|
||||
// do not simply the following code for null check,it's different from KeyComparator
|
||||
if (value1 == null && value2 == null) {
|
||||
return 0;
|
||||
} else if (value1 == null) {
|
||||
return 1;
|
||||
} else if (value2 == null) {
|
||||
return -1;
|
||||
}
|
||||
return isAscending ? KeyComparator.INSTANCE.compare(value1, value2)
|
||||
: KeyComparator.INSTANCE.compare(value2, value1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getSingleFieldValueForSort(String rowKey, String fieldName) {
|
||||
return Optional.ofNullable(orderedMatches.get(rowKey, fieldName))
|
||||
.filter(values -> !CollectionUtils.isEmpty(values))
|
||||
.map(values -> {
|
||||
if (values.size() != 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported multi field values to sort for: " + fieldName
|
||||
+ " with values: " + values);
|
||||
}
|
||||
return values.first();
|
||||
})
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private void checkFieldNameIndexed(String fieldName) {
|
||||
if (!orderedMatches.containsKey(fieldName)) {
|
||||
if (!fieldNames.contains(fieldName)) {
|
||||
throw new IllegalArgumentException("Field name " + fieldName
|
||||
+ " is not indexed, please ensure it added to the index spec before querying");
|
||||
}
|
||||
}
|
||||
|
||||
private TreeSet<String> emptySet() {
|
||||
return new TreeSet<>();
|
||||
}
|
||||
|
||||
private SetMultimap<String, String> createSetMultiMap(
|
||||
Collection<Map.Entry<String, String>> entries) {
|
||||
|
||||
SetMultimap<String, String> multiMap = MultimapBuilder.hashKeys()
|
||||
.hashSetValues()
|
||||
.build();
|
||||
for (Map.Entry<String, String> entry : entries) {
|
||||
multiMap.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return multiMap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package run.halo.app.extension.index;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link KeyComparator}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.12.0
|
||||
*/
|
||||
class KeyComparatorTest {
|
||||
|
||||
@Test
|
||||
void keyComparator() {
|
||||
var comparator = KeyComparator.INSTANCE;
|
||||
String[] strings = {"103", "101", "102", "1011", "1013", "1021", "1022", "1012", "1023"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"101", "102", "103", "1011", "1012", "1013", "1021", "1022", "1023"});
|
||||
|
||||
Arrays.sort(strings, comparator.reversed());
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"1023", "1022", "1021", "1013", "1012", "1011", "103", "102", "101"});
|
||||
|
||||
// but if we use natural order, the result is:
|
||||
Arrays.sort(strings, Comparator.naturalOrder());
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"101", "1011", "1012", "1013", "102", "1021", "1022", "1023", "103"});
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyComparator2() {
|
||||
var comparator = KeyComparator.INSTANCE;
|
||||
String[] strings =
|
||||
{"moment-101", "moment-102", "moment-103", "moment-1011", "moment-1013", "moment-1021",
|
||||
"moment-1022", "moment-1012", "moment-1023"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"moment-101", "moment-102", "moment-103",
|
||||
"moment-1011", "moment-1012", "moment-1013", "moment-1021", "moment-1022",
|
||||
"moment-1023"});
|
||||
|
||||
// date sort
|
||||
strings =
|
||||
new String[] {"2022-01-15", "2022-02-01", "2021-12-25", "2022-01-01", "2022-01-02"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"2021-12-25", "2022-01-01", "2022-01-02", "2022-01-15", "2022-02-01"});
|
||||
|
||||
// alphabet and number sort
|
||||
strings = new String[] {"abc123", "abc45", "abc9", "abc100", "abc20"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"abc9", "abc20", "abc45", "abc100", "abc123"});
|
||||
|
||||
// test for pure alphabet sort
|
||||
strings = new String[] {"xyz", "abc", "def", "abcde", "xyzabc"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"abc", "abcde", "def", "xyz", "xyzabc"});
|
||||
|
||||
// test for empty string
|
||||
strings = new String[] {"", "abc", "123", "xyz"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"", "123", "abc", "xyz"});
|
||||
|
||||
// test for the same string
|
||||
strings = new String[] {"abc", "abc", "abc", "abc"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"abc", "abc", "abc", "abc"});
|
||||
|
||||
// test for null element
|
||||
strings = new String[] {null, "abc", "123", "xyz"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"123", "abc", "xyz", null});
|
||||
}
|
||||
}
|
|
@ -36,16 +36,18 @@ public class AndTest {
|
|||
Map.entry("17", "johnniang")
|
||||
);
|
||||
var entries = Map.of("dept", deptEntry, "age", ageEntry);
|
||||
var indexView = new QueryIndexViewImpl(entries);
|
||||
|
||||
var indexView = new QueryIndexViewImpl(entries);
|
||||
var query = and(equal("dept", "B"), equal("age", "18"));
|
||||
var resultSet = query.matches(indexView);
|
||||
assertThat(resultSet).containsExactly("zhangsan");
|
||||
|
||||
indexView = new QueryIndexViewImpl(entries);
|
||||
query = and(equal("dept", "C"), equal("age", "18"));
|
||||
resultSet = query.matches(indexView);
|
||||
assertThat(resultSet).isEmpty();
|
||||
|
||||
indexView = new QueryIndexViewImpl(entries);
|
||||
query = and(
|
||||
// guqing, halo, lisi, zhangsan
|
||||
or(equal("dept", "A"), equal("dept", "B")),
|
||||
|
@ -55,6 +57,7 @@ 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")),
|
||||
|
@ -64,6 +67,7 @@ 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")),
|
||||
|
@ -76,7 +80,7 @@ public class AndTest {
|
|||
|
||||
@Test
|
||||
void andMatch2() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var query = and(equal("lastName", "Fay"),
|
||||
and(
|
||||
equal("hireDate", "17"),
|
||||
|
|
|
@ -4,7 +4,7 @@ import java.util.Collection;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class EmployeeDataSet {
|
||||
public class IndexViewDataSet {
|
||||
|
||||
/**
|
||||
* Create a {@link QueryIndexView} for employee to test.
|
||||
|
@ -95,4 +95,82 @@ public class EmployeeDataSet {
|
|||
"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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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);
|
||||
}
|
||||
}
|
|
@ -18,19 +18,36 @@ import org.junit.jupiter.api.Test;
|
|||
*/
|
||||
class QueryFactoryTest {
|
||||
|
||||
|
||||
@Test
|
||||
void allTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = all("firstName").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102", "103", "104", "105"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNullTest() {
|
||||
var indexView = IndexViewDataSet.createPostIndexViewWithNullCell();
|
||||
var resultSet = QueryFactory.isNull("publishTime").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102", "103", "104", "108"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isNotNullTest() {
|
||||
var indexView = IndexViewDataSet.createPostIndexViewWithNullCell();
|
||||
var resultSet = QueryFactory.isNotNull("publishTime").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "105", "106", "107"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = equal("lastName", "Fay").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "104", "105"
|
||||
|
@ -39,7 +56,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void equalOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = equalOtherField("managerId", "id").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102", "103"
|
||||
|
@ -48,7 +65,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void notEqualTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = notEqual("lastName", "Fay").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"101", "102", "103"
|
||||
|
@ -57,7 +74,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void notEqualOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = notEqualOtherField("managerId", "id").matches(indexView);
|
||||
// 103 102 is equal
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
|
@ -67,7 +84,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void lessThanTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.lessThan("id", "103").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102"
|
||||
|
@ -76,7 +93,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void lessThanOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.lessThanOtherField("id", "managerId").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101"
|
||||
|
@ -85,7 +102,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void lessThanOrEqualTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.lessThanOrEqual("id", "103").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102", "103"
|
||||
|
@ -94,7 +111,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void lessThanOrEqualOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet =
|
||||
QueryFactory.lessThanOrEqualOtherField("id", "managerId").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
|
@ -104,7 +121,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void greaterThanTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.greaterThan("id", "103").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"104", "105"
|
||||
|
@ -113,7 +130,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void greaterThanOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.greaterThanOtherField("id", "managerId").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"104", "105"
|
||||
|
@ -122,7 +139,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void greaterThanOrEqualTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.greaterThanOrEqual("id", "103").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103", "104", "105"
|
||||
|
@ -131,7 +148,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void greaterThanOrEqualOtherFieldTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet =
|
||||
QueryFactory.greaterThanOrEqualOtherField("id", "managerId").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
|
@ -141,7 +158,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void inTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.in("id", "103", "104").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103", "104"
|
||||
|
@ -150,7 +167,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void inTest2() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.in("lastName", "Fay").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "104", "105"
|
||||
|
@ -159,13 +176,13 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void betweenTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = between("id", "103", "105").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103", "104", "105"
|
||||
);
|
||||
|
||||
indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
resultSet = between("salary", "2000", "2400").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"101", "102", "103"
|
||||
|
@ -174,7 +191,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void betweenLowerExclusive() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet =
|
||||
QueryFactory.betweenLowerExclusive("salary", "2000", "2400").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
|
@ -184,7 +201,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void betweenUpperExclusive() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet =
|
||||
QueryFactory.betweenUpperExclusive("salary", "2000", "2400").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
|
@ -194,7 +211,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void betweenExclusive() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.betweenExclusive("salary", "2000", "2400").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102"
|
||||
|
@ -203,7 +220,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void startsWithTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.startsWith("firstName", "W").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102"
|
||||
|
@ -212,7 +229,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void endsWithTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.endsWith("firstName", "y").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103"
|
||||
|
@ -221,7 +238,7 @@ class QueryFactoryTest {
|
|||
|
||||
@Test
|
||||
void containsTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = QueryFactory.contains("firstName", "i").matches(indexView);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102"
|
||||
|
@ -231,4 +248,4 @@ class QueryFactoryTest {
|
|||
"104", "105"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
package run.halo.app.extension.index.query;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
/**
|
||||
* Tests for {@link QueryIndexViewImpl}.
|
||||
|
@ -12,9 +20,23 @@ import org.junit.jupiter.api.Test;
|
|||
*/
|
||||
class QueryIndexViewImplTest {
|
||||
|
||||
@Test
|
||||
void getAllIdsForFieldTest() {
|
||||
var indexView = IndexViewDataSet.createPostIndexViewWithNullCell();
|
||||
var resultSet = indexView.getAllIdsForField("title");
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102", "103", "104", "105", "106", "107", "108"
|
||||
);
|
||||
|
||||
resultSet = indexView.getAllIdsForField("publishTime");
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "105", "106", "107"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findIdsForFieldValueEqualTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = indexView.findIdsForFieldValueEqual("managerId", "id");
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"102", "103"
|
||||
|
@ -23,13 +45,13 @@ class QueryIndexViewImplTest {
|
|||
|
||||
@Test
|
||||
void findIdsForFieldValueGreaterThanTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", false);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"104", "105"
|
||||
);
|
||||
|
||||
indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
resultSet = indexView.findIdsForFieldValueGreaterThan("id", "managerId", true);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103", "102", "104", "105"
|
||||
|
@ -38,13 +60,13 @@ class QueryIndexViewImplTest {
|
|||
|
||||
@Test
|
||||
void findIdsForFieldValueGreaterThanTest2() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", false);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101"
|
||||
);
|
||||
|
||||
indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
resultSet = indexView.findIdsForFieldValueGreaterThan("managerId", "id", true);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102", "103"
|
||||
|
@ -53,13 +75,13 @@ class QueryIndexViewImplTest {
|
|||
|
||||
@Test
|
||||
void findIdsForFieldValueLessThanTest() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", false);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101"
|
||||
);
|
||||
|
||||
indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
resultSet = indexView.findIdsForFieldValueLessThan("id", "managerId", true);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"100", "101", "102", "103"
|
||||
|
@ -68,16 +90,129 @@ class QueryIndexViewImplTest {
|
|||
|
||||
@Test
|
||||
void findIdsForFieldValueLessThanTest2() {
|
||||
var indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
var indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
var resultSet = indexView.findIdsForFieldValueLessThan("managerId", "id", false);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"104", "105"
|
||||
);
|
||||
|
||||
indexView = EmployeeDataSet.createEmployeeIndexView();
|
||||
indexView = IndexViewDataSet.createEmployeeIndexView();
|
||||
resultSet = indexView.findIdsForFieldValueLessThan("managerId", "id", true);
|
||||
assertThat(resultSet).containsExactlyInAnyOrder(
|
||||
"103", "102", "104", "105"
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class SortTest {
|
||||
@Test
|
||||
void testSortByUnsorted() {
|
||||
Collection<Map.Entry<String, String>> entries = List.of(
|
||||
Map.entry("Item1", "Item1"),
|
||||
Map.entry("Item2", "Item2")
|
||||
);
|
||||
var indexView = new QueryIndexViewImpl(Map.of("field1", entries));
|
||||
var sort = Sort.unsorted();
|
||||
|
||||
List<String> sortedList = indexView.sortBy(sort);
|
||||
assertThat(sortedList).isEqualTo(List.of("Item1", "Item2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortBySortedAscending() {
|
||||
var indexEntries = new HashMap<String, Collection<Map.Entry<String, String>>>();
|
||||
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<String> sortedList = indexView.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsExactly("Item1", "Item2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortBySortedDescending() {
|
||||
var indexEntries = new HashMap<String, Collection<Map.Entry<String, String>>>();
|
||||
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<String> sortedList = indexView.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsExactly("Item2", "Item1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByMultipleFields() {
|
||||
var indexEntries = new LinkedHashMap<String, Collection<Map.Entry<String, String>>>();
|
||||
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<String> sortedList = indexView.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsExactly("Item2", "Item3", "Item1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByWithMissingFieldInMap() {
|
||||
var indexEntries = new LinkedHashMap<String, Collection<Map.Entry<String, String>>>();
|
||||
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<String, Collection<Map.Entry<String, String>>>();
|
||||
|
||||
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);
|
||||
|
||||
/*
|
||||
* <pre>
|
||||
* Row Key | field1 | field2 | field3
|
||||
* -------|-------|-------|-------
|
||||
* John | John | |
|
||||
* Bob | Bob | |
|
||||
* Alice | Alice | |
|
||||
* David | | David |
|
||||
* Eva | | Eva |
|
||||
* Frank | | Frank |
|
||||
* George | | | George
|
||||
* Helen | | | Helen
|
||||
* Ivy | | | Ivy
|
||||
* </pre>
|
||||
*/
|
||||
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<String> sortedList = indexView.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsSequence("John", "Bob", "Alice", "David", "Eva", "Frank",
|
||||
"George", "Helen", "Ivy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package run.halo.app.extension.index;
|
||||
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.ListMultimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import java.util.Collection;
|
||||
|
@ -42,39 +41,6 @@ public class IndexEntryImpl implements IndexEntry {
|
|||
return KeyComparator.INSTANCE.reversed();
|
||||
}
|
||||
|
||||
static class KeyComparator implements Comparator<String> {
|
||||
public static final KeyComparator INSTANCE = new KeyComparator();
|
||||
|
||||
@Override
|
||||
public int compare(String a, String b) {
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
while (i < a.length() && j < b.length()) {
|
||||
if (Character.isDigit(a.charAt(i)) && Character.isDigit(b.charAt(j))) {
|
||||
// handle number part
|
||||
int num1 = 0;
|
||||
int num2 = 0;
|
||||
while (i < a.length() && Character.isDigit(a.charAt(i))) {
|
||||
num1 = num1 * 10 + (a.charAt(i++) - '0');
|
||||
}
|
||||
while (j < b.length() && Character.isDigit(b.charAt(j))) {
|
||||
num2 = num2 * 10 + (b.charAt(j++) - '0');
|
||||
}
|
||||
if (num1 != num2) {
|
||||
return num1 - num2;
|
||||
}
|
||||
} else if (a.charAt(i) != b.charAt(j)) {
|
||||
// handle non-number part
|
||||
return a.charAt(i) - b.charAt(j);
|
||||
} else {
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return a.length() - b.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acquireReadLock() {
|
||||
this.rwl.readLock().lock();
|
||||
|
@ -151,7 +117,10 @@ public class IndexEntryImpl implements IndexEntry {
|
|||
public Collection<Map.Entry<String, String>> immutableEntries() {
|
||||
readLock.lock();
|
||||
try {
|
||||
return ImmutableListMultimap.copyOf(indexKeyObjectNamesMap).entries();
|
||||
// Copy to a new list to avoid ConcurrentModificationException
|
||||
return indexKeyObjectNamesMap.entries().stream()
|
||||
.map(entry -> Map.entry(entry.getKey(), entry.getValue()))
|
||||
.toList();
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
|
|
|
@ -2,20 +2,17 @@ package run.halo.app.extension.index;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.PriorityQueue;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
@ -23,7 +20,7 @@ import run.halo.app.extension.GroupVersionKind;
|
|||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.index.query.All;
|
||||
import run.halo.app.extension.index.query.QueryIndexViewImpl;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.extension.router.selector.LabelSelector;
|
||||
|
@ -134,8 +131,9 @@ public class IndexedQueryEngineImpl implements IndexedQueryEngine {
|
|||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start("build index entry map");
|
||||
var fieldPathEntryMap = fieldPathIndexEntryMap(indexer);
|
||||
stopWatch.stop();
|
||||
var primaryEntry = getIndexEntry(PrimaryKeySpecUtils.PRIMARY_INDEX_NAME, fieldPathEntryMap);
|
||||
stopWatch.stop();
|
||||
|
||||
// O(n) time complexity
|
||||
stopWatch.start("retrieve all metadata names");
|
||||
var allMetadataNames = new ArrayList<String>();
|
||||
|
@ -147,36 +145,38 @@ public class IndexedQueryEngineImpl implements IndexedQueryEngine {
|
|||
}
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("build index view");
|
||||
var indexViewMap = new HashMap<String, Collection<Map.Entry<String, String>>>();
|
||||
for (Map.Entry<String, IndexEntry> entry : fieldPathEntryMap.entrySet()) {
|
||||
indexViewMap.put(entry.getKey(), entry.getValue().immutableEntries());
|
||||
}
|
||||
// TODO optimize build indexView time
|
||||
var indexView = new QueryIndexViewImpl(indexViewMap);
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("retrieve matched metadata names");
|
||||
var hasLabelSelector = hasLabelSelector(options.getLabelSelector());
|
||||
final List<String> matchedByLabels = hasLabelSelector
|
||||
? retrieveForLabelMatchers(options.getLabelSelector().getMatchers(), fieldPathEntryMap,
|
||||
allMetadataNames)
|
||||
: allMetadataNames;
|
||||
indexView.removeByIdNotIn(new TreeSet<>(matchedByLabels));
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("retrieve matched metadata names by fields");
|
||||
final var hasFieldSelector = hasFieldSelector(options.getFieldSelector());
|
||||
var matchedByFields = hasFieldSelector
|
||||
? retrieveForFieldSelector(options.getFieldSelector().query(), fieldPathEntryMap)
|
||||
: allMetadataNames;
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("merge result");
|
||||
List<String> foundObjectKeys;
|
||||
if (!hasLabelSelector && !hasFieldSelector) {
|
||||
foundObjectKeys = allMetadataNames;
|
||||
} else if (!hasLabelSelector) {
|
||||
foundObjectKeys = matchedByFields;
|
||||
} else {
|
||||
foundObjectKeys = intersection(matchedByFields, matchedByLabels);
|
||||
if (hasFieldSelector) {
|
||||
var fieldSelector = options.getFieldSelector();
|
||||
var query = fieldSelector.query();
|
||||
var resultSet = query.matches(indexView);
|
||||
indexView.removeByIdNotIn(resultSet);
|
||||
}
|
||||
stopWatch.stop();
|
||||
|
||||
stopWatch.start("sort result");
|
||||
ResultSorter resultSorter = new ResultSorter(fieldPathEntryMap, foundObjectKeys);
|
||||
var result = resultSorter.sortBy(sort);
|
||||
var result = indexView.sortBy(sort);
|
||||
stopWatch.stop();
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Retrieve result from indexer, {}", stopWatch.prettyPrint());
|
||||
}
|
||||
|
@ -188,109 +188,8 @@ public class IndexedQueryEngineImpl implements IndexedQueryEngine {
|
|||
}
|
||||
|
||||
boolean hasFieldSelector(FieldSelector fieldSelector) {
|
||||
return fieldSelector != null && fieldSelector.query() != null;
|
||||
}
|
||||
|
||||
List<String> retrieveForFieldSelector(Query query, Map<String, IndexEntry> fieldPathEntryMap) {
|
||||
Map<String, Collection<Map.Entry<String, String>>> indexView = new HashMap<>();
|
||||
for (Map.Entry<String, IndexEntry> entry : fieldPathEntryMap.entrySet()) {
|
||||
indexView.put(entry.getKey(), entry.getValue().immutableEntries());
|
||||
}
|
||||
// TODO optimize build indexView time
|
||||
var queryIndexView = new QueryIndexViewImpl(indexView);
|
||||
var resultSet = query.matches(queryIndexView);
|
||||
return new ArrayList<>(resultSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given list by the given {@link Sort}.
|
||||
*/
|
||||
static class ResultSorter {
|
||||
private final Map<String, IndexEntry> fieldPathEntryMap;
|
||||
private final List<String> list;
|
||||
|
||||
public ResultSorter(Map<String, IndexEntry> fieldPathEntryMap, List<String> list) {
|
||||
this.fieldPathEntryMap = fieldPathEntryMap;
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public List<String> sortBy(@NonNull Sort sort) {
|
||||
if (sort.isUnsorted()) {
|
||||
return list;
|
||||
}
|
||||
var sortedLists = new ArrayList<List<String>>();
|
||||
for (Sort.Order order : sort) {
|
||||
var indexEntry = fieldPathEntryMap.get(order.getProperty());
|
||||
if (indexEntry == null) {
|
||||
throwNotIndexedException(order.getProperty());
|
||||
}
|
||||
var set = new HashSet<>(list);
|
||||
var objectNames = new ArrayList<String>();
|
||||
indexEntry.acquireReadLock();
|
||||
try {
|
||||
for (var entry : indexEntry.entries()) {
|
||||
var objectName = entry.getValue();
|
||||
if (set.contains(objectName)) {
|
||||
objectNames.add(objectName);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
indexEntry.releaseReadLock();
|
||||
}
|
||||
var indexOrder = indexEntry.getIndexDescriptor().getSpec().getOrder();
|
||||
var asc = IndexSpec.OrderType.ASC.equals(indexOrder);
|
||||
if (asc != order.isAscending()) {
|
||||
Collections.reverse(objectNames);
|
||||
}
|
||||
sortedLists.add(objectNames);
|
||||
}
|
||||
return mergeSortedLists(sortedLists);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Merge the given sorted lists into one sorted list.</p>
|
||||
* <p>The time complexity is O(n * log(m)), n is the number of all elements in the
|
||||
* sortedLists, m is the number of sortedLists.</p>
|
||||
*/
|
||||
private List<String> mergeSortedLists(List<List<String>> sortedLists) {
|
||||
List<String> result = new ArrayList<>();
|
||||
// Use a priority queue to store the current element of each list and its index in
|
||||
// the list
|
||||
PriorityQueue<Pair> minHeap = new PriorityQueue<>(
|
||||
Comparator.comparing(pair -> pair.value));
|
||||
|
||||
// Initialize the priority queue and add the first element of each list to the queue
|
||||
for (int i = 0; i < sortedLists.size(); i++) {
|
||||
if (!sortedLists.get(i).isEmpty()) {
|
||||
minHeap.add(new Pair(i, 0, sortedLists.get(i).get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
while (!minHeap.isEmpty()) {
|
||||
Pair current = minHeap.poll();
|
||||
result.add(current.value());
|
||||
|
||||
// Add the next element of this list to the priority queue
|
||||
if (current.indexInList() + 1 < sortedLists.get(current.listIndex()).size()) {
|
||||
var list = sortedLists.get(current.listIndex());
|
||||
minHeap.add(new Pair(current.listIndex(),
|
||||
current.indexInList() + 1,
|
||||
list.get(current.indexInList() + 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>A pair of element and its position in the original list.</p>
|
||||
* <pre>
|
||||
* listIndex: column index.
|
||||
* indexInList: element index in the list.
|
||||
* value: element value.
|
||||
* </pre>
|
||||
*/
|
||||
private record Pair(int listIndex, int indexInList, String value) {
|
||||
}
|
||||
return fieldSelector != null
|
||||
&& fieldSelector.query() != null
|
||||
&& !(fieldSelector.query() instanceof All);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package run.halo.app.extension.index;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -108,66 +105,4 @@ class IndexEntryImplTest {
|
|||
Map.entry("slug-4", "fake-name-3"));
|
||||
assertThat(entry2.indexedKeys()).containsSequence("slug-1", "slug-2", "slug-3", "slug-4");
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyComparator() {
|
||||
var comparator = IndexEntryImpl.KeyComparator.INSTANCE;
|
||||
String[] strings = {"103", "101", "102", "1011", "1013", "1021", "1022", "1012", "1023"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"101", "102", "103", "1011", "1012", "1013", "1021", "1022", "1023"});
|
||||
|
||||
Arrays.sort(strings, comparator.reversed());
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"1023", "1022", "1021", "1013", "1012", "1011", "103", "102", "101"});
|
||||
|
||||
// but if we use natural order, the result is:
|
||||
Arrays.sort(strings, Comparator.naturalOrder());
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"101", "1011", "1012", "1013", "102", "1021", "1022", "1023", "103"});
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyComparator2() {
|
||||
var comparator = IndexEntryImpl.KeyComparator.INSTANCE;
|
||||
String[] strings =
|
||||
{"moment-101", "moment-102", "moment-103", "moment-1011", "moment-1013", "moment-1021",
|
||||
"moment-1022", "moment-1012", "moment-1023"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"moment-101", "moment-102", "moment-103",
|
||||
"moment-1011", "moment-1012", "moment-1013", "moment-1021", "moment-1022",
|
||||
"moment-1023"});
|
||||
|
||||
// date sort
|
||||
strings =
|
||||
new String[] {"2022-01-15", "2022-02-01", "2021-12-25", "2022-01-01", "2022-01-02"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"2021-12-25", "2022-01-01", "2022-01-02", "2022-01-15", "2022-02-01"});
|
||||
|
||||
// alphabet and number sort
|
||||
strings = new String[] {"abc123", "abc45", "abc9", "abc100", "abc20"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(
|
||||
new String[] {"abc9", "abc20", "abc45", "abc100", "abc123"});
|
||||
|
||||
// test for pure alphabet sort
|
||||
strings = new String[] {"xyz", "abc", "def", "abcde", "xyzabc"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"abc", "abcde", "def", "xyz", "xyzabc"});
|
||||
|
||||
// test for empty string
|
||||
strings = new String[] {"", "abc", "123", "xyz"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"", "123", "abc", "xyz"});
|
||||
|
||||
// test for the same string
|
||||
strings = new String[] {"abc", "abc", "abc", "abc"};
|
||||
Arrays.sort(strings, comparator);
|
||||
assertThat(strings).isEqualTo(new String[] {"abc", "abc", "abc", "abc"});
|
||||
|
||||
// test for null element
|
||||
assertThatThrownBy(() -> Arrays.sort(new String[] {null, "abc", "123", "xyz"}, comparator))
|
||||
.isInstanceOf(NullPointerException.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ 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;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
|
@ -13,10 +12,8 @@ import static org.mockito.Mockito.verify;
|
|||
import static org.mockito.Mockito.when;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
@ -249,237 +246,4 @@ class IndexedQueryEngineImplTest {
|
|||
static class DemoExtension extends AbstractExtension {
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ResultSorterTest {
|
||||
|
||||
@Test
|
||||
void testSortByUnsorted() {
|
||||
var fieldPathEntryMap = Map.<String, IndexEntry>of();
|
||||
List<String> list = new ArrayList<>();
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
Sort sort = Sort.unsorted();
|
||||
list.add("Item1");
|
||||
list.add("Item2");
|
||||
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsExactly("Item1", "Item2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortBySortedAscending() {
|
||||
final var fieldPathEntryMap = new HashMap<String, IndexEntry>();
|
||||
var entry = mock(IndexEntry.class);
|
||||
when(entry.entries()).thenReturn(List.of(
|
||||
Map.entry("key2", "Item2"),
|
||||
Map.entry("key1", "Item1")
|
||||
));
|
||||
lenient().when(entry.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field1")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
fieldPathEntryMap.put("field1", entry);
|
||||
|
||||
var list = new ArrayList<>(Arrays.asList("Item1", "Item2"));
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
var sort = Sort.by(Sort.Order.asc("field1"));
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsExactly("Item1", "Item2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortBySortedDescending() {
|
||||
final var fieldPathEntryMap = new HashMap<String, IndexEntry>();
|
||||
var entry = mock(IndexEntry.class);
|
||||
when(entry.entries()).thenReturn(List.of(
|
||||
Map.entry("key1", "Item1"),
|
||||
Map.entry("key2", "Item2")
|
||||
));
|
||||
var indexDescriptor = new IndexDescriptor(new IndexSpec()
|
||||
.setName("field1")
|
||||
.setOrder(IndexSpec.OrderType.ASC));
|
||||
when(entry.getIndexDescriptor()).thenReturn(indexDescriptor);
|
||||
fieldPathEntryMap.put("field1", entry);
|
||||
|
||||
var list = new ArrayList<String>();
|
||||
list.add("Item1");
|
||||
list.add("Item2");
|
||||
Sort sort = Sort.by(Sort.Order.desc("field1"));
|
||||
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsSequence("Item2", "Item1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByMultipleFields() {
|
||||
final var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
|
||||
var entry1 = mock(IndexEntry.class);
|
||||
when(entry1.entries()).thenReturn(List.of(
|
||||
Map.entry("k3", "Item3"),
|
||||
Map.entry("k2", "Item2")
|
||||
));
|
||||
when(entry1.getIndexDescriptor()).thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field1")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
|
||||
var entry2 = mock(IndexEntry.class);
|
||||
lenient().when(entry2.entries()).thenReturn(List.of(
|
||||
Map.entry("k1", "Item1"),
|
||||
Map.entry("k3", "Item3")
|
||||
));
|
||||
lenient().when(entry2.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field2")
|
||||
.setOrder(IndexSpec.OrderType.ASC)));
|
||||
|
||||
fieldPathEntryMap.put("field1", entry1);
|
||||
fieldPathEntryMap.put("field2", entry2);
|
||||
|
||||
final Sort sort = Sort.by(Sort.Order.asc("field1"),
|
||||
Sort.Order.desc("field2"));
|
||||
|
||||
var list = new ArrayList<>(Arrays.asList("Item1", "Item2", "Item3"));
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsSequence("Item2", "Item3", "Item1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByMultipleFields2() {
|
||||
final var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
|
||||
var entry1 = mock(IndexEntry.class);
|
||||
when(entry1.entries()).thenReturn(List.of(
|
||||
Map.entry("John", "John"),
|
||||
Map.entry("Bob", "Bob"),
|
||||
Map.entry("Alice", "Alice")
|
||||
));
|
||||
lenient().when(entry1.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field1")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
|
||||
var entry2 = mock(IndexEntry.class);
|
||||
when(entry2.entries()).thenReturn(List.of(
|
||||
Map.entry("David", "David"),
|
||||
Map.entry("Eva", "Eva"),
|
||||
Map.entry("Frank", "Frank")
|
||||
));
|
||||
lenient().when(entry2.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field2")
|
||||
.setOrder(IndexSpec.OrderType.ASC)));
|
||||
|
||||
var entry3 = mock(IndexEntry.class);
|
||||
lenient().when(entry3.entries()).thenReturn(List.of(
|
||||
Map.entry("George", "George"),
|
||||
Map.entry("Helen", "Helen"),
|
||||
Map.entry("Ivy", "Ivy")
|
||||
));
|
||||
lenient().when(entry3.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field3")
|
||||
.setOrder(IndexSpec.OrderType.ASC)));
|
||||
|
||||
fieldPathEntryMap.put("field1", entry1);
|
||||
fieldPathEntryMap.put("field2", entry2);
|
||||
fieldPathEntryMap.put("field3", entry3);
|
||||
|
||||
var list = new ArrayList<>(
|
||||
Arrays.asList("Alice", "Bob", "Ivy", "Eva", "George"));
|
||||
final Sort sort = Sort.by(Sort.Order.desc("field1"),
|
||||
Sort.Order.asc("field2"),
|
||||
Sort.Order.asc("field3"));
|
||||
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
|
||||
assertThat(sortedList).containsSequence("Bob", "Alice", "Eva", "George", "Ivy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByWithMissingFieldInMap() {
|
||||
var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
var list = new ArrayList<>(Arrays.asList("Item1", "Item2"));
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
|
||||
Sort sort = Sort.by(Sort.Order.asc("missingField"));
|
||||
assertThatThrownBy(() -> sorter.sortBy(sort))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage(
|
||||
"No index found for fieldPath: missingField, make sure you have created an "
|
||||
+ "index for this field.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByWithEmptyMap() {
|
||||
var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
var entry = mock(IndexEntry.class);
|
||||
when(entry.entries()).thenReturn(List.of());
|
||||
lenient().when(entry.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
fieldPathEntryMap.put("field", entry);
|
||||
|
||||
var list = new ArrayList<>(Arrays.asList("Item1", "Item2"));
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
Sort sort = Sort.by(Sort.Order.asc("field"));
|
||||
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
assertThat(sortedList).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByWithEmptyList() {
|
||||
var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
var entry = mock(IndexEntry.class);
|
||||
when(entry.entries()).thenReturn(List.of(
|
||||
Map.entry("John", "John"),
|
||||
Map.entry("Bob", "Bob")
|
||||
));
|
||||
lenient().when(entry.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
fieldPathEntryMap.put("field", entry);
|
||||
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, List.of());
|
||||
Sort sort = Sort.by(Sort.Order.asc("field"));
|
||||
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
assertThat(sortedList).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSortByWithItemNotInIndex() {
|
||||
var fieldPathEntryMap = new LinkedHashMap<String, IndexEntry>();
|
||||
var entry = mock(IndexEntry.class);
|
||||
when(entry.entries()).thenReturn(List.of(
|
||||
Map.entry("Item2", "Item2"),
|
||||
Map.entry("Item1", "Item1")
|
||||
));
|
||||
lenient().when(entry.getIndexDescriptor())
|
||||
.thenReturn(new IndexDescriptor(new IndexSpec()
|
||||
.setName("field")
|
||||
.setOrder(IndexSpec.OrderType.DESC)));
|
||||
|
||||
fieldPathEntryMap.put("field", entry);
|
||||
|
||||
// Item3 is not in the index
|
||||
var list = new ArrayList<>(Arrays.asList("Item1", "Item3"));
|
||||
var sorter = new IndexedQueryEngineImpl.ResultSorter(fieldPathEntryMap, list);
|
||||
Sort sort = Sort.by(Sort.Order.asc("field"));
|
||||
|
||||
List<String> sortedList = sorter.sortBy(sort);
|
||||
assertThat(sortedList).containsExactly("Item1");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue