如何在 FaunaDB 中获取包含子字符串的文档

Posted

技术标签:

【中文标题】如何在 FaunaDB 中获取包含子字符串的文档【英文标题】:How to get documents that contain sub-string in FaunaDB 【发布时间】:2020-09-18 09:14:46 【问题描述】:

我正在尝试检索名称中包含字符串 first 的所有任务文档。

我目前有以下代码,但它只有在我传递确切名称时才有效:

res, err := db.client.Query(
    f.Map(
        f.Paginate(f.MatchTerm(f.Index("tasks_by_name"), "My first task")),
        f.Lambda("ref", f.Get(f.Var("ref"))),
    ),
)

我想我可以在某处使用ContainsStr(),但我不知道如何在查询中使用它。

另外,有没有不使用Filter() 的方法?我问是因为它似乎在分页后过滤,并且与页面混淆

【问题讨论】:

【参考方案1】:

FaunaDB 提供了很多构造,这使其功能强大,但您有很多选择。强大的力量带来了一个小的学习曲线:)。

如何阅读代码示例

为了清楚起见,我在这里使用 FQL 的 javascript 风格,通常从 JavaScript driver 公开 FQL 函数,如下所示:

const faunadb = require('faunadb')
const q = faunadb.query
const 
  Not,
  Abort,
  ...
 = q

你必须小心导出这样的地图,因为它会与 JavaScript 的地图冲突。在这种情况下,您可以只使用 q.Map。

选项 1:使用 ContainsStr() 和过滤器

根据docs的基本用法

ContainsStr('Fauna', 'a')

当然,这适用于特定值,因此为了使其工作,您需要过滤器和过滤器仅适用于分页集。这意味着我们首先需要获得一个分页集。获取一组分页文档的一种方法是:

q.Map(
  Paginate(Documents(Collection('tasks'))),
  Lambda(['ref'], Get(Var('ref')))
)

但是我们可以更有效地做到这一点,因为一次阅读 === 一次阅读并且我们不需要文档,我们将过滤掉很多文档。有趣的是,一个索引页也是一个读取,因此我们可以如下定义索引:


  name: "tasks_name_and_ref",
  unique: false,
  serialized: true,
  source: "tasks",
  terms: [],
  values: [
    
      field: ["data", "name"]
    ,
    
      field: ["ref"]
    
  ]

由于我们在值中添加了 name 和 ref,索引将返回 name 和 ref 的页面,然后我们可以使用它们进行过滤。例如,我们可以对索引做类似的事情,对它们进行映射,这将返回一个布尔数组。

Map(
  Paginate(Match(Index('tasks_name_and_ref'))),
  Lambda(['name', 'ref'], ContainsStr(Var('name'), 'first'))
)

由于过滤器也适用于数组,我们实际上可以简单地将 Map 替换为过滤器。我们还将添加一个小写字母以忽略大小写,我们有我们需要的:

Filter(
  Paginate(Match(Index('tasks_name_and_ref'))),
  Lambda(['name', 'ref'], ContainsStr(LowerCase(Var('name')), 'first'))
)

就我而言,结果是:



  "data": [
    [
      "Firstly, we'll have to go and refactor this!",
      Ref(Collection("tasks"), "267120709035098631")
    ],
    [
      "go to a big rock-concert abroad, but let's not dive in headfirst",
      Ref(Collection("tasks"), "267120846106001926")
    ],
    [
      "The first thing to do is dance!",
      Ref(Collection("tasks"), "267120677201379847")
    ]
  ]

过滤和缩小页面大小

正如您所提到的,这并不是您想要的,因为这也意味着如果您请求 500 个大小的页面,它们可能会被过滤掉,您最终可能会得到一个大小为 3 的页面,然后是 7 个页面中的一个。你可能会想,为什么我不能只在页面中获取过滤后的元素?好吧,出于性能原因,这是一个好主意,因为它基本上检查每个值。想象一下,你有一个庞大的集合并过滤掉了 99.99%。您可能必须遍历许多元素才能达到 500,所有这些都需要读取。我们希望定价是可预测的:)。

