Skip to content

Commit

Permalink
feat: 620 - autocomplete for all taxonomy names and fuzziness levels (#…
Browse files Browse the repository at this point in the history
…835)

Impacted files:
* `api_search_test.dart`: added tests for all taxonomy names and all fuzziness levels
* `fuzziness_level.dart`: added all fuzziness values
* `open_food_search_api_client.dart`: added a comment
* `taxonomy_name.dart`: added all taxonomy names
  • Loading branch information
monsieurtanuki committed Nov 27, 2023
1 parent a4de2bf commit 046c418
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 17 deletions.
3 changes: 3 additions & 0 deletions lib/src/open_food_search_api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class OpenFoodSearchAPIClient {
static String _getHost(final UriProductHelper uriHelper) =>
uriHelper.getHost(_subdomain);

/// Returns a list of suggestions.
///
/// /!\ For brands, language must be English.
static Future<AutocompleteSearchResult> autocomplete({
required String query,
required final List<TaxonomyName> taxonomyNames,
Expand Down
8 changes: 6 additions & 2 deletions lib/src/search/fuzziness_level.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'package:openfoodfacts/src/model/off_tagged.dart';

/// Fuzziness Level for Elastic Search API.
///
/// Levenshtein distance (= number of edits).
/// cf. https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness
enum Fuzziness implements OffTagged {
// TODO(monsieurtanuki): introduce other values when available, like 1 and 2.
none(offTag: '0');
none(offTag: '0'),
one(offTag: '1'),
two(offTag: '2');

const Fuzziness({
required this.offTag,
Expand Down
32 changes: 30 additions & 2 deletions lib/src/search/taxonomy_name.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import '../model/off_tagged.dart';

/// Taxonomy Name for Elastic Search API.
///
/// cf. https://github.com/openfoodfacts/search-a-licious/blob/main/data/config/openfoodfacts.yml
enum TaxonomyName implements OffTagged {
// TODO(monsieurtanuki): add other values when available.
category(offTag: 'category');
category(offTag: 'category'),
label(offTag: 'label'),
additive(offTag: 'additive'),
allergen(offTag: 'allergen'),
aminoAcid(offTag: 'amino_acid'),
country(offTag: 'country'),
dataQuality(offTag: 'data_quality'),
foodGroup(offTag: 'food_group'),
improvement(offTag: 'improvement'),
ingredient(offTag: 'ingredient'),
ingredientAnalysis(offTag: 'ingredients_analysis'),
ingredientProcessing(offTag: 'ingredients_processing'),
language(offTag: 'language'),
mineral(offTag: 'mineral'),
misc(offTag: 'misc'),
novaGroup(offTag: 'nova_group'),
nucleotide(offTag: 'nucleotide'),
nutrient(offTag: 'nutrient'),
origin(offTag: 'origin'),
otherNutritionalSubstance(offTag: 'other_nutritional_substance'),
packagingMaterial(offTag: 'packaging_material'),
packagingRecycling(offTag: 'packaging_recycling'),
packagingShape(offTag: 'packaging_shape'),
periodsAfterOpening(offTag: 'periods_after_opening'),
preservation(offTag: 'preservation'),
state(offTag: 'state'),
vitamin(offTag: 'vitamin'),
brand(offTag: 'brand');

const TaxonomyName({
required this.offTag,
Expand Down
144 changes: 131 additions & 13 deletions test/api_search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,30 @@ void main() {
group(
'$OpenFoodSearchAPIClient autocomplete',
() {
const int size = 5;
const TaxonomyName taxonomyName = TaxonomyName.category;
const int maxSize = 5;
const OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH;

void basicTest(final AutocompleteSearchResult result) {
expect(result.took, greaterThanOrEqualTo(0));
expect(result.timedOut, false);
expect(result.options, isNotNull);
expect(result.options!.length, lessThanOrEqualTo(maxSize));
}

test(
'category with existing products',
'category with existing matches',
() async {
const TaxonomyName taxonomyName = TaxonomyName.category;
final AutocompleteSearchResult result =
await OpenFoodSearchAPIClient.autocomplete(
query: 'pizza',
taxonomyNames: <TaxonomyName>[taxonomyName],
language: language,
uriHelper: uriHelper,
size: size,
size: maxSize,
);
expect(result.took, greaterThanOrEqualTo(0));
expect(result.timedOut, false);
expect(result.options, isNotNull);
expect(result.options!.length, size);
basicTest(result);
expect(result.options, hasLength(maxSize));
for (final AutocompleteSingleResult item in result.options!) {
expect(item.id, contains(':'));
expect(item.taxonomyName, taxonomyName);
Expand All @@ -39,22 +44,135 @@ void main() {
);

test(
'category with non existing products',
'category with non existing matches',
() async {
const TaxonomyName taxonomyName = TaxonomyName.category;
final AutocompleteSearchResult result =
await OpenFoodSearchAPIClient.autocomplete(
query: 'pifsehjfsjkvnskjvbehjszza',
taxonomyNames: <TaxonomyName>[taxonomyName],
language: language,
uriHelper: uriHelper,
size: size,
size: maxSize,
);
expect(result.took, greaterThanOrEqualTo(0));
expect(result.timedOut, false);
expect(result.options, isNotNull);
basicTest(result);
expect(result.options, isEmpty);
},
);

test(
'all fuzziness levels',
() async {
const TaxonomyName taxonomyName = TaxonomyName.country;
const String expectedValue = 'France';
const Map<String, Fuzziness> inputs = <String, Fuzziness>{
expectedValue: Fuzziness.none,
'Franse': Fuzziness.one,
'Frense': Fuzziness.two,
};
for (final String inputValue in inputs.keys) {
final AutocompleteSearchResult result =
await OpenFoodSearchAPIClient.autocomplete(
// possibly with a typo
query: inputValue,
taxonomyNames: <TaxonomyName>[taxonomyName],
language: language,
uriHelper: uriHelper,
size: maxSize,
// supposed to fix the typo (if relevant)
fuzziness: inputs[inputValue]!,
);
basicTest(result);
expect(result.options, isNotEmpty);
bool found = false;
for (final AutocompleteSingleResult item in result.options!) {
expect(item.id, contains(':'));
expect(item.taxonomyName, taxonomyName);
if (item.text == expectedValue) {
found = true;
}
}
expect(found, isTrue);
}
},
);

Future<void> simpleTest(
final TaxonomyName taxonomyName,
final String query,
final String expectedValue, {
final OpenFoodFactsLanguage language = OpenFoodFactsLanguage.FRENCH,
}) async {
final AutocompleteSearchResult result =
await OpenFoodSearchAPIClient.autocomplete(
query: query,
taxonomyNames: <TaxonomyName>[taxonomyName],
language: language,
uriHelper: uriHelper,
size: maxSize,
fuzziness: Fuzziness.none,
);
basicTest(result);
expect(result.options, isNotEmpty);
bool found = false;
for (final AutocompleteSingleResult item in result.options!) {
expect(item.id, contains(':'));
expect(item.taxonomyName, taxonomyName);
if (item.text == expectedValue) {
found = true;
}
}
expect(found, isTrue);
}

test(
'all taxonomy names',
() async {
await simpleTest(TaxonomyName.category, 'sky', 'Skyr nature');
await simpleTest(TaxonomyName.label, 'fsc', 'FSC Mix');
await simpleTest(TaxonomyName.additive, 'E10', 'E104');
await simpleTest(TaxonomyName.allergen, 'mouta', 'moutarde');
await simpleTest(TaxonomyName.aminoAcid, 'L-argin', 'L-arginine');
await simpleTest(TaxonomyName.country, 'fra', 'France');
await simpleTest(
TaxonomyName.dataQuality,
'Valeur nutritionnelle 3800',
'Valeur nutritionnelle supérieure à 3800 - Energie');
await simpleTest(
TaxonomyName.foodGroup, 'fromage per', 'Fromage persillé');
await simpleTest(TaxonomyName.improvement, 'Nutrition - Hau',
'Nutrition - Haut taux de sel pour la catégorie');
await simpleTest(
TaxonomyName.ingredient, 'fromage bla', 'Fromage blanc');
await simpleTest(
TaxonomyName.ingredientAnalysis, 'végé', 'Végétarien');
await simpleTest(
TaxonomyName.ingredientProcessing, 'enri', 'enrichi');
await simpleTest(TaxonomyName.language, 'fran', 'français');
await simpleTest(TaxonomyName.mineral, 'zi', 'Zinc');
await simpleTest(
TaxonomyName.misc, 'nutriscore', 'NutriScore - Calculé');
await simpleTest(
TaxonomyName.novaGroup, 'aliments', 'Aliments transformés');
await simpleTest(TaxonomyName.nucleotide, 'adénosine mon',
'Adénosine monophosphate');
await simpleTest(TaxonomyName.nutrient, 'prot', 'Protéine');
await simpleTest(TaxonomyName.origin, 'prove', 'Provence');
await simpleTest(
TaxonomyName.otherNutritionalSubstance, 'chol', 'Choline');
await simpleTest(TaxonomyName.packagingMaterial, 'verr', 'Verre');
await simpleTest(
TaxonomyName.packagingRecycling, 'compost', 'Compostable');
await simpleTest(TaxonomyName.packagingShape, 'boute', 'Bouteille');
await simpleTest(
TaxonomyName.periodsAfterOpening, '2 jours', '21 jours');
await simpleTest(TaxonomyName.preservation, 'fra', 'Frais');
await simpleTest(TaxonomyName.state, 'emball', 'Emballage complété');
await simpleTest(TaxonomyName.vitamin, 'b', 'b12');
await simpleTest(TaxonomyName.brand, 'carref', 'Carrefour',
language: OpenFoodFactsLanguage.ENGLISH);
},
);
},
);
}

0 comments on commit 046c418

Please sign in to comment.