谢谢你的问题。Ben 已经写了一个完整的例子来展示你可以做什么,我会根据他的建议来进一步澄清。
FaunaDB 的 FQL 非常强大,这意味着有多种方法可以做到这一点,但是有了这样的能力,学习曲线就会很短,所以我很乐意提供帮助 :)。回答这个问题需要一段时间的原因是,如此详尽的答案实际上值得一篇完整的博客文章。好吧,我从来没有在 Stack Overflow 上写过博客文章,一切都是第一次!
有三种方法可以执行“复合范围查询”,但有一种方法对您的用例来说性能最好,我们会看到第一种方法实际上并不完全是您所需要的。剧透,我们在这里描述的第三个选项是你需要的。
准备 - 让我们像 Ben 一样输入一些数据
我会将它保存在一个集合中以使其更简单,并在此处使用 Fauna Query Language 的 JavaScript 风格。有充分的理由在第二个集合中分离数据,但这与您的第二个地图/获取问题有关(请参阅此答案的结尾)
创建集合
CreateCollection({ name: 'place' })
输入一些数据
Do(
Select(
['ref'],
Create(Collection('place'), {
data: {
name: 'mullion',
focus: 'team-building',
camping: 1,
swimming: 7,
hiking: 3,
culture: 7,
nightlife: 10,
budget: 6
}
})
),
Select(
['ref'],
Create(Collection('place'), {
data: {
name: 'church covet',
focus: 'private',
camping: 1,
swimming: 7,
hiking: 9,
culture: 7,
nightlife: 10,
budget: 6
}
})
),
Select(
['ref'],
Create(Collection('place'), {
data: {
name: 'the great outdoors',
focus: 'private',
camping: 5,
swimming: 3,
hiking: 2,
culture: 1,
nightlife: 9,
budget: 3
}
})
)
)
选项 1:具有多个值的复合索引
我们可以在索引中放置与值一样多的术语,并使用Match和Range来查询它们。然而!如果您使用多个值,范围可能会给您带来与您预期不同的东西。Range 为您提供了索引的确切功能,并且索引按词法对值进行排序。如果我们查看文档中的Range示例,我们会看到一个示例,我们可以在该示例上扩展多个值。
想象一下,我们会有一个包含两个值的索引,然后我们写:
Range(Match(Index('people_by_age_first')), [80, 'Leslie'], [92, 'Marvin'])
然后结果将是您在左侧看到的,而不是您在右侧看到的。这是一种非常可扩展的行为,并且在没有底层索引开销的情况下暴露了原始功能,但这并不是您正在寻找的!

