Skip to content

Commit 0d28ccd

Browse files
committed
feat: add search operator and fields arguments
allow for more specific search queries by specifying the search_operator and search_fields.
1 parent 48b983d commit 0d28ccd

File tree

4 files changed

+259
-24
lines changed

4 files changed

+259
-24
lines changed

grapple/types/structures.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ def parse_literal(ast, _variables=None):
2121
return return_value
2222

2323

24+
class SearchOperatorEnum(graphene.Enum):
25+
"""
26+
Enum for search operator.
27+
"""
28+
29+
AND = "and"
30+
OR = "or"
31+
32+
def __str__(self):
33+
# the core search parser expects the operator to be a string.
34+
# the default __str__ returns SearchOperatorEnum.AND/OR,
35+
# this __str__ returns the value and/or for compatibility.
36+
return self.value
37+
38+
2439
class QuerySetList(graphene.List):
2540
"""
2641
List type with arguments used by Django's query sets.
@@ -32,6 +47,8 @@ class QuerySetList(graphene.List):
3247
* ``limit``
3348
* ``offset``
3449
* ``search_query``
50+
* ``search_operator``
51+
* ``search_fields``
3552
* ``order``
3653
3754
:param enable_in_menu: Enable in_menu filter.
@@ -42,6 +59,10 @@ class QuerySetList(graphene.List):
4259
:type enable_offset: bool
4360
:param enable_search: Enable search query argument.
4461
:type enable_search: bool
62+
:param enable_search_fields: Enable search fields argument, enable_search must also be True
63+
:type enable_search_fields: bool
64+
:param enable_search_operator: Enable search operator argument, enable_search must also be True
65+
:type enable_search_operator: bool
4566
:param enable_order: Enable ordering via query argument.
4667
:type enable_order: bool
4768
"""
@@ -50,8 +71,10 @@ def __init__(self, of_type, *args, **kwargs):
5071
enable_in_menu = kwargs.pop("enable_in_menu", False)
5172
enable_limit = kwargs.pop("enable_limit", True)
5273
enable_offset = kwargs.pop("enable_offset", True)
53-
enable_search = kwargs.pop("enable_search", True)
5474
enable_order = kwargs.pop("enable_order", True)
75+
enable_search = kwargs.pop("enable_search", True)
76+
enable_search_fields = kwargs.pop("enable_search_fields", True)
77+
enable_search_operator = kwargs.pop("enable_search_operator", True)
5578

5679
# Check if the type is a Django model type. Do not perform the
5780
# check if value is lazy.
@@ -106,6 +129,22 @@ def __init__(self, of_type, *args, **kwargs):
106129
graphene.String,
107130
description=_("Filter the results using Wagtail's search."),
108131
)
132+
if enable_search_operator:
133+
kwargs["search_operator"] = graphene.Argument(
134+
SearchOperatorEnum,
135+
description=_(
136+
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
137+
),
138+
default_value="and",
139+
)
140+
141+
if enable_search_fields:
142+
kwargs["search_fields"] = graphene.Argument(
143+
graphene.List(graphene.String),
144+
description=_(
145+
"A list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
146+
),
147+
)
109148

110149
if "id" not in kwargs:
111150
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))
@@ -152,23 +191,31 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
152191
"""
153192
Paginated QuerySet type with arguments used by Django's query sets.
154193
155-
This type setts the following arguments on itself:
194+
This type sets the following arguments on itself:
156195
157196
* ``id``
158197
* ``in_menu``
159198
* ``page``
160199
* ``per_page``
161200
* ``search_query``
201+
* ``search_operator``
202+
* ``search_fields``
162203
* ``order``
163204
164205
:param enable_search: Enable search query argument.
165206
:type enable_search: bool
207+
:param enable_search_fields: Enable search fields argument, enable_search must also be True
208+
:type enable_search_fields: bool
209+
:param enable_search_operator: Enable search operator argument, enable_search must also be True
210+
:type enable_search_operator: bool
166211
:param enable_order: Enable ordering via query argument.
167212
:type enable_order: bool
168213
"""
169214

170215
enable_in_menu = kwargs.pop("enable_in_menu", False)
171216
enable_search = kwargs.pop("enable_search", True)
217+
enable_search_fields = kwargs.pop("enable_search_fields", True)
218+
enable_search_operator = kwargs.pop("enable_search_operator", True)
172219
enable_order = kwargs.pop("enable_order", True)
173220
required = kwargs.get("required", False)
174221
type_name = type_class if isinstance(type_class, str) else type_class.__name__
@@ -225,6 +272,22 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
225272
kwargs["search_query"] = graphene.Argument(
226273
graphene.String, description=_("Filter the results using Wagtail's search.")
227274
)
275+
if enable_search_operator:
276+
kwargs["search_operator"] = graphene.Argument(
277+
SearchOperatorEnum,
278+
description=_(
279+
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
280+
),
281+
default_value="and",
282+
)
283+
284+
if enable_search_fields:
285+
kwargs["search_fields"] = graphene.Argument(
286+
graphene.List(graphene.String),
287+
description=_(
288+
"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"
289+
),
290+
)
228291

