如何仅对Django Postgres全文搜索分页结果执行SearchHeadline优化性能
实现方案
核心逻辑是拆分查询为两个阶段:第一阶段仅执行过滤、排序、去重、分页,拿到当前页的少量结果,第二阶段仅对这部分结果计算高亮,避免全量执行高开销的ts_headline。
方式1:子查询嵌套(最贴近你需要的SQL结构)
Django 3.2及以上版本可以直接通过子查询嵌套生成你预期的SQL结构:
from django.contrib.postgres.search import SearchQuery, SearchRank, SearchHeadline from django.db.models import F, Q, Func # 1. 构造基础查询:仅做过滤、排序、分页,不计算高亮 search_term = "hearing" search_query = SearchQuery(search_term, config="public.bb") # 基础查询:保留你原有的所有过滤、rank计算、排序、去重逻辑,仅移除高亮相关注解 base_qs = ( DocketsDocument.objects.filter( search_vector__contains=search_query, # 其余过滤条件原样保留 ) .annotate( rank=SearchRank(F("search_vector"), search_query) # 你原有的多表关联rank加权计算逻辑原样保留 ) .order_by("-rank", "id") .distinct() .values("id", "title", "content") # 仅查询生成高亮必需的字段 [:25] # 分页逻辑,使用Paginator时替换为对应页的切片即可 ) # 2. 外层查询仅对分页后的结果计算高亮 final_qs = ( DocketsDocument.objects.filter(id__in=base_qs.values("id")) .annotate( title_snippet=SearchHeadline( "title", search_query, config="public.bb", highlight_all=True, start_sel="<mark>", stop_sel="</mark>", ), content_snippet=SearchHeadline( Func(F("content"), function="array_to_string", template="%(function)s(%(expressions)s, ' ', '')"), search_query, config="public.bb", max_fragments=5, start_sel="<mark>", stop_sel="</mark>", ) ) # 按基础查询的rank排序,保证结果顺序正确 .order_by("-rank", "id") )
生成的SQL会和你手动优化的结构完全一致,ts_headline仅会在外层对25条分页结果执行。
方式2:先取分页结果再单独计算高亮(更简单易维护)
如果不需要严格生成嵌套子查询SQL,也可以先拿到当前页的25条记录实例,再单独为这部分记录生成高亮,性能完全相同:
# 第一步:取当前页的基础数据,无高亮 paginator = Paginator(base_qs, 25) current_page = paginator.page(page_num) page_items = list(current_page.object_list) # 第二步:仅对当前页的25条数据生成高亮 for item in page_items: item.title_snippet = SearchHeadline( item.title, search_query, config="public.bb", highlight_all=True, start_sel="<mark>", stop_sel="</mark>", ) # 同理生成content_snippet
额外优化:消除DISTINCT提升性能
你提到DISTINCT是性能瓶颈之一,这是因为左连接关联多个表时会产生重复行,你可以将关联表的匹配逻辑改为Exists子查询,避免不必要的JOIN,自然消除重复行:
from django.db.models import Exists, OuterRef # 构造关联表匹配的Exists子查询 attachment_match = Exists( DocketsAttachment.objects.filter( main_document_id=OuterRef("id"), search_vector__contains=search_query ) ) table_match = Exists( DocketsTable.objects.filter( object_id=OuterRef("id"), content_type_id=8, search_vector__contains=search_query ) ) # 基础查询过滤条件替换为Exists,无需JOIN,无需DISTINCT base_qs = DocketsDocument.objects.filter( Q(search_vector__contains=search_query) | attachment_match | table_match, # 其余过滤条件保留 ) # 后续逻辑不变,可直接删除.distinct()调用
内容的提问来源于stack exchange,提问作者Ilya Voytov
相关产品推荐
相关产品推荐