所以让我们继续另一个解决方案!
选项 2:首先是范围,然后是过滤器
另一个相当灵活的解决方案是使用 Range,然后使用 Filter。但是,如果您使用过滤器过滤掉很多内容,那么这不是一个好主意,因为您的页面将变得更加空白。想象一下,在“范围”之后的页面中有 10 个项目并使用过滤器,那么最终会得到 2、5、4 个元素的页面,具体取决于过滤掉的内容。这是一个好主意,但是如果这些属性中的一个具有如此高的基数以至于它将过滤掉大多数实体。例如,假设所有内容都带有时间戳,您希望首先获得一个日期范围,然后继续过滤只会消除一小部分结果集的内容。我相信在您的情况下,所有这些值都非常相等,因此这第三种解决方案(见下文)将是最适合您的。
在这种情况下,我们可以将所有值都放入其中,以便它们全部返回,从而避免 Get。例如,假设“露营”是我们最重要的过滤器。
CreateIndex({
name: 'all_camping_first',
source: Collection('place'),
values: [
{ field: ['data', 'camping'] },
// and the rest will not be used for filter
// but we want to return them to avoid Map/Get
{ field: ['data', 'swimming'] },
{ field: ['data', 'hiking'] },
{ field: ['data', 'culture'] },
{ field: ['data', 'nightlife'] },
{ field: ['data', 'budget'] },
{ field: ['data', 'name'] },
{ field: ['data', 'focus'] },
]
})
您现在可以编写一个查询,该查询仅根据露营值获取范围:
Paginate(Range(Match('all_camping_first'), [1], [3]))
哪个应该返回两个元素(第三个有 camping === 5) 现在假设我们要过滤这些元素,我们将页面设置为小以避免不必要的工作
Filter(
Paginate(Range(Match('all_camping_first'), [1], [3]), { size: 2 }),
Lambda(
['camping', 'swimming', 'hiking', 'culture', 'nightlife', 'budget', 'name', 'focus'],
And(GTE(Var('hiking'), 0), GTE(7, Var('hiking')))
)
)
由于我想清楚每种方法的优点和缺点,让我们通过添加另一个具有与我们的查询匹配的属性的过滤器来准确展示过滤器的工作原理。
Create(Collection('place'), {
data: {
name: 'the safari',
focus: 'team-building',
camping: 1,
swimming: 9,
hiking: 2,
culture: 4,
nightlife: 3,
budget: 10
}
})
运行相同的查询:
Filter(
Paginate(Range(Match('all_camping_first'), [1], [3]), { size: 2 }),
Lambda(
['camping', 'swimming', 'hiking', 'culture', 'nightlife', 'budget', 'name', 'focus'],
And(GTE(Var('hiking'), 0), GTE(7, Var('hiking')))
)
)
现在仍然只返回一个值,但为您提供指向下一页的“之后”光标。你可能会想:“嗯?我的页面大小是 2?”。那是因为过滤器在分页之后起作用,并且您的页面最初有两个实体,其中一个被过滤掉了。所以你只剩下一个值为 1 的页面和一个指向下一页的指针。
{
"after": [
...
],
"data": [
[
1,
7,
3,
7,
10,
6,
"mullion",
"team-building"
]
]
您也可以选择直接在 SetRef 上进行过滤,然后再分页。在这种情况下,您的页面大小将包含所需的大小。但是,请记住,这是对从 Range 返回的元素数量的 O(n) 操作。Range 使用索引,但从您使用 Filter 的那一刻起,它将遍历每个元素。
选项 3:一个值的索引 + 交叉点!
这是您的用例的最佳解决方案,但它需要更多的理解和中间索引。
当我们查看交叉点的文档示例时,我们会看到以下示例:
Paginate(
Intersection(
Match(q.Index('spells_by_element'), 'fire'),
Match(q.Index('spells_by_element'), 'water'),
)
)
这是有效的,因为它是相同索引的两倍,这意味着**结果是相似的值**(本例中的引用)。假设我们添加了一些索引。
CreateIndex({
name: 'by_camping',
source: Collection('place'),
values: [
{ field: ['data', 'camping']}, {field: ['ref']}
]
})
CreateIndex({
name: 'by_swimming',
source: Collection('place'),
values: [
{ field: ['data', 'swimming']}, {field: ['ref']}
]
})
CreateIndex({
name: 'by_hiking',
source: Collection('place'),
values: [
{ field: ['data', 'hiking']}, {field: ['ref']}
]
})
我们现在可以与它们相交,但它不会给我们正确的结果。例如......让我们称之为:
Paginate(
Intersection(
Range(Match(Index("by_camping")), [3], []),
Range(Match(Index("by_swimming")), [3], [])
)
)
结果是空的。虽然我们有一个游泳 3 和露营 5。这正是问题所在。如果游泳和露营都是相同的价值,我们会得到一个结果。因此,重要的是要注意 Intersection 与values相交,因此包括露营/游泳值和参考值。这意味着我们必须删除该值,因为我们只需要引用。在分页之前执行此操作的方法是使用连接,基本上我们将与另一个索引连接
CreateIndex({
name: 'ref_by_ref',
source: Collection('place'),
terms: [{field: ['ref']}]
})
此连接如下所示
Paginate(Join(
Range(Match(Index('by_camping')), [4], [9]),
Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
)))
在这里,我们只取了 Match(Index('by_camping')) 的结果,并通过加入一个只返回 ref 的索引来删除该值。现在让我们把它结合起来,做一个 AND 类型的范围查询;)
Paginate(Intersection(
Join(
Range(Match(Index('by_camping')), [1], [3]),
Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
)),
Join(
Range(Match(Index('by_hiking')), [0], [7]),
Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
))
))
结果是两个值,并且都在同一个页面中!
请注意,您可以通过使用本机语言(在本例中为 JS)轻松扩展或编写FQL,使其看起来更好(注意我没有测试这段代码)
const DropAllButRef = function(RangeMatch) {
return Join(
RangeMatch,
Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
))
}
Paginate(Intersection(
DropAllButRef (Range(Match(Index('by_camping')), [1], [3])),
DropAllButRef (Range(Match(Index('by_hiking')), [0], [7]))
))
最后一个扩展,它只返回索引,所以你需要映射 get。如果你真的想通过......当然有一种方法可以解决这个问题。只需使用另一个索引:)
const index = CreateIndex({
name: 'all_values_by_ref',
source: Collection('place'),
values: [
{ field: ['data', 'camping'] },
{ field: ['data', 'swimming'] },
{ field: ['data', 'hiking'] },
{ field: ['data', 'culture'] },
{ field: ['data', 'nightlife'] },
{ field: ['data', 'budget'] },
{ field: ['data', 'name'] },
{ field: ['data', 'focus'] }
],
terms: [
{ field: ['ref'] }
]
})
现在您有了范围查询,无需地图/获取即可获得所有内容:
Paginate(
Intersection(
Join(
Range(Match(Index('by_camping')), [1], [3]),
Lambda(['value', 'ref'], Match(Index('all_values_by_ref'), Var('ref'))
)),
Join(
Range(Match(Index('by_hiking')), [0], [7]),
Lambda(['value', 'ref'], Match(Index('all_values_by_ref'), Var('ref'))
))
)
)
使用这种连接方法,您甚至可以对不同集合进行范围索引,只要在相交之前将它们连接到相同的引用!很酷吧?
我可以在索引中存储更多值吗?
是的,你可以,FaunaDB 中的索引是视图,所以我们称它们为 indiviews。这是一个权衡,本质上你是在用计算交换存储。通过创建具有多个值的视图,您可以非常快速地访问数据的某个子集。但还有另一个权衡,那就是灵活性。你不能只需添加元素,因为这需要您重写整个索引。在这种情况下,如果您有很多数据(是的,这很常见),您将必须创建一个新索引并等待它构建,并确保您执行的查询(查看映射过滤器中的 lambda 参数)匹配你的新索引。之后您始终可以删除其他索引。仅使用 Map/Get 会更加灵活,数据库中的所有内容都是一种权衡,FaunaDB 为您提供了两种选择:)。我建议从您的数据模型固定并且您在应用程序中看到要优化的特定部分的那一刻起就使用这种方法。
避免 MapGet
关于 Map/Get 的第二个问题需要一些解释。如果您想使用 Join 获取实际地点,则将您将搜索的值与地点(如 Ben 所做的)分开是一个好主意更有效率。这不需要 Map Get,因此您的阅读成本要少得多,但请注意 Join 是一个遍历(它将用它加入的目标引用替换当前引用)所以如果您需要值和实际位置查询结束时一个对象中的数据比您需要 Map/Get。从这个角度来看,索引在读取方面非常便宜,您可以在这些方面走得很远,但是对于某些操作,Map/Get 是没有办法的,Get 仍然只有 1 次读取。鉴于您每天免费获得 100 000 个,这仍然不贵:)。您可以保持您的页面也相对较小(分页中的大小参数),以确保您不会进行不必要的获取,除非您的用户或应用程序需要更多页面。对于阅读本文但还不知道这一点的人:
- 1 个索引页 === 1 次阅读
- 1 次获得 === 1 次阅读
最后的笔记
我们可以而且将来会更容易做到这一点。但是,请注意,您正在使用可扩展的分布式数据库,并且通常这些事情在其他解决方案中甚至是不可能的,或者效率很低。FaunaDB 为您提供了非常强大的结构和对索引如何工作的原始访问,并为您提供了许多选项。它不会试图在幕后为您聪明,因为如果我们弄错了,这可能会导致查询效率非常低(这在可扩展的现收现付系统中会很糟糕)。