Google Firestore:查询属性值的子字符串(文本搜索)

teh*_*afe 78 firebase google-cloud-firestore

我想添加一个简单的搜索字段,想使用类似的东西

collectionRef.where('name', 'contains', 'searchTerm')

我尝试过使用where('name', '==', '%searchTerm%'),但它没有返回任何东西.

Gil*_*ert 36

虽然就限制而言Kuba的答案是正确的,但您可以使用类似集合的结构来部分模拟它:

{
  'terms': {
    'reebok': true,
    'mens': true,
    'tennis': true,
    'racket': true
  }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以查询

collectionRef.where('terms.tennis', '==', true)
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为Firestore会自动为每个字段创建索引.不幸的是,这不能直接用于复合查询,因为Firestore不会自动创建复合索引.

您仍然可以通过存储单词组合来解决这个问题,但这会很快变得难看.

使用外置全文搜索可能还是比较好的.

  • 正如Husam所提到的,所有这些字段都需要编入索引.我想启用搜索我的产品名称包含的任何字词.所以我在我的文档上创建了一个'object'类型属性,其中键是产品名称的一部分,每个都赋值给它'true',希望搜索where('nameSegments.tennis','==',true) work,但firestore建议为nameSegments.tennis创建一个索引,每个其他术语都相同.由于可以有无数个术语,所以当所有搜索术语都预先定义时,此答案仅适用于非常有限的场景. (3认同)
  • 如果您将此作为此答案的后续问题,那么:AppEngine 的全文搜索与 Firestore 完全分开,因此这不会直接帮助您。您可以使用云功能复制您的数据,但这基本上是使用外部全文搜索的建议。如果您要问其他问题,请开始一个新问题。 (2认同)
  • @epeleg在为它创建索引之后,查询将起作用,但是为产品名称包含的每个可能术语创建索引是不可行的,因此对于产品名称中术语的文本搜索,这种方法对我的情况不起作用. (2认同)

Jon*_*han 36

Full-Text Search, Relevant Search, and Trigram Search!

UPDATE - 2/17/21 - I created several new Full Text Search Options.

See Fireblog.io for details.


Also, side note, dgraph now has websockets for realtime... wow, never saw that coming, what a treat! Slash Dgraph - Amazing!


--Original Post--

A few notes here:

1.) \uf8ff works the same way as ~

2.) You can use a where clause or start end clauses:

ref.orderBy('title').startAt(term).endAt(term + '~');
Run Code Online (Sandbox Code Playgroud)

is exactly the same as

ref.where('title', '>=', term).where('title', '<=', term + '~');
Run Code Online (Sandbox Code Playgroud)

3.) No, it does not work if you reverse startAt() and endAt() in every combination, however, you can achieve the same result by creating a second search field that is reversed, and combining the results.

Example: First you have to save a reversed version of the field when the field is created. Something like this:

// collection
const postRef = db.collection('posts')

async function searchTitle(term) {

  // reverse term
  const termR = term.split("").reverse().join("");

  // define queries
  const titles = postRef.orderBy('title').startAt(term).endAt(term + '~').get();
  const titlesR = postRef.orderBy('titleRev').startAt(termR).endAt(termR + '~').get();

  // get queries
  const [titleSnap, titlesRSnap] = await Promise.all([
    titles,
    titlesR
  ]);
  return (titleSnap.docs).concat(titlesRSnap.docs);
}
Run Code Online (Sandbox Code Playgroud)

With this, you can search the last letters of a string field and the first, just not random middle letters or groups of letters. This is closer to the desired result. However, this won't really help us when we want random middle letters or words. Also, remember to save everything lowercase, or a lowercase copy for searching, so case won't be an issue.

4.) If you have only a few words, Ken Tan's Method will do everything you want, or at least after you modify it slightly. However, with only a paragraph of text, you will exponentially create more than 1MB of data, which is bigger than firestore's document size limit (I know, I tested it).

5.) If you could combine array-contains (or some form of arrays) with the \uf8ff trick, you might could have a viable search that does not reach the limits. I tried every combination, even with maps, and a no go. Anyone figures this out, post it here.

