Fix: support for custom field ordering w advanced search

This commit is contained in:
shamoon 2025-11-16 09:46:28 -08:00
parent 533b64cb70
commit 35fc33b7a0
3 changed files with 133 additions and 6 deletions

View file

@ -287,15 +287,79 @@ class DelayedQuery:
self.first_score = None
self.filter_queryset = filter_queryset
self.suggested_correction = None
self.manual_hits: list | None = None
ordering = self.query_params.get("ordering")
ordering_check = ordering.lstrip("-") if ordering else None
self._manual_sort_requested = (
ordering_check.startswith("custom_field_") if ordering_check else False
)
def __len__(self) -> int:
manual_hits = self._get_manual_hits()
if manual_hits is not None:
return len(manual_hits)
page = self[0:1]
return len(page)
def _get_manual_hits(self):
if not self._manual_sort_requested:
return None
if self.manual_hits is None:
self.manual_hits = self._build_manual_hits()
return self.manual_hits
def _build_manual_hits(self):
q, mask, suggested_correction = self._get_query()
self.suggested_correction = suggested_correction
results = self.searcher.search(
q,
mask=mask,
filter=MappedDocIdSet(self.filter_queryset, self.searcher.ixreader),
limit=None,
)
results.fragmenter = highlight.ContextFragmenter(surround=50)
results.formatter = HtmlFormatter(tagname="span", between=" ... ")
if not self.first_score and len(results) > 0:
self.first_score = results[0].score
if self.first_score:
results.top_n = list(
map(
lambda hit: (
(hit[0] / self.first_score) if self.first_score else None,
hit[1],
),
results.top_n,
),
)
hits_by_id = {hit["id"]: hit for hit in results}
matching_ids = list(hits_by_id.keys())
ordered_ids = list(
self.filter_queryset.filter(id__in=matching_ids).values_list(
"id",
flat=True,
),
)
ordered_ids = list(dict.fromkeys(ordered_ids))
return [hits_by_id[_id] for _id in ordered_ids if _id in hits_by_id]
def __getitem__(self, item):
if item.start in self.saved_results:
return self.saved_results[item.start]
manual_hits = self._get_manual_hits()
if manual_hits is not None:
start = 0 if item.start is None else item.start
stop = item.stop
page = manual_hits[start:stop] if stop is not None else manual_hits[start:]
return page
q, mask, suggested_correction = self._get_query()
self.suggested_correction = suggested_correction
sortedby, reverse = self._get_query_sortedby()

View file

@ -89,6 +89,65 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
self.assertEqual(len(results), 0)
self.assertCountEqual(response.data["all"], [])
def test_search_custom_field_ordering(self):
custom_field = CustomField.objects.create(
name="Sortable field",
data_type=CustomField.FieldDataType.INT,
)
d1 = Document.objects.create(
title="first",
content="match",
checksum="A1",
)
d2 = Document.objects.create(
title="second",
content="match",
checksum="B2",
)
d3 = Document.objects.create(
title="third",
content="match",
checksum="C3",
)
CustomFieldInstance.objects.create(
document=d1,
field=custom_field,
value_int=30,
)
CustomFieldInstance.objects.create(
document=d2,
field=custom_field,
value_int=10,
)
CustomFieldInstance.objects.create(
document=d3,
field=custom_field,
value_int=20,
)
with AsyncWriter(index.open_index()) as writer:
index.update_document(writer, d1)
index.update_document(writer, d2)
index.update_document(writer, d3)
response = self.client.get(
f"/api/documents/?query=match&ordering=custom_field_{custom_field.pk}",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[doc["id"] for doc in response.data["results"]],
[d2.id, d3.id, d1.id],
)
response = self.client.get(
f"/api/documents/?query=match&ordering=-custom_field_{custom_field.pk}",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
[doc["id"] for doc in response.data["results"]],
[d1.id, d3.id, d2.id],
)
def test_search_multi_page(self):
with AsyncWriter(index.open_index()) as writer:
for i in range(55):

View file

@ -70,18 +70,22 @@ class StandardPagination(PageNumberPagination):
def get_all_result_ids(self):
query = self.page.paginator.object_list
if isinstance(query, DelayedQuery):
try:
manual_hits = getattr(query, "manual_hits", None)
if manual_hits is not None:
ids = [hit["id"] for hit in manual_hits]
else:
first_page = query.saved_results.get(0)
if not first_page:
return []
ids = [
query.searcher.ixreader.stored_fields(
doc_num,
)["id"]
for doc_num in query.saved_results.get(0).results.docs()
for doc_num in first_page.results.docs()
]
except Exception:
pass
else:
ids = self.page.paginator.object_list.values_list("pk", flat=True)
return ids
ids = list(self.page.paginator.object_list.values_list("pk", flat=True))
return list(ids)
def get_paginated_response_schema(self, schema):
response_schema = super().get_paginated_response_schema(schema)