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

时间:2020-05-30 21:43:43

标签: go 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()的方法吗?我问是因为它似乎在分页之后就过滤了,并且弄乱了页面

1 个答案:

答案 0 :(得分:14)

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

如何阅读代码示例

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

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

您必须小心地导出Map,因为那样会与JavaScript map冲突。在这种情况下,您可以只使用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'))
)

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

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)绑定到变量,或使用以下社区驱动的库中的一个:{{ 3}}

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

提示,如果您不确定自己是否正确,则可以始终以而不是术语添加绑定,然后在https://github.com/shiftx/faunadb-fql-lib中查看是否索引实际上包含值:

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

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

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

谁会给我提供以下名称的文件: “要做的第一件事就是跳舞!”

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

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

要获得精确的包含匹配效率,您需要使用一个称为“ NGram”的函数(由于将来会更方便,因此我们尚未对其进行记录)。用ngram分隔字符串是enter image description here,通常在其他搜索引擎的幕后使用。在FaunaDB中,由于索引和绑定的强大功能,我们可以轻松地应用它。 search techniqueFwitter example中有一个自动补全的示例。此示例不适用于您的用例,但我确实将其引用给其他用户,因为它是用于自动填充短字符串,而不是像任务一样在较长的字符串中搜索短字符串。

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

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

NGram('lalala', 3, 3)

会返回:

source code

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

如果要进行模糊搜索enter image description here,在这种情况下,我们的索引将很容易,因此我们将不使用外部函数。

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

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

enter image description here

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

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次搜索,我们正在搜索所有三字母组合并将结果并集。这将返回所有包含第一个句子的句子。 enter image description here

但是,如果我们拼写错误并且写了 frst ,我们仍然会匹配所有三个字符,因为存在一个匹配的三字组(rst)