将GraphQL的搜索源从ActiveRecord更改为Elasticsearch
因为graphql-ruby默认使用ActiveRecord作为资源的数据来源,但我想将其更改为Elasticsearch,但找不到相应的库,因此我自己实现了它。
关于环境
-
- Ruby 2.7.3
-
- Rails 6.0.3.4
-
- Gem
graphql 1.12.12
elasticsearch-model 7.1.1
关于实施方法
在GraphQL中,Relay-Style Cursor Pagination是最常见的分页方式。此外,还可以通过使用kaminari等工具来实现分页,这方面的实现方法也在Qiita的文章中有介绍。
根据这篇文章的内容,如果只在内部使用的话,我认为使用kaminari的分页也是可以的,但由于最终目标是发布公开API,因此我选择了使用Relay-Style Cursor Pagination。
创建自定义连接
graphql-ruby在默认情况下为各种资源准备了连接类。它不仅支持ActiveRecord,还支持Sequel、MongoDB、数组等。但是,它不支持Elasticsearch。因此,您需要自己创建自定义连接。
在官方的自定义连接页面上,有关于如何创建自定义连接的示例简要介绍。
起初,我考虑边看这个并进行实现,但由于它写得很粗略,所以我无法理解。因此,我决定边阅读graphql-ruby的分页目录源代码来推进。
有一个名为ActiveRecordRelationConnection的类,它继承了RelationConnection类。最初打算继续继承RelationConnection类,但由于不太清楚,所以改为继承Connection类,并重新实现了在RelationConnection中定义的所有方法。
最终,由于大多数实现了RelationConnection的方法都可以直接使用,所以我将继承源更改为RelationConnection,但是我理解了处理的流程。
所以,这是我创建的班级。
module Connections
class ElasticsearchRelationConnection < GraphQL::Pagination::RelationConnection
def nodes
@nodes ||= limited_nodes.records.to_a
end
# Rubocopにload_nodesメソッドが不要と言われた
# しかし、継承元のRelationConnectionで呼ばれているのでnodesメソッドのエイリアスにしておく
# また、元々private methodだったので変更しておく
alias_method :load_nodes, :nodes
private :load_nodes
# GraphQL::Pagination::RelationConnectionの実装を改修
# `@paged_node_offset`にオフセットが入っているので、2重で足さないようにした。
def cursor_for(item)
load_nodes
# index in nodes + existing offset + 1 (because it's offset, not index)
# offset = @nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
offset = @nodes.index(item) + 1 + (@paged_nodes_offset || 0)
encode(offset.to_s)
end
private
# @param [Elasticsearch::Model::Response::Response]
# @param [Integer] size LimitSize
# @return [Boolean] sizeよりも残りが大きければtrueを返す
def relation_larger_than(relation, size)
initial_offset = relation_offset(relation)
relation_count(relation) > initial_offset + size
end
# @param [Elasticsearch::Model::Response::Response]
# @return [Integer] オフセットの値
def relation_offset(relation)
relation.search.definition.fetch(:from, 0)
end
# @param [Elasticsearch::Model::Response::Response]
# @return [Integer, nil] 取得数
def relation_limit(relation)
relation.search.definition[:size]
end
# @param [Elasticsearch::Model::Response::Response]
# @return [Integer] 総ヒット数
def relation_count(relation)
relation.results.total
end
# @param [Elasticsearch::Model::Response::Response]
# @return [ActiveRecord::Relation]
def null_relation(relation)
relation.records.none
end
def limited_nodes
super()
rescue ArgumentError => _e
# カーソルの先頭より前の要素を取得しようとするとArgumentErrorになったため、
# 例外を補足して空のActiveRecord::Relationを返すようにした
ApplicationRecord.none
end
end
end
我会让你可以使用这个。我会注册先前创建的连接,以便你可以使用它。
class MySchema < GraphQL::Schema
connections.add(Elasticsearch::Model::Response::Response, Connections::ElasticsearchRelationConnection)
# 省略
end
然后,我们定义使用这个模式。省略了User模型在Elasticsearch中的模式定义…。
module Types
class QueryType < Types::BaseObject
field :users, Objects::User.connection_type, null: false do
argument :keyword, String, required: false # 検索キーワード
end
# **argsにすることで、graphqlのページング条件などを一手に引き受けさせる
def users(keyword: nil, **args)
query = Elasticsearch::DSL::Search::Search.new
query.query do
bool do
if keyword.present?
must do
simple_query_string do
query keyword
fields ['keyword_search_field']
default_operator :and
end
end
end
end
end
es_response = User.__elasticsearch__.search(query.to_hash)
# 先ほど作ったコネクションで返す
Connections::ElasticsearchRelationConnection.new(
es_response,
first: args[:first],
last: args[:last],
before: args[:before],
after: args[:after],
)
end
end
end
现在,我们可以通过GraphQL对Elasticsearch进行搜索了。
我现在试着写一个查询。
query {
users(keyword: "山田", first: 3) {
edges {
cursor
node {
id
name
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}
}
以下是先前查询的结果(为了公开使用,数据已经进行了适当修改)。
pageInfo返回了光标的值以及前后页面的存在与否。
{
"data": {
"users": {
"edges": [
{
"cursor": "MQ",
"node": {
"id": "34",
"name": "山田 孝夫"
}
},
{
"cursor": "Mg",
"node": {
"id": "76",
"name": "山田 孝之"
}
},
{
"cursor": "Mw",
"node": {
"id": "55",
"name": "山田 太郎"
}
}
],
"pageInfo": {
"startCursor": "MQ",
"endCursor": "Mw",
"hasNextPage": true,
"hasPreviousPage": false
}
}
}
}
总结
-
- graphql-rubyはデフォルトで様々なコネクションクラスを持っている
-
- その他のリソースで検索させたい場合などはGraphQL::Pagination::Connectionクラスを継承して作ることができる
-
- graphql-rubyでElasticsearchを使いつつ、Relay-Style Cursor Paginationを実現したければ、カスタムコネクションを作る必要がある
- 上記に載せたElasticsearchRelationConnectionのコードが、それである。