RabbitMQ:在 Cloud Foundry 上启用 Grails 全文搜索

工程 | Peter Ledbrook | 2011年8月29日 | ...

在我的关于 Grails 和 Cloud Foundry 的第二篇博客中,我介绍了一个Grails Twitter 示例的变体,它可以托管在CloudFoundry.com上。当时我提到,使用 Searchable 插件进行全文搜索会将您限制为单个应用程序实例,因为搜索索引对于每个实例都是唯一的。换句话说,根据您的浏览器路由到的应用程序实例,您很容易获得不同的搜索结果。

我还说,解决此问题的一个方法是跨实例同步搜索索引。但这听起来并不容易,是吗?实际上,将 RabbitMQ 服务引入 Cloud Foundry 意味着所需的代码更改比您预期的要少得多。因此,让我们看看我如何为 Grails Twitter 状态消息添加全文搜索。

使状态消息可搜索

Searchable 插件强烈假设您希望索引标准 GORM 域类。这意味着 Hibernate/SQL。但是 Grails Twitter 状态消息存储在 MongoDB 中,而不是 MySQL 中。我们能使它们可搜索吗?可以,但需要牺牲一些功能。

与普通的域类一样,搜索Status实例的第一步是添加一个searchable属性

package org.grails.twitter

import org.grails.twitter.auth.Person

class Status {
    static mapWith = "mongo"
    static transients = ["author"]

    static searchable = {
        only = ["message", "dateCreated"]
        authorId index: "no", store: "yes"
    }
	
    String message
    Long authorId
    List<String> tags = []
    Date dateCreated
	
    Person getAuthor() {
        return Person.get(authorId)
    }

    static constraints = {
        message maxSize: 160
    }
}

在这种情况下,我希望能够搜索创建日期和消息内容,但其他内容则不行。我还想从搜索结果链接到消息的作者。但是如果authorId没有被索引,则搜索结果将不包含发布者的 ID。因此,我将authorId存储在索引中,但不使其可搜索 (index: "no")。很简单,对吧?显示搜索结果时,现在可以包含每条消息作者的姓名。

索引非 Hibernate 域类的一个重要限制是镜像不起作用。这意味着当保存新消息时,不会自动对其进行索引。幸运的是,我们实际上并不希望在这里使用此行为,因此我在Config.groovy:

searchable {
    ...
    mirrorChanges = false
    bulkIndexOnStartup = false
}

中禁用了镜像和“启动时批量索引”。当然,我们希望在启动时对状态消息进行索引,因为 Cloud Foundry 上的文件系统是临时的,因此搜索索引需要在每次启动时重建。但是自动索引也不适用于非 Hibernate 域类,因此我在:

...
class BootStrap {

    def searchableService
    def springSecurityService

    def init = { servletContext ->
        ...
        // Index all Hibernate mapped domain classes.
        searchableService.reindex()

        // Index all status messages.
        def statusMessages = Status.list()
        log.info "Indexing ${statusMessages.size()} status messages"
        Status.reindex(statusMessages)
        log.info "Finished indexing"
    }
    ...
}

代码并不多,但这足以使状态消息可搜索。剩下的就是确保对新消息进行索引,并将搜索索引同步到应用程序实例。

与 RabbitMQ 同步

保持搜索索引同步的基本模型非常简单

每次保存状态消息时,都会向 RabbitMQ 代理发送一条消息,然后代理将其转发到所有应用程序实例。然后,每个实例都会对Status由消息标识的实例进行索引。

在我们实现此功能之前,我们需要安装 RabbitMQ 插件。

    grails install-plugin rabbitmq

接下来的工作是用适当的交换和队列配置代理。我之前已经写过关于AMQP 协议RabbitMQ 插件的博客,因此我不会在这里详细介绍交换和队列。需要说明的是,我们只需要一个单一的扇出交换机(所有消息都路由到所有侦听器)和一个订阅该交换机的 Grails 服务。因此在Config.groovy我添加了

rabbitmq {
    connectionfactory {
        username = 'guest'
        password = 'guest'
        hostname = 'localhost'
    }

    queues = {
        exchange name: 'search.sync', type: fanout, durable: false
    }
}

重要的部分是交换声明:当应用程序部署到 Cloud Foundry 时,连接工厂设置会被忽略,因为 RabbitMQ 服务在运行时绑定到应用程序。

发送消息是一行代码

...
class StatusService {
    def springSecurityService
    def tagService
    
    void updateStatus(long userId, String message) {
        def status = new Status(message: message, authorId: userId).save(flush: true, failOnError: true)
        rabbitSend 'search.sync', '', "${status.id}:${status.class.name}"
        
        runAsync {
            tagService.extractTagsFromMessage(status)
        }
    }
    ...
}

并且索引状态消息的服务并没有复杂多少

package org.grails.twitter

class SyncService {
    static rabbitSubscribe = "search.sync"
    static transactional = false

    def grailsApplication
    def searchableService

    void handleMessage(String message) {
        def parts = message.split(/:/)
        if (parts.size() != 2) {
            log.error "Invalid message: $message"
            return
        }

        def domainClass = grailsApplication.getDomainClass(parts[1])
        log.debug "Reindexing instance ${parts[0]} of ${parts[1]}"
        try {
            searchableService.reindex(domainClass.clazz.get(parts[0]))
        }
        catch (Exception ex) {
            log.error "Failed to index instance ${parts[0]} of ${parts[1]}", ex
        }
    }
}

所以rabbitSend()方法用于发送包含Status实例 ID 和类名的简单字符串。在这种情况下,我们只处理Status实例,但使服务对所有潜在的可搜索域类通用是有用的。此外,使用 Groovy 方法意味着我们不必进行任何讨厌的反射:我们只需获取类并直接在其上调用我们想要的方法!

的重要部分SyncServicerabbitSubscribe属性和handleMessage()方法。前者声明该服务应该订阅交换机“search.sync”,这是我向其发送消息的交换机。该handleMessage()方法在每次从该交换机接收到消息时都会被调用,其参数为消息内容。因此,该方法提取类名和实例 ID,并使用 GrailsDomainClass.get()方法从数据存储(我们的Status消息的 MongoDB)中检索相关实例。最后,searchableService.reindex()方法将状态消息添加到本地搜索索引。当然,这会在每个应用程序实例上发生。

应用程序现在已准备好部署到 Cloud Foundry 并扩展到允许的任意多个实例!您可以在CloudFoundry.com上查看结果。请注意,在 GitHub 项目中,我已经完成了一些 UI 工作以支持全文搜索,但这些更改与本文的主题无关。

总结

我必须说,我自己也很惊讶,实现搜索索引同步所需的代码如此之少。不仅如此,我还能够专注于如何解决问题,而不是如何编写代码,因为编码非常简单。最重要的是,使用 Cloud Foundry 意味着部署包括创建和绑定 RabbitMQ 服务,然后运行grails prod cf-update命令将更改推送到服务器。很简单。

正如您所看到的,RabbitMQ 可以为与云相关的难题提供创新的解决方案,而 Grails 插件则通过其约定功能使其非常易于使用。您可以在同一应用程序的不同实例、不同的 Grails 应用程序之间甚至使用不同语言和框架编写的应用程序之间进行通信。例如,我们可以部署一个简单的 Node.js 或 Sinatra 应用程序来记录和显示“search.sync”消息,以便您可以跟踪它们。基本上,RabbitMQ 是您云工具箱中的一个重要组成部分。

获取 Spring 电子邮件简报

通过 Spring 电子邮件简报保持联系

订阅

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多信息

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部