使用Firebase实现自动完成功能

20
如何使用Firebase进行基本的自动完成/文本预览?
例如,想象一下由Firebase支持的博客,博主可以使用标签标记帖子。当博主正在为新帖子添加标签时,如果他们可以看到与他们输入的前几个按键匹配的所有当前存在的标签,那将会很有帮助。因此,如果“blog”、“black”、“blazing saddles”和“bulldogs”是标签,如果用户键入“bl”,则会得到前三个标签,但不包括“bulldogs”。
我的初步想法是,我们可以根据标签设置优先级,并使用startAt来查询,这样我们的查询看起来会像这样:
fb.child('tags').startAt('bl').limitToFirst(5).once('value', function(snap) {
  console.log(snap.val()) 
});

但这也会返回“bulldog”作为结果之一(虽然不是致命的,但也不是最好的)。使用startAt('bl').endAt('bl')没有返回结果。还有其他方法可以实现吗?
(我知道其中一个选项是我们可以使用搜索服务器,如ElasticSearch,参见https://www.firebase.com/blog/2014-01-02-queries-part-two.html,但我希望尽可能多地保留在Firebase中。)
编辑
正如Kato建议的那样,这里有一个具体的例子。我们有20,000个用户,他们的名字存储如下:
/users/$userId/name

通常,用户会通过姓名查找其他用户。当用户查找他们的好友时,我们希望出现一个下拉菜单,列出那些名字以搜索者输入的字母开头的用户列表。因此,如果我输入了“Ja”,我期望在下拉菜单中看到“Jake Heller”、“jake gyllenhaal”、“Jack Donaghy”等。


我们在谈论多少条记录?我们是要实现Google搜索还是一个真实的客户端自动完成? - Kato
好问题。对于一些我们考虑自动完成的内容,有超过200万条记录。对于其他一些内容,可能只有大约100个(网站上的不同标签)- 20,000个(网站上注册用户的名称)数量级。 - Jake
我认为我明白你的意思。我们可以使用startAt()和limit(),然后在客户端过滤掉错误的结果(例如我的例子中的“bulldogs”)。我认为无论在FB中有多少对象,只要使用limit(),这应该是高效的(对吗?)。虽然这比让Firebase为我们处理所有事情要差一些,但仍然是一个选项。 - Jake
3
针对这样一个普通的集合,以SO格式回答是非常困难的。如果您先提供一个具体案例,我可以帮助您解决细节问题。您所提到的每一个案例中,“最具性能”的解决方案可能都不同。我们可以排除200万以上的数据作为ElasticSearch/Flashlight项目。500k以下的数据,我会将它们全部获取然后在客户端进行过滤。20k个名称大概是40万个记录吧?在这里,我可能会按照名称的首字母进行索引或优先排序,并在客户端进行过滤和必要的优化。选择其中一个案例,我们可以一起解决。 - Kato
另外,请注意您可以使用Flashlight获取ElasticSearch结果,仍然只与Firebase接口交互,让您保持简单。 - Kato
嘿,Kato,我添加了一个简短的示例,可能会给你一些工作上的帮助。我还会在下面详细说明一个潜在的答案。 - Jake
4个回答

17

我知道这是一个老话题,但它仍然很相关。根据尼尔在上面的回答,你可以更容易地搜索做以下操作:

fb.child('tags').startAt(queryString).endAt(queryString + '\uf8ff').limit(5)

请参考Firebase数据检索

上述查询中使用的 \uf8ff 字符是Unicode范围内非常高的代码点。由于该字符位于大多数常规字符之后,因此查询将匹配所有以queryString开头的值。


2
虽然这个示例(和相关文档链接)特别针对Firebase在Web上的应用,但是这种技术也适用于Android。你可以执行以下操作:FirebaseDatabase.getInstance().getReference("child").startAt("string").endAt("string" + "\uf8ff").limitToFirst(5).addListenerForSingleValueEvent( [...]); - Michael De Soto

7

受Kato评论的启发,解决这个问题的一种方法是将自动完成搜索的优先级设置为要搜索的字段,并使用startAt()、limit()和客户端过滤器仅返回所需的结果。您需要确保优先级和搜索项均转换为小写,因为Firebase区分大小写。

以下是使用我在问题中提出的用户示例演示此示例的粗略示例:

对于搜索“ja”,假设所有用户的优先级都设置为用户姓名的小写版本:

fb.child('users').
  startAt('ja'). // The user-inputted search
  limitToFirst(20).
  once('value', function(snap) {
    for(key in snap.val()){
      if(snap.val()[key].indexOf('ja') === 0) {
        console.log(snap.val()[key];
      }
    }
});

这应该只返回以“ja”开头的名称(即使Firebase实际上按字母顺序在“ja”之后返回名称)。

我选择使用limitToFirst(20)来保持响应大小较小,因为实际上,您永远不需要超过20个自动完成下拉菜单。可能有更好的过滤方式,但这至少应该演示了概念。

希望这能帮助到某人! Firebase的人可能有更好的答案。

(请注意,这非常有限-如果有人搜索姓氏,它将不会返回他们正在寻找的内容。因此,“最佳”答案可能是使用类似Kato的{{link1:Flashlight}}的搜索后端。)


我同意你在这里提出的大部分内容。对于这种直接内容搜索,ElasticSearch是最简单、最干净的答案。随着您的应用程序成熟,它也会随之发展。如果我们受到严格客户端限制,我会像你一样从简单的startAt()和limit()开始。包含样式搜索可以通过简单地分页服务器数据并手动搜索来实现——对于如此小的数据集,这是一个缓慢但合理的过程。 - Kato
这个能处理10万条记录吗?有人试过用这么多数据吗? - vinesh

4

我认为有比客户端过滤或破解弹性更简单和优雅的方法来实现这个目标。

通过将搜索关键字转换为其Unicode值并将其作为优先级存储,您可以通过递增值来使用startAt()和endAt()进行搜索。

var start = "ABA";

var pad = "AAAAAAAAAA";
start += pad.substring(0, pad.length - start.length);

var blob = new Blob([start]);

var reader = new FileReader();
reader.onload = function(e) {
    var typedArray = new Uint8Array(e.target.result);
    var array = Array.prototype.slice.call(typedArray);
    var priority = parseInt(array.join(""));
    console.log("Priority of", start, "is:", priority);
}
reader.readAsArrayBuffer(blob);

您可以通过将最后一个charCode增加1并执行相同的转换,然后将搜索优先级限制在“ABB”关键字上:

var limit = String.fromCharCode(start.charCodeAt(start.length -1) +1);
limit = start.substring(0, start.length -1) +limit;

"

"ABA..." 转换成 "ABB..." 的优先级如下:

开始:65666565656565650000

结束:65666665656565650000

很简单!


1
根据Jake和Matt的答案,更新了SDK 3.1的版本。'.limit'不再可用。
firebaseDb.ref('users')
    .orderByChild('name')
    .startAt(query)
    .endAt(`${query}\uf8ff`)
    .limitToFirst(5)
    .on('child_added', (child) => {
      console.log(
          {
            id: child.key,
            name: child.val().name
          }
      )
    })

关键在于 .endAt(${query}\uf8ff)。 - Nicoara Talpes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接