summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHannes Ljungberg <hannes@5monkeys.se>2020-03-20 22:01:26 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-03-23 11:00:55 +0100
commit0b51a4f8946178daf469bec4cbedbc02a23cf814 (patch)
tree11aefd7c5b20687255010004e466a5a5af1bcaae
parent4ed534758cb6a11df9f49baddecca5a6cdda9311 (diff)
downloaddjango-0b51a4f8946178daf469bec4cbedbc02a23cf814.tar.gz
Fixed #28194 -- Added support for normalization and cover density to SearchRank.
-rw-r--r--django/contrib/postgres/search.py11
-rw-r--r--docs/ref/contrib/postgres/search.txt28
-rw-r--r--docs/releases/3.1.txt8
-rw-r--r--tests/postgres_tests/test_search.py62
4 files changed, 106 insertions, 3 deletions
diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py
index 2b2ae0c321..f1640d85ba 100644
--- a/django/contrib/postgres/search.py
+++ b/django/contrib/postgres/search.py
@@ -208,7 +208,10 @@ class SearchRank(Func):
function = 'ts_rank'
output_field = FloatField()
- def __init__(self, vector, query, weights=None):
+ def __init__(
+ self, vector, query, weights=None, normalization=None,
+ cover_density=False,
+ ):
if not hasattr(vector, 'resolve_expression'):
vector = SearchVector(vector)
if not hasattr(query, 'resolve_expression'):
@@ -218,6 +221,12 @@ class SearchRank(Func):
if not hasattr(weights, 'resolve_expression'):
weights = Value(weights)
expressions = (weights,) + expressions
+ if normalization is not None:
+ if not hasattr(normalization, 'resolve_expression'):
+ normalization = Value(normalization)
+ expressions += (normalization,)
+ if cover_density:
+ self.function = 'ts_rank_cd'
super().__init__(*expressions)
diff --git a/docs/ref/contrib/postgres/search.txt b/docs/ref/contrib/postgres/search.txt
index 65d54cfd8d..f00bddbee4 100644
--- a/docs/ref/contrib/postgres/search.txt
+++ b/docs/ref/contrib/postgres/search.txt
@@ -118,7 +118,7 @@ See :ref:`postgresql-fts-search-configuration` for an explanation of the
``SearchRank``
==============
-.. class:: SearchRank(vector, query, weights=None)
+.. class:: SearchRank(vector, query, weights=None, normalization=None, cover_density=False)
So far, we've returned the results for which any match between the vector and
the query are possible. It's likely you may wish to order the results by some
@@ -137,6 +137,32 @@ order by relevancy::
See :ref:`postgresql-fts-weighting-queries` for an explanation of the
``weights`` parameter.
+Set the ``cover_density`` parameter to ``True`` to enable the cover density
+ranking, which means that the proximity of matching query terms is taken into
+account.
+
+Provide an integer to the ``normalization`` parameter to control rank
+normalization. This integer is a bit mask, so you can combine multiple
+behaviors::
+
+ >>> from django.db.models import Value
+ >>> Entry.objects.annotate(
+ ... rank=SearchRank(
+ ... vector,
+ ... query,
+ ... normalization=Value(2).bitor(Value(4)),
+ ... )
+ ... )
+
+The PostgreSQL documentation has more details about `different rank
+normalization options`_.
+
+.. _different rank normalization options: https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING
+
+.. versionadded:: 3.1
+
+ The ``normalization`` and ``cover_density`` parameters were added.
+
``SearchHeadline``
==================
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index d9c0a5db11..9961aebbab 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -160,6 +160,14 @@ Minor features
* :lookup:`search` lookup now supports query expressions.
+* The new ``cover_density`` parameter of
+ :class:`~django.contrib.postgres.search.SearchRank` allows ranking by cover
+ density.
+
+* The new ``normalization`` parameter of
+ :class:`~django.contrib.postgres.search.SearchRank` allows rank
+ normalization.
+
:mod:`django.contrib.redirects`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/postgres_tests/test_search.py b/tests/postgres_tests/test_search.py
index b40d672920..f6c3031bd4 100644
--- a/tests/postgres_tests/test_search.py
+++ b/tests/postgres_tests/test_search.py
@@ -6,7 +6,7 @@ All text copyright Python (Monty) Pictures. Thanks to sacred-texts.com for the
transcript.
"""
from django.db import connection
-from django.db.models import F
+from django.db.models import F, Value
from django.test import modify_settings, skipUnlessDBFeature
from . import PostgreSQLSimpleTestCase, PostgreSQLTestCase
@@ -449,6 +449,66 @@ class TestRankingAndWeights(GrailTestData, PostgreSQLTestCase):
).filter(rank__gt=0.3)
self.assertSequenceEqual(searched, [self.verse0])
+ def test_cover_density_ranking(self):
+ not_dense_verse = Line.objects.create(
+ scene=self.robin,
+ character=self.minstrel,
+ dialogue=(
+ 'Bravely taking to his feet, he beat a very brave retreat. '
+ 'A brave retreat brave Sir Robin.'
+ )
+ )
+ searched = Line.objects.filter(character=self.minstrel).annotate(
+ rank=SearchRank(
+ SearchVector('dialogue'),
+ SearchQuery('brave robin'),
+ cover_density=True,
+ ),
+ ).order_by('rank', '-pk')
+ self.assertSequenceEqual(
+ searched,
+ [self.verse2, not_dense_verse, self.verse1, self.verse0],
+ )
+
+ def test_ranking_with_normalization(self):
+ short_verse = Line.objects.create(
+ scene=self.robin,
+ character=self.minstrel,
+ dialogue='A brave retreat brave Sir Robin.',
+ )
+ searched = Line.objects.filter(character=self.minstrel).annotate(
+ rank=SearchRank(
+ SearchVector('dialogue'),
+ SearchQuery('brave sir robin'),
+ # Divide the rank by the document length.
+ normalization=2,
+ ),
+ ).order_by('rank')
+ self.assertSequenceEqual(
+ searched,
+ [self.verse2, self.verse1, self.verse0, short_verse],
+ )
+
+ def test_ranking_with_masked_normalization(self):
+ short_verse = Line.objects.create(
+ scene=self.robin,
+ character=self.minstrel,
+ dialogue='A brave retreat brave Sir Robin.',
+ )
+ searched = Line.objects.filter(character=self.minstrel).annotate(
+ rank=SearchRank(
+ SearchVector('dialogue'),
+ SearchQuery('brave sir robin'),
+ # Divide the rank by the document length and by the number of
+ # unique words in document.
+ normalization=Value(2).bitor(Value(8)),
+ ),
+ ).order_by('rank')
+ self.assertSequenceEqual(
+ searched,
+ [self.verse2, self.verse1, self.verse0, short_verse],
+ )
+
class SearchVectorIndexTests(PostgreSQLTestCase):
def test_search_vector_index(self):