6.) If you must get away from ALGOLIA and ELASTIC SEARCH, and I don't blame you at all, you could always use mySQL, postSQL, or neo4Js on Google Cloud. They are all 3 easy to set up, and they have free tiers. You would have one cloud function to save the data onCreate() and another onCall() function to search the data. Simple...ish. Why not just switch to mySQL then? The real-time data of course! When someone writes DGraph with websocks for real-time data, count me in!

Algolia and ElasticSearch were built to be search-only dbs, so there is nothing as quick... but you pay for it. Google, why do you lead us away from Google, and don't you follow MongoDB noSQL and allow searches?


Kub*_*uba 25

有没有这样的运营商,允许的有==,<,<=,>,>=.

您可以通过前缀只能过滤,例如对于之间开始的一切bar,并foo可以使用

collectionRef.where('name', '>=', 'bar').where('name', '<=', 'foo')

您可以使用Algolia或ElasticSearch 等外部服务.

  • @ A.Chakroun我的回答究竟是什么粗鲁的? (11认同)
  • @tehfailsafe 好吧,您的问题是“如何查询字段是否包含字符串”,而回答是“您不能这样做”。 (8认同)
  • Firebase 的查询能力如此之弱,真是令人惊讶。如果它不支持如此简单的查询,简直不敢相信有这么多人在使用它。 (7认同)
  • 这确实是必要的。我不明白为什么Firebase的团队不考虑这一点 (5认同)
  • 那不是我要找的东西。我有很多带有长标题的产品。“ Rebok男士网球拍”。用户可能会搜索“网球”,但是基于可用的查询运算符,无法获得这些结果。不能将`&gt; =`和`&lt;=`组合使用。我当然可以使用Algolia,但也可以将它与Firebase一起使用来执行大多数查询,而无需切换到Firestore ... (2认同)

Alb*_*haw 22

Firebase不明确支持在字符串中搜索字词...

但Firebase现在支持以下内容,这将解决您的案例和许多其他问题:

截至2018年8月,他们支持array-contains查询.请参阅:https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.html

您现在可以将所有关键术语设置为数组作为字段,然后查询具有包含"X"的数组的所有文档.您可以使用逻辑AND进一步比较其他查询.(这是因为firebase 目前本身不支持多个包含数组的查询的复合查询,所以'AND'排序查询必须在客户端完成)

使用这种风格的数组将允许它们针对并发写入进行优化,这很好!没有测试它支持批量请求(文档没有说),但我打赌它自从它的官方解决方案.


用法:

collection("collectionPath").
    where("searchTermsArray", "array-contains", "term").get()
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很好的解决方案.但是,如果我错了,请纠正我,但我认为它不允许你做@tehfailsafe要求的事情.例如,如果要获取包含字符串"abc"的所有名称,则不会使用array-contains获得任何成功,因为它只会返回具有确切名称"abc"但是"abcD"的文档或"0abc"将会出局. (5认同)
  • 这不应该是公认的答案。作为建议的解决方法,它很有帮助,但它不是所提出问题的正确答案。@bholben 的答案是最好的,又名“不,Firestore 不支持这一点。” (3认同)
  • 我明白你的观点并且同意它,但我可能被“包含”这个词误导了,这正是我在许多编程语言中所指的意思。从 SQL 的角度来看,“%searchTerm%”也是如此。 (2认同)
  • @Yulian是的,我明白了.Firebase虽然是NoSQL,但它真的很擅长快速高效地进行这些类型的操作,即使它们可能仅限于一些超出范围的问题,如外卡搜索. (2认同)
  • 好吧,您可以为每个字段创建一个单独的字段,并在每次更新文档时像titleArray:['this','is','a','title']这样拆分单词。然后搜索将基于该字段而不是标题。您冷创建triiger onUpdate来创建此字段。基于搜索的文本需要做很多工作,但是我宁愿在NoSQL方面提高性能。 (2认同)

Ank*_*ati 11

我同意@Kuba的回答,但是,仍然需要添加一个小的更改以完美地进行前缀搜索。这对我有用

用于搜索以名称开头的记录 queryText

collectionRef.where('name', '>=', queryText).where('name', '<=', queryText+ '\uf8ff')

\uf8ff查询中使用的字符是Unicode范围内的一个很高的代码点(它是专用使用区[PUA]代码)。由于该查询位于Unicode中大多数常规字符之后,因此它匹配以开头的所有值queryText

  • 好答案!,这对于搜索前缀文本非常有用。要搜索文本中的单词,可以尝试“array-contains”实现,如本文所述 https://medium.com/@ken11zer01/firebase-firestore-text-search-and-pagination-91a0df8131ef (3认同)
  • 请注意,它区分大小写。被困了一会儿,不明白为什么没有返回任何内容。 (2认同)

