我正在开发一个简单的Blogging / Bookmarking平台,我正在尝试添加标签 - 资源管理器/向下钻取功能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中,为了检索标记有特定标签列表的帖子列表,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
方法应该提供一些分页机制。
你知道任何策略/想法/解决方法/黑客来解决这个问题吗?
答案 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)
现在,您甚至不必在查询中包含排序顺序,尽管按键显式排序也不会有任何影响。
要记住的事情:
答案 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')
可以为您提供包含python
和blog
标记的所有帖子。