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

时间:2017-10-04 15:02:49

标签: firebase google-cloud-firestore

我希望添加一个简单的搜索字段,想要使用像

这样的东西

where('name', '==', '%searchTerm%')

我尝试使用{{1}},但它没有返回任何内容。

21 个答案:

答案 0 :(得分:29)

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

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

现在您可以使用

进行查询
collectionRef.where('terms.tennis', '==', true)

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

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

你可能还有更好的外线full text search

答案 1 :(得分:19)

一些注意事项:

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's Method会做您想做的所有事情,或者至少在您稍加修改之后。但是,仅用一段文本,您将以指数方式创建超过1MB的数据,这超出了Firestore的文档大小限制(我知道,我已经测试过)。

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

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

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

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

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

答案 2 :(得分:17)

没有此类运营商,允许的运营商为==<<=>>=

您只能按前缀进行过滤,例如,对于在barfoo之间展开的所有内容,您都可以使用

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

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

答案 3 :(得分:9)

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

Firebase现在确实支持以下内容,这将解决您的情况以及许多其他情况:

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

现在,您可以将所有关键术语设置为一个数组作为字段,然后查询具有包含“ X”的数组的所有文档。您可以使用逻辑AND对附加查询进行进一步的比较。(这是因为firebase does not currently natively support compound queries for multiple array-contains queries,因此“ AND”排序查询必须在客户端进行)

使用这种样式的数组将使它们针对并发写入进行优化,这很好!尚未测试它是否支持批处理请求(文档未说),但我敢打赌,因为它是官方解决方案,所以支持。


用法:

collection("collectionPath").
    where("searchTermsArray", "array-contains", "term").get()

答案 4 :(得分: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
})

enter image description here

像这样的查询

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

答案 5 :(得分:6)

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

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

答案 6 :(得分:5)

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

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

搜索用户名的示例:

步骤1:将以下String扩展名添加到您的项目中。这使您可以轻松地将字符串分解为子字符串。 (I found this here)。

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....

答案 7 :(得分:2)

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

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

答案 8 :(得分:2)

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

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

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

由@Kuba和@Ankit Prajapati建议

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

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

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

方法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

我还没有尝试过{@ 3ona}建议的https://fireblog.io/blog/post/firestore-full-text-search

答案 9 :(得分:2)

我像乔纳森所说的那样使用了trigram。

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

enter image description here

我只是这样回忆

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

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

enter image description here

答案 10 :(得分:1)

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

答案 11 :(得分:1)

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

用于搜索名称为queryText的记录

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

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

答案 12 :(得分:1)

2021 年更新

从其他答案中提取了一些内容。这包括:

  • 使用拆分的多词搜索(充当 OR)
  • 使用 flat 进行多键搜索

区分大小写有点限制,您可以通过以大写形式存储重复属性来解决此问题。例如: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 调用的次数等于查询字符串中的单词数 * 您正在搜索的键数。

答案 13 :(得分:0)

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

答案 14 :(得分:0)

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

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

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

答案 15 :(得分:0)

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

答案 16 :(得分: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);
            })
   };

答案 17 :(得分:0)

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

样本数据:

在Firebase集合“用户”下

user1:{name:“ Ali”,年龄:28},

user2:{name:'Khan',age:30},

user3:{名称:“哈桑”,年龄:26},

user4:{name:'Adil',年龄:32}

TextInput :A

结果:

{姓名:“阿里”,年龄: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);

};

答案 18 :(得分:0)

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

第一步

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...

答案 19 :(得分:-2)

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

答案 20 :(得分:-8)

我们可以使用back-tick打印出字符串的值。这应该有效:

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