在GAE中结合文本搜索和查询过滤器

4 投票
1 回答
900 浏览
提问于 2025-04-17 22:27

我正在写一个基于GAE(谷歌应用引擎)的应用程序,用户可以根据多个属性来筛选商品。商品存储为NDB实体。有些属性可以通过标准的查询过滤器来匹配,但有些则需要进行“完整”的(子字符串)文本搜索,这样才能有意义。此外,还需要一些合理的排序。下面用一个虚构的例子来说明:

class Product(ndb.Model) :
  manufacturer = ndb.StringProperty()
  model = ndb.StringProperty()
  rating = ndb.IntegerProperty(choices = [1, 2, 3, 4])
  features = ndb.StringProperty(repeated = True, choices = ['feature_1', 'feature_2'])
  is_very_expensive = ndb.BooleanProperty()
  categories = ndb.KeyProperty(kind = Category, repeated = True)

所有的产品实体都有相同的祖先作为它们的“容器”。一个产品可以属于一个或多个类别,而这些类别形成一个树状结构。

现在,用户应该能够:

  • 通过选择一个类别来缩小产品范围(选择一个就可以)
  • 通过指定最低评分和期望的特性来过滤产品
  • 仅查看非常昂贵的产品或那些不贵的产品(或者查看所有产品)
  • 通过型号和/或制造商字段中的一段文本来搜索产品
  • 最终的列表可以按型号名称排序(理想情况下,用户可以选择排序方式)。

所有这些操作需要同时进行,也就是说,当提供搜索条件时,过滤和排序应该无缝应用。

问题是:如何在GAE中以高效的方式实现这样的功能?

数据库中将会有成千上万,甚至可能有数百万的产品。使用搜索API与NDB查询结合时,问题在于如何过滤搜索结果以及可能的排序。

我考虑了两种解决方案:

  1. Product模型中添加一个重复的StringProperty,用于存储来自manufacturermodel字段的所有可搜索子字符串(或者至少是前缀)。这样做简单有效,但我对性能非常担心。在我的实验中,每个“Product”平均有40-50个可搜索的单词前缀。

  2. 专门使用搜索API来完成这个任务,利用高级搜索查询。例如,我可以将产品的类别(作为ID或路径)存储在一个单独的文档字段中,并使用这个字段来获取属于特定类别的产品。这可能是可行的,但我担心的是搜索结果的限制为10,000个,以及各种使用限制/配额。我也不确定结果的排序。

还有其他方法吗?

1 个回答

3

我强烈建议你不要使用GAE(Google App Engine)。我知道这可能不是你想听的,但它并不太适合你的需求,也无法提供你在产品搜索中想要的灵活性。听起来你真正想要的是更接近于分面搜索的东西。

以下是GAE不太适合的原因:

  1. 如果你使用NDB(Google的数据库),很快就会遇到索引爆炸的问题,或者可能因为复杂的查询导致性能下降。虽然这篇文章试图为你的做法辩护,但在实际工作中,我们发现它在小数据集和少量字段之外无法扩展。你引入的过滤条件越多,问题就越多,尤其是当你需要多个不等式和排序时。

  2. GAE的全文搜索速度慢,功能有限,相比其他服务来说不够成熟和灵活。而且它的费用和配额也不太友好。你提到你担心配额,而搜索会很快消耗掉这些配额。

  3. 使用子字符串的方法会增加你保存的每条记录的大小。Django的非关系型数据库有一个索引器包,正是这样做的,但效果并不好。我不确定你是否在使用Django,但无论如何你都可以根据开源代码进行调整。记录大小增加是个坏事,因为除非你使用投影查询或仅使用键,否则你会通过网络发送很多不必要的数据。

相反,我建议你将数据推送到一个更适合这些查询的数据存储。以下是一个架构的示例:

  • 在Google Compute Engine上设置搜索服务器,以减少与App Engine的延迟。我不确定是否有办法让它们在同一地理位置,但我认为在延迟方面,选择这里而不是亚马逊会更好。显然,你可能会在这里失去一些速度,但可能仍然比内置的GAE全文搜索快。

  • 如果你需要一个集群来扩展,可以使用ElasticSearch。请注意,如果你这样做,需要在Google Compute Engine上正确设置多播。ElasticSearch提供了一个插件来实现这一点。

  • 创建一个后台进程,根据你的数据量使用推送或拉取队列来更新搜索索引。更新的频率取决于你需要数据的“新鲜度”。推送和拉取的选择主要取决于你的数据量,但我建议在这里使用拉取队列,并让专用服务器推送到你的搜索提供商。使用内置的全文搜索时,你也必须这样做。

  • 创建一个MapReduce作业,将所有数据推送到搜索索引。这对于初始化队列和定期“刷新”都很有用。

上述方法的缺点是你将大幅增加URL请求的次数,数据可能并不总是最新的。后者在大多数搜索场景中是正常的,而前者根据你的数据量可能仍然比内置的全文搜索便宜。如果数据很少变化,你可以选择将数据转存到Google Cloud Storage,然后以更便宜的方式导入。

撰写回答