Skip to content

Commit

Permalink
refactor: index mechanism to enhance overall performance
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Jun 5, 2024
1 parent ba96118 commit 2aa7ddf
Show file tree
Hide file tree
Showing 40 changed files with 1,906 additions and 1,126 deletions.
17 changes: 16 additions & 1 deletion api/src/main/java/run/halo/app/extension/ListOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,19 @@
public class ListOptions {
private LabelSelector labelSelector;
private FieldSelector fieldSelector;
}

@Override
public String toString() {
var sb = new StringBuilder();
if (fieldSelector != null) {
sb.append("fieldSelector: ").append(fieldSelector.query());
}
if (labelSelector != null) {
if (!sb.isEmpty()) {
sb.append(", ");
}
sb.append("labelSelector: ").append(labelSelector);
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class IndexDescriptor {
private final IndexSpec spec;

/**
* Record whether the index is ready, managed by {@link IndexBuilder}.
* Record whether the index is ready, managed by {@code IndexBuilder}.
*/
private boolean ready;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,27 @@ public interface IndexEntry {
Collection<Map.Entry<String, String>> entries();

/**
* Returns the immutable entries of this entry in order, it is safe to modify the returned
* result, but extra cost is made.
*
* @return immutable entries of this entry.
* <p>Returns the position of the object name in the indexed attribute value mapping for
* sorting.</p>
* For example:
* <pre>
* metadata.name | field1
* ------------- | ------
* foo | 1
* bar | 2
* baz | 2
* </pre>
* "field1" is the indexed attribute, and the position of the object name in the indexed
* attribute
* value mapping for sorting is:
* <pre>
* foo -> 0
* bar -> 1
* baz -> 1
* </pre>
* "bar" and "baz" have the same value, so they have the same position.
*/
Collection<Map.Entry<String, String>> immutableEntries();
Map<String, Integer> getIdPositionMap();

/**
* Returns the object names of this entry in order.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package run.halo.app.extension.index;

import java.util.Collection;
import java.util.NavigableSet;
import java.util.Set;

public interface IndexEntryOperator {

/**
* Search all values that key less than the target key.
*
* @param key target key
* @param orEqual whether to include the value of the target key
* @return object names that key less than the target key
*/
NavigableSet<String> lessThan(String key, boolean orEqual);

/**
* Search all values that key greater than the target key.
*
* @param key target key
* @param orEqual whether to include the value of the target key
* @return object names that key greater than the target key
*/
NavigableSet<String> greaterThan(String key, boolean orEqual);

/**
* Search all values that key in the range of [start, end].
*
* @param start start key
* @param end end key
* @param startInclusive whether to include the value of the start key
* @param endInclusive whether to include the value of the end key
* @return object names that key in the range of [start, end]
*/
NavigableSet<String> range(String start, String end, boolean startInclusive,
boolean endInclusive);

/**
* Find all values that key equals to the target key.
*
* @param key target key
* @return object names that key equals to the target key
*/
NavigableSet<String> find(String key);

NavigableSet<String> findIn(Collection<String> keys);

/**
* Get all values in the index entry.
*
* @return a set of all object names
*/
Set<String> getValues();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package run.halo.app.extension.index;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 org.springframework.util.Assert;

public class IndexEntryOperatorImpl implements IndexEntryOperator {
private final IndexEntry indexEntry;

public IndexEntryOperatorImpl(IndexEntry indexEntry) {
this.indexEntry = indexEntry;
}

private static NavigableSet<String> createNavigableSet() {
return new TreeSet<>(KeyComparator.INSTANCE);
}

@Override
public NavigableSet<String> lessThan(String key, boolean orEqual) {
Assert.notNull(key, "Key must not be null.");
indexEntry.acquireReadLock();
try {
var sortedEntries = sortedEntries();
var index = binarySearch(sortedEntries, key);

var resultSet = createNavigableSet();

if (index < 0) {
// if key not found, get the insertion point(-index-1)
index = -index - 1;
} else if (orEqual) {
// key exists and includes equal, index+1
index++;
}

// Add all values if key less than the target key (or including the target key) to the
// result set
for (int i = 0; i < index; i++) {
resultSet.add(sortedEntries.get(i).getValue());
}
return resultSet;
} finally {
indexEntry.releaseReadLock();
}
}

@Override
public NavigableSet<String> greaterThan(String key, boolean orEqual) {
Assert.notNull(key, "Key must not be null.");
indexEntry.acquireReadLock();
try {
var sortedEntries = sortedEntries();
var index = binarySearch(sortedEntries, key);

var resultSet = createNavigableSet();

if (index < 0) {
// key does not exist, get the insertion point (-index-1)
index = -index - 1;
} else if (!orEqual) {
// key exists but does not include equal, index+1
index++;
}

for (int i = index; i < sortedEntries.size(); i++) {
resultSet.add(sortedEntries.get(i).getValue());
}

return resultSet;
} finally {
indexEntry.releaseReadLock();
}
}

@Override
public NavigableSet<String> range(String start, String end, boolean startInclusive,
boolean endInclusive) {
Assert.notNull(start, "The start must not be null.");
Assert.notNull(end, "The end must not be null.");
indexEntry.acquireReadLock();
try {
var sortedEntries = sortedEntries();

var resultSet = createNavigableSet();

for (Map.Entry<String, String> entry : sortedEntries) {
String key = entry.getKey();
boolean greaterThanStart =
startInclusive ? key.compareTo(start) >= 0 : key.compareTo(start) > 0;
boolean lessThanEnd =
endInclusive ? key.compareTo(end) <= 0 : key.compareTo(end) < 0;

if (greaterThanStart && lessThanEnd) {
resultSet.add(entry.getValue());
}
}
return resultSet;
} finally {
indexEntry.releaseReadLock();
}
}

@Override
public NavigableSet<String> find(String key) {
Assert.notNull(key, "The key must not be null.");
indexEntry.acquireReadLock();
try {
var resultSet = createNavigableSet();

for (Map.Entry<String, String> entry : sortedEntries()) {
if (entry.getKey().equals(key)) {
resultSet.add(entry.getValue());
}
}

return resultSet;
} finally {
indexEntry.releaseReadLock();
}
}

@Override
public NavigableSet<String> findIn(Collection<String> keys) {
if (keys == null || keys.isEmpty()) {
return createNavigableSet();
}
var keysToSearch = new HashSet<>(keys);
indexEntry.acquireReadLock();
try {
var resultSet = createNavigableSet();

for (Map.Entry<String, String> entry : sortedEntries()) {
if (keysToSearch.contains(entry.getKey())) {
resultSet.add(entry.getValue());
}
}

return resultSet;
} finally {
indexEntry.releaseReadLock();
}
}

@Override
public Set<String> getValues() {
indexEntry.acquireReadLock();
try {
Set<String> uniqueValues = new LinkedHashSet<>();
for (Map.Entry<String, String> entry : sortedEntries()) {
uniqueValues.add(entry.getValue());
}
return uniqueValues;
} finally {
indexEntry.releaseReadLock();
}
}

int binarySearch(List<Map.Entry<String, String>> sortedEntries, String key) {
var searchEntry = new AbstractMap.SimpleEntry<String, String>(key, null);
return Collections.binarySearch(sortedEntries, searchEntry,
Map.Entry.comparingByKey()
);
}

private ArrayList<Map.Entry<String, String>> sortedEntries() {
//index entries are sorted by key, so we can use the entries directly
return new ArrayList<>(indexEntry.entries());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Iterator;
import java.util.function.Function;
import org.springframework.lang.NonNull;
import run.halo.app.extension.Extension;

/**
Expand All @@ -20,7 +21,7 @@ public interface Indexer {
/**
* <p>Index the specified {@link Extension} by {@link IndexDescriptor}s.</p>
* <p>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.</p>
*
Expand All @@ -33,7 +34,7 @@ public interface Indexer {
* <p>Update indexes for the specified {@link Extension} by {@link IndexDescriptor}s.</p>
* <p>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.</p>
*
* @param extension the {@link Extension} to be updated
Expand Down Expand Up @@ -73,19 +74,33 @@ public interface Indexer {
*/
void removeIndexRecords(Function<IndexDescriptor, Boolean> matchFn);

/**
* <p>Get the {@link IndexEntry} by index name if found and ready.</p>
*
* @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);

/**
* <p>Gets an iterator over all the ready {@link IndexEntry}s, in no particular order.</p>
*
* @return an iterator over all the ready {@link IndexEntry}s
* @link {@link IndexDescriptor#isReady()}
* @see IndexDescriptor#isReady()
*/
Iterator<IndexEntry> readyIndexesIterator();

/**
* <p>Gets an iterator over all the {@link IndexEntry}s, in no particular order.</p>
*
* @return an iterator over all the {@link IndexEntry}s
* @link {@link IndexDescriptor#isReady()}
* @see IndexDescriptor#isReady()
*/
Iterator<IndexEntry> allIndexesIterator();

void acquireReadLock();

void releaseReadLock();
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public All(String fieldName) {

@Override
public NavigableSet<String> matches(QueryIndexView indexView) {
return indexView.getAllIdsForField(fieldName);
return indexView.getIdsForField(fieldName);
}

@Override
public String toString() {
return fieldName + "!= null";
}
}
7 changes: 7 additions & 0 deletions api/src/main/java/run/halo/app/extension/index/query/And.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.NavigableSet;
import java.util.stream.Collectors;

public class And extends LogicalQuery {

Expand Down Expand Up @@ -33,4 +34,10 @@ public NavigableSet<String> matches(QueryIndexView indexView) {
}
return resultSet == null ? Sets.newTreeSet() : resultSet;
}

@Override
public String toString() {
return "(" + childQueries.stream().map(Query::toString)
.collect(Collectors.joining(" AND ")) + ")";
}
}
Loading

0 comments on commit 2aa7ddf

Please sign in to comment.