Skip to content

Commit 200b4b5

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 200b4b5

File tree

3 files changed

+167
-12
lines changed

3 files changed

+167
-12
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_field"] = 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_field"] = graphene.Argument(
260+
graphene.List(graphene.String),
261+
description=_(
262+
"A 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: 44 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,14 @@ 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+
qs = qs.search(
161+
parsed_query,
162+
order_by_relevance=order_by_relevance,
163+
operator=search_operator,
164+
fields=search_fields,
165+
)
151166
if connection.vendor != "sqlite":
152167
qs = qs.annotate_score("search_score")
153168

@@ -178,17 +193,26 @@ def get_paginated_result(qs, page, per_page):
178193
count=len(page_obj.object_list),
179194
per_page=per_page,
180195
current_page=page_obj.number,
181-
prev_page=page_obj.previous_page_number()
182-
if page_obj.has_previous()
183-
else None,
196+
prev_page=(
197+
page_obj.previous_page_number() if page_obj.has_previous() else None
198+
),
184199
next_page=page_obj.next_page_number() if page_obj.has_next() else None,
185200
total_pages=paginator.num_pages,
186201
),
187202
)
188203

189204

190205
def resolve_paginated_queryset(
191-
qs, info, page=None, per_page=None, search_query=None, id=None, order=None, **kwargs
206+
qs,
207+
info,
208+
page=None,
209+
per_page=None,
210+
id=None,
211+
order=None,
212+
search_query=None,
213+
search_operator="and",
214+
search_fields=None,
215+
**kwargs,
192216
):
193217
"""
194218
Add page, per_page and search capabilities to the query. This contains
@@ -202,11 +226,16 @@ def resolve_paginated_queryset(
202226
:type id: int
203227
:param per_page: The maximum number of items to include on a page.
204228
:type per_page: int
229+
:param order: Order the query set using the Django QuerySet order_by format.
230+
:type order: str
205231
:param search_query: Using Wagtail search, exclude objects that do not match
206232
the search query.
207233
:type search_query: str
208-
:param order: Order the query set using the Django QuerySet order_by format.
209-
:type order: str
234+
:param search_operator: The operator to use when combining search terms.
235+
Defaults to "and".
236+
:type search_operator: "and" | "or"
237+
:param search_fields: A list of fields to search. Defaults to all fields.
238+
:type search_fields: list
210239
"""
211240
page = int(page or 1)
212241
per_page = min(
@@ -231,7 +260,14 @@ def resolve_paginated_queryset(
231260
query = Query.get(search_query)
232261
query.add_hit()
233262

234-
qs = qs.search(search_query, order_by_relevance=order_by_relevance)
263+
filters, parsed_query = parse_query_string(search_query, search_operator)
264+
265+
qs = qs.search(
266+
parsed_query,
267+
order_by_relevance=order_by_relevance,
268+
operator=search_operator,
269+
fields=search_fields,
270+
)
235271
if connection.vendor != "sqlite":
236272
qs = qs.annotate_score("search_score")
237273

tests/test_grapple.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,6 @@ def test_searchQuery_order_by_relevance(self):
530530
}
531531
}
532532
"""
533-
534533
executed = self.client.execute(query, variables={"searchQuery": "Alpha"})
535534
page_data = executed["data"].get("pages")
536535
self.assertEqual(len(page_data), 6)
@@ -559,7 +558,6 @@ def test_explicit_order(self):
559558
query, variables={"searchQuery": "Gamma", "order": "-title"}
560559
)
561560
page_data = executed["data"].get("pages")
562-
563561
self.assertEqual(len(page_data), 6)
564562
self.assertEqual(page_data[0]["title"], "Gamma Gamma")
565563
self.assertEqual(page_data[1]["title"], "Gamma Beta")
@@ -568,6 +566,64 @@ def test_explicit_order(self):
568566
self.assertEqual(page_data[4]["title"], "Beta Gamma")
569567
self.assertEqual(page_data[5]["title"], "Alpha Gamma")
570568

569+
def test_search_operator_default(self):
570+
"""default operator is and"""
571+
query = """
572+
query($searchQuery: String) {
573+
pages(searchQuery: $searchQuery) {
574+
title
575+
searchScore
576+
}
577+
}
578+
"""
579+
executed = self.client.execute(query, variables={"searchQuery": "Alpha Beta"})
580+
page_data = executed["data"].get("pages")
581+
self.assertEqual(len(page_data), 2)
582+
self.assertEqual(page_data[0]["title"], "Alpha Beta")
583+
self.assertEqual(page_data[1]["title"], "Beta Alpha")
584+
585+
def test_search_operator_and(self):
586+
query = """
587+
query($searchQuery: String, $searchOperator: SearchOperatorEnum) {
588+
pages(searchQuery: $searchQuery, searchOperator: $searchOperator) {
589+
title
590+
searchScore
591+
}
592+
}
593+
"""
594+
executed = self.client.execute(
595+
query, variables={"searchQuery": "Alpha Beta", "searchOperator": "AND"}
596+
)
597+
page_data = executed["data"].get("pages")
598+
self.assertEqual(len(page_data), 2)
599+
self.assertEqual(page_data[0]["title"], "Alpha Beta")
600+
self.assertEqual(page_data[1]["title"], "Beta Alpha")
601+
602+
def test_search_operator_or(self):
603+
query = """
604+
query($searchQuery: String, $searchOperator: SearchOperatorEnum) {
605+
pages(searchQuery: $searchQuery, searchOperator: $searchOperator) {
606+
title
607+
searchScore
608+
}
609+
}
610+
"""
611+
executed = self.client.execute(
612+
query, variables={"searchQuery": "Alpha Beta", "searchOperator": "OR"}
613+
)
614+
page_data = executed["data"].get("pages")
615+
self.assertEqual(len(page_data), 10)
616+
self.assertEqual(page_data[0]["title"], "Alpha")
617+
self.assertEqual(page_data[1]["title"], "Alpha Alpha")
618+
self.assertEqual(page_data[2]["title"], "Alpha Beta")
619+
self.assertEqual(page_data[3]["title"], "Alpha Gamma")
620+
self.assertEqual(page_data[4]["title"], "Beta")
621+
self.assertEqual(page_data[5]["title"], "Beta Alpha")
622+
self.assertEqual(page_data[6]["title"], "Beta Beta")
623+
self.assertEqual(page_data[7]["title"], "Beta Gamma")
624+
self.assertEqual(page_data[8]["title"], "Gamma Alpha")
625+
self.assertEqual(page_data[9]["title"], "Gamma Beta")
626+
571627

572628
class PageUrlPathTest(BaseGrappleTest):
573629
def _query_by_path(self, path, *, in_site=False):

0 commit comments

Comments
 (0)