如何在faunadb中按多个条件查询?

Jou*_*ist 5 faunadb

我试图提高我对 FaunaDB 的理解。

我有一个包含以下记录的集合:

{
  "ref": Ref(Collection("regions"), "261442015390073344"),
  "ts": 1587576285055000,
  "data": {
    "name": "italy",
    "attributes": {
      "amenities": {
        "camping": 1,
        "swimming": 7,
        "hiking": 3,
        "culture": 7,
        "nightlife": 10,
        "budget": 6
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我想通过不同的属性以灵活的方式进行查询,例如:

  • data.attributes.amenities.camping > 5
  • data.attributes.amenities.camping > 5 AND data.attributes.amenities.hiking > 6
  • data.attributes.amenities.camping < 6 AND data.attributes.amenities.culture > 6 AND 徒步旅行 > 5 AND ...

我创建了一个包含所有属性的索引,但我不知道如何在包含多个术语的索引中进行更大的等于过滤。

我的回退是为每个属性创建一个索引并使用 Intersection 来获取我想要检查的所有子查询中的记录,但这感觉有点不对:

查询:budget >= 6 AND camping >=8 将是:

Index:
{
  name: "all_regions_by_all_attributes",
  unique: false,
  serialized: true,
  source: "regions",
  terms: [],
  values: [
    {
      field: ["data", "attributes", "amenities", "culture"]
    },
    {
      field: ["data", "attributes", "amenities", "hiking"]
    },
    {
      field: ["data", "attributes", "amenities", "swimming"]
    },
    {
      field: ["data", "attributes", "amenities", "budget"]
    },
    {
      field: ["data", "attributes", "amenities", "nightlife"]
    },
    {
      field: ["data", "attributes", "amenities", "camping"]
    },
    {
      field: ["ref"]
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

询问:

Map(
  Paginate(
    Intersection(
      Range(Match(Index("all_regions_by_all_attributes")), [0, 0, 0, 6, 0, 8], [10, 10, 10, 10, 10, 10]),
    )

  ),
  Lambda(
    ["culture", "hiking", "swimming", "budget", "nightlife", "camping", "ref"],
    Get(Var("ref"))
  )
)
Run Code Online (Sandbox Code Playgroud)

这种方法有以下缺点:

  • 它不像预期的那样工作,例如,如果第一个(文化)属性在这个范围内,但第二个(远足)不在,那么我仍然会得到一个返回值
  • 由于我需要为每个结果遵循参考,它会导致大量阅读。

是否可以将所有值存储在包含所有数据的这种索引中?我知道我可以向索引添加更多值并访问它们。但这意味着一旦我们向实体添加更多字段,我就必须创建一个新索引。但也许这是一个普遍的事情。

提前致谢

Bre*_*oms 10

谢谢你的提问。Ben 已经写了一个完整的例子来展示你可以做什么,我将根据他的建议并尝试进一步澄清。

FaunaDB 的 FQL 非常强大,这意味着有多种方法可以做到这一点,但是有了这样的强大功能,学习曲线很小,所以我很乐意提供帮助:)。回答这个问题需要一段时间的原因是,如此详尽的答案实际上值得一篇完整的博客文章。好吧,我从来没有在 Stack Overflow 上写过一篇博文,一切都是第一次!

有三种方法可以执行“类似复合范围的查询”,但有一种方法对您的用例最有效,我们将看到第一种方法实际上并不完全符合您的需要。剧透,我们在此描述的第三个选项正是您所需要的。

准备 - 让我们像 Ben 一样输入一些数据

我会将它放在一个集合中以使其更简单,并在此处使用 Fauna Query Language 的 JavaScript 风格。有一个很好的理由在第二个集合中分离数据,尽管这与您的第二个地图/获取问题有关(请参阅此答案的结尾)

创建集合

 CreateCollection({ name: 'place' })
Run Code Online (Sandbox Code Playgroud)

输入一些数据

    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
          }
        })
      )
    )
Run Code Online (Sandbox Code Playgroud)

选项 1:具有多个值的复合索引

我们可以在索引中放入与值一样多的术语,并使用MatchRange来查询它们。然而!如果您使用多个值,范围可能会为您提供与您预期不同的东西。Range 准确地为您提供索引的作用,索引按词法对值进行排序。如果我们查看文档中的Range示例,我们会在那里看到一个示例,我们可以将其扩展为多个值。

想象一下,我们有一个包含两个值的索引,然后我们写:

    Range(Match(Index('people_by_age_first')), [80, 'Leslie'], [92, 'Marvin'])
Run Code Online (Sandbox Code Playgroud)

那么结果将是你在左边看到的,而不是你在右边看到的。这是一种非常可扩展的行为,可以在没有基础索引开销的情况下公开原始功能,但并不是您正在寻找的!

范围索引行为

所以让我们转向另一个解决方案!

选项 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'] },
      ]
    })
