diff --git a/froide/helper/search/__init__.py b/froide/helper/search/__init__.py index c6b19ab2e..201056736 100644 --- a/froide/helper/search/__init__.py +++ b/froide/helper/search/__init__.py @@ -55,6 +55,12 @@ def get_default_ngram_analyzer(): ) +def get_default_query_preprocessor(): + from froide.helper.search.filters import BaseQueryPreprocessor + + return BaseQueryPreprocessor() + + def get_func(config_name, default_func): def get_it(): from django.conf import settings @@ -73,3 +79,4 @@ def get_it(): get_search_analyzer = get_func("search_analyzer", get_default_text_analyzer) get_search_quote_analyzer = get_func("search_quote_analyzer", get_default_text_analyzer) get_ngram_analyzer = get_func("ngram_analyzer", get_default_ngram_analyzer) +get_query_preprocessor = get_func("query_preprocessor", get_default_query_preprocessor) diff --git a/froide/helper/search/filters.py b/froide/helper/search/filters.py index efa1ac421..f6bbc740a 100644 --- a/froide/helper/search/filters.py +++ b/froide/helper/search/filters.py @@ -4,6 +4,8 @@ import django_filters from elasticsearch_dsl.query import Q +from froide.helper.search import get_query_preprocessor + class BaseSearchFilterSet(django_filters.FilterSet): query_fields = ["content"] @@ -19,6 +21,7 @@ class BaseSearchFilterSet(django_filters.FilterSet): def __init__(self, *args, **kwargs): self.facet_config = kwargs.pop("facet_config", {}) self.view = kwargs.pop("view", None) + self.query_preprocessor = get_query_preprocessor() super().__init__(*args, **kwargs) def apply_filter(self, qs, name, *args, **kwargs): @@ -42,13 +45,29 @@ def filter_queryset(self, queryset): def auto_query(self, qs, name, value): if value: + query = self.query_preprocessor.prepare_query(value) return qs.set_query( Q( "simple_query_string", - query=value, + query=query, fields=self.query_fields, default_operator="and", lenient=True, ) ) return qs + + +class BaseQueryPreprocessor: + """ + Base class that can be overridden for custom search query preprocessing. + """ + + def prepare_query(self, text: str): + """ + Preprocess the given search query text and return the processed text. + + This method can be overridden in subclasses to implement custom + preprocessing logic. + """ + return text diff --git a/froide/helper/tests/test_search.py b/froide/helper/tests/test_search.py new file mode 100644 index 000000000..0214f2a30 --- /dev/null +++ b/froide/helper/tests/test_search.py @@ -0,0 +1,55 @@ +from unittest.mock import MagicMock + +from froide.helper.search.filters import BaseSearchFilterSet + + +class DummyModel: + pass + + +class DummyQS: + def __init__(self): + self.model = DummyModel() + self.query = None + + def set_query(self, q): + self.query = q + return self + + +class TestBaseSearchFilterSetQueryPreprocessing: + def test_auto_query_without_query_value(self): + qs = DummyQS() + fs = BaseSearchFilterSet(queryset=qs) + + result = fs.auto_query(qs, "q", "") + + assert result is qs + assert result.query is None + + def test_auto_query_with_default_query_preprocessor(self): + qs = DummyQS() + fs = BaseSearchFilterSet(queryset=qs) + + result = fs.auto_query(qs, "q", "test query") + + assert result is qs + assert result.query is not None + assert result.query.query == "test query" + + def test_auto_query_with_custom_query_preprocessor(self, monkeypatch): + # Mock custom query preprocessor. + mock_preprocessor = MagicMock() + mock_preprocessor.prepare_query.return_value = "processed query" + monkeypatch.setattr( + "froide.helper.search.filters.get_query_preprocessor", + lambda: mock_preprocessor, + ) + + qs = DummyQS() + fs = BaseSearchFilterSet(queryset=qs) + + result = fs.auto_query(qs, "q", "original query") + + assert result.query.query == "processed query" + mock_preprocessor.prepare_query.assert_called_once_with("original query")