对实体进行排序并过滤ListProperty,而不会产生爆炸索引

时间:2011-05-24 09:01:58

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

我正在开发一个简单的Blogging / Bookmarking平台,我正在尝试添加标签 - 资源管理器/向下钻取功能làdelicious以允许用户过滤帖子指定特定标签的列表。

这样的事情: enter image description here

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

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中,为了检索标记有特定标签列表的帖子列表,Post模型公开了以下静态方法:

@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方法应该提供一些分页机制。

你知道任何策略/想法/解决方法/黑客来解决这个问题吗?

4 个答案:

答案 0 :(得分:3)

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

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

要搜索标签,您需要执行tags = Tag.all().filter('tag IN', ['python','blog','async'])

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

然后你可以获取这些帖子并通过创建(我认为)来订购它们。 Posts.all().filter('__key__ IN', post_union).order("-created")

注意:这段代码不在我的脑海中,我无法记住你是否可以像这样操纵套装。

编辑:@Yasser指出你只能对< IN进行IN查询。 30项。

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

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

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

答案 1 :(得分:3)

  

涉及密钥的查询使用索引   就像涉及的查询一样   属性。密钥查询需要   自定义索引在相同的情况下   有属性,有几个   例外:不等式过滤器或   上的升序排序不会   需要自定义索引,但是   降序排序   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%的可靠性,那么你可能不应该使用app引擎,但我想你可以在save()中包含处理关键冲突的逻辑。

答案 2 :(得分:2)

这个问题听起来类似于:

正如最后一篇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)

根据您希望通过标签查询返回的页数,可以在内存中进行排序,也可以使日期字符串表示形成Article key_name

的一部分

使用StringListProperty进行了更新,并在#appengine IRC频道{{1}}和Robert Kluin评论之后对备注进行了排序。

答案 3 :(得分:0)

一种解决方法可能是:

将帖子的标签与像|这样的分隔符排序和连接并在存储帖子时将它们存储为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标记的所有帖子。