Skip to content

Commit

Permalink
feat: add search operator and fields arguments
Browse files Browse the repository at this point in the history
allow for more specific search queries by specifying the search_operator
and search_fields.
  • Loading branch information
dopry committed Sep 20, 2024
1 parent 48b983d commit 0d28ccd
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 24 deletions.
67 changes: 65 additions & 2 deletions grapple/types/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ def parse_literal(ast, _variables=None):
return return_value


class SearchOperatorEnum(graphene.Enum):
"""
Enum for search operator.
"""

AND = "and"
OR = "or"

def __str__(self):
# the core search parser expects the operator to be a string.
# the default __str__ returns SearchOperatorEnum.AND/OR,
# this __str__ returns the value and/or for compatibility.
return self.value


class QuerySetList(graphene.List):
"""
List type with arguments used by Django's query sets.
Expand All @@ -32,6 +47,8 @@ class QuerySetList(graphene.List):
* ``limit``
* ``offset``
* ``search_query``
* ``search_operator``
* ``search_fields``
* ``order``
:param enable_in_menu: Enable in_menu filter.
Expand All @@ -42,6 +59,10 @@ class QuerySetList(graphene.List):
:type enable_offset: bool
:param enable_search: Enable search query argument.
:type enable_search: bool
:param enable_search_fields: Enable search fields argument, enable_search must also be True
:type enable_search_fields: bool
:param enable_search_operator: Enable search operator argument, enable_search must also be True
:type enable_search_operator: bool
:param enable_order: Enable ordering via query argument.
:type enable_order: bool
"""
Expand All @@ -50,8 +71,10 @@ def __init__(self, of_type, *args, **kwargs):
enable_in_menu = kwargs.pop("enable_in_menu", False)
enable_limit = kwargs.pop("enable_limit", True)
enable_offset = kwargs.pop("enable_offset", True)
enable_search = kwargs.pop("enable_search", True)
enable_order = kwargs.pop("enable_order", True)
enable_search = kwargs.pop("enable_search", True)
enable_search_fields = kwargs.pop("enable_search_fields", True)
enable_search_operator = kwargs.pop("enable_search_operator", True)

# Check if the type is a Django model type. Do not perform the
# check if value is lazy.
Expand Down Expand Up @@ -106,6 +129,22 @@ def __init__(self, of_type, *args, **kwargs):
graphene.String,
description=_("Filter the results using Wagtail's search."),
)
if enable_search_operator:
kwargs["search_operator"] = graphene.Argument(
SearchOperatorEnum,
description=_(
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
),
default_value="and",
)

if enable_search_fields:
kwargs["search_fields"] = graphene.Argument(
graphene.List(graphene.String),
description=_(
"A list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
),
)

if "id" not in kwargs:
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))
Expand Down Expand Up @@ -152,23 +191,31 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
"""
Paginated QuerySet type with arguments used by Django's query sets.
This type setts the following arguments on itself:
This type sets the following arguments on itself:
* ``id``
* ``in_menu``
* ``page``
* ``per_page``
* ``search_query``
* ``search_operator``
* ``search_fields``
* ``order``
:param enable_search: Enable search query argument.
:type enable_search: bool
:param enable_search_fields: Enable search fields argument, enable_search must also be True
:type enable_search_fields: bool
:param enable_search_operator: Enable search operator argument, enable_search must also be True
:type enable_search_operator: bool
:param enable_order: Enable ordering via query argument.
:type enable_order: bool
"""

enable_in_menu = kwargs.pop("enable_in_menu", False)
enable_search = kwargs.pop("enable_search", True)
enable_search_fields = kwargs.pop("enable_search_fields", True)
enable_search_operator = kwargs.pop("enable_search_operator", True)
enable_order = kwargs.pop("enable_order", True)
required = kwargs.get("required", False)
type_name = type_class if isinstance(type_class, str) else type_class.__name__
Expand Down Expand Up @@ -225,6 +272,22 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
kwargs["search_query"] = graphene.Argument(
graphene.String, description=_("Filter the results using Wagtail's search.")
)
if enable_search_operator:
kwargs["search_operator"] = graphene.Argument(
SearchOperatorEnum,
description=_(
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
),
default_value="and",
)

