【发布时间】:2020-06-05 03:24:22
【问题描述】:
我正在重构一个 Web 应用程序,该应用程序大量使用来自 Solr 后端的方面查询。特别是,这些是多层次的嵌套构面:例如,先对人的眼睛颜色进行构面,然后再对头发的颜色进行构面(如下面的代码示例所示)。构面结果应该是计数而不是实际记录及其字段。
我对使用 GraphQL 来解耦前端和后端很感兴趣,但我认为我无法绕过基于字段的解析执行模型。那么在 Solr 中的单个查询是什么,按眼睛颜色返回人数,然后(在每个眼睛颜色方面)按头发颜色返回,似乎必须通过 GraphQL 中的多个查询来完成。
我制作了一个独立的 nodejs 示例,其中只有两个依赖项(graphql 和 graphql-tools),它展示了我想要实现的目标。有一个facetize(data, fields) 函数可以模拟 Solr 可以做什么。它对给定字段的数据进行计数。如果给定了多个字段,那么它会按照上述方式进行嵌套分面。
我将此函数插入解析器,以便在每个所需级别仅使用一个字段调用它。它工作正常,但当然要多次调用facetize()。
我的问题是,能否以某种方式设置解析器,以便只对facetize 进行一次查询,但仍返回相同的嵌套分面数据?我不介意查询是否被展平(例如facets(field1: "eyeColor", field2: "hairColor") { ... }),但我玩了一下,结果只包含第一级计数。
而且...对于 Solr 人员来说还有一个额外的问题,对子方面进行多个 Solr 查询是否同样有效?或者甚至更高效,如果它可以被 GraphQL 执行引擎并行化?是否值得至少模拟一些东西来进行基准测试?
var { graphql, buildSchema } = require('graphql');
var { makeExecutableSchema } = require('graphql-tools');
// GraphQL schema
const typeDefs = `
type Query {
facets(field: String!) : [Facet!]!
}
type Facet {
value : String!
field : String!
count : Int!
facets(field: String!) : [Facet!]!
}
`;
// demo data
const data = [
{ name: 'John',
eyeColor: 'blue',
hairColor: 'brown'
},
{ name: 'Jane',
eyeColor: 'blue',
hairColor: 'blonde'
},
{ name: 'Jay',
eyeColor: 'green',
hairColor: 'black'
},
{ name: 'Julie',
eyeColor: 'blue',
hairColor: 'brown'
},
{ name: 'Jamal',
eyeColor: 'brown',
hairColor: 'black'
},
{ name: 'Jack',
eyeColor: 'green',
hairColor: 'blonde'
},
{ name: 'Jill',
eyeColor: 'blue',
hairColor: 'brown'
}
];
// this facetize() function is a simple recreation of
// the functionality that Solr can perform in just one request
// (keeping all the data Solr-server-side)
//
// var facets = facetize(data, ['eyeColor', 'hairColor']);
// console.log(JSON.stringify(facets, null, 2));
// // though it would display cleaner with the data fields stripped out
function facetize(data, flds, message) {
// log first-time entry to this recursive function
if (message) console.log("entering facetize from "+message+" using fields "+flds);
let fields = flds.slice(); // make a deep-ish copy
let field = fields.shift(); // deal with the first field first
// set up the empty facet objects for each unique value of field 'field'
let uniqueValues = [...new Set(data.map(elem => elem[field]))];
let facets = {};
uniqueValues.forEach(value =>
facets[value] =
{ value: value, field: field, count: 0, data: [] });
// now iterate through data counting occurrences and
// storing the data items in facet.data
data.forEach( item => {
let value = item[field];
facets[value].count++;
facets[value].data.push(item);
});
// if there are fields left to facet on, do so recursively
if (fields.length) {
uniqueValues.forEach(value =>
facets[value].facets =
facetize(facets[value].data, fields));
}
return uniqueValues.map( value => facets[value] );
}
// The root provides a resolver function for each API endpoint
const resolvers = {
Query : {
facets: (_, { field }) => facetize(data, [field], "Query.facets")
},
Facet : {
facets: (obj, { field }) => facetize(obj.data, [field], "Facet.facets")
}
};
// do some schema magic
const schema = makeExecutableSchema({ typeDefs, resolvers })
// Run the GraphQL query and print out the response
graphql(
schema, `
{
facets(field: "eyeColor") {
value
field
count
facets(field: "hairColor") {
value
field
count
}
}
}`
).then((response) => {
console.log(JSON.stringify(response,null,2));
});
还有一个输出的sn-p
entering facetize from Query.facets using fields eyeColor
entering facetize from Facet.facets using fields hairColor
entering facetize from Facet.facets using fields hairColor
entering facetize from Facet.facets using fields hairColor
{
"data": {
"facets": [
{
"value": "blue",
"field": "eyeColor",
"count": 4,
"facets": [
{
"value": "brown",
"field": "hairColor",
"count": 3
},
{
"value": "blonde",
"field": "hairColor",
"count": 1
}
]
},
{
"value": "green",
"field": "eyeColor",
"count": 2,
"facets": [
{
"value": "black",
"field": "hairColor",
"count": 1
},
{
"value": "blonde",
"field": "hairColor",
"count": 1
}
]
},
...
【问题讨论】: