diff --git a/news/1637.feature b/news/1637.feature new file mode 100644 index 0000000000..c54d2beb29 --- /dev/null +++ b/news/1637.feature @@ -0,0 +1 @@ +feat(search): show facets count and delete the facets results that don't meet the criterias @razvanMiu @dobri1408 \ No newline at end of file diff --git a/src/plone/restapi/services/querystringsearch/facet.py b/src/plone/restapi/services/querystringsearch/facet.py new file mode 100644 index 0000000000..4133ecb68f --- /dev/null +++ b/src/plone/restapi/services/querystringsearch/facet.py @@ -0,0 +1,99 @@ +from BTrees.IIBTree import intersection +from Products.CMFCore.interfaces import ICatalogTool +from zope.component import getUtility +from zope.component import getMultiAdapter +from pkg_resources import get_distribution +from pkg_resources import parse_version + +zcatalog_version = get_distribution("Products.ZCatalog").version +if parse_version(zcatalog_version) >= parse_version("5.1"): + SUPPORT_NOT_UUID_QUERIES = True +else: + SUPPORT_NOT_UUID_QUERIES = False + + +class Facet: + """Returns facet count.""" + + def __init__( + self, context, request, name, querybuilder_parameters, brains_rids_mandatory + ): + + self.context = context + self.request = request + self.name = name + self.querybuilder_parameters = querybuilder_parameters.copy() + self.querybuilder_mandatory_parameters = querybuilder_parameters.copy() + self.querybuilder_parameters["query"] = [ + qs + for qs in querybuilder_parameters.get("query", []) + if qs["i"] != self.name or ("mandatory" in qs and qs["mandatory"] is True) + ] + self.querybuilder_parameters["rids"] = True + self.querybuilder_mandatory_parameters["rids"] = True + self.querybuilder_mandatory_parameters["query"] = [ + qs + for qs in querybuilder_parameters.get("query", []) + if "mandatory" in qs and qs["mandatory"] is True + ] + self.brain_rids_mandatory = brains_rids_mandatory + + # make serch work also on Plone Root + if SUPPORT_NOT_UUID_QUERIES: + self.querybuilder_parameters.update( + dict(custom_query={"UID": {"not": self.context.UID()}}) + ) + + def getFacet(self): + ctool = getUtility(ICatalogTool) + count = {} + count_mandatory = {} + index = None + try: + index = ctool._catalog.getIndex(self.name) + finally: + if index is None: + return None + # Get the brains for the query without the facet + querybuilder = getMultiAdapter( + (self.context, self.request), name="querybuilderresults" + ) + + brains_rids = querybuilder(**self.querybuilder_parameters) + brains_rids_mandatory = self.brain_rids_mandatory + # Get the rids for the brains that have the facet index set to the value we are interested in + index_rids = index.documentToKeyMap() + rids = intersection(brains_rids, index_rids) + rids_mandatory = intersection(brains_rids_mandatory, index_rids) + + for rid in rids: + keys = index.keyForDocument(rid) + if isinstance(keys, str): + keys = [keys] + if not isinstance(keys, list): + continue + for key in keys: + if key not in count: + count[key] = 0 + count[key] += 1 + for rid in rids_mandatory: + keys = index.keyForDocument(rid) + if isinstance(keys, str): + keys = [keys] + if not isinstance(keys, list): + continue + for key in keys: + if key not in count_mandatory: + count_mandatory[key] = 0 + count_mandatory[key] += 1 + + results = { + "name": self.name, + "count": len(rids), + "data": {}, + } + + for key, _ in count_mandatory.items(): + results["data"][key] = count[key] if key in count else 0 + + return results diff --git a/src/plone/restapi/services/querystringsearch/get.py b/src/plone/restapi/services/querystringsearch/get.py index acd9f3647b..a4709e5608 100644 --- a/src/plone/restapi/services/querystringsearch/get.py +++ b/src/plone/restapi/services/querystringsearch/get.py @@ -8,6 +8,9 @@ from urllib import parse from zExceptions import BadRequest from zope.component import getMultiAdapter +from zope.interface import implementer +from zope.publisher.interfaces import IPublishTraverse +from plone.restapi.services.querystringsearch.facet import Facet zcatalog_version = get_distribution("Products.ZCatalog").version @@ -20,11 +23,65 @@ class QuerystringSearch: """Returns the querystring search results given a p.a.querystring data.""" - def __init__(self, context, request): + def __init__(self, context, request, params): self.context = context self.request = request + self.params = params def __call__(self): + self.setQuerybuilderParams() + querybuilder_mandatory_parameters = self.querybuilder_parameters.copy() + querybuilder_mandatory_parameters["query"] = [ + qs + for qs in self.querybuilder_parameters.get("query", []) + if "mandatory" in qs and qs["mandatory"] is True + ] + querybuilder_mandatory_parameters["rids"] = True + + # make serch work also on Plone Root + if SUPPORT_NOT_UUID_QUERIES: + querybuilder_mandatory_parameters.update( + dict(custom_query={"UID": {"not": self.context.UID()}}) + ) + querybuilder = getMultiAdapter( + (self.context, self.request), name="querybuilderresults" + ) + + brains_rids_mandatory = querybuilder(**querybuilder_mandatory_parameters) + + if len(self.params) > 0: + results = Facet( + self.context, + self.request, + name=self.params[0], + querybuilder_parameters=self.querybuilder_parameters, + brains_rids_mandatory=brains_rids_mandatory, + ).getFacet() + if results is None: + raise BadRequest("Invalid facet") + results["@id"] = ( + "%s/@querystring-search/%s" + % (self.context.absolute_url(), self.params[0]), + ) + else: + results = self.getResults() + results = getMultiAdapter((results, self.request), ISerializeToJson)( + fullobjects=self.fullobjects + ) + results["facets_count"] = {} + for facet in self.facets: + facet_results = Facet( + self.context, + self.request, + name=facet, + querybuilder_parameters=self.querybuilder_parameters, + brains_rids_mandatory=brains_rids_mandatory, + ).getFacet() + if facet_results: + results["facets_count"][facet] = facet_results + return results + + def setQuerybuilderParams(self): try: data = json_body(self.request) except DeserializationError as err: @@ -45,7 +102,9 @@ def __call__(self): limit = int(data.get("limit", 1000)) except ValueError: raise BadRequest("Invalid limit") - fullobjects = bool(data.get("fullobjects", False)) + + self.fullobjects = bool(data.get("fullobjects", False)) + self.facets = data.get("facets", []) if not query: raise BadRequest("No query supplied") @@ -53,11 +112,7 @@ def __call__(self): if sort_order: sort_order = "descending" if sort_order == "descending" else "ascending" - querybuilder = getMultiAdapter( - (self.context, self.request), name="querybuilderresults" - ) - - querybuilder_parameters = dict( + self.querybuilder_parameters = dict( query=query, brains=True, b_start=b_start, @@ -67,38 +122,49 @@ def __call__(self): limit=limit, ) - # Exclude "self" content item from the results when ZCatalog supports NOT UUID - # queries and it is called on a content object. if not IPloneSiteRoot.providedBy(self.context) and SUPPORT_NOT_UUID_QUERIES: - querybuilder_parameters.update( + self.querybuilder_parameters.update( dict(custom_query={"UID": {"not": self.context.UID()}}) ) - try: - results = querybuilder(**querybuilder_parameters) - except KeyError: - # This can happen if the query has an invalid operation, - # but plone.app.querystring doesn't raise an exception - # with specific info. - raise BadRequest("Invalid query.") - - results = getMultiAdapter((results, self.request), ISerializeToJson)( - fullobjects=fullobjects + def getResults(self): + querybuilder = getMultiAdapter( + (self.context, self.request), name="querybuilderresults" ) - return results + return querybuilder(**self.querybuilder_parameters) +@implementer(IPublishTraverse) class QuerystringSearchPost(Service): """Returns the querystring search results given a p.a.querystring data.""" + def __init__(self, context, request): + super().__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + # Treat any path segments after /@types as parameters + self.params.append(name) + return self + def reply(self): - querystring_search = QuerystringSearch(self.context, self.request) + querystring_search = QuerystringSearch(self.context, self.request, self.params) return querystring_search() +@implementer(IPublishTraverse) class QuerystringSearchGet(Service): """Returns the querystring search results given a p.a.querystring data.""" + def __init__(self, context, request): + super().__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + # Treat any path segments after /@types as parameters + self.params.append(name) + return self + def reply(self): # We need to copy the JSON query parameters from the querystring # into the request body, because that's where other code expects to find them @@ -108,5 +174,5 @@ def reply(self): # unset the get parameters self.request.form = {} - querystring_search = QuerystringSearch(self.context, self.request) + querystring_search = QuerystringSearch(self.context, self.request, self.params) return querystring_search()