152

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

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

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

4

21 回答 21

100

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

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

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

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

于 2019-06-29T07:18:57.807 回答
56

没有这样的运算符,允许的运算符是==, <, <=, >, >=.

您可以仅按前缀过滤,例如对于从之间开始的所有内容,barfoo可以使用

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

为此,您可以使用Algolia或 ElasticSearch 等外部服务。

于 2017-10-04T15:22:30.640 回答
47

全文搜索、相关搜索和三元组搜索!

更新 - 2/17/21 - 我创建了几个新的全文搜索选项。

有关详细信息,请参阅Fireblog.io


另外,附注,dgraph 现在有实时的 websockets ......哇,从来没有看到过,真是太好了!斜线 Dgraph - 太棒了!


--原帖--

这里有几点注意事项:

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 或 neo4Js。它们都 3 易于设置,并且具有免费层级。您将有一个云函数来保存数据 onCreate() 和另一个 onCall() 函数来搜索数据。很简单……嗯。那么为什么不直接切换到 mySQL 呢?当然是实时数据!当有人用 websocks 为实时数据编写DGraph时,算我一个!

Algolia 和 ElasticSearch 被构建为仅用于搜索的 dbs,因此没有什么比它更快的了……但是您为此付出了代价。谷歌,你为什么要让我们远离谷歌,你不遵循 MongoDB noSQL 并允许搜索?

于 2020-04-30T05:35:03.787 回答
45

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

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

现在您可以查询

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

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

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

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

于 2017-10-04T17:13:22.950 回答
44

虽然 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()
于 2018-10-09T07:33:31.583 回答
15

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

 FirebaseFirestore.getInstance().collection("users").whereGreaterThanOrEqualTo("username", "al")
于 2019-04-05T20:54:08.650 回答
15

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

推荐使用AlgoliaElastic Search等第三方搜索解决方案。

于 2018-02-07T15:03:48.323 回答
12

我确信 Firebase 很快就会推出“字符串包含”来捕获字符串中的任何 index[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()
)
于 2020-04-07T15:34:03.187 回答
10

截至今天,专家建议的基本上有 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 很明显,这种方法在每次将数据保存到数据库时都需要额外的处理,更重要的是,需要额外的空间来存储搜索字符串的映射。

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

B.3 但是,如果查询有其他条件,例如(status === "active",),似乎用户输入的每个“搜索字符串”都需要一个索引。换句话说,如果一个用户搜索“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类似,这种方法每次保存数据到数据库时都需要额外的处理,更重要的是,需要额外的空间来存储搜索字符串的数组。

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

我没有尝试过@Jonathan 建议的 https://fireblog.io/blog/post/firestore-full-text-search

于 2020-08-18T01:14:26.587 回答
8

编辑 05/2021:

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

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

原始答案:

所选答案仅适用于精确搜索,而不是自然的用户搜索行为(在“Joe ate an apple today”中搜索“apple”是行不通的)。

我认为丹费恩上面的答案应该排名更高。如果您要搜索的字符串数据很短,您可以将字符串的所有子字符串保存在 Document 的数组中,然后使用 Firebase 的 array_contains 查询搜索该数组。Firebase 文档限制为 1 MiB(1,048,576 字节)(Firebase 配额和限制),即在文档中保存大约 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])
}

第 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....
于 2020-04-02T18:12:36.317 回答
7

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

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

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

于 2019-05-22T04:57:45.363 回答
6

就像乔纳森所说的那样,我使用了三元组。

三元组是存储在数据库中以帮助搜索的 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']);
        }
      });

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

在此处输入图像描述

于 2021-03-03T08:37:35.923 回答
5

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

于 2019-07-16T21:09:32.380 回答
4

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

于 2019-06-12T18:39:58.103 回答
1

2021 更新

从其他答案中获取了一些东西。这包括:

  • 使用拆分的多词搜索(充当 OR)
  • 使用平面的多键搜索

区分大小写有点受限,您可以通过以大写形式存储重复属性来解决此问题。前任:query.toUpperCase() user.last_name_upper


// query: searchable terms as string

let users = await searchResults("Bob Dylan", 'users');

