Skip to content

Commit

Permalink
refactor: using indexes to optimize queries (#69)
Browse files Browse the repository at this point in the history
### What this PR does?
使用索引来优化瞬间查询

**how to test it?**
基于 Halo 2.12.0
可以使用以下 jar 包测试(keyword 查询目前没有对内容建索引,暂时只可以根据 owner 用户名查询):

[plugin-moments-1.0.1-SNAPSHOT.jar.zip](https://github.com/halo-sigs/plugin-moments/files/14061573/plugin-moments-1.0.1-SNAPSHOT.jar.zip)

可以使用 postgre 数据库用以下脚本插入数据测试:
<details>
<summary>点击展开查看 SQL</summary>

```sql
CREATE OR REPLACE FUNCTION random_string(length integer) RETURNS text AS
$$
DECLARE
    chars  text[]  := array ['a','b','c','d','e','f','g','h','i','j','k','l','m',
        'n','o','p','q','r','s','t','u','v','w','x','y','z',
        'A','B','C','D','E','F','G','H','I','J','K','L','M',
        'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
        '0','1','2','3','4','5','6','7','8','9'];
    result text    := '';
    i      integer := 0;
BEGIN
    FOR i IN 1..length
        LOOP
            result := result || chars[1 + floor(random() * array_length(chars, 1))];
        END LOOP;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

DO
$$
    DECLARE
        json_data  jsonb;
        bytea_data bytea;
        i          integer;
    BEGIN
        -- 可以修改此处的值来创建更大的数据集,默认 1w
        FOR i IN 1..10000
            LOOP
                -- 创建初始 JSON 数据
                json_data := '{
                  "spec": {
                    "content": {
                      "raw": "",
                      "medium": []
                    },
                    "releaseTime": "2024-01-09T07:24:46.077Z",
                    "visible": "PUBLIC",
                    "owner": "admin",
                    "tags": []
                  },
                  "status": {
                    "observedVersion": 0
                  },
                  "apiVersion": "moment.halo.run/v1alpha1",
                  "kind": "Moment",
                  "metadata": {
                    "finalizers": [
                      "moment-protection"
                    ],
                    "name": "",
                    "version": 0,
                    "creationTimestamp": "2024-01-09T07:33:56.172968174Z"
                  }
                }'::jsonb;
                -- 更新 'raw' 字段
                json_data :=
                        jsonb_set(json_data, '{spec, content, raw}', to_jsonb('<p>' || random_string(20000) || '</p>'),
                                  TRUE);
                json_data :=
                        jsonb_set(json_data, '{spec, content, html}', to_jsonb('<p>' || random_string(20000) || '</p>'),
                                  TRUE);
                -- 更新 'metadata.name' 字段
                json_data := jsonb_set(json_data, '{metadata, name}', to_jsonb('moment-' || i), TRUE);
                -- 将 JSON 数据转换为 bytea
                bytea_data := convert_to(json_data::text, 'UTF8');
                -- 插入数据
                INSERT INTO extensions (name, data, version)
                VALUES ('/registry/moment.halo.run/moments/moment-' || i, bytea_data, 0);
            END LOOP;
    END
$$;
```

</details>

```release-note
适配索引功能来优化瞬间的查询请求以提高响应速度并降低内存占用
```
  • Loading branch information
guqing authored Feb 4, 2024
1 parent 096b1b3 commit 2b2486c
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 483 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repositories {
}

dependencies {
implementation platform('run.halo.tools.platform:plugin:2.11.0-SNAPSHOT')
implementation platform('run.halo.tools.platform:plugin:2.12.0-SNAPSHOT')
compileOnly 'run.halo.app:api'

testImplementation 'run.halo.app:api'
Expand All @@ -39,5 +39,5 @@ build {
}

halo {
version = '2.11.1'
version = '2.12.0'
}
9 changes: 9 additions & 0 deletions src/main/java/run/halo/moments/Moment.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Moment extends AbstractExtension {
public static final String REQUIRE_SYNC_ON_STARTUP_INDEX_NAME = "requireSyncOnStartup";

@Schema(required = true)
private MomentSpec spec;

private Status status;

@Data
public static class MomentSpec {

Expand All @@ -41,6 +44,12 @@ public static class MomentSpec {
private Set<String> tags;
}

@Data
@Schema(name = "MomentStatus")
public static class Status {
private long observedVersion;
}

@Data
public static class MomentContent {

Expand Down
19 changes: 0 additions & 19 deletions src/main/java/run/halo/moments/MomentConfiguration.java

This file was deleted.

7 changes: 2 additions & 5 deletions src/main/java/run/halo/moments/MomentEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.endpoint.CustomEndpoint;
import run.halo.app.extension.GroupVersion;
Expand All @@ -35,8 +34,6 @@ public class MomentEndpoint implements CustomEndpoint {

private final MomentService momentService;

private final TagMomentIndexer tagMomentIndexer;

@Override
public RouterFunction<ServerResponse> endpoint() {
final var tag = "api.plugin.halo.run/v1alpha1/Moment";
Expand Down Expand Up @@ -93,14 +90,14 @@ private Mono<ServerResponse> createMoment(ServerRequest serverRequest) {
}

private Mono<ServerResponse> listMoment(ServerRequest serverRequest) {
MomentQuery query = new MomentQuery(serverRequest.queryParams());
MomentQuery query = new MomentQuery(serverRequest.exchange());
return momentService.listMoment(query)
.flatMap(listedMoments -> ServerResponse.ok().bodyValue(listedMoments));
}

private Mono<ServerResponse> listTags(ServerRequest request) {
String name = request.queryParam("name").orElse(null);
return Flux.fromIterable(tagMomentIndexer.listAllTags())
return momentService.listAllTags()
.filter(tagName -> StringUtils.isBlank(name) || StringUtils.containsIgnoreCase(tagName,
name))
.collectList()
Expand Down
80 changes: 62 additions & 18 deletions src/main/java/run/halo/moments/MomentQuery.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,39 @@
package run.halo.moments;

import static run.halo.app.extension.index.query.QueryFactory.all;
import static run.halo.app.extension.index.query.QueryFactory.and;
import static run.halo.app.extension.index.query.QueryFactory.contains;
import static run.halo.app.extension.index.query.QueryFactory.equal;
import static run.halo.app.extension.index.query.QueryFactory.greaterThanOrEqual;
import static run.halo.app.extension.index.query.QueryFactory.lessThanOrEqual;
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToListOptions;

import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import run.halo.app.extension.router.IListRequest;
import org.springframework.web.server.ServerWebExchange;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.PageRequest;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.router.SortableRequest;
import run.halo.app.extension.router.selector.FieldSelector;

/**
* A query object for {@link Moment} list.
*
* @author LIlGG
* @author guqing
* @since 1.0.0
*/
public class MomentQuery extends IListRequest.QueryListRequest {
public class MomentQuery extends SortableRequest {
private final MultiValueMap<String, String> queryParams;

public MomentQuery(MultiValueMap<String, String> queryParams) {
super(queryParams);
public MomentQuery(ServerWebExchange exchange) {
super(exchange);
this.queryParams = exchange.getRequest().getQueryParams();
}

@Nullable
Expand All @@ -42,12 +59,6 @@ public Moment.MomentVisible getVisible() {
return Moment.MomentVisible.from(visible);
}

@Schema(description = "Moment collation.")
public MomentSorter getSort() {
String sort = queryParams.getFirst("sort");
return MomentSorter.convertFrom(sort);
}

@Schema
public Instant getStartDate() {
String startDate = queryParams.getFirst("startDate");
Expand All @@ -60,19 +71,52 @@ public Instant getEndDate() {
return convertInstantOrNull(endDate);
}

@Schema(description = "ascending order If it is true; otherwise, it is in descending order.")
public Boolean getSortOrder() {
String sortOrder = queryParams.getFirst("sortOrder");
return convertBooleanOrNull(sortOrder);
/**
* Build {@link ListOptions} from query params.
*
* @return a list options.
*/
public ListOptions toListOptions() {
var listOptions =
labelAndFieldSelectorToListOptions(getLabelSelector(), getFieldSelector());
var query = all();
if (StringUtils.isNotBlank(getOwnerName())) {
query = and(query, equal("spec.owner", getOwnerName()));
}
if (StringUtils.isNotBlank(getTag())) {
query = and(query, equal("spec.tags", getTag()));
}
if (getVisible() != null) {
query = and(query, equal("spec.visible", getVisible().name()));
}

if (getStartDate() != null) {
query = and(query, greaterThanOrEqual("spec.releaseTime", getStartDate().toString()));
}
if (getEndDate() != null) {
query = and(query, lessThanOrEqual("spec.releaseTime", getEndDate().toString()));
}

if (listOptions.getFieldSelector() != null
&& listOptions.getFieldSelector().query() != null) {
query = and(query, listOptions.getFieldSelector().query());
}
if (StringUtils.isNotBlank(getKeyword())) {
query = and(query, contains("spec.owner", getKeyword()));
}
listOptions.setFieldSelector(FieldSelector.of(query));
return listOptions;
}

private Boolean convertBooleanOrNull(String value) {
return StringUtils.isBlank(value) ? null : Boolean.parseBoolean(value);
public PageRequest toPageRequest() {
var sort = getSort();
if (sort.isUnsorted()) {
sort = Sort.by("spec.releaseTime").descending();
}
return PageRequestImpl.of(getPage(), getSize(), sort);
}

private Instant convertInstantOrNull(String timeStr) {
return StringUtils.isBlank(timeStr) ? null : Instant.parse(timeStr);
}


}
21 changes: 19 additions & 2 deletions src/main/java/run/halo/moments/MomentReconciler.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package run.halo.moments;

import static run.halo.app.extension.index.query.QueryFactory.equal;

import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import run.halo.app.core.extension.notification.Subscription;
import run.halo.app.extension.DefaultExtensionMatcher;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.notification.NotificationCenter;

/**
Expand Down Expand Up @@ -37,8 +41,14 @@ public Result reconcile(Request request) {
if (ExtensionUtil.addFinalizers(moment.getMetadata(), Set.of(FINALIZER))) {
// auto subscribe to new comment on moment
createCommentSubscriptionForMoment(moment);
client.update(moment);
}
var status = moment.getStatus();
if (status == null) {
status = new Moment.Status();
moment.setStatus(status);
}
status.setObservedVersion(moment.getMetadata().getVersion() + 1);
client.update(moment);
});
return Result.doNotRetry();
}
Expand All @@ -57,9 +67,16 @@ void createCommentSubscriptionForMoment(Moment moment) {

@Override
public Controller setupWith(ControllerBuilder builder) {
final var moment = new Moment();
return builder
.extension(new Moment())
.extension(moment)
.workerCount(5)
.onAddMatcher(DefaultExtensionMatcher.builder(client, moment.groupVersionKind())
.fieldSelector(
FieldSelector.of(equal(Moment.REQUIRE_SYNC_ON_STARTUP_INDEX_NAME, "true"))
)
.build()
)
.build();
}
}
2 changes: 1 addition & 1 deletion src/main/java/run/halo/moments/MomentRouter.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public class MomentRouter {
private final ExternalUrlSupplier externalUrlSupplier;

@Bean
RouterFunction<ServerResponse> momentRouter() {
RouterFunction<ServerResponse> momentRouterFunction() {
return route(GET("/moments").or(GET("/moments/page/{page:\\d+}")), handlerFunction())
.andRoute(GET("/moments/rss.xml"), handlerRss())
.andRoute(GET("/moments/{momentName:\\S+}"), handlerMomentDefault());
Expand Down
56 changes: 0 additions & 56 deletions src/main/java/run/halo/moments/MomentSorter.java

This file was deleted.

Loading

0 comments on commit 2b2486c

Please sign in to comment.