From b25035e0d75bfa156bc234907ecfa9f215a75bd2 Mon Sep 17 00:00:00 2001 From: Marc-AntoineA Date: Wed, 19 Jun 2024 20:06:39 +0200 Subject: [PATCH 1/5] wip: working on creating api for charts. It works :) --- app/_types.py | 22 +++++- app/api.py | 3 + app/charts.py | 101 ++++++++++++++++++++++++++++ app/query.py | 3 + app/search.py | 9 ++- app/validations.py | 12 ++++ frontend/src/events.ts | 1 + frontend/src/mixins/search-ctl.ts | 3 + frontend/src/search-chart.ts | 108 +----------------------------- 9 files changed, 153 insertions(+), 109 deletions(-) create mode 100644 app/charts.py diff --git a/app/_types.py b/app/_types.py index 8195883d..985b3564 100644 --- a/app/_types.py +++ b/app/_types.py @@ -8,7 +8,7 @@ from . import config from .utils import str_utils -from .validations import check_all_facets_fields_are_agg, check_index_id_is_defined +from .validations import check_all_charts_are_valid, check_all_facets_fields_are_agg, check_index_id_is_defined #: A precise expectation of what mappings looks like in json. #: (dict where keys are always of type `str`). @@ -42,6 +42,9 @@ class FacetInfo(BaseModel): FacetsInfos = dict[str, FacetInfo] """Information about facets for a search result""" +ChartsInfos = dict[str, JSONType] +"""Information about facets for a search result""" + FacetsFilters = dict[str, list[str]] """Data about selected filters for each facet: facet name -> list of values""" @@ -67,6 +70,7 @@ class SuccessSearchResponse(BaseModel): hits: list[JSONType] aggregations: JSONType | None = None facets: FacetsInfos | None = None + charts: ChartsInfos | None = None page: int page_size: int page_count: int @@ -206,6 +210,13 @@ class SearchParameters(BaseModel): If None (default) no facets are returned.""" ), ] = None + charts: Annotated[ + list[str] | None, + Query( + description="""Name of vega representations to return in the response. + If None (default) no charts are returrned.""" + ) + ] = None sort_params: Annotated[ JSONType | None, Query( @@ -308,6 +319,14 @@ def check_facets_are_valid(self): raise ValueError(errors) return self + @model_validator(mode="after") + def check_charts_are_valid(self): + """Check that the graph names are valid.""" + errors = check_all_charts_are_valid(self.charts) + if errors: + raise ValueError(errors) + return self + @model_validator(mode="after") def check_max_results(self): """Check we don't ask too many results at once""" @@ -352,4 +371,5 @@ class GetSearchParamsTypes: fields = _annotation_new_type(str, SEARCH_PARAMS_ANN["fields"]) sort_by = SEARCH_PARAMS_ANN["sort_by"] facets = _annotation_new_type(str, SEARCH_PARAMS_ANN["facets"]) + charts = _annotation_new_type(str, SEARCH_PARAMS_ANN["charts"]) index_id = SEARCH_PARAMS_ANN["index_id"] diff --git a/app/api.py b/app/api.py index 11599dd0..3f592e85 100644 --- a/app/api.py +++ b/app/api.py @@ -107,12 +107,14 @@ def search_get( fields: GetSearchParamsTypes.fields = None, sort_by: GetSearchParamsTypes.sort_by = None, facets: GetSearchParamsTypes.facets = None, + charts: GetSearchParamsTypes.charts = None, index_id: GetSearchParamsTypes.index_id = None, ) -> SearchResponse: # str to lists langs_list = langs.split(",") if langs else ["en"] fields_list = fields.split(",") if fields else None facets_list = facets.split(",") if facets else None + charts_list = charts.split(",") if charts else None # create SearchParameters object try: search_parameters = SearchParameters( @@ -123,6 +125,7 @@ def search_get( fields=fields_list, sort_by=sort_by, facets=facets_list, + charts=charts_list, index_id=index_id, ) return app_search.search(search_parameters) diff --git a/app/charts.py b/app/charts.py new file mode 100644 index 00000000..7d25c56d --- /dev/null +++ b/app/charts.py @@ -0,0 +1,101 @@ +from ._types import ( + SuccessSearchResponse, +) + + +def build_charts( + search_result: SuccessSearchResponse, + charts_names: list[str] | None, +) -> str: + charts = {} + if charts_names is None: + return charts + + for chart_name in charts_names: + buckets = search_result.aggregations[chart_name]['buckets'] + + values = [{ 'category': bucket['key'], 'amount': bucket['doc_count'] } for bucket in buckets] + charts[chart_name] = { + "$schema": 'https://vega.github.io/schema/vega/v5.json', + "title": "test", + "autosize": {"type": 'fit', "contains": 'padding'}, + "signals": [ + { + "name": 'width', + "init": 'containerSize()[0]', + "on": [{"events": 'window:resize', "update": 'containerSize()[0]'}], + }, + { + "name": 'tooltip', + "value": {}, + "on": [ + {"events": 'rect:pointerover', "update": 'datum'}, + {"events": 'rect:pointerout', "update": '{}'}, + ], + }, + ], + "height": 140, + "padding": 5, + "data": [ + { + "name": 'table', + "values": values, + }, + ], + + "scales": [ + { + "name": 'xscale', + "type": 'band', + "domain": {"data": 'table', "field": 'category'}, + "range": 'width', + "padding": 0.05, + "round": True, + }, + { + "name": 'yscale', + "domain": {"data": 'table', "field": 'amount'}, + "nice": True, + "range": 'height', + }, + ], + "axes": [{"orient": 'bottom', "scale": 'xscale', "domain": False, "ticks": False}], + "marks": [ + { + "type": 'rect', + "from": {"data": 'table'}, + "encode": { + "enter": { + "x": {"scale": 'xscale', "field": 'category'}, + "width": {"scale": 'xscale', "band": 1}, + "y": {"scale": 'yscale', "field": 'amount'}, + "y2": {"scale": 'yscale', "value": 0}, + }, + "update": { + "fill": {"value": 'steelblue'}, + }, + "hover": { + "fill": {"value": 'red'}, + }, + }, + }, + { + "type": 'text', + "encode": { + "enter": { + "align": {"value": 'center'}, + "baseline": {"value": 'bottom'}, + "fill": {"value": '#333'}, + }, + "update": { + "x": {"scale": 'xscale', "signal": 'tooltip.category', "band": 0.5}, + "y": {"scale": 'yscale', "signal": 'tooltip.amount', "offset": -2}, + "text": {"signal": 'tooltip.amount'}, + "fillOpacity": [{"test": 'datum === tooltip', "value": 0}, {"value": 1}], + }, + }, + }, + ], + } + + return charts \ No newline at end of file diff --git a/app/query.py b/app/query.py index 97c67f63..5b504e95 100644 --- a/app/query.py +++ b/app/query.py @@ -291,6 +291,9 @@ def create_aggregation_clauses( if field.bucket_agg: # TODO - aggregation might depend on agg type or field type clauses[field.name] = A("terms", field=field.name) + clauses['nutriscore_grade'] = A("terms", field='nutriscore_grade') + clauses['nova_group'] = A("terms", field='nova_group') + # TODO: add A("terms", field=field.name) for field.name in charts return clauses diff --git a/app/search.py b/app/search.py index 08e09a6f..491573ba 100644 --- a/app/search.py +++ b/app/search.py @@ -4,6 +4,7 @@ from . import config from ._types import SearchParameters, SearchResponse, SuccessSearchResponse from .facets import build_facets +from .charts import build_charts from .postprocessing import BaseResultProcessor, load_result_processor from .query import build_elasticsearch_query_builder, build_search_query, execute_query @@ -32,18 +33,20 @@ def search( params: SearchParameters, ) -> SearchResponse: """Run a search""" + print('PARAMS', params) result_processor = cast( BaseResultProcessor, RESULT_PROCESSORS[params.valid_index_id] ) logger.debug( "Received search query: q='%s', langs='%s', page=%d, " - "page_size=%d, fields='%s', sort_by='%s'", + "page_size=%d, fields='%s', sort_by='%s', charts='%s'", params.q, params.langs_set, params.page, params.page_size, params.fields, params.sort_by, + params.charts, ) index_config = params.index_config query = build_search_query( @@ -65,10 +68,14 @@ def search( page_size=params.page_size, projection=projection, ) + # print(search_result) if isinstance(search_result, SuccessSearchResponse): search_result.facets = build_facets( search_result, query, params.main_lang, index_config, params.facets ) + print(params) + search_result.charts = build_charts(search_result, params.charts) + print(search_result.charts) # remove aggregations to avoid sending too much information search_result.aggregations = None return search_result diff --git a/app/validations.py b/app/validations.py index 9898e005..5c2b02d4 100644 --- a/app/validations.py +++ b/app/validations.py @@ -40,3 +40,15 @@ def check_all_facets_fields_are_agg( elif not index_config.fields[field_name].bucket_agg: errors.append(f"Non aggregation field name in facets: {field_name}") return errors + + +def check_all_charts_are_valid(charts: list[str] | None) -> list[str]: + """Check all charts are valid, + that is, are in the hardcoded list""" + errors: list[str] = [] + if charts is None: + return errors + for field_name in charts: + if field_name not in ["nutriscore_grade", "nova_group", "ecoscore_grade"]: + errors.append(f"Unknown field name in charts: {field_name}") + return errors diff --git a/frontend/src/events.ts b/frontend/src/events.ts index 0cc7a208..bef47bf8 100644 --- a/frontend/src/events.ts +++ b/frontend/src/events.ts @@ -15,6 +15,7 @@ export interface SearchResultDetail extends BaseSearchDetail { pageSize: number; currentPage: number; facets: Object; // FIXME: we could be more precise + charts: Object; // FIXME: we could be more precise } /** diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts index 3f04a82e..c420d100 100644 --- a/frontend/src/mixins/search-ctl.ts +++ b/frontend/src/mixins/search-ctl.ts @@ -292,11 +292,13 @@ export const SearchaliciousSearchMixin = >( page?: string; index?: string; facets?: string; + charts?: string; } = { q: queryParts.join(' '), langs: this.langs, page_size: this.pageSize.toString(), index: this.index, + charts: ['nutriscore_grade', 'nova_group', 'ecoscore_grade'].join(',') }; if (page) { params.page = page.toString(); @@ -330,6 +332,7 @@ export const SearchaliciousSearchMixin = >( currentPage: this._currentPage!, pageSize: this.pageSize, facets: data.facets, + charts: data.charts, }; this.dispatchEvent( new CustomEvent(SearchaliciousEvents.NEW_RESULT, { diff --git a/frontend/src/search-chart.ts b/frontend/src/search-chart.ts index f2e261d5..a8795148 100644 --- a/frontend/src/search-chart.ts +++ b/frontend/src/search-chart.ts @@ -65,113 +65,7 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( this.categories.map((category) => [category, 0]) ); - for (const result of event.detail.results) { - // We use ts-ignore here but it will be removed as soon as - // vega logic will be moved in the api - // @ts-ignore - values[result[this.key]] += 1; - } - - // Vega is used as a JSON visualization grammar - // Doc: https://vega.github.io/vega/docs/ - // It would have been possible to use higher lever vega-lite API, - // which is able to write vega specifications but it's probably too - // much for our usage - // Inspired by: https://vega.github.io/vega/examples/bar-chart/ - - // I recommend to search on Internet for specific uses like: - // * How to make vega responsive: - // Solution: using signals and auto-size - // https://gist.github.com/donghaoren/023b2246569e8f0615017507b473e55e - // * How to hide vertical axis: do not add { scale: yscale, ...} in axes section - - this.vegaRepresentation = { - $schema: 'https://vega.github.io/schema/vega/v5.json', - title: this.label, - // @ts-ignore - // width: container.offsetWidth, - autosize: {type: 'fit', contains: 'padding'}, - signals: [ - { - name: 'width', - init: 'containerSize()[0]', - on: [{events: 'window:resize', update: 'containerSize()[0]'}], - }, - { - name: 'tooltip', - value: {}, - on: [ - {events: 'rect:pointerover', update: 'datum'}, - {events: 'rect:pointerout', update: '{}'}, - ], - }, - ], - height: 140, - padding: 5, - data: [ - { - name: 'table', - values: Array.from(Object.entries(values), ([key, value]) => ({ - category: key, - amount: value, - })), - }, - ], - - scales: [ - { - name: 'xscale', - type: 'band', - domain: {data: 'table', field: 'category'}, - range: 'width', - padding: 0.05, - round: true, - }, - { - name: 'yscale', - domain: {data: 'table', field: 'amount'}, - nice: true, - range: 'height', - }, - ], - axes: [{orient: 'bottom', scale: 'xscale', domain: false, ticks: false}], - marks: [ - { - type: 'rect', - from: {data: 'table'}, - encode: { - enter: { - x: {scale: 'xscale', field: 'category'}, - width: {scale: 'xscale', band: 1}, - y: {scale: 'yscale', field: 'amount'}, - y2: {scale: 'yscale', value: 0}, - }, - update: { - fill: {value: 'steelblue'}, - }, - hover: { - fill: {value: 'red'}, - }, - }, - }, - { - type: 'text', - encode: { - enter: { - align: {value: 'center'}, - baseline: {value: 'bottom'}, - fill: {value: '#333'}, - }, - update: { - x: {scale: 'xscale', signal: 'tooltip.category', band: 0.5}, - y: {scale: 'yscale', signal: 'tooltip.amount', offset: -2}, - text: {signal: 'tooltip.amount'}, - fillOpacity: [{test: 'datum === tooltip', value: 0}, {value: 1}], - }, - }, - }, - ], - }; + this.vegaRepresentation = event.detail.charts[this.key!]; } testVegaInstalled() { From 91d3f7203c08ae7019f7f142bbcd4ec5f45824ac Mon Sep 17 00:00:00 2001 From: Marc-AntoineA Date: Mon, 24 Jun 2024 16:16:34 +0200 Subject: [PATCH 2/5] feat: improve PR, almost done * any agg: true search field can be used; * code mutulization with facets ; * list of charts is listed from list --- app/_types.py | 12 +- app/charts.py | 179 +++++++++++++---------- app/query.py | 15 +- app/search.py | 6 +- app/validations.py | 26 +--- frontend/public/off.html | 6 +- frontend/src/mixins/search-ctl.ts | 25 +++- frontend/src/search-chart.ts | 25 ++-- frontend/src/test/search-pages_test.ts | 1 + frontend/src/test/search-results_test.ts | 1 + 10 files changed, 160 insertions(+), 136 deletions(-) diff --git a/app/_types.py b/app/_types.py index 79ea7bfe..fa7cef04 100644 --- a/app/_types.py +++ b/app/_types.py @@ -8,11 +8,7 @@ from . import config from .utils import str_utils -from .validations import ( - check_all_charts_are_valid, - check_all_facets_fields_are_agg, - check_index_id_is_defined, -) +from .validations import check_all_values_are_fields_agg, check_index_id_is_defined #: A precise expectation of what mappings looks like in json. #: (dict where keys are always of type `str`). @@ -47,7 +43,7 @@ class FacetInfo(BaseModel): """Information about facets for a search result""" ChartsInfos = dict[str, JSONType] -"""Information about facets for a search result""" +"""Information about charts for a search result""" FacetsFilters = dict[str, list[str]] """Data about selected filters for each facet: facet name -> list of values""" @@ -319,7 +315,7 @@ def sort_by_scripts_needs_params(self): @model_validator(mode="after") def check_facets_are_valid(self): """Check that the facets names are valid.""" - errors = check_all_facets_fields_are_agg(self.index_id, self.facets) + errors = check_all_values_are_fields_agg(self.index_id, self.facets) if errors: raise ValueError(errors) return self @@ -327,7 +323,7 @@ def check_facets_are_valid(self): @model_validator(mode="after") def check_charts_are_valid(self): """Check that the graph names are valid.""" - errors = check_all_charts_are_valid(self.charts) + errors = check_all_values_are_fields_agg(self.index_id, self.charts) if errors: raise ValueError(errors) return self diff --git a/app/charts.py b/app/charts.py index 7d25c56d..6f66ec19 100644 --- a/app/charts.py +++ b/app/charts.py @@ -1,6 +1,4 @@ -from ._types import ( - SuccessSearchResponse, -) +from ._types import SuccessSearchResponse def build_charts( @@ -12,90 +10,109 @@ def build_charts( return charts for chart_name in charts_names: - buckets = search_result.aggregations[chart_name]['buckets'] + buckets = search_result.aggregations[chart_name]["buckets"] - values = [{ 'category': bucket['key'], 'amount': bucket['doc_count'] } for bucket in buckets] - charts[chart_name] = { - "$schema": 'https://vega.github.io/schema/vega/v5.json', - "title": "test", - "autosize": {"type": 'fit', "contains": 'padding'}, - "signals": [ - { - "name": 'width', - "init": 'containerSize()[0]', - "on": [{"events": 'window:resize', "update": 'containerSize()[0]'}], - }, - { - "name": 'tooltip', - "value": {}, - "on": [ - {"events": 'rect:pointerover', "update": 'datum'}, - {"events": 'rect:pointerout', "update": '{}'}, - ], - }, - ], - "height": 140, - "padding": 5, - "data": [ - { - "name": 'table', - "values": values, - }, - ], + # Filter unknown values + values = [ + {"category": bucket["key"], "amount": bucket["doc_count"]} + for bucket in buckets + if bucket["key"] != "unknown" + ] + values.sort(key=lambda x: x["category"]) - "scales": [ - { - "name": 'xscale', - "type": 'band', - "domain": {"data": 'table', "field": 'category'}, - "range": 'width', - "padding": 0.05, - "round": True, - }, - { - "name": 'yscale', - "domain": {"data": 'table', "field": 'amount'}, - "nice": True, - "range": 'height', - }, - ], - "axes": [{"orient": 'bottom', "scale": 'xscale', "domain": False, "ticks": False}], - "marks": [ - { - "type": 'rect', - "from": {"data": 'table'}, - "encode": { - "enter": { - "x": {"scale": 'xscale', "field": 'category'}, - "width": {"scale": 'xscale', "band": 1}, - "y": {"scale": 'yscale', "field": 'amount'}, - "y2": {"scale": 'yscale', "value": 0}, + charts[chart_name] = { + "$schema": "https://vega.github.io/schema/vega/v5.json", + "title": chart_name, + "autosize": {"type": "fit", "contains": "padding"}, + "signals": [ + { + "name": "width", + "init": "containerSize()[0]", + "on": [{"events": "window:resize", "update": "containerSize()[0]"}], + }, + { + "name": "tooltip", + "value": {}, + "on": [ + {"events": "rect:pointerover", "update": "datum"}, + {"events": "rect:pointerout", "update": "{}"}, + ], + }, + ], + "height": 140, + "padding": 5, + "data": [ + { + "name": "table", + "values": values, }, - "update": { - "fill": {"value": 'steelblue'}, + ], + "scales": [ + { + "name": "xscale", + "type": "band", + "domain": {"data": "table", "field": "category"}, + "range": "width", + "padding": 0.05, + "round": True, }, - "hover": { - "fill": {"value": 'red'}, + { + "name": "yscale", + "domain": {"data": "table", "field": "amount"}, + "nice": True, + "range": "height", }, - }, - }, - { - "type": 'text', - "encode": { - "enter": { - "align": {"value": 'center'}, - "baseline": {"value": 'bottom'}, - "fill": {"value": '#333'}, + ], + "axes": [ + {"orient": "bottom", "scale": "xscale", "domain": False, "ticks": False} + ], + "marks": [ + { + "type": "rect", + "from": {"data": "table"}, + "encode": { + "enter": { + "x": {"scale": "xscale", "field": "category"}, + "width": {"scale": "xscale", "band": 1}, + "y": {"scale": "yscale", "field": "amount"}, + "y2": {"scale": "yscale", "value": 0}, + }, + "update": { + "fill": {"value": "steelblue"}, + }, + "hover": { + "fill": {"value": "red"}, + }, + }, }, - "update": { - "x": {"scale": 'xscale', "signal": 'tooltip.category', "band": 0.5}, - "y": {"scale": 'yscale', "signal": 'tooltip.amount', "offset": -2}, - "text": {"signal": 'tooltip.amount'}, - "fillOpacity": [{"test": 'datum === tooltip', "value": 0}, {"value": 1}], + { + "type": "text", + "encode": { + "enter": { + "align": {"value": "center"}, + "baseline": {"value": "bottom"}, + "fill": {"value": "#333"}, + }, + "update": { + "x": { + "scale": "xscale", + "signal": "tooltip.category", + "band": 0.5, + }, + "y": { + "scale": "yscale", + "signal": "tooltip.amount", + "offset": -2, + }, + "text": {"signal": "tooltip.amount"}, + "fillOpacity": [ + {"test": "datum === tooltip", "value": 0}, + {"value": 1}, + ], + }, + }, }, - }, - }, - ], + ], } - return charts \ No newline at end of file + return charts diff --git a/app/query.py b/app/query.py index 40d6adda..3c730f20 100644 --- a/app/query.py +++ b/app/query.py @@ -281,22 +281,19 @@ def parse_sort_by_script( def create_aggregation_clauses( - config: IndexConfig, facets: list[str] | None + config: IndexConfig, fields: list[str] | None ) -> dict[str, Agg]: """Create term bucket aggregation clauses for all fields corresponding to facets, as defined in the config """ clauses = {} - if facets is not None: - for field_name in facets: + if fields is not None: + for field_name in fields: field = config.fields[field_name] if field.bucket_agg: # TODO - aggregation might depend on agg type or field type clauses[field.name] = A("terms", field=field.name) - clauses["nutriscore_grade"] = A("terms", field="nutriscore_grade") - clauses["nova_group"] = A("terms", field="nova_group") - # TODO: add A("terms", field=field.name) for field.name in charts return clauses @@ -340,7 +337,11 @@ def build_es_query( if q.filter_query: es_query = es_query.query("bool", filter=q.filter_query) - for agg_name, agg in create_aggregation_clauses(config, params.facets).items(): + # TODO: bug if one is None + agg_fields = set(params.facets) if params.facets is not None else set() + if params.charts is not None: + agg_fields.update(params.charts) + for agg_name, agg in create_aggregation_clauses(config, agg_fields).items(): es_query.aggs.bucket(agg_name, agg) sort_by: JSONType | str | None = None diff --git a/app/search.py b/app/search.py index 491573ba..51067468 100644 --- a/app/search.py +++ b/app/search.py @@ -3,8 +3,8 @@ from . import config from ._types import SearchParameters, SearchResponse, SuccessSearchResponse -from .facets import build_facets from .charts import build_charts +from .facets import build_facets from .postprocessing import BaseResultProcessor, load_result_processor from .query import build_elasticsearch_query_builder, build_search_query, execute_query @@ -33,7 +33,7 @@ def search( params: SearchParameters, ) -> SearchResponse: """Run a search""" - print('PARAMS', params) + print("PARAMS", params) result_processor = cast( BaseResultProcessor, RESULT_PROCESSORS[params.valid_index_id] ) @@ -77,5 +77,5 @@ def search( search_result.charts = build_charts(search_result, params.charts) print(search_result.charts) # remove aggregations to avoid sending too much information - search_result.aggregations = None + # search_result.aggregations = None return search_result diff --git a/app/validations.py b/app/validations.py index 5c2b02d4..00d511b3 100644 --- a/app/validations.py +++ b/app/validations.py @@ -22,33 +22,23 @@ def check_index_id_is_defined(index_id: str | None, config: Config) -> None: ) -def check_all_facets_fields_are_agg( - index_id: str | None, facets: list[str] | None +def check_all_values_are_fields_agg( + index_id: str | None, values: list[str] | None ) -> list[str]: - """Check all facets are valid, - that is, correspond to a field with aggregation""" + """Check that all values are fields with aggregate: true + property, that is, correspond to a field with aggregation. + Used to check that charts are facets are valid.""" errors: list[str] = [] - if facets is None: + if values is None: return errors global_config = cast(Config, CONFIG) index_id, index_config = global_config.get_index_config(index_id) if index_config is None: raise ValueError(f"Cannot get index config for index_id {index_id}") - for field_name in facets: + print(index_config) + for field_name in values: if field_name not in index_config.fields: errors.append(f"Unknown field name in facets: {field_name}") elif not index_config.fields[field_name].bucket_agg: errors.append(f"Non aggregation field name in facets: {field_name}") return errors - - -def check_all_charts_are_valid(charts: list[str] | None) -> list[str]: - """Check all charts are valid, - that is, are in the hardcoded list""" - errors: list[str] = [] - if charts is None: - return errors - for field_name in charts: - if field_name not in ["nutriscore_grade", "nova_group", "ecoscore_grade"]: - errors.append(f"Unknown field name in charts: {field_name}") - return errors diff --git a/frontend/public/off.html b/frontend/public/off.html index 14792410..159ab883 100644 --- a/frontend/public/off.html +++ b/frontend/public/off.html @@ -401,9 +401,9 @@
- - - + + +
diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts index 72ed3e79..e4560d39 100644 --- a/frontend/src/mixins/search-ctl.ts +++ b/frontend/src/mixins/search-ctl.ts @@ -22,6 +22,7 @@ import { SearchaliciousHistoryInterface, SearchaliciousHistoryMixin, } from './history'; +import {SearchaliciousChart} from '../search-chart'; export interface SearchParameters extends SortParameters { q: string; @@ -139,6 +140,18 @@ export const SearchaliciousSearchMixin = >( ); } + /** + * + */ + _chartsNodes(): SearchaliciousChart[] { + console.log('charts ?'); + return Array.from( + document.querySelectorAll( + `searchalicious-chart[search-name=${this.name}` + ) + ); + } + /** * Select a term by taxonomy in all facets * It will update the selected terms in facets @@ -230,6 +243,14 @@ export const SearchaliciousSearchMixin = >( return [...new Set(names)]; } + /** + * Get the list of charts we want to request + */ + _charts(): string[] { + const names = this._chartsNodes().map((chart) => chart.getName()); + return [...new Set(names)]; + } + /** * Get the filter linked to facets * @returns an expression to be added to query @@ -382,7 +403,9 @@ export const SearchaliciousSearchMixin = >( if (this._facets().length > 0) { params.facets = this._facets(); } - params.charts = ['nutriscore_grade', 'nova_group', 'ecoscore_grade']; + if (this._charts().length > 0) { + params.charts = this._charts(); + } return params; }; diff --git a/frontend/src/search-chart.ts b/frontend/src/search-chart.ts index a8795148..d18d0f41 100644 --- a/frontend/src/search-chart.ts +++ b/frontend/src/search-chart.ts @@ -14,13 +14,8 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( ) { // All these properties will change when vega logic // will be moved in API. - // TODO: fail if some required properties are unset - // (eg. key) @property() - key?: string; - - @property() - label?: string; + name = ''; @property({type: Array}) categories: Array = []; @@ -37,6 +32,10 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( this.vegaInstalled = this.testVegaInstalled(); } + getName() { + return this.name; + } + override render() { if (!this.vegaInstalled) { return html`

Please install vega to use searchalicious-chart

`; @@ -46,7 +45,7 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( return html`

no data

`; } - return html`
`; + return html`
`; } // Computes the vega representation for given results @@ -60,12 +59,8 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( return; } - // Compute the distribution - const values = Object.fromEntries( - this.categories.map((category) => [category, 0]) - ); - - this.vegaRepresentation = event.detail.charts[this.key!]; + // @ts-ignore + this.vegaRepresentation = event.detail.charts[this.name!]; } testVegaInstalled() { @@ -84,14 +79,14 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( } } - // vega rendering requires an html component with id == this.key + // vega rendering requires an html component with id == this.name // and consequently must be called AFTER render // Method updated is perfect for that // See lit.dev components lifecycle: https://lit.dev/docs/components/lifecycle/ override updated() { if (this.vegaRepresentation === undefined) return; - const container = this.renderRoot.querySelector(`#${this.key}`); + const container = this.renderRoot.querySelector(`#${this.name}`); // How to display a vega chart: https://vega.github.io/vega/usage/#view const view = new vega.View(vega.parse(this.vegaRepresentation), { diff --git a/frontend/src/test/search-pages_test.ts b/frontend/src/test/search-pages_test.ts index a7c120db..9a02dc6a 100644 --- a/frontend/src/test/search-pages_test.ts +++ b/frontend/src/test/search-pages_test.ts @@ -20,6 +20,7 @@ suite('searchalicious-pages', () => { currentPage: currentPage, pageSize: 10, facets: {}, + charts: {}, }; el._handleResults( new CustomEvent(SearchaliciousEvents.NEW_RESULT, { diff --git a/frontend/src/test/search-results_test.ts b/frontend/src/test/search-results_test.ts index f5b8421e..43937fe2 100644 --- a/frontend/src/test/search-results_test.ts +++ b/frontend/src/test/search-results_test.ts @@ -23,6 +23,7 @@ suite('searchalicious-results', () => { currentPage: 1, pageSize: 10, facets: {}, + charts: {}, }; el._handleResults( new CustomEvent(SearchaliciousEvents.NEW_RESULT, { From 5dea4e52efc9b3b39e9ab1c43eaea7f13d582413 Mon Sep 17 00:00:00 2001 From: Marc-AntoineA Date: Mon, 24 Jun 2024 16:27:48 +0200 Subject: [PATCH 3/5] =?UTF-8?q?fix:=E2=80=AFfix=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/_types.py | 4 ++-- app/charts.py | 14 +++++++++----- app/query.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/_types.py b/app/_types.py index fa7cef04..df37462c 100644 --- a/app/_types.py +++ b/app/_types.py @@ -214,8 +214,8 @@ class SearchParameters(BaseModel): list[str] | None, Query( description="""Name of vega representations to return in the response. - If None (default) no charts are returrned.""" - ) + If None (default) no charts are returned.""" + ), ] = None sort_params: Annotated[ JSONType | None, diff --git a/app/charts.py b/app/charts.py index 6f66ec19..2cefc316 100644 --- a/app/charts.py +++ b/app/charts.py @@ -1,16 +1,20 @@ -from ._types import SuccessSearchResponse +from ._types import ChartsInfos, SuccessSearchResponse def build_charts( search_result: SuccessSearchResponse, charts_names: list[str] | None, -) -> str: - charts = {} - if charts_names is None: +) -> ChartsInfos: + charts: ChartsInfos = {} + aggregations = search_result.aggregations + + if charts_names is None or aggregations is None: return charts for chart_name in charts_names: - buckets = search_result.aggregations[chart_name]["buckets"] + agg_data = aggregations.get(chart_name, {}) + + buckets = agg_data.get("buckets", []) if agg_data else [] # Filter unknown values values = [ diff --git a/app/query.py b/app/query.py index 3c730f20..a7a34444 100644 --- a/app/query.py +++ b/app/query.py @@ -281,7 +281,7 @@ def parse_sort_by_script( def create_aggregation_clauses( - config: IndexConfig, fields: list[str] | None + config: IndexConfig, fields: set[str] | list[str] | None ) -> dict[str, Agg]: """Create term bucket aggregation clauses for all fields corresponding to facets, From 0ab92468701f1ed2c3501005b81f541425897bae Mon Sep 17 00:00:00 2001 From: Marc-AntoineA Date: Mon, 24 Jun 2024 17:13:36 +0200 Subject: [PATCH 4/5] clean: autoreview, remove console.log, print and debug code --- app/query.py | 1 - app/search.py | 6 +----- frontend/src/mixins/search-ctl.ts | 3 +-- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/query.py b/app/query.py index a7a34444..3eb5bbb9 100644 --- a/app/query.py +++ b/app/query.py @@ -337,7 +337,6 @@ def build_es_query( if q.filter_query: es_query = es_query.query("bool", filter=q.filter_query) - # TODO: bug if one is None agg_fields = set(params.facets) if params.facets is not None else set() if params.charts is not None: agg_fields.update(params.charts) diff --git a/app/search.py b/app/search.py index 51067468..93606e81 100644 --- a/app/search.py +++ b/app/search.py @@ -33,7 +33,6 @@ def search( params: SearchParameters, ) -> SearchResponse: """Run a search""" - print("PARAMS", params) result_processor = cast( BaseResultProcessor, RESULT_PROCESSORS[params.valid_index_id] ) @@ -68,14 +67,11 @@ def search( page_size=params.page_size, projection=projection, ) - # print(search_result) if isinstance(search_result, SuccessSearchResponse): search_result.facets = build_facets( search_result, query, params.main_lang, index_config, params.facets ) - print(params) search_result.charts = build_charts(search_result, params.charts) - print(search_result.charts) # remove aggregations to avoid sending too much information - # search_result.aggregations = None + search_result.aggregations = None return search_result diff --git a/frontend/src/mixins/search-ctl.ts b/frontend/src/mixins/search-ctl.ts index e4560d39..237e874c 100644 --- a/frontend/src/mixins/search-ctl.ts +++ b/frontend/src/mixins/search-ctl.ts @@ -141,10 +141,9 @@ export const SearchaliciousSearchMixin = >( } /** - * + * Return the list of searchalicious-chart nodes */ _chartsNodes(): SearchaliciousChart[] { - console.log('charts ?'); return Array.from( document.querySelectorAll( `searchalicious-chart[search-name=${this.name}` From ded8a3cbb515ce6adb6664d7e977e73345937a8e Mon Sep 17 00:00:00 2001 From: Marc-AntoineA Date: Tue, 25 Jun 2024 15:39:23 +0200 Subject: [PATCH 5/5] =?UTF-8?q?clean:=E2=80=AFremove=20two=20typos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/validations.py | 1 - frontend/src/search-chart.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/validations.py b/app/validations.py index 00d511b3..c62786cf 100644 --- a/app/validations.py +++ b/app/validations.py @@ -35,7 +35,6 @@ def check_all_values_are_fields_agg( index_id, index_config = global_config.get_index_config(index_id) if index_config is None: raise ValueError(f"Cannot get index config for index_id {index_id}") - print(index_config) for field_name in values: if field_name not in index_config.fields: errors.append(f"Unknown field name in facets: {field_name}") diff --git a/frontend/src/search-chart.ts b/frontend/src/search-chart.ts index d18d0f41..e12556a0 100644 --- a/frontend/src/search-chart.ts +++ b/frontend/src/search-chart.ts @@ -17,9 +17,6 @@ export class SearchaliciousChart extends SearchaliciousResultCtlMixin( @property() name = ''; - @property({type: Array}) - categories: Array = []; - @property({attribute: false}) // eslint-disable-next-line vegaRepresentation: any = undefined;