mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Add ability to filter based on search type (#13641)
This commit is contained in:
parent
03ff3e639f
commit
cae11cbb86
@ -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,6 +386,9 @@ def events_search():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
search_types = search_type.split(",")
|
||||||
|
|
||||||
|
if "thumbnail" in search_types:
|
||||||
thumb_result = context.embeddings.thumbnail.query(
|
thumb_result = context.embeddings.thumbnail.query(
|
||||||
query_texts=[query],
|
query_texts=[query],
|
||||||
n_results=limit,
|
n_results=limit,
|
||||||
@ -398,6 +401,8 @@ def events_search():
|
|||||||
context.thumb_stats.normalize(thumb_result["distances"][0]),
|
context.thumb_stats.normalize(thumb_result["distances"][0]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "description" in search_types:
|
||||||
desc_result = context.embeddings.description.query(
|
desc_result = context.embeddings.description.query(
|
||||||
query_texts=[query],
|
query_texts=[query],
|
||||||
n_results=limit,
|
n_results=limit,
|
||||||
|
@ -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();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
|
@ -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[];
|
||||||
};
|
};
|
||||||
|
@ -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 && (
|
||||||
|
Loading…
Reference in New Issue
Block a user