async function searchResults(query = null, collection = 'users', keys = ['last_name', 'first_name', 'email']) {

    let querySnapshot = { docs : [] };

    try {
        if (query) {
            let search = async (query)=> {
                let queryWords = query.trim().split(' ');
                return queryWords.map((queryWord) => keys.map(async (key) =>
                    await firebase
                        .firestore()
                        .collection(collection)
                        .where(key, '>=', queryWord)
                        .where(key, '<=', queryWord +  '\uf8ff')
                        .get())).flat();
            }

            let results = await search(query);

            await (await Promise.all(results)).forEach((search) => {
                querySnapshot.docs = querySnapshot.docs.concat(search.docs);
            });
        } else {
            // No query
            querySnapshot = await firebase
                .firestore()
                .collection(collection)
                // Pagination (optional)
                // .orderBy(sortField, sortOrder)
                // .startAfter(startAfter)
                // .limit(perPage)
                .get();
        }
    } catch(err) {
        console.log(err)
    }

    // Appends id and creates clean Array
    const items = [];
    querySnapshot.docs.forEach(doc => {
        let item = doc.data();
        item.id = doc.id;
        items.push(item);
    });

    // Filters duplicates
    return items.filter((v, i, a) => a.findIndex(t => (t.id === v.id)) === i);
}

注意:Firebase 调用的数量等于查询字符串中的单词数 * 您正在搜索的键数。

于 2021-02-05T05:50:30.920 回答
1

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

在查询 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);
            })
   };
于 2020-06-08T15:44:24.687 回答
1

以下代码片段从用户那里获取输入,并从输入的数据开始获取数据。

样本数据:

在 Firebase 集合“用户”下

用户1:{姓名:'阿里',年龄:28},

user2: {name: 'Khan', 年龄: 30},

user3: {name: 'Hassan', 年龄: 26},

用户 4:{名称:“阿迪尔”,年龄:32}

文本输入:一个

结果:

{姓名:“阿里”,年龄:28},

{姓名:“阿迪尔”,年龄:32}

let timer;

// method called onChangeText from TextInput

const textInputSearch = (text) => {

const inputStart = text.trim();
let lastLetterCode = inputStart.charCodeAt(inputStart.length-1);
lastLetterCode++;
const newLastLetter = String.fromCharCode(lastLetterCode);
const inputEnd = inputStart.slice(0,inputStart.length-1) + lastLetterCode;

clearTimeout(timer);

timer = setTimeout(() => {
    firestore().collection('Users')
        .where('name', '>=', inputStart)
        .where('name', '<', inputEnd)
        .limit(10)
        .get()
        .then(querySnapshot => {
            const users = [];
                querySnapshot.forEach(doc => {
                    users.push(doc.data());
                })
            setUsers(users); //  Setting Respective State
        });
    }, 1000);

};
于 2020-11-25T13:03:00.030 回答
0

With Firestore you can implement a full text search but it will still cost more reads than it would have otherwise, and also you'll need to enter and index the data in a particular way, So in this approach you can use firebase cloud functions to tokenise and then hash your input text while choosing a linear hash function h(x) that satisfies the following - if x < y < z then h(x) < h (y) < h(z). For tokenisation you can choose some lightweight NLP Libraries in order to keep the cold start time of your function low that can strip unnecessary words from your sentence. Then you can run a query with less than and greater than operator in Firestore. While storing your data also, you'll have to make sure that you hash the text before storing it, and store the plain text also as if you change the plain text the hashed value will also change.

于 2020-04-11T08:48:48.347 回答
0

与@nicksarno 相同,但代码更优美,不需要任何扩展:

步骤1

func getSubstrings(from string: String, maximumSubstringLenght: Int = .max) -> [Substring] {
    let string = string.lowercased()
    let stringLength = string.count
    let stringStartIndex = string.startIndex
    var substrings: [Substring] = []
    for lowBound in 0..<stringLength {
        for upBound in lowBound..<min(stringLength, lowBound+maximumSubstringLenght) {
            let lowIndex = string.index(stringStartIndex, offsetBy: lowBound)
            let upIndex = string.index(stringStartIndex, offsetBy: upBound)
            substrings.append(string[lowIndex...upIndex])
        }
    }
    return substrings
}

第2步

let name = "Lorenzo"
ref.setData(["name": name, "nameSubstrings": getSubstrings(from: name)])

第 3 步

Firestore.firestore().collection("Users")
  .whereField("nameSubstrings", arrayContains: searchText)
  .getDocuments...
于 2021-01-29T10:55:13.040 回答
-1

Firebase 建议使用 Algolia 或 ElasticSearch 进行全文搜索,但更便宜的选择可能是 MongoDB。最便宜的集群(大约 10 美元/月)允许您为全文建立索引。

于 2021-02-25T13:15:26.560 回答
-12

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

where('name', '==', `${searchTerm}`)
于 2017-11-08T08:03:07.270 回答