mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-11 16:53:10 +00:00
Implements And/Or/Not/Term in database search backend.
This commit is contained in:
parent
4c4dfac806
commit
b36165fd6a
7 changed files with 67 additions and 85 deletions
|
|
@ -16,10 +16,11 @@ from wagtail.wagtailsearch.backends.base import (
|
|||
BaseSearchBackend, BaseSearchQueryCompiler, BaseSearchResults)
|
||||
from wagtail.wagtailsearch.index import RelatedFields, SearchField
|
||||
from wagtail.wagtailsearch.query import And, MatchAll, Not, Or, PlainText, Term
|
||||
from wagtail.wagtailsearch.utils import ADD, AND, OR
|
||||
|
||||
from .models import IndexEntry
|
||||
from .utils import (
|
||||
ADD, AND, OR, WEIGHTS_VALUES, get_ancestors_content_types_pks, get_content_type_pk,
|
||||
WEIGHTS_VALUES, get_ancestors_content_types_pks, get_content_type_pk,
|
||||
get_descendants_content_types_pks, get_postgresql_connections, get_weight, unidecode)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
import operator
|
||||
from functools import partial, reduce
|
||||
from itertools import zip_longest
|
||||
|
||||
from django.apps import apps
|
||||
|
|
@ -22,14 +20,6 @@ def get_postgresql_connections():
|
|||
if connection.vendor == 'postgresql']
|
||||
|
||||
|
||||
# Reduce any iterable to a single value using a logical OR e.g. (a | b | ...)
|
||||
OR = partial(reduce, operator.or_)
|
||||
# Reduce any iterable to a single value using a logical AND e.g. (a & b & ...)
|
||||
AND = partial(reduce, operator.and_)
|
||||
# Reduce any iterable to a single value using an addition
|
||||
ADD = partial(reduce, operator.add)
|
||||
|
||||
|
||||
def get_descendant_models(model):
|
||||
"""
|
||||
Returns all descendants of a model, including the model itself.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,36 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.expressions import Value
|
||||
|
||||
from wagtail.wagtailsearch.backends.base import (
|
||||
BaseSearchBackend, BaseSearchQueryCompiler, BaseSearchResults)
|
||||
from wagtail.wagtailsearch.query import MatchAll, PlainText
|
||||
from wagtail.wagtailsearch.query import And, MatchAll, Not, Or, PlainText, Term
|
||||
from wagtail.wagtailsearch.utils import AND, OR
|
||||
|
||||
|
||||
class DatabaseSearchQueryCompiler(BaseSearchQueryCompiler):
|
||||
DEFAULT_OPERATOR = 'and'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields_names = list(self.get_fields_names())
|
||||
|
||||
def get_fields_names(self):
|
||||
model = self.queryset.model
|
||||
fields_names = self.fields or [field.field_name for field in
|
||||
model.get_searchable_search_fields()]
|
||||
# Check if the field exists (this will filter out indexed callables)
|
||||
for field_name in fields_names:
|
||||
try:
|
||||
model._meta.get_field(field_name)
|
||||
except models.fields.FieldDoesNotExist:
|
||||
continue
|
||||
else:
|
||||
yield field_name
|
||||
|
||||
def _process_lookup(self, field, lookup, value):
|
||||
return models.Q(**{field.get_attname(self.queryset.model) + '__' + lookup: value})
|
||||
|
||||
|
|
@ -29,57 +49,47 @@ class DatabaseSearchQueryCompiler(BaseSearchQueryCompiler):
|
|||
|
||||
return q
|
||||
|
||||
def get_extra_q(self):
|
||||
# Run _get_filters_from_queryset to test that no fields that are not
|
||||
# a FilterField have been used in the query.
|
||||
self._get_filters_from_queryset()
|
||||
def build_single_term_filter(self, term):
|
||||
term_query = models.Q()
|
||||
for field_name in self.fields_names:
|
||||
term_query |= models.Q(**{field_name + '__icontains': term})
|
||||
return term_query
|
||||
|
||||
q = models.Q()
|
||||
model = self.queryset.model
|
||||
def build_database_filter(self, query=None):
|
||||
if query is None:
|
||||
query = self.query
|
||||
|
||||
if isinstance(self.query, MatchAll):
|
||||
return q
|
||||
return models.Q()
|
||||
|
||||
if not isinstance(self.query, PlainText):
|
||||
raise NotImplementedError(
|
||||
'`%s` is not supported by the database search backend.'
|
||||
% self.query.__class__.__name__)
|
||||
|
||||
# Get fields
|
||||
fields = self.fields or [field.field_name for field in model.get_searchable_search_fields()]
|
||||
|
||||
# Get terms
|
||||
terms = self.query.query_string.split()
|
||||
if not terms:
|
||||
return model.objects.none()
|
||||
|
||||
# Filter by terms
|
||||
for term in terms:
|
||||
term_query = models.Q()
|
||||
for field_name in fields:
|
||||
# Check if the field exists (this will filter out indexed callables)
|
||||
try:
|
||||
model._meta.get_field(field_name)
|
||||
except models.fields.FieldDoesNotExist:
|
||||
continue
|
||||
|
||||
# Filter on this field
|
||||
term_query |= models.Q(**{'%s__icontains' % field_name: term})
|
||||
|
||||
operator = self.query.operator
|
||||
|
||||
if operator == 'or':
|
||||
q |= term_query
|
||||
elif operator == 'and':
|
||||
q &= term_query
|
||||
|
||||
return q
|
||||
if isinstance(query, PlainText):
|
||||
return self.build_database_filter(query.to_combined_terms())
|
||||
if isinstance(query, Term):
|
||||
if query.boost != 1:
|
||||
warn('Database search backend does not support term boosting.')
|
||||
return self.build_single_term_filter(query.term)
|
||||
if isinstance(query, Not):
|
||||
return ~self.build_database_filter(query.subquery)
|
||||
if isinstance(query, And):
|
||||
return AND(self.build_database_filter(subquery)
|
||||
for subquery in query.subqueries)
|
||||
if isinstance(query, Or):
|
||||
return OR(self.build_database_filter(subquery)
|
||||
for subquery in query.subqueries)
|
||||
raise NotImplementedError(
|
||||
'`%s` is not supported by the database search backend.'
|
||||
% self.query.__class__.__name__)
|
||||
|
||||
|
||||
class DatabaseSearchResults(BaseSearchResults):
|
||||
def get_queryset(self):
|
||||
queryset = self.query_compiler.queryset
|
||||
q = self.query_compiler.get_extra_q()
|
||||
|
||||
# Run _get_filters_from_queryset to test that no fields that are not
|
||||
# a FilterField have been used in the query.
|
||||
self.query_compiler._get_filters_from_queryset()
|
||||
|
||||
q = self.query_compiler.build_database_filter()
|
||||
|
||||
return queryset.filter(q).distinct()[self.start:self.stop]
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ class PlainText(SearchQuery):
|
|||
|
||||
def to_combined_terms(self):
|
||||
return self.OPERATORS[self.operator]([
|
||||
Term(term) for term in self.query_string.split()])
|
||||
Term(term, boost=self.boost)
|
||||
for term in self.query_string.split()])
|
||||
|
||||
|
||||
class Term(SearchQuery):
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ class BackendTests(WagtailTestUtils):
|
|||
'JavaScript: The good parts'})
|
||||
|
||||
# Multiple word
|
||||
results = self.backend.search(Term('Javascript Guide'),
|
||||
results = self.backend.search(Term('Definitive Guide'),
|
||||
models.Book.objects.all())
|
||||
self.assertSetEqual({r.title for r in results},
|
||||
{'JavaScript: The Definitive Guide'})
|
||||
|
|
|
|||
|
|
@ -44,32 +44,3 @@ class TestDBBackend(BackendTests, TestCase):
|
|||
@unittest.expectedFailure
|
||||
def test_same_rank_pages(self):
|
||||
super(TestDBBackend, self).test_same_rank_pages()
|
||||
|
||||
#
|
||||
# Query classes
|
||||
#
|
||||
|
||||
# Not implemented yet
|
||||
@unittest.expectedFailure
|
||||
def test_term(self):
|
||||
super().test_term()
|
||||
|
||||
# Not implemented yet
|
||||
@unittest.expectedFailure
|
||||
def test_and(self):
|
||||
super().test_and()
|
||||
|
||||
# Not implemented yet
|
||||
@unittest.expectedFailure
|
||||
def test_or(self):
|
||||
super().test_or()
|
||||
|
||||
# Not implemented yet
|
||||
@unittest.expectedFailure
|
||||
def test_not(self):
|
||||
super().test_not()
|
||||
|
||||
# Not implemented yet
|
||||
@unittest.expectedFailure
|
||||
def test_operators_combination(self):
|
||||
super().test_operators_combination()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import operator
|
||||
import re
|
||||
import string
|
||||
from functools import partial, reduce
|
||||
|
||||
# Reduce any iterable to a single value using a logical OR e.g. (a | b | ...)
|
||||
OR = partial(reduce, operator.or_)
|
||||
# Reduce any iterable to a single value using a logical AND e.g. (a & b & ...)
|
||||
AND = partial(reduce, operator.and_)
|
||||
# Reduce any iterable to a single value using an addition
|
||||
ADD = partial(reduce, operator.add)
|
||||
|
||||
MAX_QUERY_STRING_LENGTH = 255
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue