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

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

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

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

131432 次浏览

没有这样的操作员,允许的是 ==<<=>>=

您可以只通过前缀进行筛选,例如,筛选从 barfoo之间开始的所有内容

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

您可以使用外部服务,如 阿尔戈利亚或 ElasticSearch。

虽然 Kuba 的答案是正确的,就限制而言,你可以用一个集合式的结构部分地模仿这一点:

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

现在您可以用

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

这样做是有效的,因为火恢复将自动为每个字段创建索引。不幸的是,对于复合查询,这种方法不能直接工作,因为 Firestore 不会自动创建复合索引。

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

你最好还是用舷外 全文检索全文检索

我们可以使用反勾号打印出字符串的值,这样应该可以:

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

根据 火灾恢复文档,云火恢复不支持本地索引或搜索文档中的文本字段。此外,下载整个集合来搜索客户端字段是不切实际的。

建议使用第三方搜索解决方案,如 阿尔戈利亚弹性搜寻

虽然 Firebase 不显式地支持在字符串中搜索术语,

Firebase 确实(现在)支持以下方面,这将解决你的情况和其他许多问题:

截至2018年8月,它们支持 array-contains查询

您现在可以将所有关键词设置为一个数组作为一个字段,然后查询具有包含“ X”的数组的所有文档。您可以使用逻辑 AND 对 额外查询。进行进一步的比较(这是因为 firebase 目前不支持多个数组包含查询的复合查询,所以‘ AND’排序查询必须在客户端完成)

使用这种样式的数组将允许它们针对并发写操作进行优化,这非常好!还没有测试过它是否支持批处理请求(医生没有说) ,但是我敢打赌它一定支持,因为它是一个正式的解决方案。


用法:

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

迟到的回答,但是对于任何仍然在寻找答案的人来说,让我们假设我们有一个用户集合,在这个集合的每个文档中我们有一个“用户名”字段,所以如果想要找到一个用户名以“ al”开头的文档,我们可以这样做

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

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

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

IsGreaterThanOrEqualTo 允许我们过滤掉搜索的开始部分,并在 isLessThanOrEqualTo 的末尾添加一个“ z”来限制我们的搜索不转到下一个文档。

实际上,我认为在 Firestore 内部实现这一点的最佳解决方案是将所有子字符串放在一个数组中,然后执行 array _ include 查询。这允许您进行子字符串匹配。存储所有子字符串有点过分,但是如果你的搜索条件很短,这是非常非常合理的。

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

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

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

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

如果你不想使用像 Algolia 这样的第三方服务,abc0是一个很好的选择。您可以创建一个函数,该函数可以接收输入参数,在服务器端处理记录,然后返回符合条件的参数。

编辑:

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

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

原答案:

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

我认为丹 · 费恩的答案应该排在更高的位置。如果正在搜索的 String 数据很短,那么可以将该字符串的所有子字符串保存在 Document 中的数组中,然后使用 Firebase 的 array _ include 查询在该数组中搜索。Firebase 文档限制为1 MiB (1,048,576字节)(火力基地配额及限额) ,即文档中保存的大约100万个字符(我认为1个字符 ~ = 1字节)。存储子字符串是可以的,只要您的文档不接近100万马克。

搜索用户名的示例:

步骤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: 在存储用户名时,也将此函数的结果以数组的形式存储在同一文档中。这将创建原始文本的所有变体并将它们存储在一个数组中。例如,文本输入“ Apple”将创建以下数组: [“ a”,“ p”,“ p”,“ l”,“ e”,“ ap”,“ pp”,“ pl”,“ le”,“ app”,“ ppl”,“ ple”,“ appl”,“ pple”,“ Apple”] ,它应该包含用户可能输入的所有搜索条件。如果你想要得到所有的结果,你可以把 maxumStringSize 设置为 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 _ include 函数!

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

我相信 Firebase 很快就会推出“ string-include”来捕获字符串中的任何索引[ 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()
)

使用 Firestore,你可以实现一个全文搜索,但它仍然需要比其他方式更多的读取,而且你还需要以特定的方式输入和索引数据。因此,在这种方法中,你可以使用 firebase 云函数来标记和散列你的输入文本,同时选择一个满足以下条件的线性散列函数 h(x)——如果 x < y < z then h(x) < h (y) < h(z)。对于标记,您可以选择一些轻量级的 NLP 库,以保持您的功能的冷启动时间低,可以删除不必要的单词从您的句子。然后,您可以在 Firestore 运行带有小于或大于运算符的查询。 在存储数据的同时,还必须确保在存储文本之前对其进行哈希处理,并且如果更改了纯文本,则哈希值也将随之更改。

