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


103

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

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

我尝试使用where('name', '==', '%searchTerm%'),但未返回任何内容。


2
请您找到任何解决办法。我一直在寻找不乏类似东西的日子
suulisin

1
Firebase现在支持此功能。请更新答案:stackoverflow.com/a/52715590/2057171
艾伯特·伦肖

1
我认为最好的方法是创建一个手动为每个文档建立索引的脚本。然后查询那些索引。检查一下:angularfirebase.com/lessons/…–
Kiblawi_Rabee,

Answers:


34

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

您可以通过前缀只能过滤,例如用于一切之间的启动barfoo您可以使用

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

您可以使用诸如Algolia或ElasticSearch之类的外部服务。


5
那不是我要找的东西。我有很多带有长标题的产品。“ Rebok男士网球拍”。用户可能会搜索tennis,但是基于可用的查询运算符,无法获得这些结果。结合>=<=不起作用。当然,我可以使用Algolia,但是我也可以将其与Firebase一起使用来执行大多数查询,而无需切换到Firestore ...
tehfailsafe

4
@tehfailsafe那么,您的问题是“如何查询字段是否包含字符串”,而响应是“您不能这样做”。
库巴

20
@ A.Chakroun我的答案到底有什么不礼貌?
库巴

18
这确实是必要的。我不明白为什么Firebase的团队不考虑这一点
Dani

2
Firebase的查询功能如此之弱,实在令人惊讶。如果它不能支持这么简单的查询,就无法相信有这么多人在使用它。
6

43

尽管就限制方面而言,Kuba的答案是正确的,但您可以使用类似于集合的结构来部分模拟此问题:

{
  'terms': {
    'reebok': true,
    'mens': true,
    'tennis': true,
    'racket': true
  }
}

现在您可以查询

collectionRef.where('terms.tennis', '==', true)

之所以可行,是因为Firestore会自动为每个字段创建一个索引。不幸的是,这不适用于复合查询,因为Firestore不会自动创建复合索引。

您仍然可以通过存储单词的组合来解决此问题,但这很快就会变得丑陋。

使用舷外全文搜索可能仍会更好。



1
如果您是对此问题的后续回答,那么:AppEngine的全文本搜索与Firestore完全分开,因此不会直接为您提供帮助。您可以使用云功能复制数据,但这实际上就是使用舷外全文本搜索的建议。如果您有其他疑问,请提出一个新问题。
吉尔·吉尔伯特

1
在Firestore中,您需要先索引所有术语,然后才能使用where
Husam '18

4
正如Husam提到的,所有这些字段都需要索引。我想启用搜索产品名称包含的任何术语的功能。因此,我在文档上创建了一个“对象”类型属性,并将键作为产品名称的一部分,每个键都分配了“ true”值,希望搜索where('nameSegments.tennis','==',true)工作,但firestore建议为nameSegments.tennis创建一个索引,每隔一个术语就创建一个索引。由于可以有无限多个术语,因此,当所有搜索术语都预先定义好后,此答案仅适用于非常有限的场景。
Slawoj

2
@epeleg在为它创建索引之后,该查询将起作用,但是为产品名称包含的每个可能术语创建索引都是不可行的,因此对于文本搜索产品名称中的术语,这种方法不适用于我的情况。
Slawoj

43

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

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

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

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


1
好答案!,这非常适合搜索前缀文本。要搜索文本中的单词,可以尝试按照“ post-contains”实现,如这篇文章中所述。medium.com
@

只是在想,但是从理论上讲,您可以通过创建另一个字段并反转数据来匹配所有以queryTest结尾的值...
Jonathan

是的@乔纳森,这也是可能的。
Ankit Prajapati

30

虽然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()

12
这是一个很好的解决方案。但是,如果我错了,请纠正我,但我认为它不允许您执行@tehfailsafe要求的操作。例如,如果您要获取所有包含字符串“ abc”的名称,则不会成功处理包含数组的内容,因为它将仅返回名称为“ abc”但名称为“ abcD”或“ “ 0abc”将消失。
尤连

1
@Yulian在编程世界中,Search term通常被理解为表示整个术语,两侧用空格,标点符号等分隔。如果您abcde现在使用google,您只会找到类似%20abcde.,abcde!否之类的结果abcdefghijk..。即使可以肯定在互联网上找到键入的整个字母更为普遍,但搜索不是针对abcde *,而是针对孤立的abcde
Albert Renshaw

1
'contains'理解了您的观点,但我同意这一点,但是我可能会误认为单词,这恰恰意味着我在许多编程语言中所指的含义。这同样适用于'%searchTerm%'从SQL立场。
尤连

2
@Yulian是的,我明白了。尽管Firebase是NoSQL,所以即使在某些通配符字符串搜索之类的范围外的问题可能受到限制的情况下,Firebase还是非常擅长使这些类型的操作快速有效。
艾伯特·伦肖