Run Code Online (Sandbox Code Playgroud)

您现在可以编写一个查询,根据露营值获取一个范围:

    Paginate(Range(Match('all_camping_first'), [1], [3]))
Run Code Online (Sandbox Code Playgroud)

哪个应该返回两个元素(第三个有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')))
      )
    )
Run Code Online (Sandbox Code Playgroud)

由于我想明确每种方法的优点和缺点,让我们通过添加另一个具有与我们的查询匹配的属性的方法来准确展示过滤器的工作原理。

    Create(Collection('place'), {
      data: {
        name: 'the safari',
        focus: 'team-building',
        camping: 1,
        swimming: 9,
        hiking: 2,
        culture: 4,
        nightlife: 3,
        budget: 10
      }
    })
Run Code Online (Sandbox Code Playgroud)

运行相同的查询:

    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')))
      )
    )
Run Code Online (Sandbox Code Playgroud)

现在仍然只返回一个值,但为您提供了一个指向下一页的“后”光标。您可能会想:“嗯?我的页面大小是 2?”。那是因为 FilterPagination之后起作用而您的页面最初有两​​个实体,其中一个实体被过滤掉了。所以你只剩下一个值为 1 的页面和一个指向下一页的指针。


{
  "after": [
    ... 
  ],
  "data": [
    [
      1,
      7,
      3,
      7,
      10,
      6,
      "mullion",
      "team-building"
    ]
  ]
Run Code Online (Sandbox Code Playgroud)

您也可以选择直接在 SetRef 上进行过滤,然后才进行分页。在这种情况下,您的页面大小将包含所需的大小。但是,请记住,这是对从 Range 返回的元素数量的 O(n) 运算。Range 使用索引,但从您使用 Filter 的那一刻起,它将遍历每个元素。

选项 3:索引一个值 + 交点!

这是您的用例的最佳解决方案,但它需要更多的理解和中间索引。

当我们查看交集的文档示例时,我们会看到这个示例:

    Paginate(
       Intersection(
          Match(q.Index('spells_by_element'), 'fire'),
          Match(q.Index('spells_by_element'), 'water'),
       )
    ) 
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为它是相同索引的两倍,这意味着 ** 结果是相似的值 **(在这种情况下是引用)。假设我们添加了一些索引。

    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']} 
      ]
    })

Run Code Online (Sandbox Code Playgroud)

我们现在可以在它们上相交,但它不会给我们正确的结果。例如......让我们称之为:

    Paginate(
      Intersection(
        Range(Match(Index("by_camping")), [3], []),
        Range(Match(Index("by_swimming")), [3], [])
      )
    )
Run Code Online (Sandbox Code Playgroud)