全文搜索,相关搜索,三元搜索!

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

详情请参阅 代码,构建


另外,附注,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)如果你只有几个字,谭的方法会做你想做的一切,或者至少在你稍微修改之后。但是,如果只有一段文本,您将以指数级方式创建超过1MB 的数据,这大于 firest 的文档大小限制(我知道,我测试过它)。

5)如果能够将 数组包含(或者某种形式的数组)与 \uf8ff技巧结合起来,那么您可能会得到一个不会达到极限的可行搜索。我试了所有的组合,甚至是地图,都没用。有人发现的话,就发到这里。

6)如果你必须远离 ALGOLIA 和 ELASTIC SEARCH,我一点也不怪你,你可以在 Google Cloud 上使用 mySQL、 postSQL 或者 neo4Js。它们都是3个易于设置的,并且它们有免费的层次。您可以使用一个云函数来保存数据 onCreate () ,使用另一个 onCall ()函数来搜索数据。很简单。那为什么不直接切换到 mySQL 呢?当然是实时数据啦!当有人用网袜编写 DGraph实时数据时,算我一个!

Algolia 和 ElasticSearch 都是只支持搜索的 dbs,所以没有什么比这更快了... ... 但是你要付钱。谷歌,你为什么要把我们从谷歌引开,你为什么不关注 MongoDB noSQL 并允许搜索呢?

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

在查询 firestore 时执行以下操作:

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

在你的未来建设者中这样做:

    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年8月18日) ,基本上有3个不同的解决方案,这是专家们提出的,作为对这个问题的答案。

我都试过了,我觉得把我的经历记录下来可能会有用。

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

建议:@Kuba &@Ankit Prajapati

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

Firest 查询只能在单个字段上执行范围过滤器(> ,< ,> = ,< =)。不支持在多个字段上使用范围筛选器的查询。通过使用这种方法,您不能在 db 上的任何其他字段中使用范围运算符,例如日期字段。

答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.2如果一个火还原查询只有一个类似于上面的条件,那么就不需要事先创建索引。在这种情况下,这个解决方案就可以很好地工作。

然而,如果查询有另一个条件,例如(status = = = “ active”) ,那么用户输入的每个“搜索字符串”似乎都需要一个索引。换句话说,如果一个用户搜索“ Jam”,而另一个用户搜索“ Butter”,那么应该事先为字符串“ Jam”创建一个索引,为“ Butter”创建另一个索引,等等。除非你可以预测所有可能的用户的搜索字符串,这是不工作的情况下查询有其他条件!

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


我不知道你在说什么

推荐:@Albert Renshaw 演示:@Nick Carducci

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


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

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

火还原查询在复合查询中最多只能包含一个“ array-include”或“ array-include-any”子句。

一般限制:

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

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

要获得 Firestorquery 条件的列表,请查看文档 https://firebase.google.com/docs/firestore/query-data/queries

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

下面的代码段从用户获取输入,并从类型化的输入开始获取数据。

数据样本:

在 Firebase 集合下的“用户”

User1: { name: ‘ Ali’,age: 28} ,

User2: { name: ‘ Khan’,age: 30} ,

User3: { name: ‘ Hassan’,age: 26} ,

User4: { name: ‘ Adil’,age: 32}

TextInput : A

结果:

{ name: ‘ Ali’,age: 28},

{ name: ‘ Adil’,age: 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);


};

与@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
}

第二步

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

第三步

Firestore.firestore().collection("Users")
.whereField("nameSubstrings", arrayContains: searchText)
.getDocuments...

2021年最新情况

从其他答案中借鉴了一些东西,其中包括:

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

对大小写敏感性有一点限制,您可以通过将重复属性存储为大写来解决这个问题


// 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 调用的数量相当于查询字符串中的单词数量 * 您正在搜索的键的数量。

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

就像 Jonathan 说的,我用了三角函数。

三角形是一组3个字母存储在数据库中,以帮助搜索。因此,如果我有用户的数据,我让’说,我想查询’谱’为唐纳德川普,我必须存储它这样

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

Tyesense 服务为 Firebase Cloud Firestore 数据库提供子字符串搜索。

Https://typesense.org/docs/guide/firebase-full-text-search.html

