【问题标题】:How to query by multiple conditions in faunadb?如何通过多个条件查询动物区系数据库?
【发布时间】:2020-08-13 23:19:22
【问题描述】:

我试图加深对 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
      }
    }
  }
}

我想通过不同的属性以灵活的方式查询:

  • data.attributes.amenities.camping > 5
  • data.attributes.amenities.camping > 5 AND data.attributes.amenities.hiking > 6
  • data.attributes.amenities.camping 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"]
    }
  ]
}

查询:

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

这种方法有以下缺点:

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

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

提前致谢

【问题讨论】:

    标签: faunadb


    【解决方案1】:

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

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

    'compound range-like queries'有三种方法,但有一种方法对您的用例来说性能最好,我们会看到第一种方法实际上不是完全是你需要的。剧透,我们在这里描述的第三个选项就是你需要的。

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

    我会将它保存在一个集合中以使其更简单,并在此处使用 Fauna 查询语言的 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:具有多个值的复合索引

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

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

        Range(Match(Index('people_by_age_first')), [80, 'Leslie'], [92, 'Marvin'])
    

    那么结果将是您在左侧看到的,而不是您在右侧看到的。这是一种非常可扩展的行为,并且在没有底层索引开销的情况下暴露了原始功能,但这并不是您想要的!

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

    选项 2:首先是范围,然后是过滤器

    另一个相当灵活的解决方案是先使用 Range,然后使用 Filter。但是,如果您使用过滤器过滤掉很多内容,那么这不是一个好主意,因为您的页面将变得更加空白。想象一下,在“范围”之后的页面中有 10 个项目并使用过滤器,那么最终会得到 2、5、4 个元素的页面,具体取决于过滤掉的内容。这是一个好主意,但是如果这些属性中的一个具有如此高的基数以至于它将过滤掉大多数实体。例如。假设所有内容都带有时间戳,您希望首先获得一个日期范围,然后继续过滤只会消除一小部分结果集的内容。我相信在您的情况下,所有这些值都非常相等,因此这第三种解决方案(见下文)将是最适合您的。

    在这种情况下,我们可以将所有值放入其中,以便它们all返回,从而避免 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:一个值的索引 + 交叉点!

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

    当我们查看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 与 相交,因此既包括露营/游泳值,也包括参考值。这意味着我们必须删除该值,因为我们只需要引用。 before 分页的方法是使用连接,本质上我们将与另一个索引连接,该索引将返回 ref(不指定值默认为仅 ref)

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

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

    请注意,您可以轻松地扩展撰写 FQL,只需使用本地语言(在本例中为 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 中的索引是视图,所以我们称它们为 indiviews。这是一个权衡,本质上你是在用计算交换存储。通过创建具有多个值的视图,您可以非常快速地访问数据的某个子集。但还有另一个权衡,那就是灵活性。您可以只是去添加元素,因为这需要您重写整个索引。在这种情况下,如果您有很多数据(是的,这很常见),您将必须创建一个新索引并等待它构建,并确保您执行的查询(查看映射过滤器中的 lambda 参数)匹配你的新索引。之后您始终可以删除其他索引。仅使用 Map/Get 会更加灵活,数据库中的所有内容都是一种权衡,FaunaDB 为您提供了两种选择:)。我建议从你的数据模型被修复并且你在你的应用程序中看到你想要优化的特定部分的那一刻起就使用这种方法。

    避免 MapGet

    关于 Map/Get 的第二个问题需要一些解释。如果您想使用 Join 更有效地获取实际 places,则将您将搜索的值与地点(如 Ben 所做的)分开是一个好主意。这不需要 Map Get,因此您的阅读成本要少得多,但请注意 Join 是一个遍历(它将用它加入的目标引用替换当前引用)所以如果您需要值和实际位置查询结束时一个对象中的数据比您需要 Map/Get。从这个角度来看,索引在读取方面非常便宜,您可以在这些方面走得很远,但是对于某些操作,Map/Get 是没有办法的,Get 仍然只有 1 次读取。鉴于您每天免费获得 100 000 个,这仍然不贵:)。您可以保持您的页面也相对较小(分页中的大小参数),以确保您不会进行不必要的获取,除非您的用户或应用程序需要更多页面。 对于阅读本文但还不知道的人:

    • 1 个索引页 === 1 次阅读
    • 1 次获得 === 1 次阅读

    最后的笔记

    我们可以并且将来会更容易做到这一点。但是,请注意,您正在使用可扩展的分布式数据库,并且通常这些事情在其他解决方案中甚至是不可能的,或者效率很低。 FaunaDB 为您提供了非常强大的结构和对索引如何工作的原始访问,并为您提供了许多选项。它不会试图在幕后为您聪明,因为如果我们弄错了,这可能会导致查询效率非常低(这在可扩展的现收现付系统中会很糟糕)。

    【讨论】:

    • 因为这是一个相当详尽的答案,所以我有可能犯了错误。让我知道是否是这种情况,以便我尽快纠正;)
    • 感谢这次大型 founadb 加入/条件之旅。我可能需要再读 3 遍,直到我理解每一篇文章,但这太棒了!谢谢你们,继续做你们的工作:)。
    • 如何从 Join 的源 SetRef 继续排序/排序?
    • @Micael,如果您组合多个范围,那么您有多个排序。您对连接源 SetRef 的排序是什么意思?你能详细说明一下吗?
    • 我有一个名为“产品”的集合,我正在构建一个过滤器来按类别过滤并按观看次数、低价或高价排序。所以我为每个案例创建了索引,all_products_by_views、all_products_by_price_asc 和 all_products_by_price_desc。但是,当我将 all_products_by_price_asc(每个示例)的结果与 product_by_ref 结合时,就像在这个答案中一样,我丢失了 all_products_by_asc 的原始排序/排序。我希望你能理解。
    【解决方案2】:

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

    从第一原则开始。让我们想象一下,我们只是在内存中有这些东西。如果我们有一组按属性排序的(属性、分数),那么 score 则只取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'
    }
    

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

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

    现在是更有趣的部分。索引。

    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
    }
    

    好的。一个简单的查询。谁有徒步旅行?

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

    如果没有太多的想象力,你可以偷偷调用 Get 来拉出这个地方。

    如果只徒步旅行得分超过 5 分呢?我们有一组有序的元组,因此只需提供第一个组件(分数)就足以得到我们想要的。

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

    复合条件呢? 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
    }
    

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

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

    所以最后一块去抵抗: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' }
        }
      ]
    }
    

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

    【讨论】:

    • 我试图效仿你的榜样。不幸的是,连接查询不起作用。我将其追溯到 doc_by_doc 索引的一些问题。 Paginate(Match(Index("doc_by_doc"), "264597999006515713")) 没有为我返回任何内容(我使用了收藏条目中的 ref id)。索引定义与您的非常相似。我只有一个额外的信息告诉我unique: false
    • 我的例子确实有效。您可能没有索引,但这不是示例代码的问题。
    • 我实际上看到我在解决问题时更新了索引定义并且没有更新sn-p。很傻。让我解决这个问题。对不起!
    • 其实我又撒谎了!都是正确的。我把多份笔记弄糊涂了。如果你想用你的索引不工作来打击我,我们可以试试,但我希望这个示例中的索引能够逐字工作。
    • 嘿伙计,您可能复制粘贴了插入数据的部分。链接使用的是在 Ben 的案例中返回的引用,您必须更改;)。我提议对答案进行更改,使其成为一个多合一的复制粘贴查询。
    猜你喜欢
    • 1970-01-01
    • 2011-07-11
    • 2013-10-22
    • 1970-01-01
    • 2014-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多