Halo 建站 | 搜索文档扩展

👋
本文由 Takagi @LIlGG 供稿,首发于 lixingyong.com
原文地址:https://lixingyong.com/archives/halo-cha-jian-kuo-zhan-sou-suo-yin-qing

Halo 搜索引擎拓展类型

Halo 搜索引擎在 https://github.com/halo-dev/halo/releases/tag/v2.17.0 版本后进行了重大重构,详见 PR #6082。因此,在 Halo 2.17 版本之后,插件作者可以通过插件对 Halo 搜索进行扩展。

对 Halo 搜索的扩展可以分为两个部分:搜索引擎扩展搜索文档扩展

搜索引擎扩展

插件中,可以通过扩展搜索引擎来提供更为丰富的搜索功能。例如,Halo 默认使用的是 Lucene ,但插件可以通过扩展支持其他搜索引擎,如 Solr 、 MeiliSearch 、 ElaticSearch 。

不过,搜索引擎扩展不在本文讨论范围内,有兴趣的小伙伴可以阅读 Halo 搜索引擎扩展点 以了解更多详情。

搜索文档扩展

如果你的插件提供了新的内容数据,例如 Moment ,使用搜索功能时,你会发现无法搜到 Moment 中的任何内容。这是因为 Halo 默认只提供了文章与页面这两种核心内容的搜索。

对于自建的内容数据,Halo 搜索不会自动补充,因为自定义数据结构可能与文章和页面截然不同。因此,插件作者就需要主动将自己的内容数据提交至 Halo 搜索。

接下来,我将基于 Halo Moment 插件 这个实际案例,详细介绍如何扩展搜索文档。

在这里插入图片描述

Halo 2.0 的瞬间管理插件,提供一个轻量级的内容发布功能,支持发布图文、视频、音频等内容。

Halo 搜索文档事件

Halo 搜索利用事件机制来收集核心和插件中发布的文档。当插件发布某个文档事件后,Halo 搜索会添加、更新、删除与重建目标文档索引。

要将插件的内容数据提供给搜索,最佳做法是插件主动发出特定事件供 Halo 消费。

目前(Halo 版本 2.17.1), Halo 搜索共监听了三个事件:

  • 添加文档事件 HaloDocumentAddRequestEvent
  • 删除文档事件 HaloDocumentDeleteRequestEvent
  • 重建索引事件 HaloDocumentRebuildRequestEvent

HaloDocumentAddRequestEvent

用于添加或更新文档,例如在 Moment 中添加或更新文档的示例如下:

// MomentReconciler.java
// Create moment search document
var haloDoc = converter.convert(moment).blockOptional().orElseThrow();
eventPublisher.publishEvent(new HaloDocumentAddRequestEvent(this, List.of(haloDoc)));

HaloDocumentDeleteRequestEvent

用于从搜索索引中删除文档,例如删除 Moment 搜索索引的示例如下:


```java
// MomentReconciler.java
eventPublisher.publishEvent(new HaloDocumentDeleteRequestEvent(this,
  // Get moment search document id
  List.of(converter.haloDocId(moment)))
);

HaloDocumentRebuildRequestEvent

用于重新获取搜索文档索引,示例如下:

eventPublisher.publishEvent(new HaloDocumentRebuildRequestEvent(this));

重建搜索索引

除了使用搜索文档事件添加、更新、删除某个特定文档之外,插件还需要实现 HaloDocumentsProvider 接口,该接口用于重建搜索索引。

Moment 插件搜索文档同步

了解 Halo 搜索文档同步事件之后,便可以进行文档同步了。以下是 Moment 插件搜索文档同步的步骤:

  1. 创建一个 Reconciler 用于处理 Moment 内容更新后的搜索相关业务:
@Component
@RequiredArgsConstructor
public class MomentSearchReconciler implements Reconciler<Reconciler.Request> {
 
    private static final String FINALIZER = "moment-search-protection";
 
    private final ExtensionClient client;
 
    @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))) {
                    client.update(moment);
                }
                return;
            }
            ExtensionUtil.addFinalizers(moment.getMetadata(), Set.of(FINALIZER));
            client.update(moment);
        });
        return Result.doNotRetry();
    }
 
    @Override
    public Controller setupWith(ControllerBuilder builder) {
        return builder
            .extension(new Moment())
            .workerCount(1)
            .build();
    }
}

  1. 为 MomentSearchReconciler 添加搜索文档扩展相关的内容:
@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;
    
    // Convert moment to Halo Search Document
    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))) {
                    // Delete the document index when deleting moment.
                    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();
            // When moment is updated, add or update the document.
            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();
    }
}

其中使用了 DocumentConverter ,用于将 Moment 转为 Halo Search Document 。关于 HaloDocument 的字段见 HaloDocument。以下是需要注意的几个属性:

/**
 * Document for search.
 */
@Data
public final class HaloDocument {
    /**
     * Metadata name of the corresponding extension.
     */
    @NotBlank
    private String metadataName;
 
    /**
     * Whether the document is published.
     * After setting it to false, visitors will not be able to search 
     * and will only be used for system internal search.
     */
    private boolean published;
 
    /**
     * Whether the document is recycled.
     */
    private boolean recycled;
 
    /**
     * Whether the document is exposed to the public.
     */
    private boolean exposed;
}

  1. 实现 HaloDocumentsProvider 接口,用于重建索引时调用:
 
@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"));
    }
}

至此, Moment 插件已经适配了 Halo 搜索文档。之后使用搜索功能时,就能查找到 Type 为 moment.moment.halo.run 的内容了。

结语

总体而言,Halo 在核心部分处理了搜索引擎的大部分工作,插件作者只需要关心如何将数据内容转换为 Halo Search Document ,以及何时新增、更新、删除文档即可。实现逻辑相对简单。


☘️ 希望这篇文章能够帮助到想要开发新插件的 Halo 用户!

ps:如果你觉得这篇文章还不错,别忘了去原博 lixingyong.com 为作者打气。

  • 32
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值