bho*_*ben 10

根据Firestore文档,Cloud Firestore不支持本机索引或搜索文档中的文本字段.此外,下载整个集合以搜索客户端字段是不切实际的.

建议使用AlgoliaElastic Search等第三方搜索解决方案.

  • 我已阅读过文档,但它并不理想.缺点是Algolia和Firestore有不同的定价模式......我很高兴在Firestore中拥有600,000个文档(只要我每天不查询太多).当我将它们推送到Algolia进行搜索时,我现在必须每月向Algolia支付310美元才能在我的Firestore文档上进行标题搜索. (25认同)
  • 问题是这不是免费的 (3认同)

Nic*_*cci 9

我相信 Firebase 很快就会推出“字符串包含”来捕获字符串中的任何索引 [i] startAt ......但我已经研究了网络并发现其他人想到的这个解决方案设置了你的数据,例如这个

state = {title:"Knitting"}
...
const c = this.state.title.toLowerCase()

var array = [];
for (let i = 1; i < c.length + 1; i++) {
 array.push(c.substring(0, i));
}

firebase
.firestore()
.collection("clubs")
.doc(documentId)
.update({
 title: this.state.title,
 titleAsArray: array
})
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

像这样查询

firebase
.firestore()
.collection("clubs")
.where(
 "titleAsArray",
 "array-contains",
 this.state.userQuery.toLowerCase()
)
Run Code Online (Sandbox Code Playgroud)

  • 根本不推荐。由于文档有 20k 行限制,因此您不能以这种方式使用它,除非您确定您的文档永远不会达到这样的限制 (2认同)
  • 这是目前最好的选择,还有什么推荐的? (2认同)

MoT*_*hir 8

答案较晚,但对于仍在寻找答案的任何人,假设我们有一个用户集合,并且在该集合的每个文档中都有一个“用户名”字段,因此,如果要查找用户名以“ al”开头的文档我们可以做类似的事情

 FirebaseFirestore.getInstance().collection("users").whereGreaterThanOrEqualTo("username", "al")
Run Code Online (Sandbox Code Playgroud)


Rif*_*man 8

就像乔纳森说的那样,我使用了卦。

三元组是存储在数据库中以帮助搜索的 3 个字母的组。因此,如果我有用户数据,并且我想查询唐纳德·特朗普的“trum”,我必须以这种方式存储它

在此输入图像描述

