0x01背景
在广告监控模型中,一个推广计划(campaign)对应于爱奇艺网页中的恐怖电影系列,一个推广组(adgroup)对应于恐怖电影系列的美国的恐怖电影,然后一个创意(ad)则对应于具体的某部电影上所投放的广告,然而,这个ad可能会呈现在网页上的位置,于是会有一个广告位(adunit)来记录这个ad的具体位置等信息,这个adunit会属于某个频道(channel, 比如frame)内,这个channel会隶属于某个媒体(media, 比如优酷, 爱奇艺等, 区别于网站),所有的这几个模型都会有一个字段name。他们的从属关系如下所示:
ad → adgroup → campaign → account
↑↓ ↑↓
a d u n i t → channel → media
其中→ 为多对一的关系, ↑↓ 为多对多的关系。现在要以adgroup为中心,搜索campaign, adgroup, adunit, channel, media 中同时拥有keyword1或者keyword2的字段, 并且如果某个adgroup中被搜索到的字段越多,则排序越靠前。
0x02第一版实现
在搞清楚需求后,我很快就写出了如下语句
Adgroup.objects.filter(Q(name__contains=keyword1) | Q(name__contains=keyword2).filter(Q(adgroup__campaign__name__contains=keyword1) | Q(adgroup__campaign__name__contains=keyword2).filter(Q(adgroup__account__name__contains=keyword1) | Q(adgroup__account__name__contains=keyword2)
才搜索了三层, queryset就已经写得又臭又长,而且还扩展性很不好。当我写完这条语句并且测试后, 发现在几百条数据中所搜两个关键词却需要5秒以上,打开orm的debug模式后发现,orm生成了很多子查询。
0x03依赖于django-haystack搜索
django-haystack是django的一个模块化搜索解决方案, 后端可以插入 Solr, Elasticsearch, Whoosh, Xapian等,他会对指定的model的数据建立索引,从而实现快速搜索, 然而对Q对象的支持很不好。在看完文档之后并实现一遍之后,并不能实现
0x04分组与打分机制
考虑到被搜索到的字段越多,则排序越靠前。那么如果在某个model中被搜索到一个字段,则记一分,每多被搜索到一个字段,则加一分,那么排序就很好解决了, 只需要按照分数排序就好。同时, 由于adunit和adgroup为多对多关系,并不方便直接在adgroup上反向获取adunit, channel, media。那么我们可以分开两次搜索,第一次在adgroup上执行搜索与打分, 第二次以adunit为主,进行搜索和打分,最后以adgroup为维度,进行分组,把分数相同的放在一组并去重,就可以实现需求啦。优化后测试妹子再也不会吐槽我写的程序慢啦:)
代码实例如下:
cg_q_obj = _get_q_obj('campaign__name')
cg_range_q_obj = get_range_q_obj(cg_q_obj, search_range, 'schedulelist')
p_adgroup_args = filter(None, [cg_q_obj, cg_range_q_obj])
adgroup_list = AdGroup.objects.filter(
**p_adgroup).filter(*p_adgroup_args).select_related(
'campaign').prefetch_related('adunit_set')
adgroup_group = groupby(
adgroup_list, key=lambda x: _get_score(x.campaign.name))
queue = list()
for rank, adgroup_list in adgroup_group:
for adgroup in adgroup_list:
for adunit in adgroup.adunit_set.all():
adunit.rank = rank
queue.append(adunit)
if q_obj:
m_q_obj = _get_q_obj('channel__media__name')
c_q_obj = _get_q_obj('channel__name')
q_obj = q_obj | m_q_obj | c_q_obj
adunit_list = adunit_list.filter(q_obj).prefetch_related(
adgroup_prefetch)
adunit_group = groupby(
adunit_list, key=lambda x: get_adunit_score(_get_score, x))
for rank, adunit_list in adunit_group:
for adunit in adunit_list:
adunit.rank = rank
queue.append(adunit)
adunit_set = OrderedSet(sorted(queue, key=sort_func, reverse=True))
adunit_id_list = map(lambda x: x.id, adunit_set)
adunit_list = Adunit.objects.filter(id__in=adunit_id_list).select_related(
'channel', 'channel__media').prefetch_related(adgroup_prefetch)
def get_score(search_list, name):
return sum([1 if search in name else 0 for search in search_list])
def get_q_obj(search_list, name='name', reverse=False):
name = '__'.join((name, 'contains'))
q_obj = Q()
for search in search_list:
q_obj |= Q(**{name: search})
if reverse:
q_obj = [~Q(**{name: search}) for search in search_list]
return q_obj