diff --git a/api/src/main/java/org/open4goods/api/config/ApiConfig.java b/api/src/main/java/org/open4goods/api/config/ApiConfig.java index bfc34252..18d3917f 100644 --- a/api/src/main/java/org/open4goods/api/config/ApiConfig.java +++ b/api/src/main/java/org/open4goods/api/config/ApiConfig.java @@ -127,8 +127,8 @@ SerialisationService serialisationService() { @Bean VerticalsGenerationService verticalsGenerationService(ProductRepository pRepo, SerialisationService serialisationService, LegacyAiService aiService, GoogleTaxonomyService gTaxoService, VerticalsConfigService verticalsConfigService, ResourcePatternResolver resourceResolver, - EvaluationService evaluationService, IcecatService icecatService) throws SAXException { - return new VerticalsGenerationService(apiProperties.getVerticalsGenerationConfig(), pRepo, serialisationService, aiService, gTaxoService, verticalsConfigService, resourceResolver, evaluationService, icecatService); + EvaluationService evaluationService, IcecatService icecatService, GenAiService genAiService) throws SAXException { + return new VerticalsGenerationService(apiProperties.getVerticalsGenerationConfig(), pRepo, serialisationService, aiService, gTaxoService, verticalsConfigService, resourceResolver, evaluationService, icecatService, genAiService); } @Bean diff --git a/api/src/main/java/org/open4goods/api/controller/api/VerticalsGenerationController.java b/api/src/main/java/org/open4goods/api/controller/api/VerticalsGenerationController.java index 374f6252..acc0bfcd 100644 --- a/api/src/main/java/org/open4goods/api/controller/api/VerticalsGenerationController.java +++ b/api/src/main/java/org/open4goods/api/controller/api/VerticalsGenerationController.java @@ -43,26 +43,26 @@ public VerticalsGenerationController(VerticalsGenerationService verticalsGenServ this.verticalsConfigService = verticalsConfigService; } - @GetMapping(path="/fullFromDb") - @Operation(summary="Loads the mappings from database, then clean and save to file") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void full() throws ResourceNotFoundException, IOException { - verticalsGenService.fullFromDb(); - } - - @GetMapping(path="/mappings/load/database") - @Operation(summary="Loads the mappings from database") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void load() throws ResourceNotFoundException { - verticalsGenService.loadCategoriesMappingFromDatabase(); - } - - @GetMapping(path="/mappings/load/file") - @Operation(summary="Import the mapping file from JSON") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void importMapping() throws ResourceNotFoundException, IOException { - verticalsGenService.importMappingFile(); - } +// @GetMapping(path="/fullFromDb") +// @Operation(summary="Loads the mappings from database, then clean and save to file") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public void full() throws ResourceNotFoundException, IOException { +// verticalsGenService.fullFromDb(); +// } +// +// @GetMapping(path="/mappings/load/database") +// @Operation(summary="Loads the mappings from database") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public void load() throws ResourceNotFoundException { +// verticalsGenService.loadCategoriesMappingFromDatabase(); +// } +// +// @GetMapping(path="/mappings/load/file") +// @Operation(summary="Import the mapping file from JSON") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public void importMapping() throws ResourceNotFoundException, IOException { +// verticalsGenService.importMappingFile(); +// } // @GetMapping(path="/mappings/clean/threshold") @@ -81,48 +81,66 @@ public void importMapping() throws ResourceNotFoundException, IOException { - @GetMapping(path="/mappings") - @Operation(summary="Show the mappings") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public Map get() throws ResourceNotFoundException { - return verticalsGenService.getMappings(); - - } +// @GetMapping(path="/mappings") +// @Operation(summary="Show the mappings") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public Map get() throws ResourceNotFoundException { +// return verticalsGenService.getMappings(); +// +// } +// +// @GetMapping(path="/mappings/export") +// @Operation(summary="Export the mapping file to JSON") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public void exportMapping() throws ResourceNotFoundException, IOException { +// verticalsGenService.exportMappingFile(); +// +// } + - @GetMapping(path="/mappings/export") - @Operation(summary="Export the mapping file to JSON") + + + @GetMapping(path="/misc/vertical") + @Operation(summary="Generate the vertical files. Please, use https://docs.google.com/spreadsheets/d/1AyBdagWbn_rst2xZvUH9dVF7G2y_Wm_Xrq0IxDGkXoc/edit?gid=0#gid=0") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void exportMapping() throws ResourceNotFoundException, IOException { - verticalsGenService.exportMappingFile(); + // Respond to https://docs.google.com/spreadsheets/d/1AyBdagWbn_rst2xZvUH9dVF7G2y_Wm_Xrq0IxDGkXoc/edit?gid=0#gid=0 + public void generateCategoryMappingsFragment( + @RequestParam String googleTaxonomyId, + @RequestParam String matchingCategories, + @RequestParam String urlPrefix, + @RequestParam String h1Prefix, + @RequestParam String verticalHomeUrl, + @RequestParam String verticalHomeTitle) throws ResourceNotFoundException, IOException { + + verticalsGenService.verticalTemplatetoFile(googleTaxonomyId, matchingCategories, urlPrefix, h1Prefix, verticalHomeUrl, verticalHomeTitle); } - - @GetMapping(path="/assist/attributes/{vertical}") + + @GetMapping(path="/{vertical}/attributes/") @Operation(summary="Generate attributes coverage for a vertical") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) public VerticalAttributesStats generateAttributesCoverage(@PathVariable String vertical) throws ResourceNotFoundException, IOException { return verticalsGenService.attributesStats(vertical); - } - @GetMapping(path="/assist/categories/gtin") - @Operation(summary="Generate the categories yaml fragment for a given match") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) - public String generateCategoryMappingsFragment(@RequestParam String gtins) throws ResourceNotFoundException, IOException { - return verticalsGenService.generateCategoryMappingFragmentForGtin(Arrays.asList(gtins.split(",")), null ); - - } +// @GetMapping(path="/misc/categories/gtin") +// @Operation(summary="Generate the categories yaml fragment for a given match") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) +// public String generateCategoryMappingsFragment(@RequestParam String gtins) throws ResourceNotFoundException, IOException { +// return verticalsGenService.generateCategoryMappingFragmentForGtin(Arrays.asList(gtins.split(",")), null ); +// +// } - @GetMapping(path="/assist/categories/vertical") + @GetMapping(path="/{vertical}/categories/") @Operation(summary="Generate the categories yaml fragment for a given vertical") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) - public String generateCategoryMappingsForExistinf(@RequestParam String vertical, @RequestParam(defaultValue = "5") Integer minOffersCount ) throws ResourceNotFoundException, IOException { + public String generateCategoryMappingsForExistinf(@PathVariable String vertical, @RequestParam(defaultValue = "5") Integer minOffersCount ) throws ResourceNotFoundException, IOException { VerticalConfig vc = verticalsConfigService.getConfigById(vertical); return verticalsGenService.generateMapping(vc,minOffersCount); @@ -130,44 +148,39 @@ public String generateCategoryMappingsForExistinf(@RequestParam String vertical, } - - - - - @GetMapping(path="/assist/vertical") - @Operation(summary="Generate the vertical file") + @GetMapping(path="/{vertical}/ecoscore/") + @Operation(summary="Generate the ecoscore yaml fragment for a given vertical") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void generateCategoryMappingsFragment( - @RequestParam String googleTaxonomyId, - @RequestParam String matchingCategories, - @RequestParam String urlPrefix, - @RequestParam String h1Prefix, - @RequestParam String verticalHomeUrl, - @RequestParam String verticalHomeTitle) throws ResourceNotFoundException, IOException { - - verticalsGenService.verticalTemplatetoFile(googleTaxonomyId, matchingCategories, urlPrefix, h1Prefix, verticalHomeUrl, verticalHomeTitle); + @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) + public String generateEcoscoreMappings(@PathVariable String vertical) throws ResourceNotFoundException, IOException { + + VerticalConfig vc = verticalsConfigService.getConfigById(vertical); + return verticalsGenService.generateEcoscoreYamlConfig(vc); } - @GetMapping(path="/update/verticals/categoriesmapping") - @Operation(summary="Update the categories mapping directly in the files !") - @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") - public void updateVerticalsWithMappings( - @RequestParam (defaultValue = "3") Integer minOffers) throws ResourceNotFoundException, IOException { - - //TODO(p2,conf) : from conf - verticalsGenService.updateVerticalsWithMappings("/home/goulven/git/open4goods/verticals/src/main/resources/verticals/",minOffers); - - } - @GetMapping(path="/update/verticals/categoriesmapping/{vertical}") + + +// @GetMapping(path="/update/verticals/categoriesmapping") +// @Operation(summary="Update the categories mapping directly in the files !") +// @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") +// public void updateVerticalsWithMappings( +// @RequestParam (defaultValue = "3") Integer minOffers) throws ResourceNotFoundException, IOException { +// +// //TODO(p2,conf) : from conf +// verticalsGenService.updateAllVerticalFileWithCategories("/home/goulven/git/open4goods/verticals/src/main/resources/verticals/",minOffers); +// +// } + + @GetMapping(path="/{vertical}/categories/update") @Operation(summary="Update the categories mapping for a given vertical directly in the file !") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") public void updateVerticalWithMappings( - @RequestParam (defaultValue = "tv") String vertical, + @PathVariable String vertical, @RequestParam (defaultValue = "3") Integer minOffers) throws ResourceNotFoundException, IOException { //TODO(p2,conf) : from conf @@ -175,11 +188,11 @@ public void updateVerticalWithMappings( } - @GetMapping(path="/update/verticals/attributes/{vertical}") + @GetMapping(path="/{vertical}/attributes/update") @Operation(summary="Update the suggested attributes for a given vertical directly in the file !") @PreAuthorize("hasAuthority('"+RolesConstants.ROLE_ADMIN+"')") public void updateVerticalWithAttributes( - @RequestParam (defaultValue = "tv") String vertical, + @PathVariable String vertical, @RequestParam (defaultValue = "10") Integer minCoverage, @RequestParam (defaultValue = "") String containing ) throws ResourceNotFoundException, IOException { diff --git a/api/src/main/java/org/open4goods/api/services/VerticalsGenerationService.java b/api/src/main/java/org/open4goods/api/services/VerticalsGenerationService.java index 8f87151c..659731cb 100644 --- a/api/src/main/java/org/open4goods/api/services/VerticalsGenerationService.java +++ b/api/src/main/java/org/open4goods/api/services/VerticalsGenerationService.java @@ -30,6 +30,7 @@ import org.open4goods.api.model.VerticalCategoryMapping; import org.open4goods.commons.config.yml.ui.VerticalConfig; import org.open4goods.commons.dao.ProductRepository; +import org.open4goods.commons.exceptions.ResourceNotFoundException; import org.open4goods.commons.helper.IdHelper; import org.open4goods.commons.model.product.Product; import org.open4goods.commons.services.EvaluationService; @@ -37,6 +38,7 @@ import org.open4goods.commons.services.IcecatService; import org.open4goods.commons.services.SerialisationService; import org.open4goods.commons.services.VerticalsConfigService; +import org.open4goods.commons.services.ai.GenAiService; import org.open4goods.commons.services.ai.LegacyAiService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,8 +63,9 @@ public class VerticalsGenerationService { private LegacyAiService aiService; private GoogleTaxonomyService googleTaxonomyService; private EvaluationService evalService; + private GenAiService genAiService; - public VerticalsGenerationService(VerticalsGenerationConfig config, ProductRepository repository, SerialisationService serialisationService, LegacyAiService aiService, GoogleTaxonomyService googleTaxonomyService, VerticalsConfigService verticalsConfigService, ResourcePatternResolver resourceResolver, EvaluationService evaluationService, IcecatService icecatService) { + public VerticalsGenerationService(VerticalsGenerationConfig config, ProductRepository repository, SerialisationService serialisationService, LegacyAiService aiService, GoogleTaxonomyService googleTaxonomyService, VerticalsConfigService verticalsConfigService, ResourcePatternResolver resourceResolver, EvaluationService evaluationService, IcecatService icecatService, GenAiService genAiService ) { super(); this.config = config; this.repository = repository; @@ -73,231 +76,228 @@ public VerticalsGenerationService(VerticalsGenerationConfig config, ProductRepos this.resourceResolver = resourceResolver; this.evalService = evaluationService; this.icecatService = icecatService; + this.genAiService = genAiService; } - public void fullFromDb() throws IOException { - - importMappingFile(); - LOGGER.info("loaded . {} mappings", sortedMappings.size()); - - - removeByLowHits(); - LOGGER.info("threshold-low-hits. {} mappings", sortedMappings.size()); - - - removeByAssociatedcategoryThreshold(); - LOGGER.info("threshold-clean . {} mappings", sortedMappings.size()); - - - orderMappings(); - - removeCrossReferencedMappings(); - LOGGER.info("cross-reference-clean . {} mappings", sortedMappings.size()); - - - orderMappings(); - exportMappingFile(); - - } - - /** - * Main method, that deduce verticals and load it in memory - */ - public void loadCategoriesMappingFromDatabase () { - - - ///////////////////////////////////// - // 1 - Select all products - // TODO : Filter by mandatory attributes - ///////////////////////////////////// - - // TODO : Filter, limit not working - Stream products = repository.exportForCategoriesMapping(config.getMustExistsFields(), config.getLimit()).parallel(); - - if (config.getLimit() != null) { - products=products.limit(config.getLimit()); - } - - ////////////////////////////////////////////////////////////////////////// - // 2 - Iterate on each product, to constitute the mapped attributes map - // >> - ////////////////////////////////////////////////////////////////////////// - AtomicInteger counter = new AtomicInteger(); - products.forEach(product -> { - int count = counter.incrementAndGet(); - if (count % 1000 == 0) { - LOGGER.warn ("Handled {} items",count); - } - - Map categories = product.getCategoriesByDatasources(); - categories.entrySet().forEach(category -> { - // TODO : Exclude some datasources from conf - - // Retrieving existing mapping for a given categoryPath or creating - VerticalCategoryMapping existingMapping = sortedMappings.get(category.getValue()); - if (null == existingMapping) { - existingMapping = new VerticalCategoryMapping(); - sortedMappings.put(category.getValue(), existingMapping); - } - // Updating stats for this - existingMapping.updateStats(category.getValue(), categories.values()); - }); - }); - - LOGGER.info("{} category associated mappings constructed",sortedMappings.size()); - - } - - - - /** - * Order the mappings by total hits - */ - private void orderMappings() { - this.sortedMappings = sortedMappings.entrySet().stream() - .map(e -> { - // Sorting the matchings by weight - e.getValue().setAssociatedCategories(getSortedAssociatedCategories(e.getValue().getAssociatedCategories())); - return e; - }) - .sorted(Comparator.comparing(entry -> entry.getValue().getTotalHits(), Comparator.reverseOrder())) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, // in case of duplicate keys, keep the existing one - LinkedHashMap::new // maintain insertion order for sorted entries - )); - } - - /** - * Deduplicates associated categories in each VerticalCategoryMapping object - * by removing entries with a value below a calculated threshold - * - * @param mappings Map of VerticalCategoryMapping objects keyed by String - */ - public void removeByAssociatedcategoryThreshold() { - - new HashMap<>(sortedMappings).entrySet().stream().forEach(entryKeyVal -> { - VerticalCategoryMapping currentValue = entryKeyVal.getValue(); - // Set threshold to X% of the total hits - double threshold = currentValue.getTotalHits() * config.getAssociatedCategoriesEvictionPercent(); - - // Use an iterator to safely remove entries below the threshold - Iterator> iterator = currentValue.getAssociatedCategories().entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - if (entry.getValue() < threshold) { - iterator.remove(); // Safely remove entry from the map - } - } - }); - } - - /** - * Deduplicates associated categories in each VerticalCategoryMapping object - * by removing entries with a value below a calculated threshold - * - * @param mappings Map of VerticalCategoryMapping objects keyed by String - */ - public void removeByLowHits() { - - sortedMappings = sortedMappings.entrySet().stream() - .filter(e -> e.getValue().getTotalHits() > config.getMinimumTotalHits()) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); - } - - - - /** - * Remove the cross linked mappings - * NOTE : heavy load, a one to one comparison on lot of keys - * @param mappings - * @param currentValue - */ - public void removeCrossReferencedMappings() { - sortedMappings.entrySet().forEach(actual -> { - - if (!actual.getValue().getToDelete()) { - - actual.getValue().setKeep(true); - // We are on an item. - sortedMappings.entrySet().stream() - // We exclude previously handled item - .filter(e -> !e.getValue().getKeep()) - // Find all in the order mapping that collapse definition with the actual one. - // We mark them as force - .filter(e -> hasOverlap(actual, e)) - .forEach(e -> e.getValue().setToDelete(true)); - } - - }); - - // Cleaning - sortedMappings = sortedMappings.entrySet().stream().filter(e -> e.getValue().getKeep()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - } - - /** - * Return true if a target VerticalCategoryMapping has name or associated categories that matches the actual VerticalCategoryMapping - * @param actual - * @param target - * @return - */ - private boolean hasOverlap(Entry actual, Entry target) { - - boolean ret = target.getValue().getAssociatedCategories().containsKey(actual.getKey()) -// || target.getValue().getAssociatedCategories().keySet().containsAll(actual.getValue().getAssociatedCategories().keySet()) - ; - - - return ret; - - } - - /** - * Sort the map of associated categories - * @param map - * @return - */ - public Map getSortedAssociatedCategories(Map map) { - return map.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .collect(Collectors.toMap( - Map.Entry::getKey, - Map.Entry::getValue, - (e1, e2) -> e1, // in case of duplicate keys, keep the existing one - LinkedHashMap::new // maintain insertion order for sorted entries - )); - } - /** - * Export mapping file to disk - * @throws IOException - */ - public void exportMappingFile() throws IOException { - if (sortedMappings.size() > 0) { - org.apache.commons.io.FileUtils.write(new File(config.getMappingFilePath()), serialisationService.toJson(sortedMappings,true)); - } - } - - - /** - * Import categories mapping from file - * @throws IOException - */ - public void importMappingFile() throws IOException { - String fileContent = org.apache.commons.io.FileUtils.readFileToString( - new File(config.getMappingFilePath()), - Charset.defaultCharset() - ); - - // Deserialize with proper typing - sortedMappings = serialisationService.getJsonMapper().readValue( - fileContent, - new TypeReference>() {} - ); - } - - +// public void fullFromDb() throws IOException { +// +// importMappingFile(); +// LOGGER.info("loaded . {} mappings", sortedMappings.size()); +// +// removeByLowHits(); +// LOGGER.info("threshold-low-hits. {} mappings", sortedMappings.size()); +// +// removeByAssociatedcategoryThreshold(); +// LOGGER.info("threshold-clean . {} mappings", sortedMappings.size()); +// +// orderMappings(); +// +// removeCrossReferencedMappings(); +// LOGGER.info("cross-reference-clean . {} mappings", sortedMappings.size()); +// +// orderMappings(); +// exportMappingFile(); +// +// } +// +// /** +// * Main method, that deduce verticals and load it in memory +// */ +// public void loadCategoriesMappingFromDatabase () { +// +// +// ///////////////////////////////////// +// // 1 - Select all products +// // TODO : Filter by mandatory attributes +// ///////////////////////////////////// +// +// // TODO : Filter, limit not working +// Stream products = repository.exportForCategoriesMapping(config.getMustExistsFields(), config.getLimit()).parallel(); +// +// if (config.getLimit() != null) { +// products=products.limit(config.getLimit()); +// } +// +// ////////////////////////////////////////////////////////////////////////// +// // 2 - Iterate on each product, to constitute the mapped attributes map +// // >> +// ////////////////////////////////////////////////////////////////////////// +// AtomicInteger counter = new AtomicInteger(); +// products.forEach(product -> { +// int count = counter.incrementAndGet(); +// if (count % 1000 == 0) { +// LOGGER.warn ("Handled {} items",count); +// } +// +// Map categories = product.getCategoriesByDatasources(); +// categories.entrySet().forEach(category -> { +// // TODO : Exclude some datasources from conf +// +// // Retrieving existing mapping for a given categoryPath or creating +// VerticalCategoryMapping existingMapping = sortedMappings.get(category.getValue()); +// if (null == existingMapping) { +// existingMapping = new VerticalCategoryMapping(); +// sortedMappings.put(category.getValue(), existingMapping); +// } +// // Updating stats for this +// existingMapping.updateStats(category.getValue(), categories.values()); +// }); +// }); +// +// LOGGER.info("{} category associated mappings constructed",sortedMappings.size()); +// +// } +// +// +// +// /** +// * Order the mappings by total hits +// */ +// private void orderMappings() { +// this.sortedMappings = sortedMappings.entrySet().stream() +// .map(e -> { +// // Sorting the matchings by weight +// e.getValue().setAssociatedCategories(getSortedAssociatedCategories(e.getValue().getAssociatedCategories())); +// return e; +// }) +// .sorted(Comparator.comparing(entry -> entry.getValue().getTotalHits(), Comparator.reverseOrder())) +// .collect(Collectors.toMap( +// Map.Entry::getKey, +// Map.Entry::getValue, +// (e1, e2) -> e1, // in case of duplicate keys, keep the existing one +// LinkedHashMap::new // maintain insertion order for sorted entries +// )); +// } +// +// /** +// * Deduplicates associated categories in each VerticalCategoryMapping object +// * by removing entries with a value below a calculated threshold +// * +// * @param mappings Map of VerticalCategoryMapping objects keyed by String +// */ +// public void removeByAssociatedcategoryThreshold() { +// +// new HashMap<>(sortedMappings).entrySet().stream().forEach(entryKeyVal -> { +// VerticalCategoryMapping currentValue = entryKeyVal.getValue(); +// // Set threshold to X% of the total hits +// double threshold = currentValue.getTotalHits() * config.getAssociatedCategoriesEvictionPercent(); +// +// // Use an iterator to safely remove entries below the threshold +// Iterator> iterator = currentValue.getAssociatedCategories().entrySet().iterator(); +// while (iterator.hasNext()) { +// Map.Entry entry = iterator.next(); +// if (entry.getValue() < threshold) { +// iterator.remove(); // Safely remove entry from the map +// } +// } +// }); +// } +// +// /** +// * Deduplicates associated categories in each VerticalCategoryMapping object +// * by removing entries with a value below a calculated threshold +// * +// * @param mappings Map of VerticalCategoryMapping objects keyed by String +// */ +// public void removeByLowHits() { +// +// sortedMappings = sortedMappings.entrySet().stream() +// .filter(e -> e.getValue().getTotalHits() > config.getMinimumTotalHits()) +// .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); +// } +// +// +// +// /** +// * Remove the cross linked mappings +// * NOTE : heavy load, a one to one comparison on lot of keys +// * @param mappings +// * @param currentValue +// */ +// public void removeCrossReferencedMappings() { +// sortedMappings.entrySet().forEach(actual -> { +// +// if (!actual.getValue().getToDelete()) { +// +// actual.getValue().setKeep(true); +// // We are on an item. +// sortedMappings.entrySet().stream() +// // We exclude previously handled item +// .filter(e -> !e.getValue().getKeep()) +// // Find all in the order mapping that collapse definition with the actual one. +// // We mark them as force +// .filter(e -> hasOverlap(actual, e)) +// .forEach(e -> e.getValue().setToDelete(true)); +// } +// +// }); +// +// // Cleaning +// sortedMappings = sortedMappings.entrySet().stream().filter(e -> e.getValue().getKeep()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); +// +// } +// +// /** +// * Return true if a target VerticalCategoryMapping has name or associated categories that matches the actual VerticalCategoryMapping +// * @param actual +// * @param target +// * @return +// */ +// private boolean hasOverlap(Entry actual, Entry target) { +// +// boolean ret = target.getValue().getAssociatedCategories().containsKey(actual.getKey()) +//// || target.getValue().getAssociatedCategories().keySet().containsAll(actual.getValue().getAssociatedCategories().keySet()) +// ; +// +// +// return ret; +// +// } +// +// /** +// * Sort the map of associated categories +// * @param map +// * @return +// */ +// public Map getSortedAssociatedCategories(Map map) { +// return map.entrySet().stream() +// .sorted(Map.Entry.comparingByValue().reversed()) +// .collect(Collectors.toMap( +// Map.Entry::getKey, +// Map.Entry::getValue, +// (e1, e2) -> e1, // in case of duplicate keys, keep the existing one +// LinkedHashMap::new // maintain insertion order for sorted entries +// )); +// } +// /** +// * Export mapping file to disk +// * @throws IOException +// */ +// public void exportMappingFile() throws IOException { +// if (sortedMappings.size() > 0) { +// org.apache.commons.io.FileUtils.write(new File(config.getMappingFilePath()), serialisationService.toJson(sortedMappings,true)); +// } +// } +// +// +// /** +// * Import categories mapping from file +// * @throws IOException +// */ +// public void importMappingFile() throws IOException { +// String fileContent = org.apache.commons.io.FileUtils.readFileToString( +// new File(config.getMappingFilePath()), +// Charset.defaultCharset() +// ); +// +// // Deserialize with proper typing +// sortedMappings = serialisationService.getJsonMapper().readValue( +// fileContent, +// new TypeReference>() {} +// ); +// } +// +// /** * * @return the mappings @@ -332,37 +332,7 @@ public VerticalAttributesStats attributesStats(String vertical) { return ret; } - /** - * A prompt used to enrich the VerticalConfig - * @param v - * @return - */ - private Map aiDatas(VerticalConfig v) { - // Building the prompt - - String prompt = """ -Strictly according to english google product taxonomy (https://www.google.com/basepages/producttype/taxonomy-with-ids.en-US.txt), -give me the most precise english google product category for a list of categories found on market places. - -Your response must be JSON format. The response will strictly follow this simple key/value format : -- field "googleTaxonomy" : gives the most exact, precise and appropriate google taxonomy products category. -- field "englishName": An english name that describes this category -- field "frenchName": A french name that describes this category - - -Base your analyse on the following categories : - -"""; - prompt+=StringUtils.join(v.getMatchingCategories(), "\n"); - - // Calling the GPT model - Map response = aiService.jsonPrompt(prompt); - - - return response; - } - /** @@ -491,7 +461,7 @@ public void verticalTemplatetoFile(String googleTaxonomyId, String matchingCateg * @param verticalFolderPath * @param minOffers */ - public void updateVerticalsWithMappings(String verticalFolderPath, Integer minOffers) { + public void updateAllVerticalFileWithCategories(String verticalFolderPath, Integer minOffers) { LOGGER.warn("Will update categories in vertical files. Be sure to review before publishing on github !"); List files = Arrays.asList(new File(verticalFolderPath).listFiles()); files.stream().filter(e->e.getName().endsWith("yml")).forEach(file -> { @@ -589,8 +559,6 @@ public String generateAttributesMapping(VerticalConfig verticalConfig, int minCo for (Entry cat : stats.getStats().entrySet()) { if (!exclusions.contains(cat.getKey())) { - - if (Double.valueOf(cat.getValue().getHits() / Double.valueOf(totalItems) * 100.0).intValue() > minCoverage) { LOGGER.info("Generating template for attribute : {}", cat.getKey()); // TODO(conf,p2) : numberofsamples from conf @@ -606,6 +574,71 @@ public String generateAttributesMapping(VerticalConfig verticalConfig, int minCo } + + /** + * Generates the AI yaml config defining an ecoscore for a given category + * @param vConf + * @return + */ + public String generateEcoscoreYamlConfig (VerticalConfig vConf) { + // Translate to YAML + String ret = null; + try { + Map context = new HashMap(); + + + context.put("AVAILABLE_CRITERIAS", getCriterias(vConf)); + context.put("VERTICAL_NAME", vConf.getI18n().get("fr").getVerticalHomeTitle()); + + + // Prompt + Map response = genAiService.jsonPrompt("impactscore-generation", context); + + String rawRet = serialisationService.toYaml(response); + + rawRet = rawRet.replace("---", ""); + StringBuilder buffer = new StringBuilder("impactScoreConfig:\n"); + Arrays.asList(rawRet.split("\n")).forEach(line -> { + buffer.append(" ").append(line).append("\n"); + + }); + + ret = buffer.toString(); + + + } catch (ResourceNotFoundException e) { + LOGGER.error("Ecoscore Generation failed for {} ",vConf, e); + } + + return ret; + + } + + /** + * Return known criterias with description for a vertical + * @param vConf + * @return + */ + private String getCriterias(VerticalConfig vConf) { + // TODO Auto-generated method stub + + Map criterias = repository.scoresCoverage(vConf); + + + StringBuilder ret = new StringBuilder(); + + criterias.entrySet().forEach(score -> { + ret.append(" ").append(score.getKey()).append(" : " ); + // NOTE : Spank me if NPE... + ret.append(vConf.getAvailableImpactScoreCriterias().get(score.getKey()).getDescription().get("fr")); + ret.append("\n"); + }); + + + + return ret.toString(); + } + // /** // * // * @return an attribute definition, from template diff --git a/api/src/main/java/org/open4goods/api/services/aggregation/services/batch/scores/EcoScoreAggregationService.java b/api/src/main/java/org/open4goods/api/services/aggregation/services/batch/scores/EcoScoreAggregationService.java index e644b0db..7d39c3e4 100644 --- a/api/src/main/java/org/open4goods/api/services/aggregation/services/batch/scores/EcoScoreAggregationService.java +++ b/api/src/main/java/org/open4goods/api/services/aggregation/services/batch/scores/EcoScoreAggregationService.java @@ -56,7 +56,7 @@ private Double generateEcoScore(Map scores, VerticalConfig vConf) double va = 0.0; - for (String config : vConf.getEcoscoreConfig().keySet()) { + for (String config : vConf.getImpactScoreConfig().getCriteriasPonderation().keySet()) { Score score = scores.get(config); if (null == score) { @@ -65,7 +65,7 @@ private Double generateEcoScore(Map scores, VerticalConfig vConf) // Taking on the relativ - va += score. getRelativ().getValue() * Double.valueOf(vConf.getEcoscoreConfig().get(config)); + va += score. getRelativ().getValue() * Double.valueOf(vConf.getImpactScoreConfig().getCriteriasPonderation().get(config)); } return va; diff --git a/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreConfig.java b/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreConfig.java new file mode 100644 index 00000000..d0d2fbdd --- /dev/null +++ b/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreConfig.java @@ -0,0 +1,38 @@ +package org.open4goods.commons.config.yml; + +import java.util.HashMap; +import java.util.Map; + +import org.checkerframework.checker.units.qual.K; +import org.open4goods.commons.model.Localisable; + +public class ImpactScoreConfig { + + /** + * The pondered criterias, composing the eco score + */ + private Map criteriasPonderation = new HashMap<>(); + + //////////////////////// + // Audit / justification + /////////////////////// + + private Localisable texts = new Localisable<>(); + + + // TODO : The validate method (check sums is 1) + public Map getCriteriasPonderation() { + return criteriasPonderation; + } + public void setCriteriasPonderation(Map criteriasPonderation) { + this.criteriasPonderation = criteriasPonderation; + } + public Localisable getTexts() { + return texts; + } + public void setTexts(Localisable texts) { + this.texts = texts; + } + + +} diff --git a/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreTexts.java b/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreTexts.java new file mode 100644 index 00000000..65dd25bb --- /dev/null +++ b/commons/src/main/java/org/open4goods/commons/config/yml/ImpactScoreTexts.java @@ -0,0 +1,39 @@ +package org.open4goods.commons.config.yml; + +import java.util.HashMap; +import java.util.Map; + +public class ImpactScoreTexts { + + private Map criteriasAnalysis = new HashMap(); + private String purpose; + private String criticalReview; + private String availlableDatas; + public Map getCriteriasAnalysis() { + return criteriasAnalysis; + } + public void setCriteriasAnalysis(Map criteriasAnalysis) { + this.criteriasAnalysis = criteriasAnalysis; + } + public String getPurpose() { + return purpose; + } + public void setPurpose(String purpose) { + this.purpose = purpose; + } + public String getCriticalReview() { + return criticalReview; + } + public void setCriticalReview(String criticalReview) { + this.criticalReview = criticalReview; + } + public String getAvaillableDatas() { + return availlableDatas; + } + public void setAvaillableDatas(String availlableDatas) { + this.availlableDatas = availlableDatas; + } + + + +} diff --git a/commons/src/main/java/org/open4goods/commons/config/yml/ui/GenAiConfig.java b/commons/src/main/java/org/open4goods/commons/config/yml/ui/GenAiConfig.java index 6948c13c..4ba37bf5 100644 --- a/commons/src/main/java/org/open4goods/commons/config/yml/ui/GenAiConfig.java +++ b/commons/src/main/java/org/open4goods/commons/config/yml/ui/GenAiConfig.java @@ -7,7 +7,7 @@ public class GenAiConfig { */ private String promptsTemplatesFolder; - private boolean cacheTemplates = true; + private boolean cacheTemplates = false; private String openaiApiKey; diff --git a/commons/src/main/java/org/open4goods/commons/config/yml/ui/ImpactScoreCriteria.java b/commons/src/main/java/org/open4goods/commons/config/yml/ui/ImpactScoreCriteria.java new file mode 100644 index 00000000..185fd5ad --- /dev/null +++ b/commons/src/main/java/org/open4goods/commons/config/yml/ui/ImpactScoreCriteria.java @@ -0,0 +1,31 @@ +package org.open4goods.commons.config.yml.ui; + +import org.open4goods.commons.model.Localisable; + +import com.fasterxml.jackson.annotation.JsonMerge; + +public class ImpactScoreCriteria { + + private String key; + + @JsonMerge + private Localisable description = new Localisable<>(); + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Localisable getDescription() { + return description; + } + + public void setDescription(Localisable description) { + this.description = description; + } + + +} diff --git a/commons/src/main/java/org/open4goods/commons/config/yml/ui/VerticalConfig.java b/commons/src/main/java/org/open4goods/commons/config/yml/ui/VerticalConfig.java index 20b00a14..4b827924 100644 --- a/commons/src/main/java/org/open4goods/commons/config/yml/ui/VerticalConfig.java +++ b/commons/src/main/java/org/open4goods/commons/config/yml/ui/VerticalConfig.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import org.open4goods.commons.config.yml.CommentsAggregationConfig; +import org.open4goods.commons.config.yml.ImpactScoreConfig; import org.open4goods.commons.config.yml.attributes.AttributeConfig; import org.open4goods.commons.helper.IdHelper; import org.open4goods.commons.model.constants.CacheConstants; @@ -174,16 +175,22 @@ public class VerticalConfig{ private AttributesConfig attributesConfig = new AttributesConfig(); - + /** + * The scores that are eligible to participate to the impact score construction + */ + @JsonMerge + private Map availableImpactScoreCriterias = new HashMap<>(); /** * Configuration relativ to ecoscore computation. Key / values are : scoreName -> Ponderation (0.1 = 10%) * NOTE : No json merge, since default score has to be fully described if overrided */ - private Map ecoscoreConfig = new HashMap<>(); + private ImpactScoreConfig impactScoreConfig = new ImpactScoreConfig(); + + // // /** // * Configuration relativ to ratings aggregation @@ -307,7 +314,7 @@ public Set getTokenNames(Collection additionalNames) { */ public Integer ecoscorePercentOf(String scoreName) { - Double ponderation = ecoscoreConfig.get(scoreName); + Double ponderation = impactScoreConfig.getCriteriasPonderation().get(scoreName); if (null == ponderation) { return -1; } else { @@ -507,7 +514,7 @@ public List ecoScoreDetails(Collection existing) { List ret = new ArrayList(); - ecoscoreConfig.keySet().forEach(ecoConfig-> { + impactScoreConfig.getCriteriasPonderation().keySet().forEach(ecoConfig-> { existing.forEach(exisiting -> { if (exisiting.getName().equals(ecoConfig)) { ret.add(exisiting); @@ -685,16 +692,17 @@ public void setMatchingCategories(Map> matchingCategories) { // this.unmatchingCategories = unmatchingCategories; // } - public Map getEcoscoreConfig() { - return ecoscoreConfig; + + public Integer getGoogleTaxonomyId() { + return googleTaxonomyId; } - public void setEcoscoreConfig(Map ecoscoreConfig) { - this.ecoscoreConfig = ecoscoreConfig; + public ImpactScoreConfig getImpactScoreConfig() { + return impactScoreConfig; } - public Integer getGoogleTaxonomyId() { - return googleTaxonomyId; + public void setImpactScoreConfig(ImpactScoreConfig impactScoreConfig) { + this.impactScoreConfig = impactScoreConfig; } public void setGoogleTaxonomyId(Integer taxonomyId) { @@ -834,6 +842,15 @@ public void setGenerationExcludedFromAttributesMatching(Set generationEx this.generationExcludedFromAttributesMatching = generationExcludedFromAttributesMatching; } + public Map getAvailableImpactScoreCriterias() { + return availableImpactScoreCriterias; + } + + public void setAvailableImpactScoreCriterias(Map availableImpactScoreCriterias) { + this.availableImpactScoreCriterias = availableImpactScoreCriterias; + } + + diff --git a/commons/src/main/java/org/open4goods/commons/dao/ProductRepository.java b/commons/src/main/java/org/open4goods/commons/dao/ProductRepository.java index c690205b..4731d227 100644 --- a/commons/src/main/java/org/open4goods/commons/dao/ProductRepository.java +++ b/commons/src/main/java/org/open4goods/commons/dao/ProductRepository.java @@ -1,6 +1,5 @@ package org.open4goods.commons.dao; -import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -12,7 +11,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.open4goods.model.BarcodeType; import org.open4goods.commons.config.yml.IndexationConfig; import org.open4goods.commons.config.yml.ui.VerticalConfig; import org.open4goods.commons.exceptions.ResourceNotFoundException; @@ -22,6 +20,7 @@ import org.open4goods.commons.services.SerialisationService; import org.open4goods.commons.store.repository.FullProductIndexationWorker; import org.open4goods.commons.store.repository.PartialProductIndexationWorker; +import org.open4goods.model.BarcodeType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -211,6 +210,33 @@ public Stream searchInValidPrices(String query, final String indexName, } + + + // TODO(P2,design) : in a stat service + + /** + * Return the scores coverage stats for a vertical + * @param vConf + * @return + */ + public Map scoresCoverage(VerticalConfig vConf) { + + Map ret = new HashMap<>(); + + vConf.getAvailableImpactScoreCriterias().entrySet().forEach(criteria -> { + + Long count = countMainIndexHavingScore(criteria.getKey(),vConf.getId()); + + // TODO(p2, conf) : threshold from conf + if (count > 10) { + ret.put(criteria.getKey() , count); + } + }); + + + return ret; + } + /** * Export all aggregateddatas for a vertical * @@ -633,6 +659,13 @@ public Long countMainIndexHavingRecentPrices() { return elasticsearchOperations.count(query, CURRENT_INDEX); } + @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) + public Long countMainIndexHavingScore(String scoreName, String vertical) { + CriteriaQuery query = new CriteriaQuery(new Criteria("vertical").is(vertical) .and(new Criteria("scores." + scoreName + ".value").exists())); + return elasticsearchOperations.count(query, CURRENT_INDEX); + } + + @Cacheable(keyGenerator = CacheConstants.KEY_GENERATOR, cacheNames = CacheConstants.ONE_HOUR_LOCAL_CACHE_NAME) public Long countMainIndexHavingRecentUpdate() { CriteriaQuery query = new CriteriaQuery(getRecentPriceQuery()); @@ -791,4 +824,6 @@ public ElasticsearchOperations getElasticsearchOperations() { + + } diff --git a/commons/src/main/java/org/open4goods/commons/services/VerticalsConfigService.java b/commons/src/main/java/org/open4goods/commons/services/VerticalsConfigService.java index b71a71a5..b7805a55 100644 --- a/commons/src/main/java/org/open4goods/commons/services/VerticalsConfigService.java +++ b/commons/src/main/java/org/open4goods/commons/services/VerticalsConfigService.java @@ -189,7 +189,7 @@ private List loadFromClasspath() { try { ret.add(getConfig(r.getInputStream(), getDefaultConfig())); } catch (IOException e) { - logger.error("Cannot retrieve vertical config",e); + logger.error("Cannot retrieve vertical config : {}",r.getFilename(), e); } } diff --git a/commons/src/main/java/org/open4goods/commons/services/ai/SamplePromptEntity.java b/commons/src/main/java/org/open4goods/commons/services/ai/SamplePromptEntity.java index 4de27852..bd30dce6 100644 --- a/commons/src/main/java/org/open4goods/commons/services/ai/SamplePromptEntity.java +++ b/commons/src/main/java/org/open4goods/commons/services/ai/SamplePromptEntity.java @@ -7,7 +7,7 @@ public class SamplePromptEntity { private String analysis; private String score_composition; private String critical_review; - private Map ecoscoreConfig; + private Map impactScoreConfig; public String getAnalysis() { return analysis; } @@ -26,11 +26,11 @@ public String getCritical_review() { public void setCritical_review(String critical_review) { this.critical_review = critical_review; } - public Map getEcoscoreConfig() { - return ecoscoreConfig; + public Map getimpactScoreConfig() { + return impactScoreConfig; } - public void setEcoscoreConfig(Map ecoscoreConfig) { - this.ecoscoreConfig = ecoscoreConfig; + public void setimpactScoreConfig(Map impactScoreConfig) { + this.impactScoreConfig = impactScoreConfig; } diff --git a/verticals/src/main/resources/verticals/_default.yml b/verticals/src/main/resources/verticals/_default.yml index f52fc71f..a4146c2a 100644 --- a/verticals/src/main/resources/verticals/_default.yml +++ b/verticals/src/main/resources/verticals/_default.yml @@ -42,8 +42,35 @@ excludingTokensFromCategoriesMatching: - QUINCAILLERIE - CABLE - - + + # The scores that are available to build the impact score + # The scores that are available to build the impact score +availableImpactScoreCriterias: + CLASSE_ENERGY: + key: CLASSE_ENERGY + description: + fr: "La classe énergétique" + REPAIRABILITY_INDEX: + key: REPAIRABILITY_INDEX + description: + fr: "L'indice de réparabilité de l'objet" + WEIGHT: + key: WEIGHT + description: + fr: "Le poids de l'objet" + POWER_CONSUMPTION_TYPICAL: + key: POWER_CONSUMPTION_TYPICAL + description: + fr: "La consommation électrique en marche" + POWER_CONSUMPTION_OFF: + key: POWER_CONSUMPTION_OFF + description: + fr: "La consommation électrique à l'arrêt, ou en veille" + + + + + ##################################################################################################################################### # I18N CONFIGURATION # Configure in a i18n way all the texts of a product, including the Url, the title, the description, the open graph metas, .... @@ -127,14 +154,21 @@ genAiConfig: # Weight sum MUST BE equals to 1 ############################################################################## # Sum of weight(values) must be equals to 1 -ecoscoreConfig: - WEIGHT: 0.3 - BRAND_SUSTAINABILITY: 0.3 - POWER_CONSUMPTION: 0.3 - DATA-QUALITY: 0.1 - - - +impactScoreConfig: + purpose: "Élaborer un score d'impact environnemental pour les lave-linges en utilisant des facteurs disponibles et coefficientés pour refléter leur impact relatif sur l'environnement." + availlableDatas: "Les données disponibles incluent le poids de l'objet, la consommation électrique en marche, l'évaluation ESG de la marque, et la qualité des données. Ces facteurs sont pertinents car ils couvrent des aspects clés de l'impact environnemental : l'empreinte matérielle, l'efficacité énergétique, la responsabilité sociale de l'entreprise, et la fiabilité des données." + criticalReview: "La démarche actuelle se concentre sur des facteurs mesurables et disponibles, mais pourrait être enrichie en incluant des critères tels que la durabilité des matériaux, la recyclabilité, et l'impact du cycle de vie complet. De plus, l'impact du transport et de la distribution pourrait être considéré pour une évaluation plus complète." + criteriasAnalysis: + WEIGHT: "Le poids est un indicateur de l'empreinte matérielle du produit. Un poids plus élevé peut indiquer une utilisation accrue de matériaux, ce qui peut avoir un impact environnemental plus important en termes de ressources et de transport." + POWER_CONSUMPTION: "La consommation électrique est cruciale car elle détermine l'efficacité énergétique du produit. Une consommation plus faible est préférable car elle réduit l'empreinte carbone liée à l'utilisation du produit." + BRAND_SUSTAINABILITY: "L'évaluation ESG de la marque reflète l'engagement de l'entreprise envers des pratiques durables. Une meilleure évaluation indique que la marque est plus susceptible de minimiser son impact environnemental global." + DATA-QUALITY: "La qualité des données est essentielle pour garantir la fiabilité de l'évaluation. Des données complètes et précises permettent une meilleure estimation de l'impact environnemental." + criteriasPonderation: + WEIGHT: 0.25 + POWER_CONSUMPTION: 0.35 + BRAND_SUSTAINABILITY: 0.25 + DATA-QUALITY: 0.15 + requiredAttributes: # - WEIGHT @@ -177,6 +211,8 @@ descriptionsAggregationConfig: globalTechnicalFilters: + - "WARRANTY" + - "YEAR" - "COLOR" - "WEIGHT" @@ -412,41 +448,6 @@ attributesConfig: - "/10" - - ################################## - # WARRANTY - ################################## - - - key: "WARRANTY" - faIcon: "fa-certificate" - name: - default: "Warranty" - fr: "Garantie" - filteringType: "NUMERIC" - asScore: true - - - attributeValuesOrdering: "MAPPED" - attributeValuesReverseOrder: false - synonyms: - all: - - "GARANTIE" - - "WARRANTY" - - parser: - normalize: true - trim: false - lowerCase: false - upperCase: true - removeParenthesis: false - replaceTokens: - "12 MONTHS": "1" - deleteTokens: - - "ANS" - - "AN" - - "GARANTIE" - - ################################## # ANNEE DE SORTIE ################################## @@ -594,7 +595,7 @@ attributesConfig: # + 144 more attributes... # ################################## - - key: "POWER_CONSUMPTION" + - key: "POWER_CONSUMPTION_TYPICAL" filteringType: "NUMERIC" asScore: true faIcon: ""