Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions froide/helper/search/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
21 changes: 20 additions & 1 deletion froide/helper/search/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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):
Expand All @@ -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
55 changes: 55 additions & 0 deletions froide/helper/tests/test_search.py
Original file line number Diff line number Diff line change
@@ -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")