Skip to content

Commit

Permalink
refactor: using index mechanisms to optimize comment queries (#5453)
Browse files Browse the repository at this point in the history
#### What type of PR is this?
/kind improvement
/area core
/milestone 2.14.x

#### What this PR does / why we need it:
使用索引机制优化评论数据查询以提高效率

how to test it?
- 测试 console 评论列表和筛选条件是否正确
- 测试主题端评论显示是否正确

#### Does this PR introduce a user-facing change?
```release-note
使用索引机制优化评论数据查询以提高效率
```
  • Loading branch information
guqing committed Mar 8, 2024
1 parent 92f2229 commit 20d80f8
Show file tree
Hide file tree
Showing 15 changed files with 542 additions and 949 deletions.
Expand Up @@ -116,6 +116,10 @@ public static class CommentOwner {
public String getAnnotation(String key) {
return annotations == null ? null : annotations.get(key);
}

public static String ownerIdentity(String kind, String name) {
return kind + "#" + name;
}
}

@Data
Expand All @@ -132,6 +136,10 @@ public static class CommentStatus {
private Boolean hasNewReply;
}

public static String toSubjectRefKey(Ref subjectRef) {
return subjectRef.getGroup() + "/" + subjectRef.getKind() + "/" + subjectRef.getName();
}

public static int getUnreadReplyCount(List<Reply> replies, Instant lastReadTime) {
if (CollectionUtils.isEmpty(replies)) {
return 0;
Expand Down
Expand Up @@ -120,7 +120,7 @@ public static Query and(Collection<Query> queries) {
return new And(queries);
}

public static And and(Query query1, Query query2) {
public static Query and(Query query1, Query query2) {
Collection<Query> queries = Arrays.asList(query1, query2);
return new And(queries);
}
Expand Down
199 changes: 26 additions & 173 deletions application/src/main/java/run/halo/app/content/comment/CommentQuery.java
@@ -1,29 +1,24 @@
package run.halo.app.content.comment;

import static java.util.Comparator.comparing;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;

import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.Comparators;
import run.halo.app.extension.Extension;
import run.halo.app.extension.Ref;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.IListRequest;
import run.halo.app.extension.router.selector.FieldSelector;

/**
* Query criteria for comment list.
Expand All @@ -34,12 +29,6 @@
public class CommentQuery extends IListRequest.QueryListRequest {

private final ServerWebExchange exchange;
static final Function<Comment, Instant> LAST_REPLY_TIME_FUNC =
comment -> {
Instant lastReplyTime = comment.getStatusOrDefault().getLastReplyTime();
return Optional.ofNullable(lastReplyTime)
.orElse(comment.getSpec().getCreationTime());
};

public CommentQuery(ServerRequest request) {
super(request.queryParams());
Expand All @@ -52,26 +41,6 @@ public String getKeyword() {
return StringUtils.isBlank(keyword) ? null : keyword;
}

@Schema(description = "Comments approved.")
public Boolean getApproved() {
return convertBooleanOrNull(queryParams.getFirst("approved"));
}

@Schema(description = "The comment is hidden from the theme side.")
public Boolean getHidden() {
return convertBooleanOrNull(queryParams.getFirst("hidden"));
}

@Schema(description = "Send notifications when there are new replies.")
public Boolean getAllowNotification() {
return convertBooleanOrNull(queryParams.getFirst("allowNotification"));
}

@Schema(description = "Comment top display.")
public Boolean getTop() {
return convertBooleanOrNull(queryParams.getFirst("top"));
}

@Schema(description = "Commenter kind.")
public String getOwnerKind() {
String ownerKind = queryParams.getFirst("ownerKind");
Expand All @@ -84,18 +53,6 @@ public String getOwnerName() {
return StringUtils.isBlank(ownerName) ? null : ownerName;
}

@Schema(description = "Comment subject kind.")
public String getSubjectKind() {
String subjectKind = queryParams.getFirst("subjectKind");
return StringUtils.isBlank(subjectKind) ? null : subjectKind;
}

@Schema(description = "Comment subject name.")
public String getSubjectName() {
String subjectName = queryParams.getFirst("subjectName");
return StringUtils.isBlank(subjectName) ? null : subjectName;
}

@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
Expand All @@ -104,139 +61,35 @@ public String getSubjectName() {
implementation = String.class,
example = "creationTimestamp,desc"))
public Sort getSort() {
return SortResolver.defaultInstance.resolve(exchange);
var sort = SortResolver.defaultInstance.resolve(exchange);
return sort.and(Sort.by("spec.creationTime", "metadata.name").descending());
}

/**
* Build a comparator from the query.
*
* @return comparator
*/
public Comparator<Comment> toComparator() {
var sort = getSort();
var creationTimestampOrder = sort.getOrderFor("creationTimestamp");
List<Comparator<Comment>> comparators = new ArrayList<>();
if (creationTimestampOrder != null) {
Comparator<Comment> comparator =
comparing(comment -> comment.getMetadata().getCreationTimestamp());
if (creationTimestampOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}

var replyCountOrder = sort.getOrderFor("replyCount");
if (replyCountOrder != null) {
Comparator<Comment> comparator = comparing(
comment -> defaultIfNull(comment.getStatusOrDefault().getReplyCount(), 0));
if (replyCountOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
}

var lastReplyTimeOrder = sort.getOrderFor("lastReplyTime");
if (lastReplyTimeOrder == null) {
lastReplyTimeOrder = new Sort.Order(Sort.Direction.DESC, "lastReplyTime");
}
Comparator<Comment> comparator = comparing(LAST_REPLY_TIME_FUNC,
Comparators.nullsComparator(lastReplyTimeOrder.isAscending()));
if (lastReplyTimeOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
comparators.add(Comparators.compareCreationTimestamp(false));
comparators.add(Comparators.compareName(true));
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
public PageRequest toPageRequest() {
return PageRequestImpl.of(getPage(), getSize(), getSort());
}

/**
* Build a predicate from the query.
*
* @return predicate
* Convert to list options.
*/
Predicate<Comment> toPredicate() {
Predicate<Comment> predicate = comment -> true;
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var fieldQuery = listOptions.getFieldSelector().query();

String keyword = getKeyword();
if (keyword != null) {
predicate = predicate.and(comment -> {
String raw = comment.getSpec().getRaw();
return StringUtils.containsIgnoreCase(raw, keyword);
});
}

Boolean approved = getApproved();
if (approved != null) {
predicate =
predicate.and(comment -> Objects.equals(comment.getSpec().getApproved(), approved));
}
Boolean hidden = getHidden();
if (hidden != null) {
predicate =
predicate.and(comment -> Objects.equals(comment.getSpec().getHidden(), hidden));
}

Boolean top = getTop();
if (top != null) {
predicate = predicate.and(comment -> Objects.equals(comment.getSpec().getTop(), top));
}

Boolean allowNotification = getAllowNotification();
if (allowNotification != null) {
predicate = predicate.and(
comment -> Objects.equals(comment.getSpec().getAllowNotification(),
allowNotification));
}

String ownerKind = getOwnerKind();
if (ownerKind != null) {
predicate = predicate.and(comment -> {
Comment.CommentOwner owner = comment.getSpec().getOwner();
return Objects.equals(owner.getKind(), ownerKind);
});
if (StringUtils.isNotBlank(keyword)) {
fieldQuery = and(fieldQuery, contains("spec.raw", keyword));
}

String ownerName = getOwnerName();
if (ownerName != null) {
predicate = predicate.and(comment -> {
Comment.CommentOwner owner = comment.getSpec().getOwner();
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Objects.equals(owner.getKind(), ownerKind)
&& (StringUtils.containsIgnoreCase(owner.getName(), ownerName)
|| StringUtils.containsIgnoreCase(owner.getDisplayName(), ownerName));
}
return Objects.equals(owner.getKind(), ownerKind)
&& StringUtils.containsIgnoreCase(owner.getName(), ownerName);
});
}

String subjectKind = getSubjectKind();
if (subjectKind != null) {
predicate = predicate.and(comment -> {
Ref subjectRef = comment.getSpec().getSubjectRef();
return Objects.equals(subjectRef.getKind(), subjectKind);
});
if (StringUtils.isNotBlank(ownerName)) {
String ownerKind = StringUtils.defaultIfBlank(getOwnerKind(), User.KIND);
fieldQuery = and(fieldQuery,
equal("spec.owner", Comment.CommentOwner.ownerIdentity(ownerKind, ownerName)));
}

String subjectName = getSubjectName();
if (subjectName != null) {
predicate = predicate.and(comment -> {
Ref subjectRef = comment.getSpec().getSubjectRef();
return Objects.equals(subjectRef.getKind(), subjectKind)
&& StringUtils.containsIgnoreCase(subjectRef.getName(), subjectName);
});
}

Predicate<Extension> labelAndFieldSelectorPredicate =
labelAndFieldSelectorToPredicate(getLabelSelector(),
getFieldSelector());
return predicate.and(labelAndFieldSelectorPredicate);
}

private Boolean convertBooleanOrNull(String value) {
return StringUtils.isBlank(value) ? null : Boolean.parseBoolean(value);
listOptions.setFieldSelector(FieldSelector.of(fieldQuery));
return listOptions;
}
}
Expand Up @@ -50,9 +50,8 @@ public CommentServiceImpl(ReactiveExtensionClient client,

@Override
public Mono<ListResult<ListedComment>> listComment(CommentQuery commentQuery) {
return this.client.list(Comment.class, commentQuery.toPredicate(),
commentQuery.toComparator(),
commentQuery.getPage(), commentQuery.getSize())
return this.client.listBy(Comment.class, commentQuery.toListOptions(),
commentQuery.toPageRequest())
.flatMap(comments -> Flux.fromStream(comments.get()
.map(this::toListedComment))
.concatMap(Function.identity())
Expand Down Expand Up @@ -138,32 +137,21 @@ private Mono<User> fetchCurrentUser() {
}

private Mono<ListedComment> toListedComment(Comment comment) {
ListedComment.ListedCommentBuilder commentBuilder = ListedComment.builder()
.comment(comment);
return Mono.just(commentBuilder)
.flatMap(builder -> {
Comment.CommentOwner owner = comment.getSpec().getOwner();
// not empty
return getCommentOwnerInfo(owner)
.map(builder::owner);
})
.flatMap(builder -> getCommentSubject(comment.getSpec().getSubjectRef())
.map(subject -> {
builder.subject(subject);
return builder;
})
.switchIfEmpty(Mono.just(builder))
)
.map(ListedComment.ListedCommentBuilder::build)
.flatMap(lc -> fetchStats(comment)
.doOnNext(lc::setStats)
.thenReturn(lc));
var builder = ListedComment.builder().comment(comment);
// not empty
var ownerInfoMono = getCommentOwnerInfo(comment.getSpec().getOwner())
.doOnNext(builder::owner);
var subjectMono = getCommentSubject(comment.getSpec().getSubjectRef())
.doOnNext(builder::subject);
var statsMono = fetchStats(comment.getMetadata().getName())
.doOnNext(builder::stats);
return Mono.when(ownerInfoMono, subjectMono, statsMono)
.then(Mono.fromSupplier(builder::build));
}

Mono<CommentStats> fetchStats(Comment comment) {
Assert.notNull(comment, "The comment must not be null.");
String name = comment.getMetadata().getName();
return counterService.getByName(MeterUtils.nameOf(Comment.class, name))
Mono<CommentStats> fetchStats(String commentName) {
Assert.notNull(commentName, "The commentName must not be null.");
return counterService.getByName(MeterUtils.nameOf(Comment.class, commentName))
.map(counter -> CommentStats.builder()
.upvote(counter.getUpvote())
.build()
Expand Down

0 comments on commit 20d80f8

Please sign in to comment.