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

How to get documents that contain sub-string in FaunaDB

我正在尝试检索名称中包含字符串 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() 的方法?我问是因为它似乎在分页后进行过滤,并且弄乱了页面

FaunaDB 提供了很多结构,这使得它很强大,但你有很多选择。强大的功能带来了一个小的学习曲线:)。

如何阅读代码示例

明确地说,我在这里使用 FQL 的 JavaScript 风格,并且通常公开 JavaScript driver 中的 FQL 函数,如下所示:

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

您必须小心导出这样的地图,因为它会与 JavaScripts 地图冲突。在这种情况下,您可以只使用 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 添加到值中,索引将包含 return 页面的 name 和 ref,然后我们可以使用这些页面进行过滤。例如,我们可以对索引做一些类似的事情,映射它们,这将 return 我们得到一个布尔数组。

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),或者使用这个社区中的一个驱动库:https://github.com/shiftx/faunadb-fql-lib

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 而不是 terms 中,然后您将在 fauna dashboard 您的索引是否实际包含值:

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

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

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

这将给我文件名称: "The first thing to do is dance!"

其他两个文件没有包含确切的词,那我们怎么办呢?

选项 3:索引和 Ngram(完全包含匹配)

要获得精确包含匹配的效率,您需要使用一个名为 'NGram' 的函数(仍未记录的函数,因为我们将在未来使它更容易)。在 ngrams 中划分一个字符串是一个 search technique that is often used underneath the hood in other search engines. In FaunaDB we can easily apply it as due to the power of the indexes and bindings. The Fwitter example has an example in it's source code 做自动完成。此示例不适用于您的用例,但我确实为其他用户参考了它,因为它用于自动完成短字符串,而不是像任务那样在较长的字符串中搜索短字符串。

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

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

NGram('lalala', 3, 3)

将 return:

如果我们知道我们不会搜索超过特定长度的字符串,假设长度为 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 或 trigram(模糊匹配)

如果你想要模糊搜索,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'
    }
  ]
})

如果我们再次将绑定放入值中以查看结果,我们将看到如下内容: 在这种方法中,我们在索引端和查询端都使用了三元组。在查询端,这意味着我们搜索的 'first' 词也将在八卦中划分如下:

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

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 次搜索,我们正在搜索所有的三元组并将结果合并。这将 return 我们所有包含 first 的句子。

但是如果我们拼错了它并且会写成 frst 我们仍然会匹配所有三个,因为有一个三元组 (rst) 匹配。