Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add facet count endpoint #1637

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1637.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat(search): show facets count and delete the facets results that don't meet the criterias @razvanMiu @dobri1408
99 changes: 99 additions & 0 deletions src/plone/restapi/services/querystringsearch/facet.py
Original file line number Diff line number Diff line change
@@ -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
112 changes: 89 additions & 23 deletions src/plone/restapi/services/querystringsearch/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -45,19 +102,17 @@ 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")

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,
Expand All @@ -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
Expand All @@ -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()
Loading