下面是我的项目中相关的类型感知集成代码。

Lib/utils/typesense.dart

import 'dart:convert';


import 'package:flutter_instagram_clone/model/PostModel.dart';
import 'package:http/http.dart' as http;


class Typesense {
static String baseUrl = 'http://typesense_server_ip:port/';
static String apiKey = 'xxxxxxxx'; // your Typesense API key
static String resource = 'collections/postData/documents/search';


static Future<List<PostModel>> search(String searchKey, int page, {int contentType=-1}) async {
if (searchKey.isEmpty) return [];


List<PostModel> _results = [];


var header = {'X-TYPESENSE-API-KEY': apiKey};
String strSearchKey4Url = searchKey.replaceFirst('#', '%23').replaceAll(' ', '%20');
String url = baseUrl +
resource +
'?q=${strSearchKey4Url}&query_by=postText&page=$page&sort_by=millisecondsTimestamp:desc&num_typos=0';
if(contentType==0)
{
url += "&filter_by=isSelling:false";
} else if(contentType == 1)
{
url += "&filter_by=isSelling:true";
}


var response = await http.get(Uri.parse(url), headers: header);


var data = json.decode(response.body);
for (var item in data['hits']) {
PostModel _post = PostModel.fromTypeSenseJson(item['document']);


if (searchKey.contains('#')) {
if (_post.postText.toLowerCase().contains(searchKey.toLowerCase()))
_results.add(_post);
} else {
_results.add(_post);
}
}


print(_results.length);
return _results;
}


static Future<List<PostModel>> getHubPosts(String searchKey, int page,
{List<String>? authors, bool? isSelling}) async {
List<PostModel> _results = [];


var header = {'X-TYPESENSE-API-KEY': apiKey};


String filter = "";
if (authors != null || isSelling != null) {
filter += "&filter_by=";


if (isSelling != null) {
filter += "isSelling:$isSelling";
if (authors != null && authors.isNotEmpty) {
filter += "&&";
}
}


if (authors != null && authors.isNotEmpty) {
filter += "authorID:$authors";
}
}


String url = baseUrl +
resource +
'?q=${searchKey.replaceFirst('#', '%23')}&query_by=postText&page=$page&sort_by=millisecondsTimestamp:desc&num_typos=0$filter';


var response = await http.get(Uri.parse(url), headers: header);


var data = json.decode(response.body);
for (var item in data['hits']) {
PostModel _post = PostModel.fromTypeSenseJson(item['document']);
_results.add(_post);
}


print(_results.length);


return _results;
}
}

Lib/services/hubDetailsService.dart

import 'package:flutter/material.dart';
import 'package:flutter_instagram_clone/model/PostModel.dart';
import 'package:flutter_instagram_clone/utils/typesense.dart';


class HubDetailsService with ChangeNotifier {
String searchKey = '';
List<String>? authors;
bool? isSelling;
int nContentType=-1;




bool isLoading = false;
List<PostModel> hubResults = [];
int _page = 1;
bool isMore = true;
bool noResult = false;


Future initSearch() async {
isLoading = true;
isMore = true;
noResult = false;
hubResults = [];
_page = 1;
List<PostModel> _results = await Typesense.search(searchKey, _page, contentType: nContentType);
for(var item in _results) {
hubResults.add(item);
}
isLoading = false;
if(_results.length < 10) isMore = false;
if(_results.isEmpty) noResult = true;
notifyListeners();
}


Future nextPage() async {
if(!isMore) return;
_page++;
List<PostModel> _results = await Typesense.search(searchKey, _page);
hubResults.addAll(_results);
if(_results.isEmpty) {
isMore = false;
}
notifyListeners();
}


Future refreshPage() async {
isLoading = true;
notifyListeners();
await initSearch();
isLoading = false;
notifyListeners();
}


Future search(String _searchKey) async {
isLoading = true;
notifyListeners();
searchKey = _searchKey;
await initSearch();
isLoading = false;
notifyListeners();
}
}