选项 2:索引!

每次您想做更高效的事情时,答案就在于索引。 FaunaDB 为您提供了实现不同搜索策略的原始能力,但您必须有点创意,我在这里为您提供帮助:)。

绑定

在索引绑定中,您可以转换文档的属性,在我们的第一次尝试中,我们会将字符串拆分为单词(我将实现多个,因为我不完全确定您想要哪种匹配)

我们没有字符串拆分功能,但由于 FQL 很容易扩展,我们可以自己编写它,绑定到我们的宿主语言(在本例中为 javascript)中的一个变量,或者使用这个社区驱动的库中的一个:@987654323 @

function StringSplit(string: ExprArg, delimiter = " ")
    return If(
        Not(IsString(string)),
        Abort("SplitString only accept strings"),
        q.Map(
            FindStrRegex(string, Concat(["[^\\", delimiter, "]+"])),
            Lambda("res", LowerCase(Select(["data"], Var("res"))))
        )
    )
)

并在我们的绑定中使用它。

CreateIndex(
  name: 'tasks_by_words',
  source: [
    
      collection: Collection('tasks'),
      fields: 
        words: Query(Lambda('task', StringSplit(Select(['data', 'name']))))
      
    
  ],
  terms: [
    
      binding: 'words'
    
  ]
)

提示,如果您不确定自己是否正确,您可以随时将绑定放入 values 而不是术语,然后您会在 fauna dashboard 中看到您的索引是否真的包含值:

我们做了什么?我们刚刚编写了一个绑定,它将在写入文档时将值转换为 值数组。当您在 FaunaDB 中索引文档数组时,这些值是单独的索引,但都指向同一个文档,这对于我们的搜索实现非常有用。

我们现在可以使用以下查询找到包含字符串“first”作为其单词之一的任务:

q.Map(
  Paginate(Match(Index('tasks_by_words'), 'first')),
  Lambda('ref', Get(Var('ref')))
)

这会给我带有名称的文件: “第一件事就是跳舞!”

另外两个文档没有包含确切的单词,那么我们该怎么做呢?

选项 3:索引和 Ngram(精确包含匹配)

要获得精确的包含匹配效率,您需要使用一个名为“NGram”的函数(仍然没有记录的函数,因为我们将来会使其更容易)。在 ngram 中划分字符串是 search technique,它经常在其他搜索引擎的底层使用。在 FaunaDB 中,由于索引和绑定的强大功能,我们可以轻松地应用它。 Fwitter example 有一个例子,它是 source code,它会自动完成。此示例不适用于您的用例,但我确实为其他用户引用了它,因为它用于自动完成短字符串,而不是像任务一样在较长字符串中搜索短字符串。

不过,我们会根据您的用例对其进行调整。在搜索方面,这完全是性能和存储的权衡,在 FaunaDB 中,用户可以选择他们的权衡。请注意,在前面的方法中,我们分别存储每个单词,使用 Ngrams 我们将进一步拆分单词以提供某种形式的模糊匹配。不利的一面是,如果您做出错误的选择,索引大小可能会变得非常大(搜索引擎同样如此,因此它们让您定义不同的算法)。

NGram 本质上所做的是获取一定长度的字符串的子字符串。 例如:

NGram('lalala', 3, 3)

将返回:

如果我们知道我们不会搜索超过特定长度的字符串,假设长度为 10(这是一种折衷,增加大小会增加存储要求,但允许您查询更长的字符串),您可以写如下的Ngram生成器。

function GenerateNgrams(Phrase) 
  return Distinct(
    Union(
      Let(
        
          // Reduce this array if you want less ngrams per word.
          indexes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
          indexesFiltered: Filter(
            Var('indexes'),
            // filter out the ones below 0
            Lambda('l', GT(Var('l'), 0))
          ),
          ngramsArray: q.Map(Var('indexesFiltered'), Lambda('l', NGram(LowerCase(Var('Phrase')), Var('l'), Var('l'))))
        ,
        Var('ngramsArray')
      )
    )
  )

