Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: moment content synchronization to search engine #116

Merged
merged 3 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。

## 开发环境

Expand Down Expand Up @@ -171,6 +172,12 @@ halo:
</div>
```

#### 搜索路由

**变量**:

- type: moment.moment.halo.run

### Finder API

#### listAll()
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions src/main/java/run/halo/moments/ModelConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions src/main/java/run/halo/moments/Moment.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public static class MomentSpec {
@Schema(name = "MomentStatus")
public static class Status {
private long observedVersion;

private String permalink;
}

@Data
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/run/halo/moments/MomentEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -49,7 +48,6 @@ public RouterFunction<ServerResponse> endpoint() {
.response(responseBuilder()
.implementation(ListResult.generateGenericClass(ListedMoment.class))
);
QueryParamBuildUtil.buildParametersFromType(builder, MomentQuery.class);
})
.GET("moments/{name}", this::getMoment,
builder -> builder.operationId("GetMoment")
Expand Down
81 changes: 81 additions & 0 deletions src/main/java/run/halo/moments/search/DocumentConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 java.util.Optional;
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<Moment, Mono<HaloDocument>> {

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<HaloDocument> 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());
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());
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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<HaloDocument> 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"));
}
}
64 changes: 64 additions & 0 deletions src/main/java/run/halo/moments/search/MomentSearchReconciler.java
Original file line number Diff line number Diff line change
@@ -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<Reconciler.Request> {

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();
}
}
2 changes: 0 additions & 2 deletions src/main/java/run/halo/moments/uc/UcMomentEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +58,6 @@ public RouterFunction<ServerResponse> endpoint() {
.response(responseBuilder()
.implementation(ListResult.generateGenericClass(ListedMoment.class))
);
QueryParamBuildUtil.buildParametersFromType(builder, MomentQuery.class);
})
.GET("moments/{name}", this::getMyMoment,
builder -> builder.operationId("GetMyMoment")
Expand Down