对实体进行排序和过滤 ListProperty 而不会导致索引爆炸

Posted

技术标签:

【中文标题】对实体进行排序和过滤 ListProperty 而不会导致索引爆炸【英文标题】:Sorting entities and filtering ListProperty without incurring in exploding indexes 【发布时间】:2011-05-24 09:01:58 【问题描述】:

我正在开发一个简单的博客/书签平台,我正在尝试添加一个 tags-explorer/drill-down 功能,一个 là delicious 以允许用户过滤指定一个特定标签列表。

类似这样的:

使用此简化模型在数据存储中表示帖子:

class Post(db.Model):
    title = db.StringProperty(required = True)
    link = db.LinkProperty(required = True)
    description = db.StringProperty(required = True)
    tags = db.ListProperty(str)
    created = db.DateTimeProperty(required = True, auto_now_add = True)

帖子的标签存储在ListProperty 中,为了检索带有特定标签列表的帖子列表,帖子模型公开了以下静态方法:

@staticmethod
def get_posts(limit, offset, tags_filter = []):
        posts = Post.all()
        for tag in tags_filter:
          if tag:
              posts.filter('tags', tag)
        return posts.fetch(limit = limit, offset = offset)

这很好用,虽然我没有过分强调它。

当我尝试向get_posts 方法添加“排序”顺序以保持结果按"-created" 日期排序时,问题就出现了:

@staticmethod
def get_posts(limit, offset, tags_filter = []):
        posts = Post.all()
        for tag in tags_filter:
          if tag:
              posts.filter('tags', tag)
        posts.order("-created")
        return posts.fetch(limit = limit, offset = offset)

排序顺序为每个要过滤的标签添加一个索引,导致可怕的爆炸索引问题。 最后一件让这件事变得更复杂的事情是get_posts 方法应该提供一些分页机制。

你知道解决这个问题的任何策略/想法/解决方法/Hack吗?

【问题讨论】:

【参考方案1】:

涉及键的查询使用索引 就像查询涉及 特性。对键的查询需要 在相同的情况下自定义索引 有属性,有几个 例外:不等式过滤器或 key 上的升序排序不 需要一个自定义索引,但是一个 降序排序 Entity.KEY_RESERVED_PROPERTY__ 可以。

所以对实体的主键使用可排序的日期字符串:

class Post(db.Model):
    title = db.StringProperty(required = True)
    link = db.LinkProperty(required = True)
    description = db.StringProperty(required = True)
    tags = db.ListProperty(str)
    created = db.DateTimeProperty(required = True, auto_now_add = True)

    @classmethod
    def create(*args, **kw):
         kw.update(dict(key_name=inverse_millisecond_str() + disambig_chars()))
         return Post(*args, **kw)

...

def inverse_microsecond_str(): #gives string of 8 characters from ascii 23 to 'z' which sorts in reverse temporal order
    t = datetime.datetime.now()
    inv_us = int(1e16 - (time.mktime(t.timetuple()) * 1e6 + t.microsecond)) #no y2k for >100 yrs
    base_100_chars = []
    while inv_us:
        digit, inv_us = inv_us % 100, inv_us / 100
        base_100_str = [chr(23 + digit)] + base_100_chars
    return "".join(base_100_chars)

现在,您甚至不必在查询中包含排序顺序,尽管显式按键排序并没有什么坏处。

要记住的事情:

除非您在此处为所有帖子使用“创建”,否则这将不起作用。 您必须迁移旧数据 不允许祖先。 每个索引存储一次键,因此值得保持简短;这就是我在上面进行base-100编码的原因。 这不是 100% 可靠的,因为可能发生密钥冲突。上面的代码,没有 disambig_chars,名义上给出了事务之间的微秒数的可靠性,所以如果你在高峰时间每秒有 10 个帖子,它将失败 1/100,000。但是,对于可能的应用程序引擎时钟滴答问题,我会减少几个数量级,所以我实际上只相信它的 1/1000。如果这还不够好,请添加 disambig_chars;如果您需要 100% 的可靠性,那么您可能不应该使用应用引擎,但我想您可以在 save() 上包含处理键冲突的逻辑。

【讨论】:

@Jameson 我需要一个降序,根据文档,需要一个自定义索引。 对不起,我以为我的回答很清楚。我将修改我的答案,以展示如何创建一个按时间倒序自然排序的键名。 @Jameson 我正在测试它,它似乎工作;我认为已经使用带有像这样的时间戳 (now().strftime("%Y%m%d%H%M%S")) 的 key_name 测试了这种解决方案,但它没有奏效。为什么它与您的解决方案不同? 注意int(1e16 - (time.mktime(t.timetuple()) * 1e6 + t.microsecond)) 中的减号。这意味着 inverse_microsecond_str 不是时间(以 100 为底),而是一个减去时间的大数。换句话说,随着时间的推移,它会下降,而不是上升;所以它以相反的顺序排序。 您可以使用类似B64_CHARLIST = "-0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"base_64_str = B64_CHARLIST[digit] + base_64_str 的方式将其更改为base64,并将100s 更改为64s。如果你这样做,你应该使用一些 64 的幂而不是 1e16,以确保你在 2100 年之前不会 y2k。 (或者使用“range”而不是“while”来确保所有字符串的长度相同)。【参考方案2】:

如果你颠倒了关系呢?您将拥有一个带有帖子列表的标签实体,而不是带有标签列表的帖子。

class Tag(db.Model):
  tag = db.StringProperty()
  posts = db.ListProperty(db.Key, indexed=False)

