Add ability to filter based on search type (#13641)

This commit is contained in:
Josh Hawkins 2024-09-09 14:45:19 -05:00 committed by GitHub
parent 03ff3e639f
commit cae11cbb86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 113 additions and 28 deletions

View File

@ -274,7 +274,7 @@ def event_ids():
@EventBp.route("/events/search") @EventBp.route("/events/search")
def events_search(): def events_search():
query = request.args.get("query", type=str) query = request.args.get("query", type=str)
search_type = request.args.get("search_type", "text", type=str) search_type = request.args.get("search_type", "thumbnail,description", type=str)
include_thumbnails = request.args.get("include_thumbnails", default=1, type=int) include_thumbnails = request.args.get("include_thumbnails", default=1, type=int)
limit = request.args.get("limit", 50, type=int) limit = request.args.get("limit", 50, type=int)
@ -358,7 +358,7 @@ def events_search():
thumb_ids = {} thumb_ids = {}
desc_ids = {} desc_ids = {}
if search_type == "thumbnail": 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 == query)
@ -386,29 +386,34 @@ def events_search():
) )
) )
else: else:
thumb_result = context.embeddings.thumbnail.query( search_types = search_type.split(",")
query_texts=[query],
n_results=limit, if "thumbnail" in search_types:
where=where, thumb_result = context.embeddings.thumbnail.query(
) query_texts=[query],
# Do a rudimentary normalization of the difference in distances returned by CLIP and MiniLM. n_results=limit,
thumb_ids = dict( where=where,
zip(
thumb_result["ids"][0],
context.thumb_stats.normalize(thumb_result["distances"][0]),
) )
) # Do a rudimentary normalization of the difference in distances returned by CLIP and MiniLM.
desc_result = context.embeddings.description.query( thumb_ids = dict(
query_texts=[query], zip(
n_results=limit, thumb_result["ids"][0],
where=where, context.thumb_stats.normalize(thumb_result["distances"][0]),
) )
desc_ids = dict( )
zip(
desc_result["ids"][0], if "description" in search_types:
context.desc_stats.normalize(desc_result["distances"][0]), desc_result = context.embeddings.description.query(
query_texts=[query],
n_results=limit,
where=where,
)
desc_ids = dict(
zip(
desc_result["ids"][0],
context.desc_stats.normalize(desc_result["distances"][0]),
)
) )
)
results = {} results = {}
for event_id in thumb_ids.keys() | desc_ids: for event_id in thumb_ids.keys() | desc_ids:

View File

