From 7aae3d1d78b6668d6e94364cd8050598bff56bf9 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Fri, 5 Jul 2024 12:17:20 +0800 Subject: [PATCH 1/3] feat: moment content synchronization to search engine --- build.gradle | 2 +- .../java/run/halo/moments/ModelConst.java | 1 + src/main/java/run/halo/moments/Moment.java | 2 + .../java/run/halo/moments/MomentEndpoint.java | 2 - .../moments/search/DocumentConverter.java | 79 +++++++++++++++++++ .../search/MomentHaloDocumentsProvider.java | 56 +++++++++++++ .../search/MomentSearchReconciler.java | 64 +++++++++++++++ .../run/halo/moments/uc/UcMomentEndpoint.java | 2 - 8 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 src/main/java/run/halo/moments/search/DocumentConverter.java create mode 100644 src/main/java/run/halo/moments/search/MomentHaloDocumentsProvider.java create mode 100644 src/main/java/run/halo/moments/search/MomentSearchReconciler.java diff --git a/build.gradle b/build.gradle index 1bfa4e9..acac2d9 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ repositories { } dependencies { - implementation platform('run.halo.tools.platform:plugin:2.15.0-SNAPSHOT') + implementation platform('run.halo.tools.platform:plugin:2.17.0-SNAPSHOT') compileOnly 'run.halo.app:api' testImplementation 'run.halo.app:api' diff --git a/src/main/java/run/halo/moments/ModelConst.java b/src/main/java/run/halo/moments/ModelConst.java index a1068b2..b56f126 100644 --- a/src/main/java/run/halo/moments/ModelConst.java +++ b/src/main/java/run/halo/moments/ModelConst.java @@ -10,4 +10,5 @@ public enum ModelConst { ; public static final String TEMPLATE_ID = "_templateId"; public static final Integer DEFAULT_PAGE_SIZE = 10; + public static final Integer SEARCH_DEFAULT_PAGE_SIZE = 200; } diff --git a/src/main/java/run/halo/moments/Moment.java b/src/main/java/run/halo/moments/Moment.java index b6c8fc5..3b21c68 100644 --- a/src/main/java/run/halo/moments/Moment.java +++ b/src/main/java/run/halo/moments/Moment.java @@ -55,6 +55,8 @@ public static class MomentSpec { @Schema(name = "MomentStatus") public static class Status { private long observedVersion; + + private String permalink; } @Data diff --git a/src/main/java/run/halo/moments/MomentEndpoint.java b/src/main/java/run/halo/moments/MomentEndpoint.java index 85bca40..42ae3de 100644 --- a/src/main/java/run/halo/moments/MomentEndpoint.java +++ b/src/main/java/run/halo/moments/MomentEndpoint.java @@ -23,7 +23,6 @@ import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.extension.GroupVersion; import run.halo.app.extension.ListResult; -import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.moments.service.MomentService; /** @@ -49,7 +48,6 @@ public RouterFunction endpoint() { .response(responseBuilder() .implementation(ListResult.generateGenericClass(ListedMoment.class)) ); - QueryParamBuildUtil.buildParametersFromType(builder, MomentQuery.class); }) .GET("moments/{name}", this::getMoment, builder -> builder.operationId("GetMoment") diff --git a/src/main/java/run/halo/moments/search/DocumentConverter.java b/src/main/java/run/halo/moments/search/DocumentConverter.java new file mode 100644 index 0000000..88fd70e --- /dev/null +++ b/src/main/java/run/halo/moments/search/DocumentConverter.java @@ -0,0 +1,79 @@ +package run.halo.moments.search; + +import static run.halo.moments.search.MomentHaloDocumentsProvider.MOMENT_DOCUMENT_TYPE; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.User; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.ExternalUrlSupplier; +import run.halo.app.search.HaloDocument; +import run.halo.moments.Moment; + +/** + * @author LIlGG + */ +@Component +@RequiredArgsConstructor +public class DocumentConverter implements Converter> { + + private final ReactiveExtensionClient client; + + private final ExternalUrlSupplier externalUrlSupplier; + + private final DateTimeFormatter dateFormat = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()); + + @Override + @NonNull + public Mono convert(Moment moment) { + var haloDoc = new HaloDocument(); + var momentContent = moment.getSpec().getContent(); + haloDoc.setMetadataName(moment.getMetadata().getName()); + haloDoc.setType(MOMENT_DOCUMENT_TYPE); + haloDoc.setId(haloDocId(moment)); + haloDoc.setDescription(momentContent.getHtml()); + haloDoc.setExposed(isExposed(moment)); + haloDoc.setContent(momentContent.getHtml()); + haloDoc.setTags(moment.getSpec().getTags().stream().toList()); + haloDoc.setOwnerName(moment.getSpec().getOwner()); + haloDoc.setUpdateTimestamp(moment.getSpec().getReleaseTime()); + haloDoc.setCreationTimestamp(moment.getMetadata().getCreationTimestamp()); + haloDoc.setPermalink(getPermalink(moment)); + haloDoc.setPublished(true); + + return Mono.when(getTitle(moment).doOnNext(haloDoc::setTitle)) + .then(Mono.fromSupplier(() -> haloDoc)); + } + + String haloDocId(Moment moment) { + return MOMENT_DOCUMENT_TYPE + '-' + moment.getMetadata().getName(); + } + + private Mono getTitle(Moment moment) { + return client.fetch(User.class, moment.getSpec().getOwner()) + .map(user -> user.getSpec().getDisplayName()) + .map(displayName -> { + ZonedDateTime zonedDateTime = + moment.getSpec().getReleaseTime().atZone(ZoneId.systemDefault()); + return "发表于:" + dateFormat.format(zonedDateTime) + + " by " + displayName; + }); + } + + private String getPermalink(Moment moment) { + var externalUrl = externalUrlSupplier.get(); + return externalUrl.resolve("moments/" + moment.getMetadata().getName()).toString(); + } + + private static boolean isExposed(Moment moment) { + var visible = moment.getSpec().getVisible(); + return Moment.MomentVisible.PUBLIC.equals(visible); + } +} diff --git a/src/main/java/run/halo/moments/search/MomentHaloDocumentsProvider.java b/src/main/java/run/halo/moments/search/MomentHaloDocumentsProvider.java new file mode 100644 index 0000000..233f325 --- /dev/null +++ b/src/main/java/run/halo/moments/search/MomentHaloDocumentsProvider.java @@ -0,0 +1,56 @@ +package run.halo.moments.search; + +import static run.halo.moments.ModelConst.SEARCH_DEFAULT_PAGE_SIZE; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import run.halo.app.extension.ListOptions; +import run.halo.app.extension.ListResult; +import run.halo.app.extension.PageRequest; +import run.halo.app.extension.PageRequestImpl; +import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.extension.index.query.QueryFactory; +import run.halo.app.extension.router.selector.FieldSelector; +import run.halo.app.search.HaloDocument; +import run.halo.app.search.HaloDocumentsProvider; +import run.halo.moments.Moment; + +/** + * @author LIlGG + */ +@Component +@RequiredArgsConstructor +public class MomentHaloDocumentsProvider implements HaloDocumentsProvider { + + public static final String MOMENT_DOCUMENT_TYPE = "moment.moment.halo.run"; + + private final ReactiveExtensionClient client; + + private final DocumentConverter converter; + + @Override + public Flux fetchAll() { + var options = new ListOptions(); + var notDeleted = QueryFactory.isNull("metadata.deletionTimestamp"); + var approved = QueryFactory.equal("spec.approved", "true"); + options.setFieldSelector(FieldSelector.of(notDeleted).andQuery(approved)); + var pageRequest = createPageRequest(); + // make sure the moments are approved and not deleted. + return client.listBy(Moment.class, options, pageRequest) + .map(ListResult::getItems) + .flatMapMany(Flux::fromIterable) + .flatMap(converter::convert); + } + + @Override + public String getType() { + return MOMENT_DOCUMENT_TYPE; + } + + private PageRequest createPageRequest() { + return PageRequestImpl.of(1, SEARCH_DEFAULT_PAGE_SIZE, + Sort.by("metadata.creationTimestamp", "metadata.name")); + } +} diff --git a/src/main/java/run/halo/moments/search/MomentSearchReconciler.java b/src/main/java/run/halo/moments/search/MomentSearchReconciler.java new file mode 100644 index 0000000..abdc570 --- /dev/null +++ b/src/main/java/run/halo/moments/search/MomentSearchReconciler.java @@ -0,0 +1,64 @@ +package run.halo.moments.search; + +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +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.search.event.HaloDocumentAddRequestEvent; +import run.halo.app.search.event.HaloDocumentDeleteRequestEvent; +import run.halo.moments.Moment; + +/** + * @author LIlGG + */ +@Component +@RequiredArgsConstructor +public class MomentSearchReconciler implements Reconciler { + + private static final String FINALIZER = "moment-search-protection"; + + private final ApplicationEventPublisher eventPublisher; + + private final ExtensionClient client; + + private final DocumentConverter converter; + + @Override + public Result reconcile(Request request) { + client.fetch(Moment.class, request.name()).ifPresent(moment -> { + if (ExtensionUtil.isDeleted(moment)) { + if (ExtensionUtil.removeFinalizers(moment.getMetadata(), Set.of(FINALIZER))) { + eventPublisher.publishEvent( + new HaloDocumentDeleteRequestEvent(this, + List.of(converter.haloDocId(moment))) + ); + client.update(moment); + } + return; + } + ExtensionUtil.addFinalizers(moment.getMetadata(), Set.of(FINALIZER)); + + var haloDoc = converter.convert(moment) + .blockOptional().orElseThrow(); + eventPublisher.publishEvent( + new HaloDocumentAddRequestEvent(this, List.of(haloDoc))); + + client.update(moment); + }); + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new Moment()) + .workerCount(1) + .build(); + } +} diff --git a/src/main/java/run/halo/moments/uc/UcMomentEndpoint.java b/src/main/java/run/halo/moments/uc/UcMomentEndpoint.java index 3415f09..727d83d 100644 --- a/src/main/java/run/halo/moments/uc/UcMomentEndpoint.java +++ b/src/main/java/run/halo/moments/uc/UcMomentEndpoint.java @@ -26,7 +26,6 @@ import run.halo.app.core.extension.endpoint.CustomEndpoint; import run.halo.app.extension.GroupVersion; import run.halo.app.extension.ListResult; -import run.halo.app.extension.router.QueryParamBuildUtil; import run.halo.moments.ListedMoment; import run.halo.moments.Moment; import run.halo.moments.MomentQuery; @@ -59,7 +58,6 @@ public RouterFunction endpoint() { .response(responseBuilder() .implementation(ListResult.generateGenericClass(ListedMoment.class)) ); - QueryParamBuildUtil.buildParametersFromType(builder, MomentQuery.class); }) .GET("moments/{name}", this::getMyMoment, builder -> builder.operationId("GetMyMoment") From 0a2f1002c354698da73e486d148a04566f838869 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Fri, 5 Jul 2024 12:21:32 +0800 Subject: [PATCH 2/3] update README --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5013f5d..f4783ff 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Halo 2.0 的瞬间管理插件,提供一个轻量级的内容发布功能, 3. 安装完成之后,访问 Console 左侧的**瞬间**菜单项,即可进行管理。 4. 前台访问地址为 `/moments`,需要注意的是,此插件需要主题提供模板(moments.html)才能访问 `/moments`。 5. 此插件也提供了 RSS 订阅的路由,可以访问 `/moments/rss.xml`。 +6. 此插件将数据同步至 Halo 搜索,type 为 `moment.moment.halo.run`。 ## 开发环境 @@ -171,6 +172,12 @@ halo: ``` +#### 搜索路由 + +**变量**: + +- type: moment.moment.halo.run + ### Finder API #### listAll() From 2094af6d10b01e1c5faa1a597db1e0118c1e3dc3 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Mon, 22 Jul 2024 16:14:09 +0800 Subject: [PATCH 3/3] fix: solve the problem that tag may be an error when it is empty --- src/main/java/run/halo/moments/search/DocumentConverter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/run/halo/moments/search/DocumentConverter.java b/src/main/java/run/halo/moments/search/DocumentConverter.java index 88fd70e..4f475ad 100644 --- a/src/main/java/run/halo/moments/search/DocumentConverter.java +++ b/src/main/java/run/halo/moments/search/DocumentConverter.java @@ -5,6 +5,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Optional; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; @@ -41,7 +42,8 @@ public Mono convert(Moment moment) { haloDoc.setDescription(momentContent.getHtml()); haloDoc.setExposed(isExposed(moment)); haloDoc.setContent(momentContent.getHtml()); - haloDoc.setTags(moment.getSpec().getTags().stream().toList()); + var tags = moment.getSpec().getTags(); + Optional.ofNullable(tags).ifPresent((tag) -> haloDoc.setTags(tag.stream().toList())); haloDoc.setOwnerName(moment.getSpec().getOwner()); haloDoc.setUpdateTimestamp(moment.getSpec().getReleaseTime()); haloDoc.setCreationTimestamp(moment.getMetadata().getCreationTimestamp());