Skip to content

Commit ad0ab8a

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 0d20a8c commit ad0ab8a

File tree

4 files changed

+254
-24
lines changed

4 files changed

+254
-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.
@@ -31,6 +46,8 @@ class QuerySetList(graphene.List):
3146
* ``limit``
3247
* ``offset``
3348
* ``search_query``
49+
* ``search_operator``
50+
* ``search_fields``
3451
* ``order``
3552
3653
:param enable_limit: Enable limit argument.
@@ -39,15 +56,21 @@ class QuerySetList(graphene.List):
3956
:type enable_offset: bool
4057
:param enable_search: Enable search query argument.
4158
:type enable_search: bool
59+
:param enable_search_fields: Enable search fields argument, enable_search must also be True
60+
:type enable_search_fields: bool
61+
:param enable_search_operator: Enable search operator argument, enable_search must also be True
62+
:type enable_search_operator: bool
4263
:param enable_order: Enable ordering via query argument.
4364
:type enable_order: bool
4465
"""
4566

4667
def __init__(self, of_type, *args, **kwargs):
4768
enable_limit = kwargs.pop("enable_limit", True)
4869
enable_offset = kwargs.pop("enable_offset", True)
49-
enable_search = kwargs.pop("enable_search", True)
5070
enable_order = kwargs.pop("enable_order", True)
71+
enable_search = kwargs.pop("enable_search", True)
72+
enable_search_fields = kwargs.pop("enable_search_fields", True)
73+
enable_search_operator = kwargs.pop("enable_search_operator", True)
5174

5275
# Check if the type is a Django model type. Do not perform the
5376
# check if value is lazy.
@@ -92,6 +115,22 @@ def __init__(self, of_type, *args, **kwargs):
92115
graphene.String,
93116
description=_("Filter the results using Wagtail's search."),
94117
)
118+
if enable_search_operator:
119+
kwargs["search_operator"] = graphene.Argument(
120+
SearchOperatorEnum,
121+
description=_(
122+
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
123+
),
124+
default_value="and",
125+
)
126+
127+
if enable_search_fields:
128+
kwargs["search_fields"] = graphene.Argument(
129+
graphene.List(graphene.String),
130+
description=_(
131+
"A list of fields to search in. see: https://docs.wagtail.org/en/stable/topics/search/searching.html#specifying-the-fields-to-search"
132+
),
133+
)
95134

96135
if "id" not in kwargs:
97136
kwargs["id"] = graphene.Argument(graphene.ID, description=_("Filter by ID"))
@@ -138,21 +177,29 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
138177
"""
139178
Paginated QuerySet type with arguments used by Django's query sets.
140179
141-
This type setts the following arguments on itself:
180+
This type sets the following arguments on itself:
142181
143182
* ``id``
144183
* ``page``
145184
* ``per_page``
146185
* ``search_query``
186+
* ``search_operator``
187+
* ``search_fields``
147188
* ``order``
148189
149190
:param enable_search: Enable search query argument.
150191
:type enable_search: bool
192+
:param enable_search_fields: Enable search fields argument, enable_search must also be True
193+
:type enable_search_fields: bool
194+
:param enable_search_operator: Enable search operator argument, enable_search must also be True
195+
:type enable_search_operator: bool
151196
:param enable_order: Enable ordering via query argument.
152197
:type enable_order: bool
153198
"""
154199

155200
enable_search = kwargs.pop("enable_search", True)
201+
enable_search_fields = kwargs.pop("enable_search_fields", True)
202+
enable_search_operator = kwargs.pop("enable_search_operator", True)
156203
enable_order = kwargs.pop("enable_order", True)
157204
required = kwargs.get("required", False)
158205
type_name = type_class if isinstance(type_class, str) else type_class.__name__
@@ -199,6 +246,22 @@ def PaginatedQuerySet(of_type, type_class, **kwargs):
199246
kwargs["search_query"] = graphene.Argument(
200247
graphene.String, description=_("Filter the results using Wagtail's search.")
201248
)
249+
if enable_search_operator:
250+
kwargs["search_operator"] = graphene.Argument(
251+
SearchOperatorEnum,
252+
description=_(
253+
"Specify search operator (and/or), see: https://docs.wagtail.org/en/stable/topics/search/searching.html#search-operator"
254+
),
255+
default_value="and",
256+
)
257+
258+
if enable_search_fields:
259+
kwargs["search_fields"] = graphene.Argument(
260+
graphene.List(graphene.String),
261+
description=_(
262+
"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"
263+
),
264+
)
202265

203266
if "id" not in kwargs:
204267
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
@@ -100,6 +101,8 @@ def resolve_queryset(
100101
id=None,
101102
order=None,
102103
collection=None,
104+
search_operator="and",
105+
search_fields=None,
103106
**kwargs,
104107
):
105108
"""
@@ -121,6 +124,11 @@ def resolve_queryset(
121124
:type order: str
122125
:param collection: Use Wagtail's collection id to filter images or documents
123126
:type collection: int
127+
:param search_operator: The operator to use when combining search terms.
128+
Defaults to "and".
129+
:type search_operator: "and" | "or"
130+
:param search_fields: A list of fields to search. Defaults to all fields.
131+
:type search_fields: list
124132
"""
125133

126134
qs = qs.all() if id is None else qs.filter(pk=id)
@@ -147,7 +155,18 @@ def resolve_queryset(
147155
query = Query.get(search_query)
148156
query.add_hit()
149157

150-
qs = qs.search(search_query, order_by_relevance=order_by_relevance)
158+
filters, parsed_query = parse_query_string(search_query, str(search_operator))
159+
160+
# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
161+
if search_fields is None:
162+
search_fields = filters.getlist("fields", None)
163+
164+
qs = qs.search(
165+
parsed_query,
166+
order_by_relevance=order_by_relevance,
167+
operator=search_operator,
168+
fields=search_fields,
169+
)
151170
if connection.vendor != "sqlite":
152171
qs = qs.annotate_score("search_score")
153172

@@ -178,17 +197,26 @@ def get_paginated_result(qs, page, per_page):
178197
count=len(page_obj.object_list),
179198
per_page=per_page,
180199
current_page=page_obj.number,
181-
prev_page=page_obj.previous_page_number()
182-
if page_obj.has_previous()
183-
else None,
200+
prev_page=(
201+
page_obj.previous_page_number() if page_obj.has_previous() else None
202+
),
184203
next_page=page_obj.next_page_number() if page_obj.has_next() else None,
185204
total_pages=paginator.num_pages,
186205
),
187206
)
188207

189208

190209
def resolve_paginated_queryset(
191-
qs, info, page=None, per_page=None, search_query=None, id=None, order=None, **kwargs
210+
qs,
211+
info,
212+
page=None,
213+
per_page=None,
214+
id=None,
215+
order=None,
216+
search_query=None,
217+
search_operator="and",
218+
search_fields=None,
219+
**kwargs,
192220
):
193221
"""
194222
Add page, per_page and search capabilities to the query. This contains
@@ -202,11 +230,16 @@ def resolve_paginated_queryset(
202230
:type id: int
203231
:param per_page: The maximum number of items to include on a page.
204232
:type per_page: int
233+
:param order: Order the query set using the Django QuerySet order_by format.
234+
:type order: str
205235
:param search_query: Using Wagtail search, exclude objects that do not match
206236
the search query.
207237
:type search_query: str
208-
:param order: Order the query set using the Django QuerySet order_by format.
209-
:type order: str
238+
:param search_operator: The operator to use when combining search terms.
239+
Defaults to "and".
240+
:type search_operator: "and" | "or"
241+
:param search_fields: A list of fields to search. Defaults to all fields.
242+
:type search_fields: list
210243
"""
211244
page = int(page or 1)
212245
per_page = min(
@@ -231,7 +264,18 @@ def resolve_paginated_queryset(
231264
query = Query.get(search_query)
232265
query.add_hit()
233266

234-
qs = qs.search(search_query, order_by_relevance=order_by_relevance)
267+
filters, parsed_query = parse_query_string(search_query, search_operator)
268+
269+
# check if search_fields is provided in the query string if it isn't provided as a graphQL argument
270+
if search_fields is None:
271+
search_fields = filters.getlist("fields", None)
272+
273+
qs = qs.search(
274+
parsed_query,
275+
order_by_relevance=order_by_relevance,
276+
operator=search_operator,
277+
fields=search_fields,
278+
)
235279
if connection.vendor != "sqlite":
236280
qs = qs.annotate_score("search_score")
237281

0 commit comments

Comments
 (0)