if enable_search_fields:
kwargs["search_fields"] = graphene.Argument(
graphene.List(graphene.String),
description=_(
"A comma-separated list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
),
)

if "id" not in kwargs:
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))
Expand Down
60 changes: 52 additions & 8 deletions grapple/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from wagtail import VERSION as WAGTAIL_VERSION
from wagtail.models import Site
from wagtail.search.index import class_is_indexed
from wagtail.search.utils import parse_query_string

from .settings import grapple_settings
from .types.structures import BasePaginatedType, PaginationType
Expand Down Expand Up @@ -101,6 +102,8 @@ def resolve_queryset(
order=None,
collection=None,
in_menu=None,
search_operator="and",
search_fields=None,
**kwargs,
):
"""
Expand All @@ -122,6 +125,11 @@ def resolve_queryset(
:type order: str
:param collection: Use Wagtail's collection id to filter images or documents
:type collection: int
:param search_operator: The operator to use when combining search terms.
Defaults to "and".
:type search_operator: "and" | "or"
:param search_fields: A list of fields to search. Defaults to all fields.
:type search_fields: list
"""

qs = qs.all() if id is None else qs.filter(pk=id)
Expand Down Expand Up @@ -152,7 +160,18 @@ def resolve_queryset(
query = Query.get(search_query)
query.add_hit()

qs = qs.search(search_query, order_by_relevance=order_by_relevance)
filters, parsed_query = parse_query_string(search_query, str(search_operator))

# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
if search_fields is None:
search_fields = filters.getlist("fields", None)

qs = qs.search(
parsed_query,
order_by_relevance=order_by_relevance,
operator=search_operator,
fields=search_fields,
)
if connection.vendor != "sqlite":
qs = qs.annotate_score("search_score")

Expand Down Expand Up @@ -183,17 +202,26 @@ def get_paginated_result(qs, page, per_page):
count=len(page_obj.object_list),
per_page=per_page,
current_page=page_obj.number,
prev_page=page_obj.previous_page_number()
if page_obj.has_previous()
else None,
prev_page=(
page_obj.previous_page_number() if page_obj.has_previous() else None
),
next_page=page_obj.next_page_number() if page_obj.has_next() else None,
total_pages=paginator.num_pages,
),
)


def resolve_paginated_queryset(
qs, info, page=None, per_page=None, search_query=None, id=None, order=None, **kwargs
qs,
info,
page=None,
per_page=None,
id=None,
order=None,
search_query=None,
search_operator="and",
search_fields=None,
**kwargs,
):
"""
Add page, per_page and search capabilities to the query. This contains
Expand All @@ -207,11 +235,16 @@ def resolve_paginated_queryset(
:type id: int
:param per_page: The maximum number of items to include on a page.
:type per_page: int
:param order: Order the query set using the Django QuerySet order_by format.
:type order: str
:param search_query: Using Wagtail search, exclude objects that do not match
the search query.
:type search_query: str
:param order: Order the query set using the Django QuerySet order_by format.
:type order: str
:param search_operator: The operator to use when combining search terms.
Defaults to "and".
:type search_operator: "and" | "or"
:param search_fields: A list of fields to search. Defaults to all fields.
:type search_fields: list
"""
page = int(page or 1)
per_page = min(
Expand All @@ -236,7 +269,18 @@ def resolve_paginated_queryset(
query = Query.get(search_query)
query.add_hit()

qs = qs.search(search_query, order_by_relevance=order_by_relevance)
filters, parsed_query = parse_query_string(search_query, search_operator)

# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
if search_fields is None:
search_fields = filters.getlist("fields", None)

qs = qs.search(
parsed_query,
order_by_relevance=order_by_relevance,
operator=search_operator,
fields=search_fields,
)
if connection.vendor != "sqlite":
qs = qs.annotate_score("search_score")

Expand Down
Loading

0 comments on commit 0d28ccd

Please sign in to comment.