我只是这样回忆

 onPressed: () {
      //LET SAY YOU TYPE FOR 'tru' for trump
      List<String> search = ['tru', 'rum'];
      Future<QuerySnapshot> inst = FirebaseFirestore.instance
          .collection("users")
          .where('trigram', arrayContainsAny: search)
          .get();
      print('result=');
      inst.then((value) {
        for (var i in value.docs) {
          print(i.data()['name']);
        }
      });
Run Code Online (Sandbox Code Playgroud)

无论如何都会得到正确的结果

在此输入图像描述


nic*_*rno 7

编辑 05/2021:

Google Firebase 现在有一个扩展来使用 Algolia 实现搜索。Algolia 是一个全文搜索平台,具有广泛的功能列表。你需要在 Firebase 上有一个“Blaze”计划,并且有与 Algolia 查询相关的费用,但这将是我推荐的生产应用程序的方法。如果您更喜欢免费的基本搜索,请参阅下面我的原始答案。

https://firebase.google.com/products/extensions/firestore-algolia-search https://www.algolia.com

原始答案:

所选答案仅适用于精确搜索,而不是自然的用户搜索行为(在“今天乔吃了一个苹果”中搜索“苹果”是行不通的)。

我认为上面丹费恩的回答应该排名更高。如果您搜索的字符串数据很短,您可以将字符串的所有子字符串保存在文档中的数组中,然后使用 Firebase 的 array_contains 查询搜索该数组。Firebase 文档限制为 1 MiB(1,048,576 字节)(Firebase Quotas and Limits),即文档中保存了大约 100 万个字符(我认为 1 个字符 ~= 1 个字节)。只要您的文档不接近 100 万个标记,就可以存储子字符串。

搜索用户名的示例:

步骤 1:将以下字符串扩展添加到您的项目中。这使您可以轻松地将字符串分解为子字符串。(我在这里找到了这个)。

extension String {

var length: Int {
    return count
}

subscript (i: Int) -> String {
    return self[i ..< i + 1]
}

func substring(fromIndex: Int) -> String {
    return self[min(fromIndex, length) ..< length]
}

func substring(toIndex: Int) -> String {
    return self[0 ..< max(0, toIndex)]
}

subscript (r: Range<Int>) -> String {
    let range = Range(uncheckedBounds: (lower: max(0, min(length, r.lowerBound)),
                                        upper: min(length, max(0, r.upperBound))))
    let start = index(startIndex, offsetBy: range.lowerBound)
    let end = index(start, offsetBy: range.upperBound - range.lowerBound)
    return String(self[start ..< end])
}
Run Code Online (Sandbox Code Playgroud)

第 2 步:当您存储用户名时,也将这个函数的结果作为数组存储在同一个 Document 中。这将创建原始文本的所有变体并将它们存储在一个数组中。例如,文本输入“Apple”将创建以下数组:["a", "p", "p", "l", "e", "ap", "pp", "pl", "le ", "app", "ppl", "ple", "appl", "pple", "apple"],应该包含用户可能输入的所有搜索条件。如果您想要所有结果,您可以将 maximumStringSize 保留为 nil,但是,如果有长文本,我建议在文档大小变得太大之前将其设置为上限 - 大约 15 对我来说很好用(大多数人无论如何都不会搜索长短语)。

func createSubstringArray(forText text: String, maximumStringSize: Int?) -> [String] {
    
    var substringArray = [String]()
    var characterCounter = 1
    let textLowercased = text.lowercased()
    
    let characterCount = text.count
    for _ in 0...characterCount {
        for x in 0...characterCount {
            let lastCharacter = x + characterCounter
            if lastCharacter <= characterCount {
                let substring = textLowercased[x..<lastCharacter]
                substringArray.append(substring)
            }
        }
        characterCounter += 1
        
        if let max = maximumStringSize, characterCounter > max {
            break
        }
    }
    
    print(substringArray)
    return substringArray
}
Run Code Online (Sandbox Code Playgroud)

第 3 步:您可以使用 Firebase 的 array_contains 函数!

[yourDatabasePath].whereField([savedSubstringArray], arrayContains: searchText).getDocuments....
Run Code Online (Sandbox Code Playgroud)


Dan*_*ein 6

实际上,我认为在 Firestore 中执行此操作的最佳解决方案是将所有子字符串放入一个数组中,然后执行 array_contains 查询。这允许您进行子字符串匹配。存储所有子字符串有点矫枉过正,但如果您的搜索词很短,那么这是非常非常合理的。


Bil*_*een 6

截至今天,专家建议的基本上有 3 种不同的解决方法作为问题的答案。

我都试过了。我认为记录我对每个人的体验可能会很有用。

方法-A:使用: (dbField ">=" searchString) & (dbField "<=" searchString + "\uf8ff")

由@Kuba 和@Ankit Prajapati 推荐

.where("dbField1", ">=", searchString)
.where("dbField1", "<=", searchString + "\uf8ff");
Run Code Online (Sandbox Code Playgroud)

A.1 Firestore 查询只能对单个字段执行范围过滤器(>、<、>=、<=)。不支持对多个字段使用范围过滤器的查询。通过使用此方法,您不能在数据库的任何其他字段(例如日期字段)中使用范围运算符。

A2。此方法不适用于同时在多个字段中搜索。例如,您无法检查搜索字符串是否在任何字段(姓名、注释和地址)中。

方法 B:对映射中的每个条目使用带有“true”的搜索字符串 MAP,并在查询中使用“==”运算符

由@Gil Gilbert 推荐

document1 = {
  'searchKeywordsMap': {
    'Jam': true,
    'Butter': true,
    'Muhamed': true,
    'Green District': true,
    'Muhamed, Green District': true,
  }
}

.where(`searchKeywordsMap.${searchString}`, "==", true);
Run Code Online (Sandbox Code Playgroud)

B.1 显然,这种方法每次将数据保存到数据库时都需要额外的处理,更重要的是,需要额外的空间来存储搜索字符串的映射。

B.2 如果一个 Firestore 查询只有一个类似上面的条件,则不需要事先创建索引。在这种情况下,此解决方案可以正常工作。

B.3 但是,如果查询有另一个条件,例如 (status === "active",) 似乎用户输入的每个“搜索字符串”都需要一个索引。换句话说,如果一个用户搜索“Jam”而另一个用户搜索“Butter”,则应事先为字符串“Jam”创建一个索引,并为“Butter”等创建另一个索引。除非您可以预测所有可能的用户的搜索字符串,这不起作用 - 如果查询有其他条件!

.where(searchKeywordsMap["Jam"], "==", true); // requires an index on searchKeywordsMap["Jam"]
.where("status", "==", "active");

Run Code Online (Sandbox Code Playgroud)

**方法-C:使用搜索字符串数组和“数组包含”运算符

由@Albert Renshaw 推荐并由@Nick Carducci 演示

document1 = {
  'searchKeywordsArray': [
    'Jam',
    'Butter',
    'Muhamed',
    'Green District',
    'Muhamed, Green District',
  ]
}

.where("searchKeywordsArray", "array-contains", searchString); 
Run Code Online (Sandbox Code Playgroud)

C.1 与方法B类似,该方法每次将数据保存到数据库时都需要额外的处理,更重要的是,需要额外的空间来存储搜索字符串数组。

C.2 Firestore 查询在复合查询中最多可以包含一个“array-contains”或“array-contains-any”子句。

一般限制:

  1. 这些解决方案似乎都不支持搜索部分字符串。例如,如果 db 字段包含“1 Peter St, Green District”,则无法搜索字符串“strict”。
  2. 几乎不可能涵盖预期搜索字符串的所有可能组合。例如,如果 db 字段包含“1 Mohamed St, Green District”,您可能无法搜索字符串“Green Mohamed”,该字符串的单词顺序与 db 中使用的顺序不同场地。

没有一种解决方案适合所有人。每种解决方法都有其局限性。我希望以上信息可以在这些变通方法之间的选择过程中帮助您。

有关 Firestore 查询条件的列表,请查看文档https://firebase.google.com/docs/firestore/query-data/queries

我没试过https://fireblog.io/blog/post/firestore-full-text-search,这是由@Jonathan建议。


eri*_*579 6

截至2023 年 3 月,Firestore 的新OR查询允许消除前缀搜索区分大小写的问题(在某种程度上):

query(
  collection(DB, 'some/collection'),
  or(
    // query as-is:
    and(
      where('name', '>=', queryString),
      where('name', '<=', queryString + '\uf8ff')
    ),
    // capitalize first letter:
    and(
      where('name', '>=', queryString.charAt(0).toUpperCase() + queryString.slice(1)),
      where('name', '<=', queryString.charAt(0).toUpperCase() + queryString.slice(1) + '\uf8ff')
    ),
    // lowercase:
    and(
      where('name', '>=', queryString.toLowerCase()),
      where('name', '<=', queryString.toLowerCase() + '\uf8ff')
    )
  )
);
Run Code Online (Sandbox Code Playgroud)


小智 5

我刚刚遇到了这个问题,并提出了一个非常简单的解决方案。

String search = "ca";
Firestore.instance.collection("categories").orderBy("name").where("name",isGreaterThanOrEqualTo: search).where("name",isLessThanOrEqualTo: search+"z")
Run Code Online (Sandbox Code Playgroud)

isGreaterThanOrEqualTo 让我们过滤掉搜索的开头,并通过在 isLessThanOrEqualTo 的末尾添加一个“z”来限制我们的搜索,使其不会滚动到下一个文档。

  • 我试过这个解决方案,但对我来说它只有在输入完整字符串时才有效。例如,如果我想得到“免费”这个词,如果我开始输入“fr”,什么都不会返回。一旦我输入“免费”,这个词就会给我它的快照。 (3认同)

Rap*_*Rap 5

如果您不想使用像 Algolia 这样的第三方服务,Firebase Cloud Functions是一个不错的选择。您可以创建一个可以接收输入参数的函数,通过服务器端的记录进行处理,然后返回符合您条件的记录。

  • 您仍然需要阅读所有文档才能进行搜索,这会产生费用并且耗费大量时间。 (6认同)