谢谢你的提问。 Ben 已经写出了一个完整的示例,展示了您可以做什么,我将根据他的建议并尝试进一步澄清。
FaunaDB 的 FQL 非常强大,这意味着有多种方法可以做到这一点,但如此强大的功能带来的学习曲线很小,所以我很乐意提供帮助:)。花了一段时间来回答这个问题的原因是,如此详尽的答案实际上值得一篇完整的博客文章。好吧,我从来没有在 Stack Overflow 上写过博客文章,凡事都有第一次!
有以下三种方法可以做到“复合范围查询”但是有一种方法对于您的用例来说是最有效的,我们会看到第一种方法实际上并不完全是您所需要的。剧透,我们在这里描述的第三个选项正是您所需要的。
准备 - 让我们像 Ben 一样输入一些数据
我将把它保存在一个集合中以使其更简单,并且在这里使用 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 and Range来查询那些。然而!如果您使用多个值,范围可能会给您带来与预期不同的结果。 Range 准确地为您提供了索引的功能,并且索引按词法对值进行排序。如果我们看一下这个例子Range https://docs.fauna.com/fauna/current/api/fql/functions/range在文档中,我们看到一个示例,我们可以扩展多个值。
想象一下,我们有一个包含两个值的索引,我们这样写:
Range(Match(Index('people_by_age_first')), [80, 'Leslie'], [92, 'Marvin'])
那么结果将是您在左侧看到的而不是您在右侧看到的。这是一种非常可扩展的行为,并且在没有底层索引开销的情况下公开了原始功能,但这并不是您正在寻找的!
那么让我们转向另一个解决方案!
选项 2:首先选择范围,然后选择筛选
另一个相当灵活的解决方案是使用 Range,然后使用 Filter。然而,如果您使用过滤器过滤掉很多内容,那么这并不是一个好主意,因为您的页面会变得更加空。想象一下,在“范围”之后,页面中有 10 个项目,并使用过滤器,那么最终会得到包含 2、5、4 个元素的页面,具体取决于过滤掉的内容。这是一个好主意,但是如果这些属性之一具有如此高的基数,它将过滤掉大多数实体。例如。假设所有内容都带有时间戳,您希望首先获取日期范围,然后继续过滤只会消除结果集中一小部分的内容。我相信在您的情况下,所有这些值都相当相等,因此第三个解决方案(见下文)将是最适合您的。
在这种情况下,我们可以将所有值放入其中,以便它们allget 返回,避免了 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]))
它应该返回两个元素(第三个元素有露营 === 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?”。那是因为 Filter 起作用了after分页和您的页面最初有两个实体,其中一个被过滤掉。因此,您剩下一个值为 1 的页面和一个指向下一页的指针。
{
"after": [
...
],
"data": [
[
1,
7,
3,
7,
10,
6,
"mullion",
"team-building"
]
]
您还可以选择直接在 SetRef 上过滤,然后才分页。在这种情况下,页面的大小将包含所需的大小。但是,请记住,这是对从 Range 返回的元素数量进行 O(n) 操作。 Range 使用索引,但从使用 Filter 的那一刻起,它将循环遍历每个元素。
选项 3:一个值的索引 + 交集!
这是适合您的用例的最佳解决方案,但它需要更多的理解和中间索引。
当我们查看文档示例时路口 https://docs.fauna.com/fauna/current/api/fql/functions/intersection我们看到这个例子:
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,因此包括露营/游泳值以及参考值。这意味着我们必须删除该值,因为我们只需要引用。这样做的方法before分页是通过连接进行的,本质上我们将与另一个索引连接,该索引将只是..返回引用(不指定值默认仅返回引用)
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')) 的结果,并通过连接仅返回引用的索引来删除该值。现在让我们将其结合起来并执行 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'))
))
))
结果是两个值,并且都在同一页面中!
请注意,您可以轻松地extend or composeFQL 通过仅使用本机语言(在本例中为 JS)来使其看起来更好(注意我没有测试这段代码)
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 中的索引是视图,所以我们称它们为独立视图。这是一种权衡,本质上是用计算来交换存储。通过创建包含许多值的视图,您可以非常快速地访问数据的某个子集。但还有另一个权衡,那就是灵活性。你可以not只需添加元素,因为这需要您重写整个索引。在这种情况下,如果您有大量数据(是的,这很常见),您将必须创建一个新索引并等待它构建,并确保您执行的查询(查看映射过滤器中的 lambda 参数)匹配你的新索引。之后您可以随时删除其他索引。仅使用 Map/Get 会更加灵活,数据库中的所有内容都是一种权衡,而 FaunaDB 为您提供了两种选择:)。我建议从数据模型固定并且您在应用程序中看到要优化的特定部分开始就使用这种方法。
避免 MapGet
关于 Map/Get 的第二个问题需要一些解释。如果您想使用 Join 来获取实际值,那么将要搜索的值与位置分开(就像 Ben 所做的那样)是一个好主意。places更有效率。这不需要 Map Get,因此读取的成本要少得多,但请注意,Join 更像是一种遍历(它将用它连接到的目标引用替换当前引用),因此如果您需要值和实际位置查询结束时一个对象中的数据比您需要 Map/Get 的数据要多。从这个角度来看,索引在读取方面非常便宜,你可以走得很远,但对于某些操作来说,没有办法绕过 Map/Get,Get 仍然只有 1 次读取。考虑到你每天免费获得 100 000 个,这仍然不贵:)。您可以将页面保持相对较小(分页中的大小参数),以确保您不会进行不必要的获取,除非您的用户或应用程序需要更多页面。
对于那些还不知道这一点的阅读本文的人:
- 1 个索引页 === 1 次阅读
- 1 获取 === 1 阅读
最后的笔记
我们可以而且将来会让这变得更容易。但是,请注意,您正在使用可扩展的分布式数据库,并且通常这些事情在其他解决方案中甚至是不可能的,或者效率非常低。 FaunaDB 为您提供了非常强大的结构和对索引如何工作的原始访问,并为您提供了许多选项。它不会试图在幕后为您聪明,因为如果我们出错,这可能会导致非常低效的查询(这在可扩展的即用即付系统中将是一个无赖)。