【问题标题】:Sorting entities and filtering ListProperty without incurring in exploding indexes对实体进行排序和过滤 ListProperty 而不会导致索引爆炸
【发布时间】: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吗?

【问题讨论】:

    标签: python google-app-engine indexing google-cloud-datastore explode


    【解决方案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】:

    这个问题听起来类似于:

    正如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 频道上更新了StringListPropertyRobert KluinWooble 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 个索引,对吧?因此,在使用其他索引后,您可以拥有尽可能多的应用程序。这将是帖子可以拥有的标签数量的上限。
    猜你喜欢
    • 2011-08-22
    • 1970-01-01
    • 2019-07-24
    • 1970-01-01
    • 2013-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多