mirror of https://github.com/django/django.git
Fixed #27899 -- Added support for phrase/raw searching in SearchQuery.
Thanks Tim Graham, Nick Pope, and Claude Paroz for contribution and review.
This commit is contained in:
parent
c5e450ac95
commit
f5e347a640
1
AUTHORS
1
AUTHORS
|
@ -312,6 +312,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Graham Carlyle <graham.carlyle@maplecroft.net>
|
||||
Grant Jenks <contact@grantjenks.com>
|
||||
Greg Chapple <gregchapple1@gmail.com>
|
||||
Gregor Allensworth <greg.allensworth@gmail.com>
|
||||
Gregor Müllegger <gregor@muellegger.de>
|
||||
Grigory Fateyev <greg@dial.com.ru>
|
||||
Grzegorz Ślusarek <grzegorz.slusarek@gmail.com>
|
||||
|
|
|
@ -123,10 +123,18 @@ class SearchQueryCombinable:
|
|||
|
||||
class SearchQuery(SearchQueryCombinable, Value):
|
||||
output_field = SearchQueryField()
|
||||
SEARCH_TYPES = {
|
||||
'plain': 'plainto_tsquery',
|
||||
'phrase': 'phraseto_tsquery',
|
||||
'raw': 'to_tsquery',
|
||||
}
|
||||
|
||||
def __init__(self, value, output_field=None, *, config=None, invert=False):
|
||||
def __init__(self, value, output_field=None, *, config=None, invert=False, search_type='plain'):
|
||||
self.config = config
|
||||
self.invert = invert
|
||||
if search_type not in self.SEARCH_TYPES:
|
||||
raise ValueError("Unknown search_type argument '%s'." % search_type)
|
||||
self.search_type = search_type
|
||||
super().__init__(value, output_field=output_field)
|
||||
|
||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
||||
|
@ -140,12 +148,13 @@ class SearchQuery(SearchQueryCombinable, Value):
|
|||
|
||||
def as_sql(self, compiler, connection):
|
||||
params = [self.value]
|
||||
function = self.SEARCH_TYPES[self.search_type]
|
||||
if self.config:
|
||||
config_sql, config_params = compiler.compile(self.config)
|
||||
template = 'plainto_tsquery({}::regconfig, %s)'.format(config_sql)
|
||||
template = '{}({}::regconfig, %s)'.format(function, config_sql)
|
||||
params = config_params + [self.value]
|
||||
else:
|
||||
template = 'plainto_tsquery(%s)'
|
||||
template = '{}(%s)'.format(function)
|
||||
if self.invert:
|
||||
template = '!!({})'.format(template)
|
||||
return template, params
|
||||
|
|
|
@ -57,6 +57,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
def is_postgresql_9_5(self):
|
||||
return self.connection.pg_version >= 90500
|
||||
|
||||
@cached_property
|
||||
def is_postgresql_9_6(self):
|
||||
return self.connection.pg_version >= 90600
|
||||
|
||||
@cached_property
|
||||
def is_postgresql_10(self):
|
||||
return self.connection.pg_version >= 100000
|
||||
|
@ -67,3 +71,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
has_brin_autosummarize = is_postgresql_10
|
||||
has_gin_pending_list_limit = is_postgresql_9_5
|
||||
supports_ignore_conflicts = is_postgresql_9_5
|
||||
has_phraseto_tsquery = is_postgresql_9_6
|
||||
|
|
|
@ -70,13 +70,28 @@ and ``weight`` parameters.
|
|||
``SearchQuery``
|
||||
===============
|
||||
|
||||
.. class:: SearchQuery(value, config=None)
|
||||
.. class:: SearchQuery(value, config=None, search_type='plain')
|
||||
|
||||
``SearchQuery`` translates the terms the user provides into a search query
|
||||
object that the database compares to a search vector. By default, all the words
|
||||
the user provides are passed through the stemming algorithms, and then it
|
||||
looks for matches for all of the resulting terms.
|
||||
|
||||
If ``search_type`` is ``'plain'``, which is the default, the terms are treated
|
||||
as separate keywords. If ``search_type`` is ``'phrase'``, the terms are treated
|
||||
as a single phrase. If ``search_type`` is ``'raw'``, then you can provide a
|
||||
formatted search query with terms and operators. Read PostgreSQL's `Full Text
|
||||
Search docs`_ to learn about differences and syntax. Examples:
|
||||
|
||||
.. _Full Text Search docs: https://www.postgresql.org/docs/current/static/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
|
||||
|
||||
>>> from django.contrib.postgres.search import SearchQuery
|
||||
>>> SearchQuery('red tomato') # two keywords
|
||||
>>> SearchQuery('tomato red') # same results as above
|
||||
>>> SearchQuery('red tomato', search_type='phrase') # a phrase
|
||||
>>> SearchQuery('tomato red', search_type='phrase') # a different phrase
|
||||
>>> SearchQuery("'tomato' & ('red' | 'green')", search_type='raw') # boolean operators
|
||||
|
||||
``SearchQuery`` terms can be combined logically to provide more flexibility::
|
||||
|
||||
>>> from django.contrib.postgres.search import SearchQuery
|
||||
|
@ -87,6 +102,10 @@ looks for matches for all of the resulting terms.
|
|||
See :ref:`postgresql-fts-search-configuration` for an explanation of the
|
||||
``config`` parameter.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
The `search_type` parameter was added.
|
||||
|
||||
``SearchRank``
|
||||
==============
|
||||
|
||||
|
|
|
@ -90,6 +90,10 @@ Minor features
|
|||
* :class:`~django.contrib.postgres.indexes.BrinIndex` now has the
|
||||
``autosummarize`` parameter.
|
||||
|
||||
* The new ``search_type`` parameter of
|
||||
:class:`~django.contrib.postgres.search.SearchQuery` allows searching for
|
||||
a phrase or raw expression.
|
||||
|
||||
:mod:`django.contrib.redirects`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.contrib.postgres.search import (
|
|||
SearchQuery, SearchRank, SearchVector,
|
||||
)
|
||||
from django.db.models import F
|
||||
from django.test import SimpleTestCase, modify_settings
|
||||
from django.test import SimpleTestCase, modify_settings, skipUnlessDBFeature
|
||||
|
||||
from . import PostgreSQLTestCase
|
||||
from .models import Character, Line, Scene
|
||||
|
@ -75,7 +75,7 @@ class GrailTestData:
|
|||
cls.french = Line.objects.create(
|
||||
scene=trojan_rabbit,
|
||||
character=guards,
|
||||
dialogue='Oh. Un cadeau. Oui oui.',
|
||||
dialogue='Oh. Un beau cadeau. Oui oui.',
|
||||
dialogue_config='french',
|
||||
)
|
||||
|
||||
|
@ -161,6 +161,46 @@ class MultipleFieldsTest(GrailTestData, PostgreSQLTestCase):
|
|||
).filter(search=str(self.crowd.id))
|
||||
self.assertSequenceEqual(searched, [self.crowd])
|
||||
|
||||
@skipUnlessDBFeature('has_phraseto_tsquery')
|
||||
def test_phrase_search(self):
|
||||
line_qs = Line.objects.annotate(search=SearchVector('dialogue'))
|
||||
searched = line_qs.filter(search=SearchQuery('burned body his away', search_type='phrase'))
|
||||
self.assertSequenceEqual(searched, [])
|
||||
searched = line_qs.filter(search=SearchQuery('his body burned away', search_type='phrase'))
|
||||
self.assertSequenceEqual(searched, [self.verse1])
|
||||
|
||||
@skipUnlessDBFeature('has_phraseto_tsquery')
|
||||
def test_phrase_search_with_config(self):
|
||||
line_qs = Line.objects.annotate(
|
||||
search=SearchVector('scene__setting', 'dialogue', config='french'),
|
||||
)
|
||||
searched = line_qs.filter(
|
||||
search=SearchQuery('cadeau beau un', search_type='phrase', config='french'),
|
||||
)
|
||||
self.assertSequenceEqual(searched, [])
|
||||
searched = line_qs.filter(
|
||||
search=SearchQuery('un beau cadeau', search_type='phrase', config='french'),
|
||||
)
|
||||
self.assertSequenceEqual(searched, [self.french])
|
||||
|
||||
def test_raw_search(self):
|
||||
line_qs = Line.objects.annotate(search=SearchVector('dialogue'))
|
||||
searched = line_qs.filter(search=SearchQuery('Robin', search_type='raw'))
|
||||
self.assertEqual(set(searched), {self.verse0, self.verse1})
|
||||
searched = line_qs.filter(search=SearchQuery("Robin & !'Camelot'", search_type='raw'))
|
||||
self.assertSequenceEqual(searched, [self.verse1])
|
||||
|
||||
def test_raw_search_with_config(self):
|
||||
line_qs = Line.objects.annotate(search=SearchVector('dialogue', config='french'))
|
||||
searched = line_qs.filter(
|
||||
search=SearchQuery("'cadeaux' & 'beaux'", search_type='raw', config='french'),
|
||||
)
|
||||
self.assertSequenceEqual(searched, [self.french])
|
||||
|
||||
def test_bad_search_type(self):
|
||||
with self.assertRaisesMessage(ValueError, "Unknown search_type argument 'foo'."):
|
||||
SearchQuery('kneecaps', search_type='foo')
|
||||
|
||||
def test_config_query_explicit(self):
|
||||
searched = Line.objects.annotate(
|
||||
search=SearchVector('scene__setting', 'dialogue', config='french'),
|
||||
|
|
Loading…
Reference in New Issue