mirror of
https://github.com/django/django.git
synced 2025-01-03 06:55:47 +00:00
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>
|
Graham Carlyle <graham.carlyle@maplecroft.net>
|
||||||
Grant Jenks <contact@grantjenks.com>
|
Grant Jenks <contact@grantjenks.com>
|
||||||
Greg Chapple <gregchapple1@gmail.com>
|
Greg Chapple <gregchapple1@gmail.com>
|
||||||
|
Gregor Allensworth <greg.allensworth@gmail.com>
|
||||||
Gregor Müllegger <gregor@muellegger.de>
|
Gregor Müllegger <gregor@muellegger.de>
|
||||||
Grigory Fateyev <greg@dial.com.ru>
|
Grigory Fateyev <greg@dial.com.ru>
|
||||||
Grzegorz Ślusarek <grzegorz.slusarek@gmail.com>
|
Grzegorz Ślusarek <grzegorz.slusarek@gmail.com>
|
||||||
|
@ -123,10 +123,18 @@ class SearchQueryCombinable:
|
|||||||
|
|
||||||
class SearchQuery(SearchQueryCombinable, Value):
|
class SearchQuery(SearchQueryCombinable, Value):
|
||||||
output_field = SearchQueryField()
|
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.config = config
|
||||||
self.invert = invert
|
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)
|
super().__init__(value, output_field=output_field)
|
||||||
|
|
||||||
def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False):
|
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):
|
def as_sql(self, compiler, connection):
|
||||||
params = [self.value]
|
params = [self.value]
|
||||||
|
function = self.SEARCH_TYPES[self.search_type]
|
||||||
if self.config:
|
if self.config:
|
||||||
config_sql, config_params = compiler.compile(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]
|
params = config_params + [self.value]
|
||||||
else:
|
else:
|
||||||
template = 'plainto_tsquery(%s)'
|
template = '{}(%s)'.format(function)
|
||||||
if self.invert:
|
if self.invert:
|
||||||
template = '!!({})'.format(template)
|
template = '!!({})'.format(template)
|
||||||
return template, params
|
return template, params
|
||||||
|
@ -57,6 +57,10 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
def is_postgresql_9_5(self):
|
def is_postgresql_9_5(self):
|
||||||
return self.connection.pg_version >= 90500
|
return self.connection.pg_version >= 90500
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_postgresql_9_6(self):
|
||||||
|
return self.connection.pg_version >= 90600
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def is_postgresql_10(self):
|
def is_postgresql_10(self):
|
||||||
return self.connection.pg_version >= 100000
|
return self.connection.pg_version >= 100000
|
||||||
@ -67,3 +71,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
has_brin_autosummarize = is_postgresql_10
|
has_brin_autosummarize = is_postgresql_10
|
||||||
has_gin_pending_list_limit = is_postgresql_9_5
|
has_gin_pending_list_limit = is_postgresql_9_5
|
||||||
supports_ignore_conflicts = 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``
|
``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
|
``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
|
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
|
the user provides are passed through the stemming algorithms, and then it
|
||||||
looks for matches for all of the resulting terms.
|
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::
|
``SearchQuery`` terms can be combined logically to provide more flexibility::
|
||||||
|
|
||||||
>>> from django.contrib.postgres.search import SearchQuery
|
>>> 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
|
See :ref:`postgresql-fts-search-configuration` for an explanation of the
|
||||||
``config`` parameter.
|
``config`` parameter.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
|
||||||
|
The `search_type` parameter was added.
|
||||||
|
|
||||||
``SearchRank``
|
``SearchRank``
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
@ -90,6 +90,10 @@ Minor features
|
|||||||
* :class:`~django.contrib.postgres.indexes.BrinIndex` now has the
|
* :class:`~django.contrib.postgres.indexes.BrinIndex` now has the
|
||||||
``autosummarize`` parameter.
|
``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`
|
:mod:`django.contrib.redirects`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from django.contrib.postgres.search import (
|
|||||||
SearchQuery, SearchRank, SearchVector,
|
SearchQuery, SearchRank, SearchVector,
|
||||||
)
|
)
|
||||||
from django.db.models import F
|
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 . import PostgreSQLTestCase
|
||||||
from .models import Character, Line, Scene
|
from .models import Character, Line, Scene
|
||||||
@ -75,7 +75,7 @@ class GrailTestData:
|
|||||||
cls.french = Line.objects.create(
|
cls.french = Line.objects.create(
|
||||||
scene=trojan_rabbit,
|
scene=trojan_rabbit,
|
||||||
character=guards,
|
character=guards,
|
||||||
dialogue='Oh. Un cadeau. Oui oui.',
|
dialogue='Oh. Un beau cadeau. Oui oui.',
|
||||||
dialogue_config='french',
|
dialogue_config='french',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -161,6 +161,46 @@ class MultipleFieldsTest(GrailTestData, PostgreSQLTestCase):
|
|||||||
).filter(search=str(self.crowd.id))
|
).filter(search=str(self.crowd.id))
|
||||||
self.assertSequenceEqual(searched, [self.crowd])
|
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):
|
def test_config_query_explicit(self):
|
||||||
searched = Line.objects.annotate(
|
searched = Line.objects.annotate(
|
||||||
search=SearchVector('scene__setting', 'dialogue', config='french'),
|
search=SearchVector('scene__setting', 'dialogue', config='french'),
|
||||||
|
Loading…
Reference in New Issue
Block a user