From 4dcd2968b37002be872cffcbae744312c5f3fce9 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 14 Feb 2026 08:33:17 -0600 Subject: [PATCH] consolidate attribute filtering to match non-english and url encoded values (#22002) --- frigate/api/event.py | 42 +++++++++++-------- frigate/test/http_api/test_http_event.py | 51 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/frigate/api/event.py b/frigate/api/event.py index ea5cfb29c..c03cfb431 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -69,6 +69,25 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=[Tags.events]) +def _build_attribute_filter_clause(attributes: str): + filtered_attributes = [ + attr.strip() for attr in attributes.split(",") if attr.strip() + ] + attribute_clauses = [] + + for attr in filtered_attributes: + attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') + + escaped_attr = json.dumps(attr, ensure_ascii=True)[1:-1] + if escaped_attr != attr: + attribute_clauses.append(Event.data.cast("text") % f'*:"{escaped_attr}"*') + + if not attribute_clauses: + return None + + return reduce(operator.or_, attribute_clauses) + + @router.get( "/events", response_model=list[EventResponse], @@ -193,14 +212,9 @@ def events( if attributes != "all": # Custom classification results are stored as data[model_name] = result_value - filtered_attributes = attributes.split(",") - attribute_clauses = [] - - for attr in filtered_attributes: - attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') - - attribute_clause = reduce(operator.or_, attribute_clauses) - clauses.append(attribute_clause) + attribute_clause = _build_attribute_filter_clause(attributes) + if attribute_clause is not None: + clauses.append(attribute_clause) if recognized_license_plate != "all": filtered_recognized_license_plates = recognized_license_plate.split(",") @@ -508,7 +522,7 @@ def events_search( cameras = params.cameras labels = params.labels sub_labels = params.sub_labels - attributes = params.attributes + attributes = unquote(params.attributes) zones = params.zones after = params.after before = params.before @@ -607,13 +621,9 @@ def events_search( if attributes != "all": # Custom classification results are stored as data[model_name] = result_value - filtered_attributes = attributes.split(",") - attribute_clauses = [] - - for attr in filtered_attributes: - attribute_clauses.append(Event.data.cast("text") % f'*:"{attr}"*') - - event_filters.append(reduce(operator.or_, attribute_clauses)) + attribute_clause = _build_attribute_filter_clause(attributes) + if attribute_clause is not None: + event_filters.append(attribute_clause) if zones != "all": zone_clauses = [] diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index fc895fabf..bc7f388e1 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -168,6 +168,57 @@ class TestHttpApp(BaseTestHttp): assert events[0]["id"] == id assert events[1]["id"] == id2 + def test_get_event_list_match_multilingual_attribute(self): + event_id = "123456.zh" + attribute = "中文标签" + + with AuthTestClient(self.app) as client: + super().insert_mock_event(event_id, data={"custom_attr": attribute}) + + events = client.get("/events", params={"attributes": attribute}).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + events = client.get( + "/events", params={"attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE"} + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + def test_events_search_match_multilingual_attribute(self): + event_id = "123456.zh.search" + attribute = "中文标签" + mock_embeddings = Mock() + mock_embeddings.search_thumbnail.return_value = [(event_id, 0.05)] + + self.app.frigate_config.semantic_search.enabled = True + self.app.embeddings = mock_embeddings + + with AuthTestClient(self.app) as client: + super().insert_mock_event(event_id, data={"custom_attr": attribute}) + + events = client.get( + "/events/search", + params={ + "search_type": "similarity", + "event_id": event_id, + "attributes": attribute, + }, + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + + events = client.get( + "/events/search", + params={ + "search_type": "similarity", + "event_id": event_id, + "attributes": "%E4%B8%AD%E6%96%87%E6%A0%87%E7%AD%BE", + }, + ).json() + assert len(events) == 1 + assert events[0]["id"] == event_id + def test_get_good_event(self): id = "123456.random"