要搜索标签,你会做tags = Tag.all().filter('tag IN', ['python','blog','async'])

这有望为您提供 3 个或更多标签实体,每个标签实体都有一个使用该标签的帖子列表。然后你可以通过post_union = set(tags[0].posts).intersection(tags[1].posts, tags[2].posts) 找到包含所有标签的帖子集。

然后您可以获取这些帖子并按创建排序(我认为)。 Posts.all().filter('__key__ IN', post_union).order("-created")

注意:这段代码在我脑海中浮现,我不记得你是否可以这样操作集合。

编辑:@Yasser 指出您只能对

相反,您可以让每个帖子的键名从创建时间开始。然后您可以对通过第一个查询检索到的键进行排序,然后执行Posts.get(sorted_posts)

不知道这将如何扩展到具有数百万帖子和/或标签的系统。

Edit2:我的意思是设置交集,而不是联合。

【讨论】:

IN 查询只能用于成员集中少于 30 个项目。因此,如果一个标签出现在 30 多个帖子中,这可能不起作用。 @Cal 感谢您的帮助。我不使用 key_name 来生成帖子,但也许我可以使用 sorted_posts = sorted(posts_keys, lambda x: x.id(), reverse=True) 按 ID 排序。当要过滤的标签数量小于或等于 2 时,我正在考虑使用 order("-created") 混合排序策略,在其他情况下,我会在内存中排序,以检索您建议的键。你怎么看? 不,按 id 排序是not going to work。 如果您还没有数百万个帖子,我可能会返回运行迁移,将帖子与以 [created date]_[uuid] 作为键名的帖子交换。这样做可能会在不应用“-created”过滤器的情况下按创建顺序返回您的帖子。没有把握。只要帖子的搜索频率高于创建帖子的频率,我认为这将是一场胜利。查看 gae-sessions 中的 __make_sid 方法,了解如何执行此操作的示例。 @Calvin 不,即使使用创建日期创建带有键名的帖子也不起作用。我已经使用这样创建的键名对其进行了测试:now().strftime("%Y%m%d%H%M%S")。帖子从最旧到最新检索,不幸的是这不是我想要的;我想将最后一个插入列表顶部。【参考方案3】:

这个问题听起来类似于:

Data Modelling Advice for Blog Tagging system on Google App Engine Mapping Data for a Google App Engine Blog Application: parent->child relationships in appengine python (bigtable)

正如Robert Kluin 在最后一个中所指出的,您还可以考虑使用类似于in this Google I/O presentation 所述的“关系索引”的模式。

# Model definitions
class Article(db.Model):
  title = db.StringProperty()
  content = db.StringProperty()

class TagIndex(db.Model):
  tags = db.StringListProperty()

# Tags are child entities of Articles
article1 = Article(title="foo", content="foo content")
article1.put()
TagIndex(parent=article1, tags=["hop"]).put()

# Get all articles for a given tag
tags = db.GqlQuery("SELECT __key__ FROM Tag where tags = :1", "hop")
keys = (t.parent() for t in tags)
articles = db.get(keys)

根据您希望通过标签查询返回多少页,可以在内存中进行排序,也可以通过将日期字符串表示为Articlekey_name 的一部分来进行排序

#appengine IRC 频道上更新了StringListProperty 和Robert Kluin 和Wooble cmets 之后的注释。

【讨论】:

我不仅需要获取给定标签的文章,还需要获取给定标签列表的文章。 @systempuntoout: 带有所有标签的文章,任何标签? 所以我的意思是你需要获得 1/ 带有所有标签的文章,还是 2/ 任何标签?【参考方案4】:

一种解决方法可能是这样的:

使用分隔符对帖子的标签进行排序和连接,例如 |并在存储帖子时将它们存储为 StringProperty。当您收到 tags_filter 时,您可以对它们进行排序和连接以为帖子创建单个 StringProperty 过滤器。显然,这将是一个 AND 查询,而不是一个 OR 查询,但这就是您当前的代码似乎也在做的事情。

编辑:正如正确指出的那样,这只会匹配精确的标签列表而不是部分标签列表,这显然不是很有用。

编辑:如果您使用布尔占位符为标签建模您的 Post 模型,例如b1、b2、b3 等。定义新标签后,您可以将其映射到下一个可用的占位符,例如blog=b1, python=b2, async=b3 并将映射保存在单独的实体中。将标签分配给帖子时,只需将其等效占位符值切换为 True。

这样,当您收到 tag_filter 集时,您可以从地图构建查询,例如

Post.all().filter("b1",True).filter("b2",True).order('-created')

可以给你所有标签pythonblog的帖子。

【讨论】:

搜索 blog|python 应该返回所有至少有这两个标签的帖子;例如,搜索应该与标记为 async|blog|python 的帖子匹配,并且您的方法在这种情况下似乎不起作用。感谢您的帮助。 对不起,我不明白。使用这种方法的 Post 模型应该有多少个布尔占位符? 您最多可以拥有 200 个索引,对吧?因此,在使用其他索引后,您可以拥有尽可能多的应用程序。这将是帖子可以拥有的标签数量的上限。

以上是关于对实体进行排序和过滤 ListProperty 而不会导致索引爆炸的主要内容,如果未能解决你的问题,请参考以下文章

对许多实体和实体关系进行过滤的核心数据提取

GAE 数据存储性能(列与 ListProperty)

ES聚合过滤与排序

如何在Vue中对对象数组进行排序和过滤

如何使用 QAbstractTableModel 而不是 QSortFilterProxyModel 进行排序和过滤

对计算字段进行排序和过滤