结果是空的。虽然我们有一个游泳 3 和露营 5。这正是问题所在。如果游泳和露营的值相同,我们就会得到一个结果。因此,重要的是要注意 Intersection 与values相交,因此包括露营/游泳值以及参考值。这意味着我们必须删除该值,因为我们只需要引用。分页之前这样做的方法是使用连接,本质上我们将与另一个索引连接,该索引将只是..返回引用(不指定值默认为仅引用)

CreateIndex({
  name: 'ref_by_ref',
  source: Collection('place'),
  terms: [{field:  ['ref']}]
})
Run Code Online (Sandbox Code Playgroud)

此连接如下所示

    Paginate(Join(
      Range(Match(Index('by_camping')), [4], [9]),
      Lambda(['value', 'ref'], Match(Index('ref_by_ref'), Var('ref'))
    )))
Run Code Online (Sandbox Code Playgroud)

在这里,我们只是获取了 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'))
      ))
    ))
Run Code Online (Sandbox Code Playgroud)

结果是两个值,并且都在同一页面中!

请注意,您可以通过使用本机语言(在本例中为 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]))
    ))
Run Code Online (Sandbox Code Playgroud)

最后一个扩展,这只返回索引,所以你需要映射 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'] }
      ]
    }) 
Run Code Online (Sandbox Code Playgroud)

现在您有了范围查询,将在没有地图/获取的情况下获取所有内容:

  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'))
      ))
    )
  )

Run Code Online (Sandbox Code Playgroud)

使用这种连接方法,您甚至可以对不同的集合进行范围索引,只要在相交之前将它们连接到相同的引用即可!很酷吧?

我可以在索引中存储更多值吗?

