Halo 插件自定义模型索引支持

背景

如果直接通过字段查询或者分页查询数据,本质上是会查询当前模型下所有的数据后再进行对应的筛选返回,当对应模型的数据量到达一定的量级后会导致数据返回速率大幅下降,请求返回时间随着数据量增大而增大。所以需要先通过索引的方式筛选可用数据后再进行数据库查询操作已达到最优的查询速率。

实现

索引定义

定义模型CutomEntity

package run.halo.learning.extension;

import static run.halo.learning.extension.CutomEntity.GROUP;
import static run.halo.learning.extension.CutomEntity.KIND;
import static run.halo.learning.extension.CutomEntity.VERSION;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
import java.time.Instant;
import java.util.Set;

/**
 * 自定义模型
 *
 * @author ShrChang.Liu
 * @version v1.0
 * @date 2024/8/8 11:42 AM
 **/
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@GVK(group = GROUP,
    version = VERSION,
    kind = KIND,
    plural = "roles",
    singular = "role")
public class CustomEntity extends AbstractExtension {

    public static final String GROUP = "learning.halo.run";
    public static final String VERSION = "v1alpha1";
    public static final String KIND = "CutomEntity";

    @Schema(requiredMode = REQUIRED)
    private Spec spec;

    @Data
    public static class Spec {
        private boolean enable;
        private Status status;
        private int num;
        private Instant updateInstant;
        private Set<String> tags;
    }

    public enum Status {
        doing, ok, error;
    }
}

定义索引信息

package run.halo.learning.starter;

import static run.halo.app.extension.index.IndexAttributeFactory.multiValueAttribute;
import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute;

import java.util.Optional;
import java.util.Set;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import run.halo.app.extension.Scheme;
import run.halo.app.extension.SchemeManager;
import run.halo.app.extension.index.IndexSpec;
import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;
import run.halo.learning.extension.CustomEntity;

@Component
public class StarterPlugin extends BasePlugin {
    private final SchemeManager schemeManager;

    public StarterPlugin(PluginContext pluginContext, SchemeManager schemeManager) {
        super(pluginContext);
        this.schemeManager = schemeManager;
    }

    @Override
    public void start() {
        schemeManager.register(CustomEntity.class, indexSpecs -> {
            //bool 型
            indexSpecs.add(new IndexSpec()
                .setName("spec.enable")//索引名称
                .setIndexFunc(simpleAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .map(spec -> String.valueOf(spec.isEnable()))
                        .orElse(null);
                })));
            //字符型
            indexSpecs.add(new IndexSpec()
                .setName("spec.name")//索引名称
                .setIndexFunc(simpleAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .map(spec -> spec.getName())
                        .orElse(null);
                })));
            //枚举
            indexSpecs.add(new IndexSpec()
                .setName("spec.status")//索引名称
                .setIndexFunc(simpleAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .filter(spec -> !ObjectUtils.isEmpty(spec.getStatus()))
                        .map(spec -> spec.getStatus().name())
                        .orElse(null);
                })));
            //数值型
            indexSpecs.add(new IndexSpec()
                .setName("spec.num")//索引名称
                .setIndexFunc(simpleAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .map(spec -> String.valueOf(spec.getNum()))
                        .orElse(null);
                })));
            //时间型
            indexSpecs.add(new IndexSpec()
                .setName("spec.updateInstant")//索引名称
                .setIndexFunc(simpleAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .filter(spec -> !ObjectUtils.isEmpty(spec.getUpdateInstant()))
                        .map(spec -> spec.getUpdateInstant().toString())
                        .orElse(null);
                })));
            //集合型
            indexSpecs.add(new IndexSpec()
                .setName("spec.tags")//索引名称
                .setIndexFunc(multiValueAttribute(CustomEntity.class, customEntity -> {
                    return Optional.ofNullable(customEntity.getSpec())
                        .filter(spec -> !CollectionUtils.isEmpty(spec.getTags()))
                        .map(spec -> spec.getTags())
                        .orElse(Set.of());
                })));
        });
        System.out.println("插件启动成功!");
    }

    @Override
    public void stop() {
        schemeManager.unregister(Scheme.buildFromType(CustomEntity.class));
        System.out.println("插件停止!");
    }
}

索引使用

