Add fulltext search
This commit is contained in:
parent
aa1b627e6f
commit
36e505952c
7 changed files with 96 additions and 76 deletions
25
judge/migrations/0173_fulltext.py
Normal file
25
judge/migrations/0173_fulltext.py
Normal 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,
|
||||
),
|
||||
]
|
|
@ -24,6 +24,7 @@ from judge.models.submission import Submission
|
|||
from judge.ratings import rate_contest
|
||||
from judge.models.pagevote import PageVotable
|
||||
from judge.models.bookmark import Bookmarkable
|
||||
from judge.fulltext import SearchManager
|
||||
|
||||
__all__ = [
|
||||
"Contest",
|
||||
|
@ -309,6 +310,7 @@ class Contest(models.Model, PageVotable, Bookmarkable):
|
|||
comments = GenericRelation("Comment")
|
||||
pagevote = GenericRelation("PageVote")
|
||||
bookmark = GenericRelation("BookMark")
|
||||
objects = SearchManager(("key", "name"))
|
||||
|
||||
@cached_property
|
||||
def format_class(self):
|
||||
|
|
|
@ -107,9 +107,7 @@ class License(models.Model):
|
|||
|
||||
class TranslatedProblemQuerySet(SearchQuerySet):
|
||||
def __init__(self, **kwargs):
|
||||
super(TranslatedProblemQuerySet, self).__init__(
|
||||
("code", "name", "description"), **kwargs
|
||||
)
|
||||
super(TranslatedProblemQuerySet, self).__init__(("code", "name"), **kwargs)
|
||||
|
||||
def add_i18n_name(self, language):
|
||||
return self.annotate(
|
||||
|
|
|
@ -186,9 +186,16 @@ class ContestList(
|
|||
self.request.GET.getlist("contest")
|
||||
).strip()
|
||||
if query:
|
||||
queryset = queryset.filter(
|
||||
substr_queryset = queryset.filter(
|
||||
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:
|
||||
self.org_query = [self.request.organization.id]
|
||||
if self.show_orgs:
|
||||
|
|
|
@ -466,10 +466,14 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
manual_sort = frozenset(("name", "group", "solved", "type"))
|
||||
all_sorts = sql_sort | manual_sort
|
||||
default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
|
||||
default_sort = "-date"
|
||||
first_page_href = None
|
||||
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(
|
||||
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
|
||||
):
|
||||
|
@ -485,42 +489,46 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
)
|
||||
if not self.in_contest:
|
||||
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
sort_key = self.order.lstrip("-")
|
||||
if sort_key in self.sql_sort:
|
||||
queryset = queryset.order_by(self.order)
|
||||
elif sort_key == "name":
|
||||
queryset = queryset.order_by(self.order.replace("name", "i18n_name"))
|
||||
elif sort_key == "group":
|
||||
queryset = queryset.order_by(self.order + "__name")
|
||||
elif sort_key == "solved":
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
solved = user_completed_ids(profile)
|
||||
attempted = user_attempted_ids(profile)
|
||||
|
||||
def _solved_sort_order(problem):
|
||||
if problem.id in solved:
|
||||
return 1
|
||||
if problem.id in attempted:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=_solved_sort_order, reverse=self.order.startswith("-")
|
||||
)
|
||||
elif sort_key == "type":
|
||||
if self.show_types:
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=lambda problem: problem.types_list[0]
|
||||
if problem.types_list
|
||||
else "",
|
||||
reverse=self.order.startswith("-"),
|
||||
)
|
||||
queryset = self.sort_queryset(queryset)
|
||||
paginator.object_list = queryset
|
||||
return paginator
|
||||
|
||||
def sort_queryset(self, queryset):
|
||||
sort_key = self.order.lstrip("-")
|
||||
if sort_key in self.sql_sort:
|
||||
queryset = queryset.order_by(self.order)
|
||||
elif sort_key == "name":
|
||||
queryset = queryset.order_by(self.order.replace("name", "i18n_name"))
|
||||
elif sort_key == "group":
|
||||
queryset = queryset.order_by(self.order + "__name")
|
||||
elif sort_key == "solved":
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
solved = user_completed_ids(profile)
|
||||
attempted = user_attempted_ids(profile)
|
||||
|
||||
def _solved_sort_order(problem):
|
||||
if problem.id in solved:
|
||||
return 1
|
||||
if problem.id in attempted:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=_solved_sort_order, reverse=self.order.startswith("-")
|
||||
)
|
||||
elif sort_key == "type":
|
||||
if self.show_types:
|
||||
queryset = list(queryset)
|
||||
queryset.sort(
|
||||
key=lambda problem: problem.types_list[0]
|
||||
if problem.types_list
|
||||
else "",
|
||||
reverse=self.order.startswith("-"),
|
||||
)
|
||||
return queryset
|
||||
|
||||
@cached_property
|
||||
def profile(self):
|
||||
if not self.request.user.is_authenticated:
|
||||
|
@ -611,36 +619,28 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
self.request.GET.getlist("search")
|
||||
).strip()
|
||||
if query:
|
||||
if settings.ENABLE_FTS and self.full_text:
|
||||
queryset = queryset.search(query, queryset.BOOLEAN).extra(
|
||||
order_by=["-relevance"]
|
||||
substr_queryset = queryset.filter(
|
||||
Q(code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(
|
||||
translations__name__icontains=query,
|
||||
translations__language=self.request.LANGUAGE_CODE,
|
||||
)
|
||||
)
|
||||
if settings.ENABLE_FTS:
|
||||
queryset = (
|
||||
queryset.search(query, queryset.BOOLEAN).extra(
|
||||
order_by=["-relevance"]
|
||||
)
|
||||
| substr_queryset
|
||||
)
|
||||
else:
|
||||
queryset = queryset.filter(
|
||||
Q(code__icontains=query)
|
||||
| Q(name__icontains=query)
|
||||
| Q(
|
||||
translations__name__icontains=query,
|
||||
translations__language=self.request.LANGUAGE_CODE,
|
||||
)
|
||||
)
|
||||
queryset = substr_queryset
|
||||
self.prepoint_queryset = queryset
|
||||
if self.point_start is not None:
|
||||
queryset = queryset.filter(points__gte=self.point_start)
|
||||
if self.point_end is not None:
|
||||
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()
|
||||
|
||||
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["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["have_editorial"] = 0 if self.in_contest else int(self.have_editorial)
|
||||
context["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.full_text = self.GET_with_session(request, "full_text")
|
||||
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.search_query = None
|
||||
|
@ -816,7 +814,6 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
|
|||
"show_types",
|
||||
"full_text",
|
||||
"show_editorial",
|
||||
"have_editorial",
|
||||
"show_solved_only",
|
||||
)
|
||||
for key in to_update:
|
||||
|
@ -862,9 +859,6 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
self.show_types = 1
|
||||
queryset = super(ProblemFeed, self).get_queryset()
|
||||
|
||||
if self.have_editorial:
|
||||
queryset = queryset.filter(has_public_editorial=1)
|
||||
|
||||
user = self.request.profile
|
||||
|
||||
if self.feed_type == "new":
|
||||
|
@ -886,6 +880,8 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
.order_by("?")
|
||||
.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:
|
||||
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
|
||||
|
||||
|
@ -946,7 +942,6 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
context["title"] = self.title
|
||||
context["feed_type"] = self.feed_type
|
||||
context["has_show_editorial_option"] = False
|
||||
context["has_have_editorial_option"] = False
|
||||
|
||||
return context
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
|
||||
$('#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();
|
||||
($('<form>').attr('action', window.location.pathname + '?' + form_serialize())
|
||||
.append($('<input>').attr('type', 'hidden').attr('name', 'csrfmiddlewaretoken')
|
||||
|
|
|
@ -34,13 +34,6 @@
|
|||
<label for="show_editorial">{{ _('Show editorial') }}</label>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<div class="filter-form-group">
|
||||
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label>
|
||||
|
|
Loading…
Reference in a new issue