2
好吧,您可以为每个字段创建一个单独的字段,并在每次更新文档时像titleArray:['this','is','a','title']这样拆分单词。然后搜索将基于该字段而不是标题。您冷创建triiger onUpdate来创建此字段。基于搜索的文本需要大量工作,但是我宁愿在NoSQL方面进行性能改进。
sfratini

14

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

建议使用第三方搜索解决方案,例如AlgoliaElastic Search


46
我已经阅读了文档,尽管并不理想。缺点是Algolia和Firestore的定价模式不同...只要在我的每日查询量不太多的情况下,我可以在Firestore中愉快地拥有600,000个文档。当我将它们推送到Algolia进行搜索时,我现在必须每月向Algolia支付$ 310,以便能够在我的Firestore文档中进行标题搜索。
tehfailsafe

2
问题是这不是免费的
Dani

这是对提出的问题的正确答案,应该被认为是最好的。
briznad

11

这里有一些注意事项:

1.) \uf8ff~

2)您可以使用where子句或start end子句:

ref.orderBy('title').startAt(term).endAt(term + '~');

与...完全相同

ref.where('title', '>=', term).where('title', '<=', term + '~');

3)否,如果您进行反向操作startAt()endAt()以每种组合方式都无法使用,但是,可以通过创建另一个反向的搜索字段并组合结果来获得相同的结果。

示例:首先,您必须在创建字段时保存字段的反向版本。像这样:

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

这样,您可以搜索字符串字段的最后一个字母和第一个(不是随机的中间字母或字母组)。这更接近期望的结果。但是,当我们想要随机的中间字母或单词时,这并不会真正帮助我们。另外,请记住将所有内容保存为小写或小写副本以进行搜索,因此大小写不会成为问题。

4.)如果您只说几句话,那么Ken Tan的方法将完成您想要的所有事情,或者至少在您稍加修改之后。但是,仅用一段文本,您将以指数方式创建超过1MB的数据,这超出了Firestore的文档大小限制(我知道,我已经测试过)。

5)如果您可以将包含数组的数组(或某种形式的数组)与\uf8ff技巧结合使用,则可能进行的搜索没有达到限制。我尝试了所有组合,即使使用地图也没有尝试。任何人都可以解决,将其发布在这里。

6.)如果您必须远离ALGOLIA和ELASTIC SEARCH,而我一点也不怪,您可以在Google Cloud上始终使用mySQL,postSQL或neo4J。它们都是3种易于设置的,并且具有免费等级。您将具有一个云函数来保存数据onCreate()和另一个onCall()函数来搜索数据。简单...有点。那么为什么不直接切换到mySQL呢?当然是实时数据!当有人用websocks为实时数据编写DGraph时,请指望我!

Algolia和ElasticSearch被构建为仅搜索数据库,因此没有什么比这快的了,但是您需要为此付费。Google,为什么您将我们带离Google,又不遵循MongoDB noSQL并允许搜索?

更新-我创建了一个解决方案:

https://fireblog.io/blog/post/firestore-full-text-search


很棒的概述,非常有帮助。
RedFilter

太棒了!支持结构合理且信息丰富的响应。
丛林之王

10

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

 FirebaseFirestore.getInstance().collection("users").whereGreaterThanOrEqualTo("username", "al")

这是一个很好的简单解决方案,谢谢。但是,如果您想检查多个字段,该怎么办。像通过OR连接的“名称”和“描述”?
试试,

我认为您不能基于两个字段进行查询,可悲的是,
firebase

1
确认,@ MoTahir。Firestore中没有“或”。
说唱时间

该解决方案与以“ al”开头的用户名不匹配...例如,将匹配“ hello”(“ hello”>“ al”)
antoine129

通过OR查询仅是将两个搜索结果组合在一起的问题。对这些结果进行排序是一个不同的问题……
Jonathan

7

我确定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
})

在此处输入图片说明

像这样查询

firebase
.firestore()
.collection("clubs")
.where(
 "titleAsArray",
 "array-contains",
 this.state.userQuery.toLowerCase()
)

完全不推荐。由于文档的行数限制为2万行,因此您只能以这种方式使用它,除非您确定文档永远不会达到这样的限制
Sandeep

目前,这是最好的选择,还有什么建议呢?
尼克·卡杜奇

1
@Sandeep我很确定大小限制为1MB,每个文档的深度限制为20级。2万行是什么意思?如果无法使用Algolia或ElasticSearch,这是目前最好的解决方法
ppicom

5

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


1
android呢?
Pratik Butani

您是否建议人们遍历集合中的每条记录?
DarkNeuron

并不是的。我会使用Array.prototype。*-像.every()、. some()、. map()、. filter()等。这是在Firebase Function中的服务器上的Node中完成的,然后再将值返回给客户。
说唱时间

