diff --git a/datasette/facets.py b/datasette/facets.py index 01628760..ff6396d7 100644 --- a/datasette/facets.py +++ b/datasette/facets.py @@ -101,6 +101,14 @@ class Facet: # [('_foo', 'bar'), ('_foo', '2'), ('empty', '')] return urllib.parse.parse_qsl(self.request.query_string, keep_blank_values=True) + def get_facet_size(self): + facet_size = self.ds.setting("default_facet_size") + max_returned_rows = self.ds.setting("max_returned_rows") + custom_facet_size = self.request.args.get("_facet_size") + if custom_facet_size and custom_facet_size.isdigit(): + facet_size = int(custom_facet_size) + return min(facet_size, max_returned_rows) + async def suggest(self): return [] @@ -136,7 +144,7 @@ class ColumnFacet(Facet): async def suggest(self): row_count = await self.get_row_count() columns = await self.get_columns(self.sql, self.params) - facet_size = self.ds.setting("default_facet_size") + facet_size = self.get_facet_size() suggested_facets = [] already_enabled = [c["config"]["simple"] for c in self.get_configs()] for column in columns: @@ -186,7 +194,7 @@ class ColumnFacet(Facet): qs_pairs = self.get_querystring_pairs() - facet_size = self.ds.setting("default_facet_size") + facet_size = self.get_facet_size() for source_and_config in self.get_configs(): config = source_and_config["config"] source = source_and_config["source"] @@ -338,7 +346,7 @@ class ArrayFacet(Facet): facet_results = {} facets_timed_out = [] - facet_size = self.ds.setting("default_facet_size") + facet_size = self.get_facet_size() for source_and_config in self.get_configs(): config = source_and_config["config"] source = source_and_config["source"] @@ -449,7 +457,7 @@ class DateFacet(Facet): facet_results = {} facets_timed_out = [] args = dict(self.get_querystring_pairs()) - facet_size = self.ds.setting("default_facet_size") + facet_size = self.get_facet_size() for source_and_config in self.get_configs(): config = source_and_config["config"] source = source_and_config["source"] diff --git a/docs/facets.rst b/docs/facets.rst index 3f2f6879..5061d11c 100644 --- a/docs/facets.rst +++ b/docs/facets.rst @@ -84,6 +84,8 @@ This works for both the HTML interface and the ``.json`` view. When enabled, fac If Datasette detects that a column is a foreign key, the ``"label"`` property will be automatically derived from the detected label column on the referenced table. +The default number of facet results returned is 30, controlled by the :ref:`setting_default_facet_size` setting. You can increase this on an individual page by adding ``?_facet_size=100`` to the query string, up to a maximum of :ref:`setting_max_returned_rows` (which defaults to 1000). + Facets in metadata.json ----------------------- diff --git a/docs/json_api.rst b/docs/json_api.rst index 0f88cb07..9efacf35 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -371,6 +371,12 @@ Special table arguments Pagination by continuation token - pass the token that was returned in the ``"next"`` property by the previous page. +``?_facet=column`` + Facet by column. Can be applied multiple times, see :ref:`facets`. Only works on the default JSON output, not on any of the custom shapes. + +``?_facet_size=100`` + Increase the number of facet results returned for each facet. + ``?_trace=1`` Turns on tracing for this page: SQL queries executed during the request will be gathered and included in the response, either in a new ``"_traces"`` key diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 0a176add..7a1645ec 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -619,6 +619,7 @@ Each Facet subclass implements a new type of facet operation. The class should l # using self.sql and self.params as the starting point facet_results = {} facets_timed_out = [] + facet_size = self.get_facet_size() # Do some calculations here... for column in columns_selected_for_facet: try: diff --git a/tests/test_facets.py b/tests/test_facets.py index 31518682..a1a14e71 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -347,3 +347,55 @@ async def test_json_array_with_blanks_and_nulls(): "toggle_url": "http://localhost/test_json_array/foo.json?_facet_array=json_column", } ] + + +@pytest.mark.asyncio +async def test_facet_size(): + ds = Datasette([], memory=True, config={"max_returned_rows": 50}) + db = ds.add_database(Database(ds, memory_name="test_facet_size")) + await db.execute_write( + "create table neighbourhoods(city text, neighbourhood text)", block=True + ) + for i in range(1, 51): + for j in range(1, 4): + await db.execute_write( + "insert into neighbourhoods (city, neighbourhood) values (?, ?)", + ["City {}".format(i), "Neighbourhood {}".format(j)], + block=True, + ) + response = await ds.client.get("/test_facet_size/neighbourhoods.json") + data = response.json() + assert data["suggested_facets"] == [ + { + "name": "neighbourhood", + "toggle_url": "http://localhost/test_facet_size/neighbourhoods.json?_facet=neighbourhood", + } + ] + # Bump up _facet_size= to suggest city too + response2 = await ds.client.get( + "/test_facet_size/neighbourhoods.json?_facet_size=50" + ) + data2 = response2.json() + assert sorted(data2["suggested_facets"], key=lambda f: f["name"]) == [ + { + "name": "city", + "toggle_url": "http://localhost/test_facet_size/neighbourhoods.json?_facet_size=50&_facet=city", + }, + { + "name": "neighbourhood", + "toggle_url": "http://localhost/test_facet_size/neighbourhoods.json?_facet_size=50&_facet=neighbourhood", + }, + ] + # Facet by city should return expected number of results + response3 = await ds.client.get( + "/test_facet_size/neighbourhoods.json?_facet_size=50&_facet=city" + ) + data3 = response3.json() + assert len(data3["facet_results"]["city"]["results"]) == 50 + # Reduce max_returned_rows and check that it's respected + ds._settings["max_returned_rows"] = 20 + response4 = await ds.client.get( + "/test_facet_size/neighbourhoods.json?_facet_size=50&_facet=city" + ) + data4 = response4.json() + assert len(data4["facet_results"]["city"]["results"]) == 20