我在建立用Rails + elasticsearch构建搜索界面时考虑了什么以及做了哪些事情
首先
你好,我是@highwide,我在转职会议团队担任工程师。
在转职会议上,我们在2016年使用Rails + elasticsearch实现了一部分搜索界面。在此过程中,我学到了很多新知识,因此我想把这件事作为今年Advent Calendar的话题。
请注意
由于有很多关于Rails 4.2系列和Elasticsearch 2.4系列的信息,请在参考时与最新的信息进行对照,这样可以帮助您。
文件设计
elasticsearch的文件结构具有以下层次结构。
- index
└ type(s)
└ document(s)
顺便提一下,在开始学习的时候,
索引是关系型数据库中的数据库,类型是关系型数据库中的表格。
我看到了这样的信息。
确实,在理解层次结构方面,这不是错误的信息,但如果按照RDBMS的DATABASE/TABLE的概念来设计elasticsearch的文档,可能会遇到许多困难,所以请注意。
比如说,
-
- 没有提供只删除单一类型的API。
- 类型间的关系可以通过父子关系实现一对多,但无法实现多对多。
以下是一些特点。
关于第一个特点,当我在Twitter上提出了疑问后,我收到了elastic公司的@johtani先生的回复。(非常感谢!)
@highwide 由于整合性不易实现,因此已经取消了。如果可能的话,最好将生命周期不同的类型分开索引。- Jun Ohtani (@johtani) 2015年12月28日
因此,如果按照RDBMS的观念,将一个应用程序使用的所有文档放入一个相同的索引中,将会变得困难。过多地为类型之间建立关联关系,会使查询变得困难,所以尽可能选择扁平的文档结构作为类型,并将可以同时删除的项目放入同一个索引,这似乎是一个好的选择。
使用elasticsearch-persistence。
我们选择了elasticsearch-persistence作为从Rails发送请求到elasticsearch的客户端库。
在网上看到了一些在Rails中使用elasticsearch的案例,有时候会看到使用elasticsearch-model的例子,将关系数据库的表结构直接作为elasticsearch的文档。但是这次我们想要将多个现有的表合并成一个文档,所以选择了能够使用ActiveModel(带有Virtus)来表示文档的elasticsearch-persistence。
此外,由于此 gem 是 elastic/elasticsearch-ruby 的封装,所以可以使用大多数的 elasticsearch 的索引、文档等操作的 API。
索引器的实现
我决定在Rails上实现一个将数据注册到elasticsearch(进行索引化)的应用程序“indexer”。这时,刚才提到的elasticsearch-persistence发挥了作用。我使用这个gem,并定义了与elasticsearch文档一对一对应的可搜索对象,其名称以Searchable_*为前缀。
生成可搜索性的generate方法
我已经准备了一个名为generate的对象类方法,它接受原始的AR对象并将其转化为可搜索的Searchable_对象。
或许可以这样理解。
class SearchableHoge
include Elasticsearch::Persistence::Model
# attribute定義は省略
class << self
def generate(hoge)
SerchableHoge.new(
title: hoge.title,
body: hoge.body,
fuga_bodies: hoge.fugas.pluck(:body)
)
end
end
end
我现在觉得,如果给hoge添加一个.searchablize的方法可能是一个更好的设计。
我已经准备了一个用于bulk import的API转换方法。
这次在添加职位时,我们决定使用elasticsearch的Bulk API。这是因为我们希望同时索引父文档和子文档。
Elasticsearch的批量API可以通过发送以下参数来实现。
client.bulk body: [
{ index: { _index: 'myindex', _type: 'mytype', _id: 1 } },
{ title: 'foo' },
{ index: { _index: 'myindex', _type: 'mytype', _id: 2 } },
{ title: 'foo' },
{ delete: { _index: 'myindex', _type: 'mytype', _id: 3 } }
]
※ https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/bulk.rb#L13
※ https://github.com/elastic/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/bulk.rb#L13
需要将标题行和文档实际值行分开。
文档实际值可以通过 to_h 方法处理。
对于标题行,我编写了这样的方法来适应接收多个 Searchable_* 对象的模型。
def bulk_params(docs)
{ body: docs.map { |doc| [{ index: header(doc) }, doc.to_h] }.flatten }
end
def header(doc)
header = {
_index: doc.class::INDEX,
_type: doc.class::TYPE,
_id: doc._id
}
header[:_parent] = doc.parent if doc.respond_to?(:parent)
header[:_routing] = doc.routing if doc.respond_to?(:routing)
header
end
重点在于父节点和路由,在进行批量导入时,需要明确指定父节点的id才能进行父节点的索引。因此,在子节点的searchable_*对象中添加了parent和routing属性。
因此
索引化是按照以下流程进行的。
-
- 使用ActiveRecord从数据库获取职位的原始数据
-
- 将其转换为使用include Elasticsearch::Persistence::Model 的对象
-
- 将其转换为用于批量API的参数
- 向elasticsearch发送请求
我认为,您可以写一个调用上面处理的Rake任务等来进行首次的整体更新。
实际上,参考了Cookpad先生的 Elasticsearch 的索引无间断重建,并使其可以在切换别名的同时无间断进行更新。
实施搜索功能
搜索是指定义了一个名为hoge_search_condition的搜寻条件处理ActiveModel的应用程序来进行搜索,并通过form_for来接收参数。
正直說,我們通過力量的手段來將多個搜索條件轉化為elasticsearch的查詢。我們實現了一個類似於hoge_query_builder的東西,並將其包含在hoge_search_condition中。通過將我們創建的查詢傳遞給具有與索引器相同定義的Searchable_*對象的search方法,我們可以實現複雜的搜索。
顺便提一下,elasticsearch的查询很容易变成庞大的JSON(在Ruby世界中可以使用Hash),并且根据上下文需要使用should,must,filter等语句,以及根据父子关系使用has_parent和has_child等方法进行细分。这样做会使得测试变得困难和痛苦。
搜索框为空的处理
在搜索项中有一些空白的。
{ term: { some_attribute: nil } }
当向elasticsearch发送这样的查询时,elasticsearch会返回错误。因此,如果搜索值为nil,我们会准备一个简易的Helper方法,它不会使用elasticsearch的查询诸如term和range,而是返回nil。我们会在更高级的布尔层使用Array#compact方法来丢弃这个值。
查询构建器的考虑
顺便提一下,关于elasticsearch的查询构建器,似乎有几个gem可以使用,但似乎没有任何一个被认为是最佳选择,所以我决定自己将其封装为concern并进行维护,这样会更安全一些。
如果你有什么推荐的,请告诉我…。
最大结果窗口的考虑
最初我们没有考虑到这一点,导致对此做出了延迟的处理。在 elasticsearch 2 系列中,默认将搜索结果的最大显示数量设置为10000条。
由于尝试超越这个限制并访问结果会导致 Elasticsearch 错误,因此我们与 Kaminari 合作,采取以下处理方式:
@results = Kaminari
.paginate_array(
@condition.search(params[:page].to_i),
total_count: [
@condition.total_count,
SearchableHoge::MAX_RESULT_WINDOW
].min
).page(params[:page].to_i)
.per(50)
在画面上通过显示“在搜索结果中共有XX条,仅显示XX条”这样的对应方法来应对。
最後
实际上我感觉应该要煞费苦心多些,但暂且就这样吧。关于自然语言搜索的准确度,我希望能更加详细地讨论一下。
在进行实现时,我参考了官方的文档,以及Qiita和博客文章,并参加了与elasticsearch相关的研讨会,总结了一些在实施过程中的想法。然而,我感觉自己在很大程度上是在摸索,所以如果有更好的方法,请务必告诉我!
这篇文章是在”以上,Livesense Advent Calendar その1″的第20天上发表的。