package run.halo.learning.service;

import static org.springframework.data.domain.Sort.Order.asc;
import static org.springframework.data.domain.Sort.Order.desc;
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.greaterThan;
import static run.halo.app.extension.index.query.QueryFactory.lessThan;
import static run.halo.app.extension.index.query.QueryFactory.or;

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.PageRequestImpl;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.learning.extension.CustomEntity;

/**
 * 自定义模型查询服务
 *
 * @author ShrChang.Liu
 * @version v1.0
 * @date 2024/8/8 1:50 PM
 **/
@Service
@RequiredArgsConstructor
public class CustomEntityService {

    private final ReactiveExtensionClient client;

    /**
     * 分页排序查询
     *
     * @param queryRequest
     * @return
     */
    public Mono<ListResult<CustomEntity>> search(CustomEntityQueryRequest queryRequest, int page,
        int size) {
        //排序字段也必须是索引名称
        var pageable = PageRequestImpl.of(page, size,
            Sort.by(desc("spec.num")));
        return client.listBy(CustomEntity.class, toListOptions(queryRequest), pageable);
    }

    /**
     * 查询所有
     *
     * @return
     */
    public Mono<List<CustomEntity>> listAll(CustomEntityQueryRequest queryRequest) {
        //排序字段也必须是索引名称
        var sort = Sort.by(
            desc("metadata.creationTimestamp"),
            asc("metadata.name"));
        return client.listAll(CustomEntity.class, toListOptions(queryRequest),sort)
            .collectList();
    }

    /**
     * 查询条件
     *
     * @param queryRequest
     * @return
     */
    ListOptions toListOptions(CustomEntityQueryRequest queryRequest) {
        var builder = ListOptions.builder();
        if (!ObjectUtils.isEmpty(queryRequest)) {
            //模糊查询name字段
            Optional.ofNullable(queryRequest.getName())
                .ifPresent(name -> builder.andQuery(contains("spec.name", name)));
            //精准匹配bool型
            Optional.ofNullable(queryRequest.getEnable())
                .ifPresent(enable -> builder.andQuery(equal("spec.name", String.valueOf(enable))));
            //精准匹配枚举类型
            Optional.ofNullable(queryRequest.getStatus())
                .ifPresent(status -> builder.andQuery(equal("spec.status", status.name())));
            //大于或小于数值型
            Optional.ofNullable(queryRequest.getNum())
                .ifPresent(num -> builder.andQuery(or(lessThan("spec.num", String.valueOf(num)),
                    greaterThan("spec.num", String.valueOf(num)))));
            //大于或小于时间型
            Optional.ofNullable(queryRequest.getUpdateInstant())
                .ifPresent(instant -> builder.andQuery(
                    or(lessThan("spec.updateInstant", instant.toString()),
                        greaterThan("spec.updateInstant", instant.toString()))));
            //集合型包含某一个
            Optional.ofNullable(queryRequest.getTag())
                .ifPresent(tag -> builder.andQuery(equal("spec.tags", tag)));
            //还有包含一些其余的查询操作符见下:
            //notEqual 不等于
            //isNull 值为空
            //isNotNull 不为空
            //in 包含多个值的用于集合判断
            //startsWith 已什么字符开头
            //endsWith 已什么字符结尾
            //not 条件反
        }
        return builder.build();
    }


    /**
     * 查询请求
     */
    @Data
    public static class CustomEntityQueryRequest {
        private Boolean enable;
        private String name;
        private CustomEntity.Status status;
        private Integer num;
        private Instant updateInstant;
        private String tag;
    }
}

注意事项

  • 必须基于 Halo v2.12.0 以上的版本开发。

  • 定义索引后默认支持metadata.name metadata.creationTimestamp metadata.lables

  • 索引的建立取决于查询要求并不是建立的索引越多越好,索引会占用一定的内存空间。

  • 如果根据索引查询某一个条件下是否包含数据建议采用分页并将分页数量设置为 1 查询而不是查询所有去判断。

  • 如果模型的数据量是可预估的且不影响查询性能则不建议定义索引。

  • 如果使用索引的方式进行查询那排序字段也必须是索引中定义的名称进行排序。

  • 如果通过索引查询条件字段或排序字段中出现非索引字段的时候会导致无法查询。