Lib/ui/hub/hubDetailsScreen.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_instagram_clone/constants.dart';
import 'package:flutter_instagram_clone/main.dart';
import 'package:flutter_instagram_clone/model/MessageData.dart';
import 'package:flutter_instagram_clone/model/SocialReactionModel.dart';
import 'package:flutter_instagram_clone/model/User.dart';
import 'package:flutter_instagram_clone/model/hubModel.dart';
import 'package:flutter_instagram_clone/services/FirebaseHelper.dart';
import 'package:flutter_instagram_clone/services/HubService.dart';
import 'package:flutter_instagram_clone/services/helper.dart';
import 'package:flutter_instagram_clone/services/hubDetailsService.dart';
import 'package:flutter_instagram_clone/ui/fullScreenImageViewer/FullScreenImageViewer.dart';
import 'package:flutter_instagram_clone/ui/home/HomeScreen.dart';
import 'package:flutter_instagram_clone/ui/hub/editHubScreen.dart';
import 'package:provider/provider.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';


class HubDetailsScreen extends StatefulWidget {


final HubModel hub;
HubDetailsScreen(this.hub);


@override
_HubDetailsScreenState createState() => _HubDetailsScreenState();
}


class _HubDetailsScreenState extends State<HubDetailsScreen> {


late HubDetailsService _service;
List<SocialReactionModel?> _reactionsList = [];
final fireStoreUtils = FireStoreUtils();
late Future<List<SocialReactionModel>> _myReactions;
final scrollController = ScrollController();
bool _isSubLoading = false;




@override
void initState() {
// TODO: implement initState
super.initState();
_service = Provider.of<HubDetailsService>(context, listen: false);
print(_service.isLoading);
init();
}


init() async {


_service.searchKey = "";


if(widget.hub.contentWords.length>0)
{
for(var item in widget.hub.contentWords) {
_service.searchKey += item + " ";
}
}
switch(widget.hub.contentType) {
case 'All':
break;
case 'Marketplace':
_service.isSelling = true;
_service.nContentType = 1;
break;
case 'Post Only':
_service.isSelling = false;
_service.nContentType = 0;
break;
case 'Keywords':
break;
}


for(var item in widget.hub.exceptWords) {
if(item == 'Marketplace') {
_service.isSelling = _service.isSelling != null?true:false;
} else {
_service.searchKey += "-" + item + "";
}
}


if(widget.hub.fromUserType == 'Followers') {
List<User> _followers = await fireStoreUtils.getFollowers(MyAppState.currentUser!.userID);


_service.authors = [];
for(var item in _followers)
_service.authors!.add(item.userID);


}


if(widget.hub.fromUserType == 'Selected') {
_service.authors = widget.hub.fromUserIds;
}


_service.initSearch();


_myReactions = fireStoreUtils.getMyReactions()
..then((value) {
_reactionsList.addAll(value);
});


scrollController.addListener(pagination);
}




void pagination(){
if(scrollController.position.pixels ==
scrollController.position.maxScrollExtent) {
_service.nextPage();
}
}


@override
Widget build(BuildContext context) {


Provider.of<HubDetailsService>(context);


PageController _controller = PageController(
initialPage: 0,
);


return Scaffold(
backgroundColor: Colors.white,
body: RefreshIndicator(
onRefresh: () async {
_service.refreshPage();
},
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverAppBar(
centerTitle: false,
expandedHeight: MediaQuery.of(context).size.height * 0.25,
pinned: true,
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: (){
Navigator.pop(context);
},
child: Container(
width: 35, height: 35,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20)
),
child: Center(
child: Icon(Icons.arrow_back),
),
),
),


if(widget.hub.user.userID == MyAppState.currentUser!.userID)
InkWell(
onTap: () async {
var _hub = await push(context, EditHubScreen(widget.hub));


if(_hub != null) {
Navigator.pop(context, true);
}


},
child: Container(
width: 35, height: 35,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20)
),
child: Center(
child: Icon(Icons.edit, color: Colors.black, size: 20,),
),
),
),
],


),
automaticallyImplyLeading: false,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Container(color: Colors.grey,
child: Stack(
children: [
PageView.builder(
controller: _controller,
itemCount: widget.hub.medias.length,
itemBuilder: (context, index) {
Url postMedia = widget.hub.medias[index];
return GestureDetector(
onTap: () => push(
context,
FullScreenImageViewer(
imageUrl: postMedia.url)),
child: displayPostImage(postMedia.url));
}),
if (widget.hub.medias.length > 1)
Padding(
padding: const EdgeInsets.only(bottom: 30.0),
child: Align(
alignment: Alignment.bottomCenter,
child: SmoothPageIndicator(
controller: _controller,
count: widget.hub.medias.length,
effect: ScrollingDotsEffect(
dotWidth: 6,
dotHeight: 6,
dotColor: isDarkMode(context)
? Colors.white54
: Colors.black54,
activeDotColor: Color(COLOR_PRIMARY)),
),
),
),
],
),
)
),
),


