diff --git a/udata/core/dataservices/search.py b/udata/core/dataservices/search.py index 4abe7e039c..b9995da0bc 100644 --- a/udata/core/dataservices/search.py +++ b/udata/core/dataservices/search.py @@ -83,6 +83,7 @@ class DataserviceSearch(ModelSearchAdapter): model = Dataservice service_class = DataserviceService consumer_class = DataserviceConsumer + configurable_size_facets = ["organization_id_with_name"] sorts = {"created": "created_at", "views": "views", "followers": "followers"} diff --git a/udata/core/dataset/search.py b/udata/core/dataset/search.py index 947d211103..fa73af9746 100644 --- a/udata/core/dataset/search.py +++ b/udata/core/dataset/search.py @@ -34,6 +34,7 @@ class DatasetSearch(ModelSearchAdapter): model = Dataset service_class = DatasetService consumer_class = DatasetConsumer + configurable_size_facets = ["organization_id_with_name"] sorts = { "created": "created_at_internal", diff --git a/udata/core/reuse/search.py b/udata/core/reuse/search.py index b10e7ec3c8..fac57f81de 100644 --- a/udata/core/reuse/search.py +++ b/udata/core/reuse/search.py @@ -25,6 +25,7 @@ class ReuseSearch(ModelSearchAdapter): model = Reuse service_class = ReuseService consumer_class = ReuseConsumer + configurable_size_facets = ["organization_id_with_name"] sorts = { "created": "created_at", diff --git a/udata/core/topic/search.py b/udata/core/topic/search.py index 316c2809e8..550a9344b8 100644 --- a/udata/core/topic/search.py +++ b/udata/core/topic/search.py @@ -22,6 +22,7 @@ class TopicSearch(ModelSearchAdapter): model = Topic service_class = TopicService consumer_class = TopicConsumer + configurable_size_facets = ["organization_id_with_name"] sorts = { "name": "name", diff --git a/udata/search/adapter.py b/udata/search/adapter.py index cd712cc831..cca5fe844a 100644 --- a/udata/search/adapter.py +++ b/udata/search/adapter.py @@ -13,6 +13,7 @@ class ModelSearchAdapter: model = None sorts = None filters = {} + configurable_size_facets = [] service_class = None consumer_class = None @@ -57,6 +58,13 @@ def as_request_parser(cls, paginate=True, store_missing: bool = True): parser.add_argument( "page_size", type=int, location="args", default=20, help="The page size" ) + for facet_name in cls.configurable_size_facets: + parser.add_argument( + f"facet_size__{facet_name}", + type=int, + location="args", + help=f"Number of {facet_name} facet values to return", + ) return parser @classmethod diff --git a/udata/search/query.py b/udata/search/query.py index 509a7e174f..48c5407c4b 100644 --- a/udata/search/query.py +++ b/udata/search/query.py @@ -11,9 +11,22 @@ DEFAULT_PAGE_SIZE = 20 # Elasticsearch default max_result_window is 10000 ES_MAX_RESULT_WINDOW = 10000 +DEFAULT_MAX_FACET_SIZE = 500 log = logging.getLogger(__name__) +def parse_facet_size(key, raw, max_facet_size): + try: + value = int(raw) + except (ValueError, TypeError): + abort(400, f"Invalid value for {key}: {raw!r} is not an integer.") + if value < 1: + abort(400, f"Invalid value for {key}: must be at least 1.") + if value > max_facet_size: + abort(400, f"Invalid value for {key}: must be at most {max_facet_size}.") + return value + + class SearchQuery: adapter = None model = None @@ -32,6 +45,11 @@ def __init__(self, params): ) self._query = params.pop("q", "") self.sort = params.pop("sort", None) + max_facet_size = current_app.config.get("MAX_FACET_SIZE", DEFAULT_MAX_FACET_SIZE) + self._facet_sizes = {} + for key in [k for k in list(params.keys()) if k.startswith("facet_size__")]: + facet_name = key[len("facet_size__") :] + self._facet_sizes[facet_name] = parse_facet_size(key, params.pop(key), max_facet_size) self._filters = {} self.extract_filters(params) @@ -81,6 +99,7 @@ def to_search_params(self): "page": self.page, "page_size": self.page_size, "sort": self.sort, + "facet_sizes": self._facet_sizes, } params.update(self._filters) return params diff --git a/udata/tests/search/test_adapter.py b/udata/tests/search/test_adapter.py index 6d5d927241..412bdf6fe4 100644 --- a/udata/tests/search/test_adapter.py +++ b/udata/tests/search/test_adapter.py @@ -560,6 +560,22 @@ def test_serialize_includes_producer_type_user(self): assert serialized["producer_type"] == [USER] +class ConfigurableSizeFacetsTest(APITestCase): + def test_facet_size_params_in_request_parser(self): + for adapter in [DatasetSearch, ReuseSearch, DataserviceSearch]: + parser = adapter.as_request_parser() + arg_names = [arg.name for arg in parser.args] + assert "facet_size__organization_id_with_name" in arg_names, ( + f"{adapter.__name__} parser is missing facet_size__organization_id_with_name — " + f"it would be silently dropped from API requests" + ) + + def test_facet_size_param_is_int(self): + parser = DatasetSearch.as_request_parser() + arg = next(a for a in parser.args if a.name == "facet_size__organization_id_with_name") + assert arg.type is int + + class DataserviceSearchAdapterTest(APITestCase): def test_serialize_includes_access_type(self): """Test that DataserviceSearch.serialize includes access_type in the serialized document""" diff --git a/udata/tests/search/test_query.py b/udata/tests/search/test_query.py index 9e6f0fcc75..ae72431dbe 100644 --- a/udata/tests/search/test_query.py +++ b/udata/tests/search/test_query.py @@ -1,4 +1,9 @@ -from udata.search.query import DEFAULT_PAGE_SIZE, SearchQuery +from udata.search.query import ( + DEFAULT_MAX_FACET_SIZE, + DEFAULT_PAGE_SIZE, + SearchQuery, + parse_facet_size, +) from udata.tests.api import APITestCase @@ -55,3 +60,84 @@ def test_search_query_to_search_params(self): assert params["sort"] == "-created" assert params["organization"] == "534fff81a3a7292c64a77e5c" assert params["tag"] == ["tag-1", "tag-2"] + + def test_facet_sizes_default_empty(self): + search_query = SearchQuery(params={}) + assert search_query._facet_sizes == {} + + def test_facet_size_param_parsed(self): + query = {"facet_size__organization_id_with_name": "200"} + search_query = SearchQuery(params=query) + assert search_query._facet_sizes == {"organization_id_with_name": 200} + + def test_multiple_facet_size_params_parsed(self): + query = {"facet_size__organization_id_with_name": "200", "facet_size__tag": "100"} + search_query = SearchQuery(params=query) + assert search_query._facet_sizes == {"organization_id_with_name": 200, "tag": 100} + + def test_facet_size_params_not_in_filters(self): + query = {"facet_size__organization_id_with_name": "200", "tag": "transport"} + search_query = SearchQuery(params=query) + assert "facet_size__organization_id_with_name" not in search_query._filters + assert search_query._filters == {"tag": "transport"} + + def test_facet_sizes_included_in_search_params(self): + query = {"facet_size__organization_id_with_name": "200", "q": "test"} + search_query = SearchQuery(params=query) + params = search_query.to_search_params() + assert params["facet_sizes"] == {"organization_id_with_name": 200} + + def test_facet_size_zero_raises_400(self): + with self.assertRaises(Exception) as ctx: + SearchQuery(params={"facet_size__tag": "0"}) + assert ctx.exception.code == 400 + + def test_facet_size_negative_raises_400(self): + with self.assertRaises(Exception) as ctx: + SearchQuery(params={"facet_size__tag": "-1"}) + assert ctx.exception.code == 400 + + def test_facet_size_non_integer_raises_400(self): + with self.assertRaises(Exception) as ctx: + SearchQuery(params={"facet_size__tag": "big"}) + assert ctx.exception.code == 400 + + def test_facet_size_exceeds_max_raises_400(self): + with self.assertRaises(Exception) as ctx: + SearchQuery(params={"facet_size__tag": str(DEFAULT_MAX_FACET_SIZE + 1)}) + assert ctx.exception.code == 400 + + def test_facet_size_at_max_is_valid(self): + search_query = SearchQuery(params={"facet_size__tag": str(DEFAULT_MAX_FACET_SIZE)}) + assert search_query._facet_sizes["tag"] == DEFAULT_MAX_FACET_SIZE + + +class ParseFacetSizeTest(APITestCase): + def test_valid_integer(self): + assert parse_facet_size("facet_size__tag", "50", 500) == 50 + + def test_non_integer_raises_400(self): + with self.assertRaises(Exception) as ctx: + parse_facet_size("facet_size__tag", "abc", 500) + assert ctx.exception.code == 400 + + def test_zero_raises_400(self): + with self.assertRaises(Exception) as ctx: + parse_facet_size("facet_size__tag", "0", 500) + assert ctx.exception.code == 400 + + def test_negative_raises_400(self): + with self.assertRaises(Exception) as ctx: + parse_facet_size("facet_size__tag", "-5", 500) + assert ctx.exception.code == 400 + + def test_exceeds_max_raises_400(self): + with self.assertRaises(Exception) as ctx: + parse_facet_size("facet_size__tag", "501", 500) + assert ctx.exception.code == 400 + + def test_at_max_is_valid(self): + assert parse_facet_size("facet_size__tag", "500", 500) == 500 + + def test_at_min_is_valid(self): + assert parse_facet_size("facet_size__tag", "1", 500) == 1 diff --git a/udata/tests/search/test_services.py b/udata/tests/search/test_services.py new file mode 100644 index 0000000000..6a8d1be3ed --- /dev/null +++ b/udata/tests/search/test_services.py @@ -0,0 +1,38 @@ +from unittest.mock import MagicMock + +from udata_search_service.services import DatasetService + + +def make_service(): + mock_client = MagicMock() + mock_client.query_datasets.return_value = (0, [], {}) + return DatasetService(mock_client), mock_client + + +def base_filters(): + return {"q": "", "page": 1, "page_size": 20, "sort": None} + + +def test_facet_sizes_passed_to_client_query(): + service, mock_client = make_service() + filters = {**base_filters(), "facet_sizes": {"organization_id_with_name": 200}} + service.search(filters) + _, kwargs = mock_client.query_datasets.call_args + assert kwargs["facet_sizes"] == {"organization_id_with_name": 200} + + +def test_empty_facet_sizes_when_not_provided(): + service, mock_client = make_service() + service.search(base_filters()) + _, kwargs = mock_client.query_datasets.call_args + assert kwargs["facet_sizes"] == {} + + +def test_facet_sizes_not_passed_as_filter(): + service, mock_client = make_service() + filters = {**base_filters(), "facet_sizes": {"tag": 100}} + service.search(filters) + args, _ = mock_client.query_datasets.call_args + # 4th positional arg is the filters dict + filters_arg = args[3] + assert "facet_sizes" not in filters_arg diff --git a/udata_search_service/search_clients.py b/udata_search_service/search_clients.py index 1cb1e1af02..0445192469 100644 --- a/udata_search_service/search_clients.py +++ b/udata_search_service/search_clients.py @@ -326,6 +326,7 @@ def query_organizations( page_size: int, filters: dict, sort: Optional[str] = None, + facet_sizes: dict = {}, ) -> Tuple[int, List[dict], dict]: search = SearchableOrganization.search() @@ -382,7 +383,12 @@ def query_organizations( ) ) - search.aggs.bucket("producer_type", "terms", field="producer_type", size=50) + search.aggs.bucket( + "producer_type", + "terms", + field="producer_type", + size=facet_sizes.get("producer_type", 50), + ) search.aggs.metric("total_count", "cardinality", field="_id") if post_filters: @@ -427,6 +433,7 @@ def query_topics( page_size: int, filters: dict, sort: Optional[str] = None, + facet_sizes: dict = {}, ) -> Tuple[int, List[dict], dict]: search = SearchableTopic.search() @@ -503,10 +510,10 @@ def get_filters_except(exclude_key): tag_agg = search.aggs.bucket( "tag_filtered", "filter", filter=query.Bool(must=tag_filters) ) - tag_agg.bucket("tag", "terms", field="tags", size=50) + tag_agg.bucket("tag", "terms", field="tags", size=facet_sizes.get("tag", 50)) tag_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("tag", "terms", field="tags", size=50) + search.aggs.bucket("tag", "terms", field="tags", size=facet_sizes.get("tag", 50)) search.aggs.metric("tag_total", "cardinality", field="_id") org_filters = get_filters_except("organization_id_with_name") @@ -515,12 +522,18 @@ def get_filters_except(exclude_key): "organization_id_with_name_filtered", "filter", filter=query.Bool(must=org_filters) ) org_agg.bucket( - "organization_id_with_name", "terms", field="organization_with_id", size=50 + "organization_id_with_name", + "terms", + field="organization_with_id", + size=facet_sizes.get("organization_id_with_name", 50), ) org_agg.metric("total", "cardinality", field="_id") else: search.aggs.bucket( - "organization_id_with_name", "terms", field="organization_with_id", size=50 + "organization_id_with_name", + "terms", + field="organization_with_id", + size=facet_sizes.get("organization_id_with_name", 50), ) search.aggs.metric("organization_id_with_name_total", "cardinality", field="_id") @@ -529,10 +542,20 @@ def get_filters_except(exclude_key): producer_agg = search.aggs.bucket( "producer_type_filtered", "filter", filter=query.Bool(must=producer_filters) ) - producer_agg.bucket("producer_type", "terms", field="producer_type", size=50) + producer_agg.bucket( + "producer_type", + "terms", + field="producer_type", + size=facet_sizes.get("producer_type", 50), + ) producer_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("producer_type", "terms", field="producer_type", size=50) + search.aggs.bucket( + "producer_type", + "terms", + field="producer_type", + size=facet_sizes.get("producer_type", 50), + ) search.aggs.metric("producer_type_total", "cardinality", field="_id") last_update_filters = get_filters_except("last_update_range") @@ -642,6 +665,7 @@ def query_datasets( page_size: int, filters: dict, sort: Optional[str] = None, + facet_sizes: dict = {}, ) -> Tuple[int, List[dict], dict]: search = SearchableDataset.search() @@ -822,10 +846,20 @@ def get_filters_except(exclude_key): format_agg = search.aggs.bucket( "format_family_filtered", "filter", filter=query.Bool(must=format_filters) ) - format_agg.bucket("format_family", "terms", field="format_family", size=50) + format_agg.bucket( + "format_family", + "terms", + field="format_family", + size=facet_sizes.get("format_family", 50), + ) format_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("format_family", "terms", field="format_family", size=50) + search.aggs.bucket( + "format_family", + "terms", + field="format_family", + size=facet_sizes.get("format_family", 50), + ) search.aggs.metric("format_family_total", "cardinality", field="_id") access_filters = get_filters_except("access_type") @@ -833,10 +867,14 @@ def get_filters_except(exclude_key): access_agg = search.aggs.bucket( "access_type_filtered", "filter", filter=query.Bool(must=access_filters) ) - access_agg.bucket("access_type", "terms", field="access_type", size=50) + access_agg.bucket( + "access_type", "terms", field="access_type", size=facet_sizes.get("access_type", 50) + ) access_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("access_type", "terms", field="access_type", size=50) + search.aggs.bucket( + "access_type", "terms", field="access_type", size=facet_sizes.get("access_type", 50) + ) search.aggs.metric("access_type_total", "cardinality", field="_id") producer_filters = get_filters_except("producer_type") @@ -844,10 +882,20 @@ def get_filters_except(exclude_key): producer_agg = search.aggs.bucket( "producer_type_filtered", "filter", filter=query.Bool(must=producer_filters) ) - producer_agg.bucket("producer_type", "terms", field="producer_type", size=50) + producer_agg.bucket( + "producer_type", + "terms", + field="producer_type", + size=facet_sizes.get("producer_type", 50), + ) producer_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("producer_type", "terms", field="producer_type", size=50) + search.aggs.bucket( + "producer_type", + "terms", + field="producer_type", + size=facet_sizes.get("producer_type", 50), + ) search.aggs.metric("producer_type_total", "cardinality", field="_id") org_name_filters = get_filters_except("organization_id_with_name") @@ -858,12 +906,18 @@ def get_filters_except(exclude_key): filter=query.Bool(must=org_name_filters), ) org_name_agg.bucket( - "organization_id_with_name", "terms", field="organization_with_id", size=50 + "organization_id_with_name", + "terms", + field="organization_with_id", + size=facet_sizes.get("organization_id_with_name", 50), ) org_name_agg.metric("total", "cardinality", field="_id") else: search.aggs.bucket( - "organization_id_with_name", "terms", field="organization_with_id", size=50 + "organization_id_with_name", + "terms", + field="organization_with_id", + size=facet_sizes.get("organization_id_with_name", 50), ) search.aggs.metric("organization_id_with_name_total", "cardinality", field="_id") @@ -901,10 +955,10 @@ def get_filters_except(exclude_key): tag_agg = search.aggs.bucket( "tag_filtered", "filter", filter=query.Bool(must=tag_filters) ) - tag_agg.bucket("tag", "terms", field="tags", size=50) + tag_agg.bucket("tag", "terms", field="tags", size=facet_sizes.get("tag", 50)) tag_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("tag", "terms", field="tags", size=50) + search.aggs.bucket("tag", "terms", field="tags", size=facet_sizes.get("tag", 50)) search.aggs.metric("tag_total", "cardinality", field="_id") license_filters = get_filters_except("license") @@ -912,10 +966,14 @@ def get_filters_except(exclude_key): license_agg = search.aggs.bucket( "license_filtered", "filter", filter=query.Bool(must=license_filters) ) - license_agg.bucket("license", "terms", field="license", size=50) + license_agg.bucket( + "license", "terms", field="license", size=facet_sizes.get("license", 50) + ) license_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("license", "terms", field="license", size=50) + search.aggs.bucket( + "license", "terms", field="license", size=facet_sizes.get("license", 50) + ) search.aggs.metric("license_total", "cardinality", field="_id") format_filters = get_filters_except("format") @@ -923,10 +981,12 @@ def get_filters_except(exclude_key): format_agg = search.aggs.bucket( "format_filtered", "filter", filter=query.Bool(must=format_filters) ) - format_agg.bucket("format", "terms", field="format", size=50) + format_agg.bucket("format", "terms", field="format", size=facet_sizes.get("format", 50)) format_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("format", "terms", field="format", size=50) + search.aggs.bucket( + "format", "terms", field="format", size=facet_sizes.get("format", 50) + ) search.aggs.metric("format_total", "cardinality", field="_id") schema_filters = get_filters_except("schema") @@ -934,10 +994,12 @@ def get_filters_except(exclude_key): schema_agg = search.aggs.bucket( "schema_filtered", "filter", filter=query.Bool(must=schema_filters) ) - schema_agg.bucket("schema", "terms", field="schema", size=50) + schema_agg.bucket("schema", "terms", field="schema", size=facet_sizes.get("schema", 50)) schema_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("schema", "terms", field="schema", size=50) + search.aggs.bucket( + "schema", "terms", field="schema", size=facet_sizes.get("schema", 50) + ) search.aggs.metric("schema_total", "cardinality", field="_id") geozone_filters = get_filters_except("geozone") @@ -945,10 +1007,14 @@ def get_filters_except(exclude_key): geozone_agg = search.aggs.bucket( "geozone_filtered", "filter", filter=query.Bool(must=geozone_filters) ) - geozone_agg.bucket("geozone", "terms", field="geozones", size=50) + geozone_agg.bucket( + "geozone", "terms", field="geozones", size=facet_sizes.get("geozone", 50) + ) geozone_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("geozone", "terms", field="geozones", size=50) + search.aggs.bucket( + "geozone", "terms", field="geozones", size=facet_sizes.get("geozone", 50) + ) search.aggs.metric("geozone_total", "cardinality", field="_id") granularity_filters = get_filters_except("granularity") @@ -956,10 +1022,14 @@ def get_filters_except(exclude_key): granularity_agg = search.aggs.bucket( "granularity_filtered", "filter", filter=query.Bool(must=granularity_filters) ) - granularity_agg.bucket("granularity", "terms", field="granularity", size=50) + granularity_agg.bucket( + "granularity", "terms", field="granularity", size=facet_sizes.get("granularity", 50) + ) granularity_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("granularity", "terms", field="granularity", size=50) + search.aggs.bucket( + "granularity", "terms", field="granularity", size=facet_sizes.get("granularity", 50) + ) search.aggs.metric("granularity_total", "cardinality", field="_id") badge_filters = get_filters_except("badge") @@ -967,10 +1037,10 @@ def get_filters_except(exclude_key): badge_agg = search.aggs.bucket( "badge_filtered", "filter", filter=query.Bool(must=badge_filters) ) - badge_agg.bucket("badge", "terms", field="badges", size=50) + badge_agg.bucket("badge", "terms", field="badges", size=facet_sizes.get("badge", 50)) badge_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("badge", "terms", field="badges", size=50) + search.aggs.bucket("badge", "terms", field="badges", size=facet_sizes.get("badge", 50)) search.aggs.metric("badge_total", "cardinality", field="_id") topics_filters = get_filters_except("topics") @@ -978,10 +1048,12 @@ def get_filters_except(exclude_key): topics_agg = search.aggs.bucket( "topics_filtered", "filter", filter=query.Bool(must=topics_filters) ) - topics_agg.bucket("topics", "terms", field="topics", size=50) + topics_agg.bucket("topics", "terms", field="topics", size=facet_sizes.get("topics", 50)) topics_agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket("topics", "terms", field="topics", size=50) + search.aggs.bucket( + "topics", "terms", field="topics", size=facet_sizes.get("topics", 50) + ) search.aggs.metric("topics_total", "cardinality", field="_id") post_filters = [] @@ -1063,6 +1135,7 @@ def query_reuses( page_size: int, filters: dict, sort: Optional[str] = None, + facet_sizes: dict = {}, ) -> Tuple[int, List[dict], dict]: search = SearchableReuse.search() @@ -1238,10 +1311,12 @@ def get_filters_except(exclude_key: str): agg = search.aggs.bucket( f"{agg_name}_filtered", "filter", filter=query.Bool(must=f) ) - agg.bucket(agg_name, "terms", field=es_field, size=50) + agg.bucket(agg_name, "terms", field=es_field, size=facet_sizes.get(agg_name, 50)) agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket(agg_name, "terms", field=es_field, size=50) + search.aggs.bucket( + agg_name, "terms", field=es_field, size=facet_sizes.get(agg_name, 50) + ) search.aggs.metric(f"{agg_name}_total", "cardinality", field="_id") f = get_filters_except("last_update_range") @@ -1355,6 +1430,7 @@ def query_dataservices( page_size: int, filters: dict, sort: Optional[str] = None, + facet_sizes: dict = {}, ): search = SearchableDataservice.search() @@ -1524,10 +1600,12 @@ def get_filters_except(exclude_key: str): agg = search.aggs.bucket( f"{agg_name}_filtered", "filter", filter=query.Bool(must=f) ) - agg.bucket(agg_name, "terms", field=es_field, size=50) + agg.bucket(agg_name, "terms", field=es_field, size=facet_sizes.get(agg_name, 50)) agg.metric("total", "cardinality", field="_id") else: - search.aggs.bucket(agg_name, "terms", field=es_field, size=50) + search.aggs.bucket( + agg_name, "terms", field=es_field, size=facet_sizes.get(agg_name, 50) + ) search.aggs.metric(f"{agg_name}_total", "cardinality", field="_id") # last_update facet diff --git a/udata_search_service/services.py b/udata_search_service/services.py index 7769773eb5..ac2f36f407 100644 --- a/udata_search_service/services.py +++ b/udata_search_service/services.py @@ -38,13 +38,14 @@ def search(self, filters: dict) -> Tuple[List[EntityBase], int, int, dict]: page_size = filters.pop("page_size") search_text = filters.pop("q") sort = self.format_sort(filters.pop("sort", None)) + facet_sizes = filters.pop("facet_sizes", {}) offset = page_size * (page - 1) if page > 1 else 0 self.format_filters(filters) results_number, search_results, facets = self._client_query( - search_text, offset, page_size, filters, sort + search_text, offset, page_size, filters, sort, facet_sizes=facet_sizes ) results = [self.entity_class.load_from_dict(hit) for hit in search_results] total_pages = ceil(results_number / page_size) or 1