diff --git a/app/packages/core/src/components/Filters/StringFilter/state.ts b/app/packages/core/src/components/Filters/StringFilter/state.ts index 8315313aa3..ff1babaf06 100644 --- a/app/packages/core/src/components/Filters/StringFilter/state.ts +++ b/app/packages/core/src/components/Filters/StringFilter/state.ts @@ -68,12 +68,17 @@ export const stringSearchResults = selectorFamily< }; if (!modal && get(fos.queryPerformance)) { + const filters = Object.fromEntries( + Object.entries(get(fos.filters) || {}).filter(([p]) => p !== path) + ); + return { values: get( fos.lightningStringResults({ path, search, exclude: [...selected.filter((s) => s !== null)] as string[], + filters, }) )?.map((value) => ({ value, count: null })), }; diff --git a/app/packages/core/src/components/Filters/StringFilter/useSelected.ts b/app/packages/core/src/components/Filters/StringFilter/useSelected.ts index 7aa023459a..3b4d90da1e 100644 --- a/app/packages/core/src/components/Filters/StringFilter/useSelected.ts +++ b/app/packages/core/src/components/Filters/StringFilter/useSelected.ts @@ -29,12 +29,13 @@ export default function ( const shown = (!modal && queryPerformance) || (resultsLoadable.state !== "loading" && (length >= CHECKBOX_LIMIT || id)); + const isFrameField = useRecoilValue(fos.isFrameField(path)); return { results, useSearch: - path === "_label_tags" && queryPerformance && !modal + path === "_label_tags" && queryPerformance && !isFrameField && !modal ? undefined : useSearch, showSearch: Boolean(shown) && !boolean, diff --git a/app/packages/relay/src/queries/__generated__/lightningQuery.graphql.ts b/app/packages/relay/src/queries/__generated__/lightningQuery.graphql.ts index 789d1edc49..4749ee6299 100644 --- a/app/packages/relay/src/queries/__generated__/lightningQuery.graphql.ts +++ b/app/packages/relay/src/queries/__generated__/lightningQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<19f0c37a7189c44264fe0a982eca857b>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,7 @@ export type LightningInput = { }; export type LightningPathInput = { exclude?: ReadonlyArray | null; + filters?: object | null; first?: number | null; path: string; search?: string | null; diff --git a/app/packages/state/src/recoil/filters.ts b/app/packages/state/src/recoil/filters.ts index 20b37fa63b..bee314b677 100644 --- a/app/packages/state/src/recoil/filters.ts +++ b/app/packages/state/src/recoil/filters.ts @@ -10,7 +10,7 @@ import { indexedPaths } from "./queryPerformance"; import { expandPath, fields } from "./schema"; import { hiddenLabelIds } from "./selectors"; import { sidebarExpandedStore } from "./sidebarExpanded"; -import { State } from "./types"; +import type { State } from "./types"; export const modalFilters = sessionAtom({ key: "modalFilters", diff --git a/app/packages/state/src/recoil/pathData/lightningString.ts b/app/packages/state/src/recoil/pathData/lightningString.ts index 18ab1ff923..01fce3ea53 100644 --- a/app/packages/state/src/recoil/pathData/lightningString.ts +++ b/app/packages/state/src/recoil/pathData/lightningString.ts @@ -1,9 +1,15 @@ +import type { SerializableParam } from "recoil"; import { selectorFamily } from "recoil"; import { lightningQuery } from "../queryPerformance"; export const lightningStringResults = selectorFamily< string[], - { path: string; search?: string; exclude?: string[] } + { + path: string; + search?: string; + exclude?: string[]; + filters: SerializableParam; + } >({ key: "lightningStringResults", get: diff --git a/app/packages/state/src/recoil/types.ts b/app/packages/state/src/recoil/types.ts index 07168a7f5c..b30a413be5 100644 --- a/app/packages/state/src/recoil/types.ts +++ b/app/packages/state/src/recoil/types.ts @@ -155,20 +155,6 @@ export namespace State { info: { [key: string]: string }; } - /** - * @hidden - */ - export interface CategoricalFilter { - values: T[]; - isMatching: boolean; - exclude: boolean; - } - - /** - * @hidden - */ - export type Filter = CategoricalFilter; - export interface SortBySimilarityParameters { brainKey: string; distField?: string; @@ -178,8 +164,13 @@ export namespace State { queryIds?: string[]; } + type FilterValues = string | boolean | number | null | undefined; + + export interface Filter { + [key: string]: FilterValues | Array; + } + export interface Filters { - _label_tags?: CategoricalFilter; [key: string]: Filter; } diff --git a/app/schema.graphql b/app/schema.graphql index 5ea4f28935..2b843f3a7a 100644 --- a/app/schema.graphql +++ b/app/schema.graphql @@ -455,6 +455,7 @@ input LightningPathInput { exclude: [String!] = null first: Int = 200 search: String = null + filters: BSON = null } interface LightningResult { diff --git a/fiftyone/server/lightning.py b/fiftyone/server/lightning.py index 4992c27dd4..0682f510c4 100644 --- a/fiftyone/server/lightning.py +++ b/fiftyone/server/lightning.py @@ -22,7 +22,9 @@ import fiftyone.server.constants as foc from fiftyone.server.data import Info +from fiftyone.server.scalars import BSON from fiftyone.server.utils import meets_type +from fiftyone.server.view import get_view _TWENTY_FOUR = 24 @@ -37,6 +39,7 @@ class LightningPathInput: ) first: t.Optional[int] = foc.LIST_LIMIT search: t.Optional[str] = None + filters: t.Optional[BSON] = None @gql.input @@ -135,7 +138,7 @@ async def lightning_resolver( for collection, sublist in zip(collections, queries) for item in sublist ] - result = await _do_async_pooled_queries(flattened) + result = await _do_async_pooled_queries(dataset, flattened) results = [] offset = 0 @@ -154,6 +157,7 @@ class DistinctQuery: is_object_id_field: bool exclude: t.Optional[t.List[str]] = None search: t.Optional[str] = None + filters: t.Optional[BSON] = None def _resolve_lightning_path_queries( @@ -258,6 +262,7 @@ def _resolve_object_id(results): d["has_list"] = _has_list(dataset, field_path, is_frame_field) d["is_object_id_field"] = True d["path"] = field_path + d["filters"] = path.filters return ( collection, [DistinctQuery(**d)], @@ -273,6 +278,7 @@ def _resolve_string(results): d["has_list"] = _has_list(dataset, field_path, is_frame_field) d["is_object_id_field"] = False d["path"] = field_path + d["filters"] = path.filters return ( collection, [DistinctQuery(**d)], @@ -283,24 +289,29 @@ def _resolve_string(results): async def _do_async_pooled_queries( + dataset: fo.Dataset, queries: t.List[ t.Tuple[AsyncIOMotorCollection, t.Union[DistinctQuery, t.List[t.Dict]]] - ] + ], ): return await asyncio.gather( - *[_do_async_query(collection, query) for collection, query in queries] + *[ + _do_async_query(dataset, collection, query) + for collection, query in queries + ] ) async def _do_async_query( + dataset: fo.Dataset, collection: AsyncIOMotorCollection, query: t.Union[DistinctQuery, t.List[t.Dict]], ): if isinstance(query, DistinctQuery): - if query.has_list: + if query.has_list and not query.filters: return await _do_distinct_query(collection, query) - return await _do_distinct_pipeline(collection, query) + return await _do_distinct_pipeline(dataset, collection, query) return [i async for i in collection.aggregate(query)] @@ -336,9 +347,28 @@ async def _do_distinct_query( async def _do_distinct_pipeline( - collection: AsyncIOMotorCollection, query: DistinctQuery + dataset: fo.Dataset, + collection: AsyncIOMotorCollection, + query: DistinctQuery, ): - pipeline = [{"$sort": {query.path: 1}}] + pipeline = [] + if query.filters: + pipeline += get_view(dataset, filters=query.filters)._pipeline() + + pipeline += [{"$sort": {query.path: 1}}] + + if query.search: + if query.is_object_id_field: + add = (_TWENTY_FOUR - len(query.search)) * "0" + value = {"$gte": ObjectId(f"{query.search}{add}")} + else: + value = Regex(f"^{query.search}") + pipeline.append({"$match": {query.path: value}}) + + pipeline += _match_arrays(dataset, query.path, False) + _unwind( + dataset, query.path, False + ) + if query.search: if query.is_object_id_field: add = (_TWENTY_FOUR - len(query.search)) * "0"