3
您仍然必须阅读所有文档以进行搜索,这会产生费用,并且对Time来说是昂贵的。
乔纳森

3

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


2

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

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

isGreaterThanOrEqualTo使我们可以过滤掉搜索的开始,并在isLessThanOrEqualTo的末尾添加一个“ z”,以限制搜索范围而不移至下一个文档。


3
香港专业教育学院尝试过这种解决方案,但对我来说,它只在输入完整的字符串时才有效。例如,如果我想获得“免费”一词,如果我开始输入“ fr”,则不会返回任何内容。一旦我输入“免费”,该词就会给我它的快照。
克里斯,

您是否使用相同的代码格式?这个术语在Firestore中是字符串吗?我知道您无法按documentId进行过滤。
雅各布·邦克

2

所选答案仅适用于精确搜索,并且不是自然的用户搜索行为(在“今天吃个苹果”中搜索“苹果”将不起作用)。

我认为Dan Fein在上述问题上的答案应该排名更高。如果您要搜索的字符串数据很短,则可以将字符串的所有子字符串保存在文档中的数组中,然后使用Firebase的array_contains查询搜索该数组。Firebase文档限制为1 MiB(1,048,576字节)(Firebase配额和限制),即文档中保存的大约100万个字符(我认为1个字符〜= 1个字节)。只要您的文档不接近一百万个标记,就可以存储子字符串。

搜索用户名的示例:

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

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

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

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

[yourDatabasePath].whereField([savedSubstringArray], arrayContains: searchText).getDocuments....

0

借助Firestore,您可以实施全文搜索,但与其他方式相比,它的读取费用仍然更高,而且您还需要以特定方式输入数据并为其建立索引,因此,通过这种方法,您可以使用Firebase云功能来选择h(x)满足以下条件的线性散列函数进行标记化,然后对输入文本进行散列x < y < z then h(x) < h (y) < h(z)。对于标记化,您可以选择一些轻量级的NLP库,以使函数的冷启动时间保持在较低水平,这样可以从句子中去除不必要的单词。然后,您可以在Firestore中使用小于和大于运算符来运行查询。在存储数据的同时,还必须确保在存储文本之前对文本进行哈希处理,并且还要存储纯文本,就好像您更改了纯文本一样,哈希值也会发生变化。


0

这对我来说非常有效,但可能会导致性能问题。

在查询Firestore时,请执行以下操作:

   Future<QuerySnapshot> searchResults = collectionRef
        .where('property', isGreaterThanOrEqualTo: searchQuery.toUpperCase())
        .getDocuments();

在FutureBuilder中执行以下操作:

    return FutureBuilder(
          future: searchResults,
          builder: (context, snapshot) {           
            List<Model> searchResults = [];
            snapshot.data.documents.forEach((doc) {
              Model model = Model.fromDocumet(doc);
              if (searchQuery.isNotEmpty &&
                  !model.property.toLowerCase().contains(searchQuery.toLowerCase())) {
                return;
              }

              searchResults.add(model);
            })
   };

0

截止到今天,专家们提出了3种不同的解决方法,作为对这个问题的答案。

我已经尝试了所有。我认为记录每个人的经历可能会很有用。

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

由@Kuba和@Ankit Prajapati建议

.where("dbField1", ">=", searchString)
.where("dbField1", "<=", searchString + "\uf8ff");

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

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

B.2如果Firestore查询具有上述单个条件,则无需事先创建索引。在这种情况下,此解决方案会很好用。

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

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

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

由@Albert Renshaw建议和由@Nick Carducci演示

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

.where("searchKeywordsArray", "array-contains", searchString); 

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

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

一般限制:

  1. 这些解决方案似乎都不支持搜索部分字符串。例如,如果db字段包含“ Green District Peter St,1”,则无法搜索字符串“ strict”。
  2. 几乎不可能涵盖预期搜索字符串的所有可能组合。例如,如果数据库字段包含“格林威治区穆罕默德街1号”,则您可能无法搜索字符串“格林穆罕默德”,该字符串的单词顺序与数据库中使用的顺序不同领域。

没有一种解决方案能适合所有人。每个解决方法都有其局限性。我希望以上信息可以在选择这些变通办法期间为您提供帮助。

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

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


-10

我们可以使用反引号来打印出字符串的值。这应该工作:

where('name', '==', `${searchTerm}`)

谢谢,但是这个问题是关于获取非精确值的。例如,所讨论的示例确实可以找到名称是否正确。如果我有名称为“ Test”的文档,然后搜索“ Test”,则可以使用。但是我希望能够搜索“ tes”或“ est”,并且仍然获得“ Test”结果。想象一下带有书名的用例。人们经常搜索部分书名而不是完全搜索精确书名。
tehfailsafe

13
@suulisin您说得对,我没有认真阅读它,因为我想分享自己的发现。感谢您的努力指出这一点,我会更加小心
Zach J
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.