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