diff --git a/CHANGES.md b/CHANGES.md index a8caa7a..4208033 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Changelog +## 1.0.0b67 (unreleased) + +### Fixed + +- Full-text search now picks up the current site/negotiated language when the + query has no explicit `Language` parameter, instead of falling back to the + `simple` text-search configuration. On multilingual sites this means BM25 / + `tsvector` ranking uses the right language config (stemming, stop words) for + queries that don't pass `Language` (e.g. plain `SearchableText` searches). + The language is resolved from the request, the context, or the portal default + via a small vendored helper (no `plone.api` dependency). #166 + ## 1.0.0b66 (2026-06-29) ### Added @@ -64,6 +76,7 @@ fast with a clear hint pointing at the required extra; the S3 path raises an actionable error too. #171 + ## 1.0.0b65 ### Added @@ -119,6 +132,7 @@ ``storage.clear_caches()`` after ``restore()``; this requires ``zodb-pgjsonb >= 1.13.0`` (the dependency floor is bumped accordingly). + ## 1.0.0b64 ### Fixed diff --git a/src/plone/pgcatalog/query.py b/src/plone/pgcatalog/query.py index 6a55198..36d3ccc 100644 --- a/src/plone/pgcatalog/query.py +++ b/src/plone/pgcatalog/query.py @@ -13,6 +13,7 @@ from plone.pgcatalog.columns import get_registry from plone.pgcatalog.columns import IndexType from plone.pgcatalog.columns import validate_identifier +from plone.pgcatalog.utils import get_current_language from psycopg.types.json import Json from typing import ClassVar @@ -609,6 +610,12 @@ def _handle_text(self, name, idx_key, spec): lang_val = self._query.get("Language") if isinstance(lang_val, dict): lang_val = lang_val.get("query", "") + if not lang_val: + # Try getting the current language from the environment + current_language = get_current_language() + if current_language is not None: + lang_val = current_language + lang_val = _bool_to_lower_str(lang_val) if lang_val else "" clause, params, rank_expr = get_backend().build_search_clause( diff --git a/src/plone/pgcatalog/utils.py b/src/plone/pgcatalog/utils.py new file mode 100644 index 0000000..cba1c8b --- /dev/null +++ b/src/plone/pgcatalog/utils.py @@ -0,0 +1,41 @@ +# Implementation taken from plone.api.portal to avoid +# having plone.api as a dependency + +from Acquisition import aq_inner +from plone.registry.interfaces import IRegistry +from zope.component import getUtility +from zope.globalrequest import getRequest + + +def get_default_language(): + """Return the default language. + + :returns: language identifier + :rtype: string + :Example: :ref:`portal-get-default-language-example` + """ + from plone.i18n.interfaces import ILanguageSchema + + registry = getUtility(IRegistry) + settings = registry.forInterface(ILanguageSchema, prefix="plone") + return settings.default_language + + +def get_current_language(context=None): + """Return the current negotiated language. + + :param context: context object + :type context: object + :returns: language identifier + :rtype: string + :Example: :ref:`portal-get-current-language-example` + """ + request = getRequest() + if request is not None: + return ( + request.get("LANGUAGE", None) + or (context and aq_inner(context).Language()) + or get_default_language() + ) + + return None diff --git a/tests/test_query.py b/tests/test_query.py index 07e91f7..5ccef27 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1378,3 +1378,47 @@ def test_empty_when_only_unknown_keys(self): assert ( strip_non_index_keys({"metadata_fields": ["Title"]}, ["portal_type"]) == {} ) + + +class TestSearchableTextCurrentLanguage: + """#166: when the query has no Language, fall back to the current/site + language for the search text-search configuration instead of 'simple'.""" + + def test_uses_current_language_when_language_absent(self, monkeypatch): + import plone.pgcatalog.query as q + + monkeypatch.setattr(q, "get_current_language", lambda *a, **k: "de") + qr = build_query({"SearchableText": "hallo"}) + assert [v for v in qr["params"].values() if v == "de"], qr["params"] + + def test_explicit_language_wins_over_current_language(self, monkeypatch): + import plone.pgcatalog.query as q + + monkeypatch.setattr(q, "get_current_language", lambda *a, **k: "de") + qr = build_query({"SearchableText": "bonjour", "Language": "fr"}) + assert [v for v in qr["params"].values() if v == "fr"], qr["params"] + assert not [v for v in qr["params"].values() if v == "de"], qr["params"] + + def test_no_request_falls_back_to_simple(self, monkeypatch): + """No current language available → empty string ('simple' config).""" + import plone.pgcatalog.query as q + + monkeypatch.setattr(q, "get_current_language", lambda *a, **k: None) + qr = build_query({"SearchableText": "hello"}) + assert [v for v in qr["params"].values() if v == ""], qr["params"] + + +class TestGetCurrentLanguage: + """Unit tests for the vendored get_current_language helper (#166).""" + + def test_returns_none_without_request(self, monkeypatch): + import plone.pgcatalog.utils as u + + monkeypatch.setattr(u, "getRequest", lambda: None) + assert u.get_current_language() is None + + def test_returns_request_language(self, monkeypatch): + import plone.pgcatalog.utils as u + + monkeypatch.setattr(u, "getRequest", lambda: {"LANGUAGE": "eu"}) + assert u.get_current_language() == "eu"