Add fulltext search

This commit is contained in:
cuom1999 2023-10-14 14:56:22 -05:00
parent aa1b627e6f
commit 36e505952c
7 changed files with 96 additions and 76 deletions

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.18 on 2023-10-14 00:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("judge", "0172_index_rating"),
]
operations = [
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS code_name_index ON judge_problem (code, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS key_name_index ON judge_contest (`key`, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
]

View file

@ -24,6 +24,7 @@ from judge.models.submission import Submission
from judge.ratings import rate_contest from judge.ratings import rate_contest
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable from judge.models.bookmark import Bookmarkable
from judge.fulltext import SearchManager
__all__ = [ __all__ = [
"Contest", "Contest",
@ -309,6 +310,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
comments = GenericRelation("Comment") comments = GenericRelation("Comment")
pagevote = GenericRelation("PageVote") pagevote = GenericRelation("PageVote")
bookmark = GenericRelation("BookMark") bookmark = GenericRelation("BookMark")
objects = SearchManager(("key", "name"))
@cached_property @cached_property
def format_class(self): def format_class(self):

View file

@ -107,9 +107,7 @@ class License(models.Model):
class TranslatedProblemQuerySet(SearchQuerySet): class TranslatedProblemQuerySet(SearchQuerySet):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(TranslatedProblemQuerySet, self).__init__( super(TranslatedProblemQuerySet, self).__init__(("code", "name"), **kwargs)
("code", "name", "description"), **kwargs
)
def add_i18n_name(self, language): def add_i18n_name(self, language):
return self.annotate( return self.annotate(

View file

@ -186,9 +186,16 @@ class ContestList(
self.request.GET.getlist("contest") self.request.GET.getlist("contest")
).strip() ).strip()
if query: if query:
queryset = queryset.filter( substr_queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query) Q(key__icontains=query) | Q(name__icontains=query)
) )
if settings.ENABLE_FTS:
queryset = (
queryset.search(query).extra(order_by=["-relevance"])
| substr_queryset
)
else:
queryset = substr_queryset
if not self.org_query and self.request.organization: if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id] self.org_query = [self.request.organization.id]
if self.show_orgs: if self.show_orgs:

View file

@ -466,10 +466,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
manual_sort = frozenset(("name", "group", "solved", "type")) manual_sort = frozenset(("name", "group", "solved", "type"))
all_sorts = sql_sort | manual_sort all_sorts = sql_sort | manual_sort
default_desc = frozenset(("date", "points", "ac_rate", "user_count")) default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
default_sort = "-date"
first_page_href = None first_page_href = None
filter_organization = False filter_organization = False
def get_default_sort_order(self, request):
if "search" in request.GET and settings.ENABLE_FTS:
return "-relevance"
return "-date"
def get_paginator( def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
): ):
@ -485,6 +489,11 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
) )
if not self.in_contest: if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
queryset = self.sort_queryset(queryset)
paginator.object_list = queryset
return paginator
def sort_queryset(self, queryset):
sort_key = self.order.lstrip("-") sort_key = self.order.lstrip("-")
if sort_key in self.sql_sort: if sort_key in self.sql_sort:
queryset = queryset.order_by(self.order) queryset = queryset.order_by(self.order)
@ -518,8 +527,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
else "", else "",
reverse=self.order.startswith("-"), reverse=self.order.startswith("-"),
) )
paginator.object_list = queryset return queryset
return paginator
@cached_property @cached_property
def profile(self): def profile(self):
@ -611,12 +619,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
self.request.GET.getlist("search") self.request.GET.getlist("search")
).strip() ).strip()
if query: if query:
if settings.ENABLE_FTS and self.full_text: substr_queryset = queryset.filter(
queryset = queryset.search(query, queryset.BOOLEAN).extra(
order_by=["-relevance"]
)
else:
queryset = queryset.filter(
Q(code__icontains=query) Q(code__icontains=query)
| Q(name__icontains=query) | Q(name__icontains=query)
| Q( | Q(
@ -624,23 +627,20 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
translations__language=self.request.LANGUAGE_CODE, translations__language=self.request.LANGUAGE_CODE,
) )
) )
if settings.ENABLE_FTS:
queryset = (
queryset.search(query, queryset.BOOLEAN).extra(
order_by=["-relevance"]
)
| substr_queryset
)
else:
queryset = substr_queryset
self.prepoint_queryset = queryset self.prepoint_queryset = queryset
if self.point_start is not None: if self.point_start is not None:
queryset = queryset.filter(points__gte=self.point_start) queryset = queryset.filter(points__gte=self.point_start)
if self.point_end is not None: if self.point_end is not None:
queryset = queryset.filter(points__lte=self.point_end) queryset = queryset.filter(points__lte=self.point_end)
queryset = queryset.annotate(
has_public_editorial=Case(
When(
solution__is_public=True,
solution__publish_on__lte=timezone.now(),
then=True,
),
default=False,
output_field=BooleanField(),
)
)
return queryset.distinct() return queryset.distinct()
def get_queryset(self): def get_queryset(self):
@ -658,7 +658,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
context["show_types"] = 0 if self.in_contest else int(self.show_types) context["show_types"] = 0 if self.in_contest else int(self.show_types)
context["full_text"] = 0 if self.in_contest else int(self.full_text) context["full_text"] = 0 if self.in_contest else int(self.full_text)
context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial) context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial)
context["have_editorial"] = 0 if self.in_contest else int(self.have_editorial)
context["show_solved_only"] = ( context["show_solved_only"] = (
0 if self.in_contest else int(self.show_solved_only) 0 if self.in_contest else int(self.show_solved_only)
) )
@ -768,7 +767,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
self.show_types = self.GET_with_session(request, "show_types") self.show_types = self.GET_with_session(request, "show_types")
self.full_text = self.GET_with_session(request, "full_text") self.full_text = self.GET_with_session(request, "full_text")
self.show_editorial = self.GET_with_session(request, "show_editorial") self.show_editorial = self.GET_with_session(request, "show_editorial")
self.have_editorial = self.GET_with_session(request, "have_editorial")
self.show_solved_only = self.GET_with_session(request, "show_solved_only") self.show_solved_only = self.GET_with_session(request, "show_solved_only")
self.search_query = None self.search_query = None
@ -816,7 +814,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
"show_types", "show_types",
"full_text", "full_text",
"show_editorial", "show_editorial",
"have_editorial",
"show_solved_only", "show_solved_only",
) )
for key in to_update: for key in to_update:
@ -862,9 +859,6 @@ class ProblemFeed(ProblemList, FeedView):
self.show_types = 1 self.show_types = 1
queryset = super(ProblemFeed, self).get_queryset() queryset = super(ProblemFeed, self).get_queryset()
if self.have_editorial:
queryset = queryset.filter(has_public_editorial=1)
user = self.request.profile user = self.request.profile
if self.feed_type == "new": if self.feed_type == "new":
@ -886,6 +880,8 @@ class ProblemFeed(ProblemList, FeedView):
.order_by("?") .order_by("?")
.add_i18n_name(self.request.LANGUAGE_CODE) .add_i18n_name(self.request.LANGUAGE_CODE)
) )
if "search" in self.request.GET:
return queryset.add_i18n_name(self.request.LANGUAGE_CODE)
if not settings.ML_OUTPUT_PATH or not user: if not settings.ML_OUTPUT_PATH or not user:
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE) return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
@ -946,7 +942,6 @@ class ProblemFeed(ProblemList, FeedView):
context["title"] = self.title context["title"] = self.title
context["feed_type"] = self.feed_type context["feed_type"] = self.feed_type
context["has_show_editorial_option"] = False context["has_show_editorial_option"] = False
context["has_have_editorial_option"] = False
return context return context

View file

@ -114,7 +114,7 @@
$('#go').click(clean_submit); $('#go').click(clean_submit);
$('input#full_text, input#hide_solved, input#show_types, input#show_editorial, input#have_editorial, input#show_solved_only').click(function () { $('input#full_text, input#hide_solved, input#show_types, input#have_editorial, input#show_solved_only').click(function () {
prep_form(); prep_form();
($('<form>').attr('action', window.location.pathname + '?' + form_serialize()) ($('<form>').attr('action', window.location.pathname + '?' + form_serialize())
.append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken') .append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken')

View file

@ -34,13 +34,6 @@
<label for="show_editorial">{{ _('Show editorial') }}</label> <label for="show_editorial">{{ _('Show editorial') }}</label>
</div> </div>
{% endif %} {% endif %}
{% if has_have_editorial_option %}
<div>
<input id="have_editorial" type="checkbox" name="have_editorial" value="1"
{% if have_editorial %} checked{% endif %}>
<label for="have_editorial">{{ _('Have editorial') }}</label>
</div>
{% endif %}
{% if organizations %} {% if organizations %}
<div class="filter-form-group"> <div class="filter-form-group">
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label> <label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label>