然后您可以按如下方式编写索引:

CreateIndex(
  name: 'tasks_by_ngrams_exact',
  // we actually want to sort to get the shortest word that matches first
  source: [
    
      // If your collections have the same property tht you want to access you can pass a list to the collection
      collection: [Collection('tasks')],
      fields: 
        wordparts: Query(Lambda('task', GenerateNgrams(Select(['data', 'name'], Var('task')))))
      
    
  ],
  terms: [
    
      binding: 'wordparts'
    
  ]
)

你有一个索引支持的搜索,你的页面是你请求的大小。

q.Map(
  Paginate(Match(Index('tasks_by_ngrams_exact'), 'first')),
  Lambda('ref', Get(Var('ref')))
)

选项 4:索引和大小为 3 的 Ngram 或三元组(模糊匹配)

如果你想要模糊搜索,often trigrams are used,在这种情况下,我们的索引会很简单,所以我们不会使用外部函数。

CreateIndex(
  name: 'tasks_by_ngrams',
  source: 
    collection: Collection('tasks'),
    fields: 
      ngrams: Query(Lambda('task', Distinct(NGram(LowerCase(Select(['data', 'name'], Var('task'))), 3, 3))))
    
  ,
  terms: [
    
      binding: 'ngrams'
    
  ]
)

如果我们再次将绑定放在值中以查看结果,我们将看到如下内容: 在这种方法中,我们在索引端和查询端一样使用两个三元组。在查询方面,这意味着我们搜索的“第一个”单词也将在 Trigrams 中划分如下:

例如,我们现在可以进行如下模糊搜索:

q.Map(
  Paginate(Union(q.Map(NGram('first', 3, 3), Lambda('ngram', Match(Index('tasks_by_ngrams'), Var('ngram')))))),
  Lambda('ref', Get(Var('ref')))
)

在这种情况下,我们实际上进行了 3 次搜索,我们正在搜索所有三元组并将结果合并。这将返回我们所有包含 first 的句子

但如果我们拼写错误并写成 frst,我们仍然会匹配所有三个,因为存在匹配的三元组 (rst)

【讨论】:

哇,多么棒的答案!您对这些方法的性能/读取成本有估计吗?有明显的赢家,还是有取舍? 我没有估计,但我相信 Filter 是不可扩展的,因为它是 O(n) 行为,其中 n 是您需要检查和过滤的项目数量,直到您有足够的项目填满一页。虽然 Ngram 解决方案使用索引,但性能影响实际上已转移到另一个位置。拥有像这样的索引绑定(ngrams 字段)会创建对索引的写入扇出,这会减慢写入速度。 Fauna 旨在保持一致,而不是搜索引擎,因此您不希望对具有许多写入的集合执行此操作,其中每个单词/短语都有许多 ngram 拆分。 也就是说,查找本身应该非常快。 有没有一种方法可以根据匹配的索引数对选项 4(索引和 ngram)进行排名? 如果不写出来我不能完全确定(我与 Fauna 合作已经有一段时间了)。但是,如果我没记错的话,您可以在第一个 Map 之后减少结果以获取每个引用的出现次数。我不认为有一种简单的方法可以做到这一点,因为它本质上是某种“分组依据”,可能很难用 FQL 编写。但是,由于就复杂性而言,这将是一个 O(n) 操作,您可能也可以在调用代码中执行此操作(当然,除非您正在制作基于 UDF 的 API)。

以上是关于如何在 FaunaDB 中获取包含子字符串的文档的主要内容,如果未能解决你的问题,请参考以下文章

如何在 FaunaDB 中获取嵌套文档?

如何从 FaunaDB 的集合中获取所有文档?

如何获取 FaunaDB 类中的所有实例

如何获取 FaunaDB 类的字段或模式

如何在 Mongoose 中更改文档子数组的对象内的布尔值?

如何在 GraphQL 突变中使用 FaunaDB 将数组传递给字段