mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Fix similarity search (#13856)
* add event_id param to api * exclude query from filtertype * update review pane link for similarity search * update filter group for similarity param and fix switch bug * unneeded prop * update query and input for similarity search param * use undefined instead of empty string for query with similarity search
This commit is contained in:
parent
1a51ce712c
commit
176af55e8c
@ -353,7 +353,10 @@ def events_search():
|
|||||||
after = request.args.get("after", type=float)
|
after = request.args.get("after", type=float)
|
||||||
before = request.args.get("before", type=float)
|
before = request.args.get("before", type=float)
|
||||||
|
|
||||||
if not query:
|
# for similarity search
|
||||||
|
event_id = request.args.get("event_id", type=str)
|
||||||
|
|
||||||
|
if not query and not event_id:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
{
|
{
|
||||||
@ -432,7 +435,7 @@ def events_search():
|
|||||||
if search_type == "similarity":
|
if search_type == "similarity":
|
||||||
# Grab the ids of events that match the thumbnail image embeddings
|
# Grab the ids of events that match the thumbnail image embeddings
|
||||||
try:
|
try:
|
||||||
search_event: Event = Event.get(Event.id == query)
|
search_event: Event = Event.get(Event.id == event_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify(
|
jsonify(
|
||||||
|
@ -43,7 +43,6 @@ type SearchFilterGroupProps = {
|
|||||||
className: string;
|
className: string;
|
||||||
filters?: SearchFilters[];
|
filters?: SearchFilters[];
|
||||||
filter?: SearchFilter;
|
filter?: SearchFilter;
|
||||||
searchTerm: string;
|
|
||||||
filterList?: FilterList;
|
filterList?: FilterList;
|
||||||
onUpdateFilter: (filter: SearchFilter) => void;
|
onUpdateFilter: (filter: SearchFilter) => void;
|
||||||
};
|
};
|
||||||
@ -51,7 +50,6 @@ export default function SearchFilterGroup({
|
|||||||
className,
|
className,
|
||||||
filters = DEFAULT_REVIEW_FILTERS,
|
filters = DEFAULT_REVIEW_FILTERS,
|
||||||
filter,
|
filter,
|
||||||
searchTerm,
|
|
||||||
filterList,
|
filterList,
|
||||||
onUpdateFilter,
|
onUpdateFilter,
|
||||||
}: SearchFilterGroupProps) {
|
}: SearchFilterGroupProps) {
|
||||||
@ -213,7 +211,7 @@ export default function SearchFilterGroup({
|
|||||||
)}
|
)}
|
||||||
{config?.semantic_search?.enabled &&
|
{config?.semantic_search?.enabled &&
|
||||||
filters.includes("source") &&
|
filters.includes("source") &&
|
||||||
!searchTerm.includes("similarity:") && (
|
!filter?.search_type?.includes("similarity") && (
|
||||||
<SearchTypeButton
|
<SearchTypeButton
|
||||||
selectedSearchSources={
|
selectedSearchSources={
|
||||||
filter?.search_type ?? ["thumbnail", "description"]
|
filter?.search_type ?? ["thumbnail", "description"]
|
||||||
@ -914,7 +912,7 @@ export function SearchTypeContent({
|
|||||||
<div className="my-2.5 flex flex-col gap-2.5">
|
<div className="my-2.5 flex flex-col gap-2.5">
|
||||||
<FilterSwitch
|
<FilterSwitch
|
||||||
label="Thumbnail Image"
|
label="Thumbnail Image"
|
||||||
isChecked={selectedSearchSources?.includes("thumbnail") ?? false}
|
isChecked={currentSearchSources?.includes("thumbnail") ?? false}
|
||||||
onCheckedChange={(isChecked) => {
|
onCheckedChange={(isChecked) => {
|
||||||
const updatedSources = currentSearchSources
|
const updatedSources = currentSearchSources
|
||||||
? [...currentSearchSources]
|
? [...currentSearchSources]
|
||||||
|
@ -180,17 +180,11 @@ export default function InputWithTags({
|
|||||||
|
|
||||||
const createFilter = useCallback(
|
const createFilter = useCallback(
|
||||||
(type: FilterType, value: string) => {
|
(type: FilterType, value: string) => {
|
||||||
if (
|
if (allSuggestions[type as FilterType]?.includes(value)) {
|
||||||
allSuggestions[type as keyof SearchFilter]?.includes(value) ||
|
|
||||||
type === "before" ||
|
|
||||||
type === "after"
|
|
||||||
) {
|
|
||||||
const newFilters = { ...filters };
|
const newFilters = { ...filters };
|
||||||
let timestamp = 0;
|
let timestamp = 0;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "query":
|
|
||||||
break;
|
|
||||||
case "before":
|
case "before":
|
||||||
case "after":
|
case "after":
|
||||||
timestamp = convertLocalDateToTimestamp(value);
|
timestamp = convertLocalDateToTimestamp(value);
|
||||||
@ -268,9 +262,7 @@ export default function InputWithTags({
|
|||||||
(filterType: FilterType, filterValue: string) => {
|
(filterType: FilterType, filterValue: string) => {
|
||||||
const trimmedValue = filterValue.trim();
|
const trimmedValue = filterValue.trim();
|
||||||
if (
|
if (
|
||||||
allSuggestions[filterType as keyof SearchFilter]?.includes(
|
allSuggestions[filterType]?.includes(trimmedValue) ||
|
||||||
trimmedValue,
|
|
||||||
) ||
|
|
||||||
((filterType === "before" || filterType === "after") &&
|
((filterType === "before" || filterType === "after") &&
|
||||||
trimmedValue.match(/^\d{8}$/))
|
trimmedValue.match(/^\d{8}$/))
|
||||||
) {
|
) {
|
||||||
@ -429,14 +421,14 @@ export default function InputWithTags({
|
|||||||
}, [currentFilterType, inputValue, updateSuggestions]);
|
}, [currentFilterType, inputValue, updateSuggestions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (search?.startsWith("similarity:")) {
|
if (filters?.search_type && filters?.search_type.includes("similarity")) {
|
||||||
setIsSimilaritySearch(true);
|
setIsSimilaritySearch(true);
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
} else {
|
} else {
|
||||||
setIsSimilaritySearch(false);
|
setIsSimilaritySearch(false);
|
||||||
setInputValue(search || "");
|
setInputValue(search || "");
|
||||||
}
|
}
|
||||||
}, [search]);
|
}, [filters, search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -585,56 +577,57 @@ export default function InputWithTags({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{Object.entries(filters).map(([filterType, filterValues]) =>
|
{Object.entries(filters).map(([filterType, filterValues]) =>
|
||||||
Array.isArray(filterValues) ? (
|
Array.isArray(filterValues)
|
||||||
filterValues
|
? filterValues
|
||||||
.filter(() => filterType !== "query")
|
.filter(() => filterType !== "query")
|
||||||
.map((value, index) => (
|
.filter(() => !filterValues.includes("similarity"))
|
||||||
|
.map((value, index) => (
|
||||||
|
<span
|
||||||
|
key={`${filterType}-${index}`}
|
||||||
|
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||||
|
>
|
||||||
|
{filterType.replaceAll("_", " ")}:{" "}
|
||||||
|
{value.replaceAll("_", " ")}
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
removeFilter(filterType as FilterType, value)
|
||||||
|
}
|
||||||
|
className="ml-1 focus:outline-none"
|
||||||
|
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
|
||||||
|
>
|
||||||
|
<LuX className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: filterType !== "event_id" && (
|
||||||
<span
|
<span
|
||||||
key={`${filterType}-${index}`}
|
key={filterType}
|
||||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
||||||
>
|
>
|
||||||
{filterType.replaceAll("_", " ")}:{" "}
|
{filterType}:
|
||||||
{value.replaceAll("_", " ")}
|
{filterType === "before" || filterType === "after"
|
||||||
|
? new Date(
|
||||||
|
(filterType === "before"
|
||||||
|
? (filterValues as number) + 1
|
||||||
|
: (filterValues as number)) * 1000,
|
||||||
|
).toLocaleDateString(
|
||||||
|
window.navigator?.language || "en-US",
|
||||||
|
)
|
||||||
|
: filterValues}
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
removeFilter(filterType as FilterType, value)
|
removeFilter(
|
||||||
|
filterType as FilterType,
|
||||||
|
filterValues as string | number,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="ml-1 focus:outline-none"
|
className="ml-1 focus:outline-none"
|
||||||
aria-label={`Remove ${filterType}:${value.replaceAll("_", " ")} filter`}
|
aria-label={`Remove ${filterType}:${filterValues} filter`}
|
||||||
>
|
>
|
||||||
<LuX className="h-3 w-3" />
|
<LuX className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
))
|
),
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
key={filterType}
|
|
||||||
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
|
|
||||||
>
|
|
||||||
{filterType}:
|
|
||||||
{filterType === "before" || filterType === "after"
|
|
||||||
? new Date(
|
|
||||||
(filterType === "before"
|
|
||||||
? (filterValues as number) + 1
|
|
||||||
: (filterValues as number)) * 1000,
|
|
||||||
).toLocaleDateString(
|
|
||||||
window.navigator?.language || "en-US",
|
|
||||||
)
|
|
||||||
: filterValues}
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
removeFilter(
|
|
||||||
filterType as FilterType,
|
|
||||||
filterValues as string | number,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="ml-1 focus:outline-none"
|
|
||||||
aria-label={`Remove ${filterType}:${filterValues} filter`}
|
|
||||||
>
|
|
||||||
<LuX className="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
@ -370,7 +370,9 @@ function EventItem({
|
|||||||
<Chip
|
<Chip
|
||||||
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/explore?similarity_search_id=${event.id}`);
|
navigate(
|
||||||
|
`/explore?search_type=similarity&event_id=${event.id}`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaImages className="size-4 text-white" />
|
<FaImages className="size-4 text-white" />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
||||||
import { useSearchEffect } from "@/hooks/use-overlay-state";
|
|
||||||
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
||||||
import SearchView from "@/views/search/SearchView";
|
import SearchView from "@/views/search/SearchView";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
@ -21,37 +20,22 @@ export default function Explore() {
|
|||||||
[searchSearchParams],
|
[searchSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
// search filter
|
const similaritySearch = useMemo(
|
||||||
|
() => searchSearchParams["search_type"] == "similarity",
|
||||||
const similaritySearch = useMemo(() => {
|
[searchSearchParams],
|
||||||
if (!searchTerm.includes("similarity:")) {
|
);
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchTerm.split(":")[1];
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
// search api
|
|
||||||
|
|
||||||
useSearchEffect("query", (query) => {
|
|
||||||
setSearch(query);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useSearchEffect("similarity_search_id", (similarityId) => {
|
|
||||||
setSearch(`similarity:${similarityId}`);
|
|
||||||
// @ts-expect-error we want to clear this
|
|
||||||
setSearchFilter({ ...searchFilter, similarity_search_id: undefined });
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchTerm && !search) {
|
if (!searchTerm && !search) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// switch back to normal search when query is entered
|
||||||
setSearchFilter({
|
setSearchFilter({
|
||||||
...searchFilter,
|
...searchFilter,
|
||||||
|
search_type:
|
||||||
|
similaritySearch && search ? undefined : searchFilter?.search_type,
|
||||||
|
event_id: similaritySearch && search ? undefined : searchFilter?.event_id,
|
||||||
query: search.length > 0 ? search : undefined,
|
query: search.length > 0 ? search : undefined,
|
||||||
});
|
});
|
||||||
// only update when search is updated
|
// only update when search is updated
|
||||||
@ -59,41 +43,18 @@ export default function Explore() {
|
|||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
const searchQuery: SearchQuery = useMemo(() => {
|
const searchQuery: SearchQuery = useMemo(() => {
|
||||||
if (similaritySearch) {
|
// no search parameters
|
||||||
return [
|
if (searchSearchParams && Object.keys(searchSearchParams).length === 0) {
|
||||||
"events/search",
|
return null;
|
||||||
{
|
|
||||||
query: similaritySearch,
|
|
||||||
cameras: searchSearchParams["cameras"],
|
|
||||||
labels: searchSearchParams["labels"],
|
|
||||||
sub_labels: searchSearchParams["subLabels"],
|
|
||||||
zones: searchSearchParams["zones"],
|
|
||||||
before: searchSearchParams["before"],
|
|
||||||
after: searchSearchParams["after"],
|
|
||||||
include_thumbnails: 0,
|
|
||||||
search_type: "similarity",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
// parameters, but no search term and not similarity
|
||||||
return [
|
if (
|
||||||
"events/search",
|
searchSearchParams &&
|
||||||
{
|
Object.keys(searchSearchParams).length !== 0 &&
|
||||||
query: searchTerm,
|
!searchTerm &&
|
||||||
cameras: searchSearchParams["cameras"],
|
!similaritySearch
|
||||||
labels: searchSearchParams["labels"],
|
) {
|
||||||
sub_labels: searchSearchParams["subLabels"],
|
|
||||||
zones: searchSearchParams["zones"],
|
|
||||||
before: searchSearchParams["before"],
|
|
||||||
after: searchSearchParams["after"],
|
|
||||||
search_type: searchSearchParams["search_type"],
|
|
||||||
include_thumbnails: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchSearchParams && Object.keys(searchSearchParams).length !== 0) {
|
|
||||||
return [
|
return [
|
||||||
"events",
|
"events",
|
||||||
{
|
{
|
||||||
@ -112,7 +73,26 @@ export default function Explore() {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// parameters and search term
|
||||||
|
if (!similaritySearch) {
|
||||||
|
setSearch(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"events/search",
|
||||||
|
{
|
||||||
|
query: similaritySearch ? undefined : searchTerm,
|
||||||
|
cameras: searchSearchParams["cameras"],
|
||||||
|
labels: searchSearchParams["labels"],
|
||||||
|
sub_labels: searchSearchParams["subLabels"],
|
||||||
|
zones: searchSearchParams["zones"],
|
||||||
|
before: searchSearchParams["before"],
|
||||||
|
after: searchSearchParams["after"],
|
||||||
|
search_type: searchSearchParams["search_type"],
|
||||||
|
event_id: searchSearchParams["event_id"],
|
||||||
|
include_thumbnails: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
}, [searchTerm, searchSearchParams, similaritySearch]);
|
}, [searchTerm, searchSearchParams, similaritySearch]);
|
||||||
|
|
||||||
// paging
|
// paging
|
||||||
@ -205,7 +185,13 @@ export default function Explore() {
|
|||||||
searchResults={searchResults}
|
searchResults={searchResults}
|
||||||
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
||||||
setSearch={setSearch}
|
setSearch={setSearch}
|
||||||
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
setSimilaritySearch={(search) => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
search_type: ["similarity"],
|
||||||
|
event_id: search.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
setSearchFilter={setSearchFilter}
|
setSearchFilter={setSearchFilter}
|
||||||
onUpdateFilter={setSearchFilter}
|
onUpdateFilter={setSearchFilter}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
@ -56,7 +56,7 @@ export type SearchQueryParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type SearchQuery = [string, SearchQueryParams] | null;
|
export type SearchQuery = [string, SearchQueryParams] | null;
|
||||||
export type FilterType = keyof SearchFilter;
|
export type FilterType = Exclude<keyof SearchFilter, "query">;
|
||||||
|
|
||||||
export type SavedSearchQuery = {
|
export type SavedSearchQuery = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -281,7 +281,6 @@ export default function SearchView({
|
|||||||
"w-full justify-between md:justify-start lg:justify-end",
|
"w-full justify-between md:justify-start lg:justify-end",
|
||||||
)}
|
)}
|
||||||
filter={searchFilter}
|
filter={searchFilter}
|
||||||
searchTerm={searchTerm}
|
|
||||||
onUpdateFilter={onUpdateFilter}
|
onUpdateFilter={onUpdateFilter}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user