diff --git a/frigate/api/event.py b/frigate/api/event.py index 7640e979c9..5f55f1c831 100644 --- a/frigate/api/event.py +++ b/frigate/api/event.py @@ -199,18 +199,14 @@ def events( sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: - lowered = label.lower() - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) == lowered - ) # include exact matches (case-insensitive) + # SQLite LOWER() only folds ASCII, so lower both sides in SQL to + # keep non-ASCII names matching exactly + stored = fn.LOWER(Event.sub_label.cast("text")) + sub_label_clauses.append(stored == fn.LOWER(label)) - # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" - ) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" - ) + # include this label when part of a list + sub_label_clauses.append(stored % fn.LOWER(f"*{label},*")) + sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*")) sub_label_clause = reduce(operator.or_, sub_label_clauses) clauses.append(sub_label_clause) @@ -633,18 +629,14 @@ def events_search( sub_label_clauses.append(Event.sub_label.is_null()) for label in filtered_sub_labels: - lowered = label.lower() - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) == lowered - ) # include exact matches (case-insensitive) + # SQLite LOWER() only folds ASCII, so lower both sides in SQL to + # keep non-ASCII names matching exactly + stored = fn.LOWER(Event.sub_label.cast("text")) + sub_label_clauses.append(stored == fn.LOWER(label)) - # include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*" - ) - sub_label_clauses.append( - fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*" - ) + # include this label when part of a list + sub_label_clauses.append(stored % fn.LOWER(f"*{label},*")) + sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*")) event_filters.append(reduce(operator.or_, sub_label_clauses)) diff --git a/frigate/test/http_api/test_http_event.py b/frigate/test/http_api/test_http_event.py index f6b128b5fa..c758bbdeb7 100644 --- a/frigate/test/http_api/test_http_event.py +++ b/frigate/test/http_api/test_http_event.py @@ -485,6 +485,40 @@ class TestHttpApp(BaseTestHttp): assert sub_labels assert sub_labels == [sub_label] + def test_get_event_list_filter_sub_labels(self): + sub_labels = { + "1.cyrillic": "Кузнецова Ира", + "2.joined": "Ира, Bob", + "3.ascii": "Alice", + "4.none": None, + } + mock_embeddings = Mock() + mock_embeddings.search_thumbnail.return_value = [(id, 0.1) for id in sub_labels] + + self.app.frigate_config.semantic_search.enabled = True + self.app.embeddings = mock_embeddings + + with AuthTestClient(self.app) as client: + for id, sub_label in sub_labels.items(): + super().insert_mock_event(id) + Event.update(sub_label=sub_label).where(Event.id == id).execute() + + search_params = {"search_type": "similarity", "event_id": "1.cyrillic"} + + for path, params in (("/events", {}), ("/events/search", search_params)): + + def ids(query: str) -> set[str]: + events = client.get( + path, params={**params, "sub_labels": query} + ).json() + return {event["id"] for event in events} + + assert ids("Кузнецова Ира") == {"1.cyrillic"} + assert ids("Ира") == {"2.joined"} + assert ids("bob") == {"2.joined"} + assert ids("ALICE") == {"3.ascii"} + assert ids("None") == {"4.none"} + #################################################################################################################### ################################### GET /metrics Endpoint ######################################################### ####################################################################################################################