_service.isLoading?
SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(),
),
):
SliverList(
delegate: SliverChildListDelegate([


if(widget.hub.userId != MyAppState.currentUser!.userID)
_isSubLoading?
Center(
child: Padding(
padding: EdgeInsets.all(5),
child: CircularProgressIndicator(),
),
):
Padding(
padding: EdgeInsets.symmetric(horizontal: 5),
child: widget.hub.shareUserIds.contains(MyAppState.currentUser!.userID)?
ElevatedButton(
onPressed: () async {
setState(() {
_isSubLoading = true;
});


await Provider.of<HubService>(context, listen: false).unsubscribe(widget.hub);


setState(() {
_isSubLoading = false;
widget.hub.shareUserIds.remove(MyAppState.currentUser!.userID);
});
},
style: ElevatedButton.styleFrom(
primary: Colors.red
),
child: Text(
"Unsubscribe",
),
):
ElevatedButton(
onPressed: () async {
setState(() {
_isSubLoading = true;
});


await Provider.of<HubService>(context, listen: false).subscribe(widget.hub);


setState(() {
_isSubLoading = false;
widget.hub.shareUserIds.add(MyAppState.currentUser!.userID);
});
},
style: ElevatedButton.styleFrom(
primary: Colors.green
),
child: Text(
"Subscribe",
),
),
),


Padding(
padding: EdgeInsets.all(15,),
child: Text(
widget.hub.name,
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold
),
),
),


..._service.hubResults.map((e) {
if(e.isAuction && (e.auctionEnded || DateTime.now().isAfter(e.auctionEndTime??DateTime.now()))) {
return Container();
}
return PostWidget(post: e);
}).toList(),


if(_service.noResult)
Padding(
padding: EdgeInsets.all(20),
child: Text(
'No results for this hub',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold
),
),
),


if(_service.isMore)
Center(
child: Container(
padding: EdgeInsets.all(5),
child: CircularProgressIndicator(),
),
)


]),
)
],
),
)
);
}
}

您可以尝试使用2 lambdas 和 S3。这些资源非常便宜,只有当应用程序有极端使用(如果商业模式是好的,那么高使用率-> 更高的收入) ,你才会收取费用。

第一个 lambda 将用于将文本文档映射推送到 S3json 文件。

第二个 lambda 基本上就是您的搜索 api,您将使用它来查询 s3中的 JSON 并返回结果。

缺点可能是从 s3到 lambda 的延迟。

我和 Vue js 一起用的

query(collection(db,'collection'),where("name",">=",'searchTerm'),where("name","<=","~"))

我也无法使用建议和 Firebase 工具创建 Firebase 的搜索函数,所以我创建了自己的“ field-string include search-string (substring) check”,使用。包含() Kotlin 函数:

firestoreDB.collection("products")
.get().addOnCompleteListener { task->
if (task.isSuccessful){
val document = task.result
if (!document.isEmpty) {
if (document != null) {
for (documents in document) {
var name = documents.getString("name")
var type = documents.getString("type")
if (name != null && type != null) {
if (name.contains(text, ignoreCase = true) || type.contains(text, ignoreCase = true)) {
// do whatever you want with the document
} else {
showNoProductsMsg()
}
}
}
}
binding.progressBarSearch.visibility = View.INVISIBLE
} else {
showNoProductsMsg()
}
} else{
showNoProductsMsg()
}
}

首先,获得所需集合中的所有文档,然后使用以下方法对其进行过滤:

for (documents in document) {
var name = documents.getString("name")
var type = documents.getString("type")
if (name != null && type != null) {
if (name.contains(text, ignoreCase = true) || type.contains(text, ignoreCase = true)) {
//do whatever you want with this document
} else {
showNoProductsMsg()
}
}
}

在我的例子中,我通过产品的 name和它的 type来过滤它们,然后我使用布尔 name.contains(string, ignoreCase = true) OR type.contains(string, ignoreCase = truestring是我在应用程序的搜索栏中得到的文本,我建议你使用 ignoreCase = true。如果该语句为 true,则可以对文档进行任何操作。

我认为这是最好的解决方案,因为 Firest 只支持数字和精确字符串查询,所以如果您的代码无法这样做:

collection.whereGreaterThanOrEqualTo("name", querySearch)
collection.whereLessThanOrEqualTo("name", querySearch)

不客气:)因为我所做的是有效的!