From 5740c9473f5d9e1d02404221260e68c058308355 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Mon, 16 Feb 2026 21:42:49 +0200 Subject: [PATCH 1/3] NGO search exact match --- backend/donations/views/common/search.py | 32 +++++++++++++++++-- .../redirectioneaza/settings/feature_flags.py | 1 + 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/backend/donations/views/common/search.py b/backend/donations/views/common/search.py index 1f6623a9..6d3f07cd 100644 --- a/backend/donations/views/common/search.py +++ b/backend/donations/views/common/search.py @@ -72,6 +72,22 @@ def search(self, queryset: QuerySet[Any] | None = None) -> QuerySet: return self.get_search_results(queryset, query, language_code) +class NgoExactSearchMixin(CommonSearchMixin): + @classmethod + def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) -> QuerySet: + query_filter = Q(name__icontains=query) + + # Try to guess is the user is looking for a registration number + if query.isnumeric(): + query_filter = query_filter | Q(registration_number=query) + elif query.upper().startswith("RO") and query[2:].isnumeric(): + query_filter = query_filter | Q(registration_number=query[2:]) + + ngos: QuerySet[Ngo] = queryset.filter(query_filter).order_by("name").distinct("name") + + return ngos + + class NgoSearchMixin(CommonSearchMixin): @classmethod def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) -> QuerySet: @@ -129,12 +145,22 @@ def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) class NgoCauseMixedSearchMixin(CommonSearchMixin): @classmethod def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) -> QuerySet[Cause]: - ngos = NgoSearchMixin.get_search_results(Ngo.active, query, language_code) - ngos_causes = Cause.public_active.filter(ngo__in=ngos).distinct("name") + exact_causes = queryset.none() + # If exact match search is enabled, only perform it if the search query excluding the first two symbols + # is numeric (in order to look for registration numbers) or if the search query is longer than 20 chars + if settings.ENABLE_NGO_SEARCH_EXACT_MATCH and (query[2:].isnumeric() or len(query) > 20): + exact_ngos = NgoExactSearchMixin.get_search_results(Ngo.active, query, language_code) + exact_causes = Cause.public_active.filter(ngo__in=exact_ngos).distinct("name") + + if exact_causes: + return exact_causes + + fuzzy_ngos = NgoSearchMixin.get_search_results(Ngo.active, query, language_code) + fuzzy_causes = Cause.public_active.filter(ngo__in=fuzzy_ngos).distinct("name") searched_causes = CauseSearchMixin.get_search_results(queryset, query, language_code) - return searched_causes | ngos_causes + return searched_causes | fuzzy_causes class DonorSearchMixin(CommonSearchMixin): diff --git a/backend/redirectioneaza/settings/feature_flags.py b/backend/redirectioneaza/settings/feature_flags.py index 4a5f5fee..9368830c 100644 --- a/backend/redirectioneaza/settings/feature_flags.py +++ b/backend/redirectioneaza/settings/feature_flags.py @@ -4,6 +4,7 @@ # Search tweaks ENABLE_NGO_SEARCH_WORD_SIMILARITY = env.bool("ENABLE_NGO_SEARCH_WORD_SIMILARITY", False) ENABLE_CAUSE_SEARCH_WORD_SIMILARITY = env.bool("ENABLE_CAUSE_SEARCH_WORD_SIMILARITY", False) +ENABLE_NGO_SEARCH_EXACT_MATCH = env.bool("ENABLE_NGO_SEARCH_EXACT_MATCH", True) # Feature flags ENABLE_FLAG_CONTACT = env.bool("ENABLE_FLAG_CONTACT", False) From 16866e39f003a41594ec17f9c32827d87c01de9c Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Mon, 16 Feb 2026 21:49:07 +0200 Subject: [PATCH 2/3] Fix typo --- backend/donations/views/common/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/donations/views/common/search.py b/backend/donations/views/common/search.py index 6d3f07cd..571b3700 100644 --- a/backend/donations/views/common/search.py +++ b/backend/donations/views/common/search.py @@ -77,7 +77,7 @@ class NgoExactSearchMixin(CommonSearchMixin): def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) -> QuerySet: query_filter = Q(name__icontains=query) - # Try to guess is the user is looking for a registration number + # Try to guess if the user is looking for a registration number if query.isnumeric(): query_filter = query_filter | Q(registration_number=query) elif query.upper().startswith("RO") and query[2:].isnumeric(): From 4c0812352134a3c87a1b080bc0f1b4cdb07aa674 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Mon, 16 Feb 2026 21:56:26 +0200 Subject: [PATCH 3/3] Refactor the registration number guesser --- backend/donations/views/common/search.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/donations/views/common/search.py b/backend/donations/views/common/search.py index 571b3700..9bee8805 100644 --- a/backend/donations/views/common/search.py +++ b/backend/donations/views/common/search.py @@ -17,6 +17,17 @@ from donations.models.ngos import Cause, Ngo +def probably_registration_number(query: str) -> str | None: + """ + Try to extract a registration number from the query string + """ + if query.isnumeric(): + return query + elif query.upper().startswith("RO") and query[2:].isnumeric(): + return query[2:] + return None + + class ConfigureSearch: @staticmethod def query(query: str, language_code: str) -> SearchQuery: @@ -76,12 +87,9 @@ class NgoExactSearchMixin(CommonSearchMixin): @classmethod def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) -> QuerySet: query_filter = Q(name__icontains=query) - - # Try to guess if the user is looking for a registration number - if query.isnumeric(): - query_filter = query_filter | Q(registration_number=query) - elif query.upper().startswith("RO") and query[2:].isnumeric(): - query_filter = query_filter | Q(registration_number=query[2:]) + registration_number = probably_registration_number(query) + if registration_number: + query_filter = query_filter | Q(registration_number=registration_number) ngos: QuerySet[Ngo] = queryset.filter(query_filter).order_by("name").distinct("name") @@ -148,7 +156,7 @@ def get_search_results(cls, queryset: QuerySet, query: str, language_code: str) exact_causes = queryset.none() # If exact match search is enabled, only perform it if the search query excluding the first two symbols # is numeric (in order to look for registration numbers) or if the search query is longer than 20 chars - if settings.ENABLE_NGO_SEARCH_EXACT_MATCH and (query[2:].isnumeric() or len(query) > 20): + if settings.ENABLE_NGO_SEARCH_EXACT_MATCH and (probably_registration_number(query) or len(query) > 20): exact_ngos = NgoExactSearchMixin.get_search_results(Ngo.active, query, language_code) exact_causes = Cause.public_active.filter(ngo__in=exact_ngos).distinct("name")