@ -17,7 +17,7 @@ import FilterSwitch from "./FilterSwitch";
import { FilterList } from "@/types/filter"; import { FilterList } from "@/types/filter";
import { CalendarRangeFilterButton } from "./CalendarFilterButton"; import { CalendarRangeFilterButton } from "./CalendarFilterButton";
import { CamerasFilterButton } from "./CamerasFilterButton"; import { CamerasFilterButton } from "./CamerasFilterButton";
import { SearchFilter } from "@/types/search"; import { SearchFilter, SearchSource } from "@/types/search";
import { DateRange } from "react-day-picker"; import { DateRange } from "react-day-picker";
const SEARCH_FILTERS = ["cameras", "date", "general"] as const; const SEARCH_FILTERS = ["cameras", "date", "general"] as const;
@ -103,6 +103,7 @@ export default function SearchFilterGroup({
cameras: Object.keys(config?.cameras || {}), cameras: Object.keys(config?.cameras || {}),
labels: Object.values(allLabels || {}), labels: Object.values(allLabels || {}),
zones: Object.values(allZones || {}), zones: Object.values(allZones || {}),
search_type: ["thumbnail", "description"] as SearchSource[],
}), }),
[config, allLabels, allZones], [config, allLabels, allZones],
); );
@ -178,12 +179,18 @@ export default function SearchFilterGroup({
selectedLabels={filter?.labels} selectedLabels={filter?.labels}
allZones={filterValues.zones} allZones={filterValues.zones}
selectedZones={filter?.zones} selectedZones={filter?.zones}
selectedSearchSources={
filter?.search_type ?? ["thumbnail", "description"]
}
updateLabelFilter={(newLabels) => { updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels }); onUpdateFilter({ ...filter, labels: newLabels });
}} }}
updateZoneFilter={(newZones) => updateZoneFilter={(newZones) =>
onUpdateFilter({ ...filter, zones: newZones }) onUpdateFilter({ ...filter, zones: newZones })
} }
updateSearchSourceFilter={(newSearchSource) =>
onUpdateFilter({ ...filter, search_type: newSearchSource })
}
/> />
)} )}
{isMobile && mobileSettingsFeatures.length > 0 && ( {isMobile && mobileSettingsFeatures.length > 0 && (
@ -211,16 +218,20 @@ type GeneralFilterButtonProps = {
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
allZones: string[]; allZones: string[];
selectedZones?: string[]; selectedZones?: string[];
selectedSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
updateZoneFilter: (zones: string[] | undefined) => void; updateZoneFilter: (zones: string[] | undefined) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void;
}; };
function GeneralFilterButton({ function GeneralFilterButton({
allLabels, allLabels,
selectedLabels, selectedLabels,
allZones, allZones,
selectedZones, selectedZones,
selectedSearchSources,
updateLabelFilter, updateLabelFilter,
updateZoneFilter, updateZoneFilter,
updateSearchSourceFilter,
}: GeneralFilterButtonProps) { }: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
@ -229,6 +240,9 @@ function GeneralFilterButton({
const [currentZones, setCurrentZones] = useState<string[] | undefined>( const [currentZones, setCurrentZones] = useState<string[] | undefined>(
selectedZones, selectedZones,
); );
const [currentSearchSources, setCurrentSearchSources] = useState<
SearchSource[]
>(selectedSearchSources);
const trigger = ( const trigger = (
<Button <Button
@ -256,10 +270,14 @@ function GeneralFilterButton({
allZones={allZones} allZones={allZones}
selectedZones={selectedZones} selectedZones={selectedZones}
currentZones={currentZones} currentZones={currentZones}
selectedSearchSources={selectedSearchSources}
currentSearchSources={currentSearchSources}
setCurrentZones={setCurrentZones} setCurrentZones={setCurrentZones}
updateZoneFilter={updateZoneFilter} updateZoneFilter={updateZoneFilter}
updateLabelFilter={updateLabelFilter}
setCurrentLabels={setCurrentLabels} setCurrentLabels={setCurrentLabels}
updateLabelFilter={updateLabelFilter}
setCurrentSearchSources={setCurrentSearchSources}
updateSearchSourceFilter={updateSearchSourceFilter}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
/> />
); );
@ -308,10 +326,14 @@ type GeneralFilterContentProps = {
allZones?: string[]; allZones?: string[];
selectedZones?: string[]; selectedZones?: string[];
currentZones?: string[]; currentZones?: string[];
selectedSearchSources: SearchSource[];
currentSearchSources: SearchSource[];
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void; setCurrentLabels: (labels: string[] | undefined) => void;
updateZoneFilter?: (zones: string[] | undefined) => void; updateZoneFilter?: (zones: string[] | undefined) => void;
setCurrentZones?: (zones: string[] | undefined) => void; setCurrentZones?: (zones: string[] | undefined) => void;
setCurrentSearchSources: (sources: SearchSource[]) => void;
updateSearchSourceFilter: (sources: SearchSource[]) => void;
onClose: () => void; onClose: () => void;
}; };
export function GeneralFilterContent({ export function GeneralFilterContent({
@ -321,15 +343,62 @@ export function GeneralFilterContent({
allZones, allZones,
selectedZones, selectedZones,
currentZones, currentZones,
selectedSearchSources,
currentSearchSources,
updateLabelFilter, updateLabelFilter,
setCurrentLabels, setCurrentLabels,
updateZoneFilter, updateZoneFilter,
setCurrentZones, setCurrentZones,
setCurrentSearchSources,
updateSearchSourceFilter,
onClose, onClose,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
return ( return (
<> <>
<div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden"> <div className="scrollbar-container h-auto max-h-[80dvh] overflow-y-auto overflow-x-hidden">
<div className="my-2.5 flex flex-col gap-2.5">
<FilterSwitch
label="Thumbnail Image"
isChecked={currentSearchSources?.includes("thumbnail") ?? false}
onCheckedChange={(isChecked) => {
const updatedSources = currentSearchSources
? [...currentSearchSources]
: [];
if (isChecked) {
updatedSources.push("thumbnail");
setCurrentSearchSources(updatedSources);
} else {
if (updatedSources.length > 1) {
const index = updatedSources.indexOf("thumbnail");
if (index !== -1) updatedSources.splice(index, 1);
setCurrentSearchSources(updatedSources);
}
}
}}
/>
<FilterSwitch
label="Description"
isChecked={currentSearchSources?.includes("description") ?? false}
onCheckedChange={(isChecked) => {
const updatedSources = currentSearchSources
? [...currentSearchSources]
: [];
if (isChecked) {
updatedSources.push("description");
setCurrentSearchSources(updatedSources);
} else {
if (updatedSources.length > 1) {
const index = updatedSources.indexOf("description");
if (index !== -1) updatedSources.splice(index, 1);
setCurrentSearchSources(updatedSources);
}
}
}}
/>
<DropdownMenuSeparator />
</div>
<div className="mb-5 mt-2.5 flex items-center justify-between"> <div className="mb-5 mt-2.5 flex items-center justify-between">
<Label <Label
className="mx-2 cursor-pointer text-primary" className="mx-2 cursor-pointer text-primary"
@ -351,6 +420,7 @@ export function GeneralFilterContent({
<div className="my-2.5 flex flex-col gap-2.5"> <div className="my-2.5 flex flex-col gap-2.5">
{allLabels.map((item) => ( {allLabels.map((item) => (
<FilterSwitch <FilterSwitch
key={item}
label={item.replaceAll("_", " ")} label={item.replaceAll("_", " ")}
isChecked={currentLabels?.includes(item) ?? false} isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
@ -397,6 +467,7 @@ export function GeneralFilterContent({
<div className="my-2.5 flex flex-col gap-2.5"> <div className="my-2.5 flex flex-col gap-2.5">
{allZones.map((item) => ( {allZones.map((item) => (
<FilterSwitch <FilterSwitch
key={item}
label={item.replaceAll("_", " ")} label={item.replaceAll("_", " ")}
isChecked={currentZones?.includes(item) ?? false} isChecked={currentZones?.includes(item) ?? false}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
@ -438,6 +509,10 @@ export function GeneralFilterContent({
updateZoneFilter(currentZones); updateZoneFilter(currentZones);
} }
if (selectedSearchSources != currentSearchSources) {
updateSearchSourceFilter(currentSearchSources);
}
onClose(); onClose();
}} }}
> >

View File

@ -75,7 +75,7 @@ export default function Search() {
before: searchSearchParams["before"], before: searchSearchParams["before"],
after: searchSearchParams["after"], after: searchSearchParams["after"],
include_thumbnails: 0, include_thumbnails: 0,
search_type: "thumbnail", search_type: "similarity",
}, },
]; ];
} }
@ -89,6 +89,7 @@ export default function Search() {
zones: searchSearchParams["zones"], zones: searchSearchParams["zones"],
before: searchSearchParams["before"], before: searchSearchParams["before"],
after: searchSearchParams["after"], after: searchSearchParams["after"],
search_type: searchSearchParams["search_type"],
include_thumbnails: 0, include_thumbnails: 0,
}, },
]; ];
@ -192,6 +193,7 @@ export default function Search() {
allPreviews={allPreviews} allPreviews={allPreviews}
isLoading={isLoading} isLoading={isLoading}
setSearch={setSearch} setSearch={setSearch}
similaritySearch={similaritySearch}
setSimilaritySearch={setSimilaritySearch} setSimilaritySearch={setSimilaritySearch}
onUpdateFilter={onUpdateFilter} onUpdateFilter={onUpdateFilter}
onOpenSearch={onOpenSearch} onOpenSearch={onOpenSearch}

View File

@ -1,4 +1,4 @@
type SearchSource = "thumbnail" | "description"; export type SearchSource = "similarity" | "thumbnail" | "description";
export type SearchResult = { export type SearchResult = {
id: string; id: string;
@ -21,4 +21,5 @@ export type SearchFilter = {
zones?: string[]; zones?: string[];
before?: number; before?: number;
after?: number; after?: number;
search_type?: SearchSource[];
}; };

View File

@ -32,6 +32,7 @@ type SearchViewProps = {
searchResults?: SearchResult[]; searchResults?: SearchResult[];
allPreviews?: Preview[]; allPreviews?: Preview[];
isLoading: boolean; isLoading: boolean;
similaritySearch?: SearchResult;
setSearch: (search: string) => void; setSearch: (search: string) => void;
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
onUpdateFilter: (filter: SearchFilter) => void; onUpdateFilter: (filter: SearchFilter) => void;
@ -44,6 +45,7 @@ export default function SearchView({
searchResults, searchResults,
allPreviews, allPreviews,
isLoading, isLoading,
similaritySearch,
setSearch, setSearch,
setSimilaritySearch, setSimilaritySearch,
onUpdateFilter, onUpdateFilter,
@ -112,7 +114,7 @@ export default function SearchView({
placeholder={ placeholder={
isMobileOnly ? "Search" : "Search for a detected object..." isMobileOnly ? "Search" : "Search for a detected object..."
} }
value={search} value={similaritySearch ? "" : search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
{search && ( {search && (