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:
Josh Hawkins 2024-09-20 12:05:55 -05:00 committed by GitHub
parent 1a51ce712c
commit 176af55e8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 100 additions and 119 deletions

View File

@ -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(

View File

@ -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]

View File

@ -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>

View File

@ -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" />

View File

@ -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}

View File

@ -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;

View File

@ -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}
/> />
)} )}