是的,你可以,FaunaDB 中的索引是视图,所以我们称它们为独立视图。这是一种权衡,本质上您是在用计算交换存储。通过创建具有多个值的视图,您可以非常快速地访问数据的某个子集。但还有另一个权衡,那就是灵活性。你不能只需添加元素,因为这需要您重写整个索引。在这种情况下,如果您有很多数据(是的,这很常见),您将不得不创建一个新索引并等待它构建,并确保您执行的查询(查看映射过滤器中的 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 为您提供了非常强大的结构和对索引工作方式的原始访问,并为您提供了许多选择。它不会试图在幕后为您聪明,因为如果我们弄错了,这可能会导致非常低效的查询(这在可扩展的即用即付系统中会很糟糕)。


ben*_*nji 6

我认为有一些误解会让你误入歧途。最重要的一个:Match(Index($x))生成一个集合引用,它是一组有序的元组。元组对应于索引的值部分中存在的字段数组。默认情况下,这将只是一个单元组,其中包含对索引选择的集合中文档的引用。Range 对集合引用进行操作,并且对用于选择返回的集合引用的术语一无所知。那么我们如何编写查询呢?

从第一原则开始。让我们想象一下,我们的内存中只有这些东西。如果我们有一组按属性排序的(属性,分数),评分然后只取那些attribute == $attribute可以让我们接近的那些,然后过滤score > $score会让我们得到我们想要的。假设我们将属性值对建模为文档,这完全对应于以属性为术语的分数范围查询。我们还可以将指针嵌入回该位置,以便我们也可以在同一查询中检索该位置。废话不多说,开始吧:

第一站:我们的收藏。

jnr> CreateCollection({name: "place_attribute"})
{
  ref: Collection("place_attribute"),
  ts: 1588528443250000,
  history_days: 30,
  name: 'place_attribute'
}
jnr> CreateCollection({name: "place"})
{
  ref: Collection("place"),
  ts: 1588528453350000,
  history_days: 30,
  name: 'place'
}
Run Code Online (Sandbox Code Playgroud)

接下来上一些数据。我们将选择几个地方并赋予它们一些属性。

jnr> Create(Collection("place"), {data: {"name": "mullion"}})
jnr> Create(Collection("place"), {data: {"name": "church cove"}})
jnr> Create(Collection("place_attribute"), {data: {"attribute": "swimming", "score": 3, "place": Ref(Collection("place"), 264525084639625739)}})
jnr> Create(Collection("place_attribute"), {data: {"attribute": "hiking", "score": 1, "place": Ref(Collection("place"), 264525084639625739)}}) 
jnr> Create(Collection("place_attribute"), {data: {"attribute": "hiking", "score": 7, "place": Ref(Collection("place"), 264525091487875586)}})
Run Code Online (Sandbox Code Playgroud)

现在是更有趣的部分。指数。

jnr> CreateIndex({name: "attr_score", source: Collection("place_attribute"), terms:[{"field":["data", "attribute"]}], values:[{"field": ["data", "score"]}, {"field": ["data", "place"]}]})
{
  ref: Index("attr_score"),
  ts: 1588529816460000,
  active: true,
  serialized: true,
  name: 'attr_score',
  source: Collection("place_attribute"),
  terms: [ { field: [ 'data', 'attribute' ] } ],
  values: [ { field: [ 'data', 'score' ] }, { field: [ 'data', 'place' ] } ],
  partitions: 1
}
Run Code Online (Sandbox Code Playgroud)

好的。一个简单的查询。谁有远足?

jnr> Paginate(Match(Index("attr_score"), "hiking"))
{
  data: [
    [ 1, Ref(Collection("place"), "264525084639625730") ],
    [ 7, Ref(Collection("place"), "264525091487875600") ]
  ]
}
Run Code Online (Sandbox Code Playgroud)

没有太多的想象力,一个人可以偷偷调用一个 Get 调用来把这个地方拉出来。

只有得分超过 5 分的徒步旅行怎么样?我们有一组有序的元组,所以只提供第一个组件(分数)就足以让我们得到我们想要的。

jnr> Paginate(Range(Match(Index("attr_score"), "hiking"), [5], null))
{ data: [ [ 7, Ref(Collection("place"), "264525091487875600") ] ] }

Run Code Online (Sandbox Code Playgroud)

复合条件呢?5 岁以下远足和游泳(任何分数)。这是事情发生一些转折的地方。我们想要建模连接,这在动物群中意味着相交集。我们遇到的问题是,到目前为止,我们一直在使用一个返回分数和位置引用的索引。为了使交集起作用,我们只需要参考。是时候耍花招了:

jnr> Get(Index("doc_by_doc"))
{
  ref: Index("doc_by_doc"),
  ts: 1588530936380000,
  active: true,
  serialized: true,
  name: 'doc_by_doc',
  source: Collection("place"),
  terms: [ { field: [ 'ref' ] } ],
  partitions: 1
}
Run Code Online (Sandbox Code Playgroud)

你问这样的索引有什么意义?好吧,我们可以使用它从任何索引中删除我们喜欢的任何数据,并通过 join 只留下 refs。这为我们提供了远足分数小于 5 的地方参考(空数组在任何之前排序,因此用作下限的占位符)。

jnr> Paginate(Join(Range(Match(Index("attr_score"), "hiking"), [], [5]), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p")))))
{ data: [ Ref(Collection("place"), "264525084639625739") ] }
Run Code Online (Sandbox Code Playgroud)

所以最后是抵抗力:所有地方swimming and (hiking < 5)

jnr> Let({
...   hiking: Join(Range(Match(Index("attr_score"), "hiking"), [], [5]), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p")))),
...   swimming: Join(Match(Index("attr_score"), "swimming"), Lambda(["s", "p"], Match(Index("doc_by_doc"), Var("p"))))
... },
... Map(Paginate(Intersection(Var("hiking"), Var("swimming"))), Lambda("ref", Get(Var("ref"))))
... )
{
  data: [
    {
      ref: Ref(Collection("place"), "264525084639625739"),
      ts: 1588529629270000,
      data: { name: 'mullion' }
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

多田。这可以通过几个 udf 来整理很多,留给读者练习。涉及的条件or可以以几乎相同的方式使用 union 进行管理。