229292
if "id" not in kwargs:
230293
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))

grapple/utils.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from wagtail import VERSION as WAGTAIL_VERSION
99
from wagtail.models import Site
1010
from wagtail.search.index import class_is_indexed
11+
from wagtail.search.utils import parse_query_string
1112

1213
from .settings import grapple_settings
1314
from .types.structures import BasePaginatedType, PaginationType
@@ -101,6 +102,8 @@ def resolve_queryset(
101102
order=None,
102103
collection=None,
103104
in_menu=None,
105+
search_operator="and",
106+
search_fields=None,
104107
**kwargs,
105108
):
106109
"""
@@ -122,6 +125,11 @@ def resolve_queryset(
122125
:type order: str
123126
:param collection: Use Wagtail's collection id to filter images or documents
124127
:type collection: int
128+
:param search_operator: The operator to use when combining search terms.
129+
Defaults to "and".
130+
:type search_operator: "and" | "or"
131+
:param search_fields: A list of fields to search. Defaults to all fields.
132+
:type search_fields: list
125133
"""
126134

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

155-
qs = qs.search(search_query, order_by_relevance=order_by_relevance)
163+
filters, parsed_query = parse_query_string(search_query, str(search_operator))
164+
165+
# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
166+
if search_fields is None:
167+
search_fields = filters.getlist("fields", None)
168+
169+
qs = qs.search(
170+
parsed_query,
171+
order_by_relevance=order_by_relevance,
172+
operator=search_operator,
173+
fields=search_fields,
174+
)
156175
if connection.vendor != "sqlite":
157176
qs = qs.annotate_score("search_score")
158177

@@ -183,17 +202,26 @@ def get_paginated_result(qs, page, per_page):
183202
count=len(page_obj.object_list),
184203
per_page=per_page,
185204
current_page=page_obj.number,
186-
prev_page=page_obj.previous_page_number()
187-
if page_obj.has_previous()
188-
else None,
205+
prev_page=(
206+
page_obj.previous_page_number() if page_obj.has_previous() else None
207+
),
189208
next_page=page_obj.next_page_number() if page_obj.has_next() else None,
190209
total_pages=paginator.num_pages,
191210
),
192211
)
193212

194213

195214
def resolve_paginated_queryset(
196-
qs, info, page=None, per_page=None, search_query=None, id=None, order=None, **kwargs
215+
qs,
216+
info,
217+
page=None,
218+
per_page=None,
219+
id=None,
220+
order=None,
221+
search_query=None,
222+
search_operator="and",
223+
search_fields=None,
224+
**kwargs,
197225
):
198226
"""
199227
Add page, per_page and search capabilities to the query. This contains
@@ -207,11 +235,16 @@ def resolve_paginated_queryset(
207235
:type id: int
208236
:param per_page: The maximum number of items to include on a page.
209237
:type per_page: int
238+
:param order: Order the query set using the Django QuerySet order_by format.
239+
:type order: str
210240
:param search_query: Using Wagtail search, exclude objects that do not match
211241
the search query.
212242
:type search_query: str
213-
:param order: Order the query set using the Django QuerySet order_by format.
214-
:type order: str
243+
:param search_operator: The operator to use when combining search terms.
244+
Defaults to "and".
245+
:type search_operator: "and" | "or"
246+
:param search_fields: A list of fields to search. Defaults to all fields.
247+
:type search_fields: list
215248
"""
216249
page = int(page or 1)
217250
per_page = min(
@@ -236,7 +269,18 @@ def resolve_paginated_queryset(
236269
query = Query.get(search_query)
237270
query.add_hit()
238271

239-
qs = qs.search(search_query, order_by_relevance=order_by_relevance)
272+
filters, parsed_query = parse_query_string(search_query, search_operator)
273+
274+
# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
275+
if search_fields is None:
276+
search_fields = filters.getlist("fields", None)
277+
278+
qs = qs.search(
279+
parsed_query,
280+
order_by_relevance=order_by_relevance,
281+
operator=search_operator,
282+
fields=search_fields,
283+
)
240284
if connection.vendor != "sqlite":
241285
qs = qs.annotate_score("search_score")
242286

0 commit comments

Comments
 (0)