diff --git a/dev/test-studio/schema/author.ts b/dev/test-studio/schema/author.ts index 5c14c19f0c0..e84fe5271c2 100644 --- a/dev/test-studio/schema/author.ts +++ b/dev/test-studio/schema/author.ts @@ -1,5 +1,5 @@ import {UserIcon as icon} from '@sanity/icons' -import {type Rule} from 'sanity' +import {defineField, defineType, type Rule} from 'sanity' const AUTHOR_ROLES = [ {value: 'developer', title: 'Developer'}, @@ -7,14 +7,12 @@ const AUTHOR_ROLES = [ {value: 'ops', title: 'Operations'}, ] -export default { +export default defineType({ name: 'author', type: 'document', title: 'Author', icon, description: 'This represents an author', - // eslint-disable-next-line camelcase - __experimental_search: [{path: 'name', weight: 10}], preview: { select: { title: 'name', @@ -36,12 +34,15 @@ export default { }, }, fields: [ - { + defineField({ name: 'name', title: 'Name', type: 'string', + options: { + search: {weight: 100}, + }, validation: (rule: Rule) => rule.required(), - }, + }), { name: 'bestFriend', title: 'Best friend', @@ -125,4 +126,4 @@ export default { }, }, }), -} +}) diff --git a/packages/@sanity/types/src/crossDatasetReference/types.ts b/packages/@sanity/types/src/crossDatasetReference/types.ts index 2863b184601..6f6568f62e7 100644 --- a/packages/@sanity/types/src/crossDatasetReference/types.ts +++ b/packages/@sanity/types/src/crossDatasetReference/types.ts @@ -40,7 +40,7 @@ export interface CrossDatasetType { title?: string icon: ComponentType preview: PreviewConfig - /** @alpha */ + /** @deprecated Unused. Configuring search is no longer supported for cross-dataset references. */ __experimental_search: ObjectSchemaType['__experimental_search'] } diff --git a/packages/@sanity/types/src/schema/definition/type/array.ts b/packages/@sanity/types/src/schema/definition/type/array.ts index c76ca7b4a7c..5a789f742e5 100644 --- a/packages/@sanity/types/src/schema/definition/type/array.ts +++ b/packages/@sanity/types/src/schema/definition/type/array.ts @@ -6,10 +6,10 @@ import { type IntrinsicTypeName, type TypeAliasDefinition, } from '../schemaDefinition' -import {type BaseSchemaDefinition, type TitledListValue} from './common' +import {type BaseSchemaDefinition, type SearchConfiguration, type TitledListValue} from './common' /** @public */ -export interface ArrayOptions { +export interface ArrayOptions extends SearchConfiguration { list?: TitledListValue[] | V[] /** * layout: 'tags' only works for string array diff --git a/packages/@sanity/types/src/schema/definition/type/common.ts b/packages/@sanity/types/src/schema/definition/type/common.ts index 826daf45728..66d06e4f6db 100644 --- a/packages/@sanity/types/src/schema/definition/type/common.ts +++ b/packages/@sanity/types/src/schema/definition/type/common.ts @@ -69,3 +69,24 @@ export interface EnumListProps { layout?: 'radio' | 'dropdown' direction?: 'horizontal' | 'vertical' } + +/** @public */ +export interface SearchConfiguration { + search?: { + /** + * Defines a search weight for this field to prioritize its importance + * during search operations in the Studio. This setting allows the specified + * field to be ranked higher in search results compared to other fields. + * + * By default, all fields are assigned a weight of 1. However, if a field is + * chosen as the `title` in the preview configuration's `select` option, it + * will automatically receive a default weight of 10. Similarly, if selected + * as the `subtitle`, the default weight is 5. Fields marked as + * `hidden: true` (no function) are assigned a weight of 0 by default. + * + * Note: Search weight configuration is currently supported only for fields + * of type string or portable text arrays. + */ + weight?: number + } +} diff --git a/packages/@sanity/types/src/schema/definition/type/crossDatasetReference.ts b/packages/@sanity/types/src/schema/definition/type/crossDatasetReference.ts index 4208c6db475..41dba483340 100644 --- a/packages/@sanity/types/src/schema/definition/type/crossDatasetReference.ts +++ b/packages/@sanity/types/src/schema/definition/type/crossDatasetReference.ts @@ -15,7 +15,7 @@ export interface CrossDatasetReferenceDefinition extends BaseSchemaDefinition { preview?: PreviewConfig /** - * @deprecated Configuring search is no longer supported + * @deprecated Unused. Configuring search is no longer supported. */ __experimental_search?: {path: string | string[]; weight?: number; mapWith?: string}[] }[] diff --git a/packages/@sanity/types/src/schema/definition/type/document.ts b/packages/@sanity/types/src/schema/definition/type/document.ts index 9d5e35aabc8..0783ada5cee 100644 --- a/packages/@sanity/types/src/schema/definition/type/document.ts +++ b/packages/@sanity/types/src/schema/definition/type/document.ts @@ -24,7 +24,7 @@ export interface DocumentDefinition extends Omit { options?: DocumentOptions validation?: ValidationBuilder initialValue?: InitialValueProperty> - /** @alpha */ + /** @deprecated Unused. Use the new field-level search config. */ __experimental_search?: {path: string; weight: number; mapWith?: string}[] /** @alpha */ __experimental_omnisearch_visibility?: boolean diff --git a/packages/@sanity/types/src/schema/definition/type/string.ts b/packages/@sanity/types/src/schema/definition/type/string.ts index 7d379bede0d..0b708079e3b 100644 --- a/packages/@sanity/types/src/schema/definition/type/string.ts +++ b/packages/@sanity/types/src/schema/definition/type/string.ts @@ -1,11 +1,11 @@ import {type FieldReference} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition, type EnumListProps} from './common' +import {type BaseSchemaDefinition, type EnumListProps, type SearchConfiguration} from './common' /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StringOptions extends EnumListProps {} +export interface StringOptions extends EnumListProps, SearchConfiguration {} /** @public */ export interface StringRule extends RuleDef { diff --git a/packages/@sanity/types/src/schema/types.ts b/packages/@sanity/types/src/schema/types.ts index fc0ad9f0bbe..7db84e38b0c 100644 --- a/packages/@sanity/types/src/schema/types.ts +++ b/packages/@sanity/types/src/schema/types.ts @@ -391,7 +391,7 @@ export interface ObjectSchemaType extends BaseSchemaType { // Note: `path` is a string in the _specification_, but converted to a // string/number array in the schema normalization/compilation step // a path segment is a number when specified like array.0.prop in preview config. - /** @alpha */ + /** @deprecated Unused. Use the new field-level search config. */ __experimental_search: {path: (string | number)[]; weight: number; mapWith?: string}[] /** @alpha */ __experimental_omnisearch_visibility?: boolean diff --git a/packages/sanity/src/_internal/browser.ts b/packages/sanity/src/_internal/browser.ts index bbf6b21aa6e..eee1da13137 100644 --- a/packages/sanity/src/_internal/browser.ts +++ b/packages/sanity/src/_internal/browser.ts @@ -1,2 +1,2 @@ -export {createSearch, getSearchableTypes, getSearchTypesWithMaxDepth} from '../core/search' +export {createSearch, getSearchableTypes} from '../core/search' export {useSearchMaxFieldDepth} from '../core/studio/components/navbar/search/hooks/useSearchMaxFieldDepth' diff --git a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts index 1c0be7cc497..699120fc4a6 100644 --- a/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts +++ b/packages/sanity/src/core/form/studio/inputs/client-adapters/reference.ts @@ -1,10 +1,11 @@ import {type SanityClient} from '@sanity/client' +import {DEFAULT_MAX_FIELD_DEPTH} from '@sanity/schema/_internal' import {type ReferenceFilterSearchOptions, type ReferenceSchemaType} from '@sanity/types' import {combineLatest, type Observable, of} from 'rxjs' import {map, mergeMap, startWith, switchMap} from 'rxjs/operators' import {type DocumentPreviewStore, getPreviewPaths, prepareForPreview} from '../../../../preview' -import {createSearch, getSearchTypesWithMaxDepth} from '../../../../search' +import {createSearch} from '../../../../search' import {collate, type CollatedHit, getDraftId, getIdPair, isRecord} from '../../../../util' import {type ReferenceInfo, type ReferenceSearchHit} from '../../../inputs/ReferenceInput/types' @@ -193,9 +194,10 @@ export function referenceSearch( options: ReferenceFilterSearchOptions, unstable_enableNewSearch: boolean, ): Observable { - const search = createSearch(getSearchTypesWithMaxDepth(type.to, options.maxFieldDepth), client, { + const search = createSearch(type.to, client, { ...options, unstable_enableNewSearch, + maxDepth: options.maxFieldDepth || DEFAULT_MAX_FIELD_DEPTH, }) return search(textTerm, {includeDrafts: true}).pipe( map(({hits}) => hits.map(({hit}) => hit)), diff --git a/packages/sanity/src/core/form/studio/inputs/crossDatasetReference/datastores/search.ts b/packages/sanity/src/core/form/studio/inputs/crossDatasetReference/datastores/search.ts index 3dc37a9ad2e..f588326d102 100644 --- a/packages/sanity/src/core/form/studio/inputs/crossDatasetReference/datastores/search.ts +++ b/packages/sanity/src/core/form/studio/inputs/crossDatasetReference/datastores/search.ts @@ -1,5 +1,4 @@ import {type SanityClient} from '@sanity/client' -import {resolveSearchConfigForBaseFieldPaths} from '@sanity/schema/_internal' import { type CrossDatasetReferenceSchemaType, type ReferenceFilterSearchOptions, @@ -22,20 +21,15 @@ export function search( type: CrossDatasetReferenceSchemaType, options: ReferenceFilterSearchOptions, ): Observable { - const searchWeighted = createSearch( - type.to.map((crossDatasetType) => ({ - name: crossDatasetType.type, - // eslint-disable-next-line camelcase - __experimental_search: resolveSearchConfigForBaseFieldPaths( - crossDatasetType, - options.maxFieldDepth, - ), - })), - client, - options, - ) + const searchStrategy = createSearch(type.to, client, { + ...options, + maxDepth: options.maxFieldDepth, + }) - return searchWeighted(textTerm, {includeDrafts: false}).pipe( + return searchStrategy(textTerm, { + includeDrafts: false, + isCrossDataset: true, + }).pipe( map(({hits}) => hits.map(({hit}) => hit)), map(collate), map((collated) => diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts index 736961170cc..95cbd95247a 100644 --- a/packages/sanity/src/core/index.ts +++ b/packages/sanity/src/core/index.ts @@ -12,13 +12,7 @@ export * from './i18n' export * from './presence' export * from './preview' export * from './schema' -export type { - SearchableType, - SearchFactoryOptions, - SearchOptions, - SearchSort, - SearchTerms, -} from './search' +export type {SearchFactoryOptions, SearchOptions, SearchSort, SearchTerms} from './search' export * from './store' export * from './studio' export * from './studioClient' diff --git a/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts b/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts new file mode 100644 index 00000000000..706f2136f1f --- /dev/null +++ b/packages/sanity/src/core/search/common/__tests__/deriveSearchWeightsFromType.test.ts @@ -0,0 +1,320 @@ +import {describe, expect, it} from '@jest/globals' +import {defineField, defineType} from '@sanity/types' + +import {createSchema} from '../../../schema' +import {deriveSearchWeightsFromType} from '../deriveSearchWeightsFromType' + +describe('deriveSearchWeightsFromType', () => { + it('finds all the strings and PT fields within a document type', () => { + const schema = createSchema({ + name: 'default', + types: [ + defineType({ + name: 'namedObject', + type: 'object', + fields: [ + defineField({name: 'nestedStringField', type: 'string'}), + defineField({name: 'nestedPtField', type: 'array', of: [{type: 'block'}]}), + ], + }), + defineType({ + name: 'testType', + type: 'document', + preview: {select: {}}, + fields: [ + defineField({name: 'simpleStringField', type: 'string'}), + defineField({ + name: 'simplePtField', + type: 'array', + of: [{type: 'block'}], + }), + defineField({ + name: 'simpleObject', + type: 'object', + fields: [ + defineField({name: 'nestedStringField', type: 'string'}), + defineField({name: 'nestedPtField', type: 'array', of: [{type: 'block'}]}), + ], + }), + defineField({ + name: 'hasNestedObject', + type: 'object', + fields: [ + defineField({ + type: 'object', + name: 'nestedObject', + fields: [ + defineField({name: 'nestedNestedStringField', type: 'string'}), + defineField({ + name: 'nestedNestedPtField', + type: 'array', + of: [{type: 'block'}], + }), + ], + }), + ], + }), + defineField({ + name: 'namedObjectField', + type: 'namedObject', + }), + defineField({ + name: 'simpleArrayOfStrings', + type: 'array', + of: [{type: 'string'}], + }), + defineField({ + name: 'arrayOfObjects', + type: 'array', + of: [ + { + type: 'object', + fields: [ + {name: 'stringInArray', type: 'string'}, + {name: 'ptInArray', type: 'array', of: [{type: 'block'}]}, + ], + }, + ], + }), + defineField({ + name: 'arrayOfNamedType', + type: 'array', + of: [{type: 'namedObject'}], + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 5, + }), + ).toEqual({ + typeName: 'testType', + paths: [ + {path: '_id', weight: 1}, + {path: '_type', weight: 1}, + {path: 'simpleStringField', weight: 1}, + {path: 'simplePtField', weight: 1, mapWith: 'pt::text'}, + {path: 'simpleObject.nestedStringField', weight: 1}, + {path: 'simpleObject.nestedPtField', weight: 1, mapWith: 'pt::text'}, + {path: 'hasNestedObject.nestedObject.nestedNestedStringField', weight: 1}, + {path: 'hasNestedObject.nestedObject.nestedNestedPtField', weight: 1, mapWith: 'pt::text'}, + {path: 'namedObjectField.nestedStringField', weight: 1}, + {path: 'namedObjectField.nestedPtField', weight: 1, mapWith: 'pt::text'}, + {path: 'simpleArrayOfStrings[]', weight: 1}, + {path: 'arrayOfObjects[].stringInArray', weight: 1}, + {path: 'arrayOfObjects[].ptInArray', weight: 1, mapWith: 'pt::text'}, + {path: 'arrayOfNamedType[].nestedStringField', weight: 1}, + {path: 'arrayOfNamedType[].nestedPtField', weight: 1, mapWith: 'pt::text'}, + ], + }) + }) + + it('returns a weight of 0 for hidden string and PT fields', () => { + const schema = createSchema({ + name: 'default', + types: [ + defineType({ + name: 'testType', + type: 'document', + preview: {select: {}}, + fields: [ + defineField({name: 'simpleStringField', type: 'string', hidden: true}), + defineField({ + name: 'simplePtField', + type: 'array', + of: [{type: 'block'}], + hidden: true, + }), + defineField({ + name: 'simpleObject', + type: 'object', + fields: [ + defineField({name: 'nestedStringField', type: 'string'}), + defineField({name: 'nestedPtField', type: 'array', of: [{type: 'block'}]}), + ], + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 5, + }), + ).toEqual({ + typeName: 'testType', + paths: [ + {path: '_id', weight: 1}, + {path: '_type', weight: 1}, + {path: 'simpleObject.nestedStringField', weight: 1}, + {path: 'simpleObject.nestedPtField', weight: 1, mapWith: 'pt::text'}, + {path: 'simpleStringField', weight: 0}, + {path: 'simplePtField', weight: 0, mapWith: 'pt::text'}, + ], + }) + }) + + it('respects `maxDepth`', () => { + const schema = createSchema({ + name: 'default', + types: [ + defineType({ + name: 'testType', + type: 'document', + preview: {select: {}}, + fields: [ + defineField({name: 'simpleStringField', type: 'string'}), + defineField({ + name: 'simpleObject', + type: 'object', + fields: [ + defineField({name: 'nestedString', type: 'string'}), + defineField({ + name: 'nestedObject', + type: 'object', + fields: [ + defineField({ + name: 'nestedNestedStringField', + type: 'string', + }), + ], + }), + ], + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 2, + }), + ).toEqual({ + typeName: 'testType', + paths: [ + {path: '_id', weight: 1}, + {path: '_type', weight: 1}, + {path: 'simpleStringField', weight: 1}, + {path: 'simpleObject.nestedString', weight: 1}, + ], + }) + }) + + it('returns special weights for fields that are selected in the preview config', () => { + const schema = createSchema({ + name: 'default', + types: [ + defineType({ + name: 'testType', + type: 'document', + preview: { + select: { + title: 'simpleObject.titleField', + subtitle: 'arrayOfObjects.0.subtitleField', + description: 'descriptionField', + }, + }, + fields: [ + defineField({ + name: 'simpleObject', + type: 'object', + fields: [defineField({name: 'titleField', type: 'string'})], + }), + defineField({ + name: 'arrayOfObjects', + type: 'array', + of: [ + { + type: 'object', + fields: [defineField({name: 'subtitleField', type: 'string'})], + }, + ], + }), + defineField({ + name: 'descriptionField', + type: 'array', + of: [{type: 'block'}], + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 5, + }), + ).toEqual({ + typeName: 'testType', + paths: [ + {path: '_id', weight: 1}, + {path: '_type', weight: 1}, + {path: 'simpleObject.titleField', weight: 10}, + {path: 'arrayOfObjects[].subtitleField', weight: 5}, + {path: 'descriptionField', weight: 1.5, mapWith: 'pt::text'}, + ], + }) + }) + + it('always returns the user set weights, ignoring all other derived fields', () => { + const schema = createSchema({ + name: 'default', + types: [ + defineType({ + name: 'testType', + type: 'document', + preview: { + select: { + title: 'titleField', + subtitle: 'subtitleField', + description: 'descriptionField', + }, + }, + fields: [ + defineField({name: 'titleField', type: 'string', options: {search: {weight: 7}}}), + defineField({name: 'subtitleField', type: 'string', options: {search: {weight: 7}}}), + defineField({name: 'descriptionField', type: 'string', options: {search: {weight: 7}}}), + defineField({ + name: 'hiddenField', + type: 'string', + hidden: true, + options: {search: {weight: 7}}, + }), + defineField({ + name: 'normalStringField', + type: 'string', + options: {search: {weight: 7}}, + }), + ], + }), + ], + }) + + expect( + deriveSearchWeightsFromType({ + schemaType: schema.get('testType')!, + maxDepth: 5, + }), + ).toEqual({ + typeName: 'testType', + paths: [ + {path: '_id', weight: 1}, + {path: '_type', weight: 1}, + {path: 'hiddenField', weight: 7}, + {path: 'titleField', weight: 7}, + {path: 'subtitleField', weight: 7}, + {path: 'descriptionField', weight: 7}, + {path: 'normalStringField', weight: 7}, + ], + }) + }) +}) diff --git a/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts b/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts new file mode 100644 index 00000000000..abc6fdf4a24 --- /dev/null +++ b/packages/sanity/src/core/search/common/deriveSearchWeightsFromType.ts @@ -0,0 +1,237 @@ +import {type CrossDatasetType, type SchemaType, type SearchConfiguration} from '@sanity/types' + +import {isRecord} from '../../util' +import {type SearchPath, type SearchSpec} from './types' + +interface SearchWeightEntry { + path: string + weight: number + type: 'string' | 'pt' +} + +const CACHE = new WeakMap() +const PREVIEW_FIELD_WEIGHT_MAP = { + title: 10, + subtitle: 5, + description: 1.5, +} +const BASE_WEIGHTS: Record> = { + _id: {weight: 1, type: 'string'}, + _type: {weight: 1, type: 'string'}, +} +const GROQ_RESERVED_KEYWORDS = ['true', 'false', 'null'] +const builtInObjectTypes = ['reference', 'crossDatasetReference'] + +const getTypeChain = (type: SchemaType | undefined): SchemaType[] => + type ? [type, ...getTypeChain(type.type)] : [] + +const isPtField = (type: SchemaType | undefined) => + type?.jsonType === 'array' && + type.of.some((arrType) => getTypeChain(arrType).some(({name}) => name === 'block')) + +const isStringField = (schemaType: SchemaType | undefined) => + getTypeChain(schemaType).some((type) => type.name === 'string') + +const isSearchConfiguration = (options: unknown): options is SearchConfiguration => + isRecord(options) && 'search' in options && isRecord(options.search) + +function isSchemaType(input: SchemaType | CrossDatasetType | undefined): input is SchemaType { + return typeof input !== 'undefined' && 'name' in input +} + +/** + * Serialize field path for GROQ query. + * + * Reserved keywords, such as `null`, cannot be accessed using dot notation. These fields will + * instead be serialized using square bracket notation. + */ +function pathToString(...path: string[]): string { + const cleanPath = path.filter(Boolean) + return cleanPath.slice(1).reduce((nextPath, segment) => { + if (GROQ_RESERVED_KEYWORDS.includes(segment)) { + return `${nextPath}['${segment}']` + } + return `${nextPath}.${segment}` + }, cleanPath[0]) +} + +function getLeafWeights( + schemaType: SchemaType | CrossDatasetType | undefined, + maxDepth: number, + getWeight: (schemaType: SchemaType, path: string) => number | null, +): Record { + function traverse( + type: SchemaType | undefined, + path: string, + depth: number, + ): SearchWeightEntry[] { + if (!type) return [] + if (depth > maxDepth) return [] + + const typeChain = getTypeChain(type) + + if (isStringField(type) || isPtField(type)) { + const weight = getWeight(type, path) + + if (typeof weight !== 'number') return [] + return [{path, weight, type: isPtField(type) ? 'pt' : 'string'}] + } + + const results: SearchWeightEntry[] = [] + const objectTypes = typeChain.filter( + (t): t is Extract => + t.jsonType === 'object' && !!t.fields?.length && !builtInObjectTypes.includes(t.name), + ) + for (const objectType of objectTypes) { + for (const field of objectType.fields) { + const nextPath = pathToString(path, field.name) + results.push(...traverse(field.type, nextPath, depth + 1)) + } + } + + const arrayTypes = typeChain.filter( + (t): t is Extract => + t.jsonType === 'array' && !!t.of?.length, + ) + for (const arrayType of arrayTypes) { + for (const arrayItemType of arrayType.of) { + const nextPath = `${path}[]` + results.push(...traverse(arrayItemType, nextPath, depth + 1)) + } + } + + return results + } + + // Cross Dataset Reference are not part of the schema, so we should not attempt to reconcile them. + if (!isSchemaType(schemaType)) { + return {} + } + + return traverse(schemaType, '', 0).reduce>( + (acc, {path, weight, type}) => { + acc[path] = {weight, type, path} + return acc + }, + {}, + ) +} + +const getUserSetWeight = (schemaType: SchemaType) => { + const searchOptions = getTypeChain(schemaType) + .map((type) => type.options) + .find(isSearchConfiguration) + + return typeof searchOptions?.search?.weight === 'number' ? searchOptions.search.weight : null +} + +const getHiddenWeight = (schemaType: SchemaType) => { + const hidden = getTypeChain(schemaType).some((type) => type.hidden) + return hidden ? 0 : null +} + +const getDefaultWeights = (schemaType: SchemaType) => { + // if there is no user set weight or a `0` weight due to be hidden, + // then we can return the default weight of `1` + const result = getUserSetWeight(schemaType) ?? getHiddenWeight(schemaType) + return typeof result === 'number' ? null : 1 +} + +const getPreviewWeights = ( + schemaType: SchemaType | CrossDatasetType | undefined, + maxDepth: number, + isCrossDataset?: boolean, +): Record | null => { + const select = schemaType?.preview?.select + if (!select) return null + + const selectionKeysBySelectionPath = Object.fromEntries( + Object.entries(select).map(([selectionKey, selectionPath]) => [ + // replace indexed paths with `[]` + // e.g. `arrayOfObjects.0.myField` becomes `arrayOfObjects[].myField` + selectionPath.replace(/\.\d+/g, '[]'), + selectionKey, + ]), + ) + + const defaultWeights = getLeafWeights(schemaType, maxDepth, getDefaultWeights) + const nestedWeightsBySelectionPath = Object.fromEntries( + Object.entries(defaultWeights) + .map(([path, {type}]) => ({path, type})) + .filter(({path}) => selectionKeysBySelectionPath[path]) + .map(({path, type}) => [ + path, + { + type, + weight: + PREVIEW_FIELD_WEIGHT_MAP[ + selectionKeysBySelectionPath[path] as keyof typeof PREVIEW_FIELD_WEIGHT_MAP + ], + }, + ]), + ) + + if (isCrossDataset) { + return Object.fromEntries( + Object.values(selectionKeysBySelectionPath).map((path) => { + return [ + path, + { + path, + type: 'string', + weight: PREVIEW_FIELD_WEIGHT_MAP[path as keyof typeof PREVIEW_FIELD_WEIGHT_MAP], + }, + ] + }), + ) + } + + return getLeafWeights(schemaType, maxDepth, (_, path) => { + const nested = nestedWeightsBySelectionPath[path] + return nested ? nested.weight : null + }) +} + +export interface DeriveSearchWeightsFromTypeOptions { + schemaType: SchemaType | CrossDatasetType + maxDepth: number + isCrossDataset?: boolean + processPaths?: (paths: SearchPath[]) => SearchPath[] +} + +export function deriveSearchWeightsFromType({ + schemaType, + maxDepth, + isCrossDataset, + processPaths = (paths) => paths, +}: DeriveSearchWeightsFromTypeOptions): SearchSpec { + const cached = CACHE.get(schemaType) + if (cached) return cached + + const userSetWeights = getLeafWeights(schemaType, maxDepth, getUserSetWeight) + const hiddenWeights = getLeafWeights(schemaType, maxDepth, getHiddenWeight) + const defaultWeights = getLeafWeights(schemaType, maxDepth, getDefaultWeights) + const previewWeights = getPreviewWeights(schemaType, maxDepth, isCrossDataset) + + const weights: Record> = { + ...BASE_WEIGHTS, + ...defaultWeights, + ...hiddenWeights, + ...previewWeights, + ...userSetWeights, + } + + const result = { + typeName: isSchemaType(schemaType) ? schemaType.name : schemaType.type, + paths: processPaths( + Object.entries(weights).map(([path, {type, weight}]) => ({ + path, + weight, + ...(type === 'pt' && {mapWith: 'pt::text'}), + })), + ), + } + + CACHE.set(schemaType, result) + return result +} diff --git a/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.test.ts b/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.test.ts deleted file mode 100644 index db9aef47166..00000000000 --- a/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {describe, expect, it} from '@jest/globals' -import {Schema} from '@sanity/schema' - -import {getSearchableTypes} from '../common' -import {getSearchTypesWithMaxDepth} from './getSearchTypesWithMaxDepth' - -const mockSchema = Schema.compile({ - name: 'default', - types: [ - {name: 'book', title: 'Book', type: 'document', fields: [{name: 'title', type: 'string'}]}, - { - name: 'nestedDocument', - title: 'Nested document', - type: 'document', - fields: [ - {name: 'title', type: 'string'}, - { - name: 'nestedObject', - title: 'Nested object', - type: 'object', - fields: [ - {name: 'field1', type: 'string', description: 'This is a string field'}, - {name: 'field2', type: 'string', description: 'This is a collapsed field'}, - { - name: 'field3', - type: 'object', - fields: [ - {name: 'nested1', title: 'nested1', type: 'string'}, - { - name: 'nested2', - title: 'nested2', - type: 'object', - fields: [ - { - name: 'ge', - title: 'hello', - type: 'string', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], -}) - -describe('getSearchTypesWithMaxDepth', () => { - it('should limit searchable field depth to 5 levels by default if maxFieldDepth is not set', async () => { - const searchableTypes = getSearchTypesWithMaxDepth(getSearchableTypes(mockSchema)) - - expect(searchableTypes[0].__experimental_search).toEqual([ - {path: ['_id'], weight: 1}, - {path: ['_type'], weight: 1}, - {path: ['title'], weight: 10}, - ]) - expect(searchableTypes[1].__experimental_search).toEqual([ - {path: ['_id'], weight: 1}, - {path: ['_type'], weight: 1}, - {path: ['title'], weight: 10}, - {path: ['nestedObject', 'field1'], weight: 1}, - {path: ['nestedObject', 'field2'], weight: 1}, - {path: ['nestedObject', 'field3', 'nested1'], weight: 1}, - {path: ['nestedObject', 'field3', 'nested2', 'ge'], weight: 1}, - ]) - // expect(result[1].score).toEqual(10) - }) - - it('should limit search fields if maxFieldDepth is set to level 3', async () => { - const searchableTypesL3 = getSearchTypesWithMaxDepth(getSearchableTypes(mockSchema), 3) - - expect(searchableTypesL3[0].__experimental_search).toEqual([ - {path: ['_id'], weight: 1}, - {path: ['_type'], weight: 1}, - {path: ['title'], weight: 10}, - ]) - - expect(searchableTypesL3[1].__experimental_search).toEqual([ - {path: ['_id'], weight: 1}, - {path: ['_type'], weight: 1}, - {path: ['title'], weight: 10}, - {path: ['nestedObject', 'field1'], weight: 1}, - {path: ['nestedObject', 'field2'], weight: 1}, - ]) - }) - - it('should limit search fields if maxFieldDepth is set to level 4', async () => { - const searchableTypesL4 = getSearchTypesWithMaxDepth(getSearchableTypes(mockSchema), 4) - - expect(searchableTypesL4[1].__experimental_search).toEqual([ - {path: ['_id'], weight: 1}, - {path: ['_type'], weight: 1}, - {path: ['title'], weight: 10}, - {path: ['nestedObject', 'field1'], weight: 1}, - {path: ['nestedObject', 'field2'], weight: 1}, - {path: ['nestedObject', 'field3', 'nested1'], weight: 1}, - ]) - }) -}) diff --git a/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.ts b/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.ts deleted file mode 100644 index c71485f02f2..00000000000 --- a/packages/sanity/src/core/search/common/getSearchTypesWithMaxDepth.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {resolveSearchConfig} from '@sanity/schema/_internal' -import {type ObjectSchemaType} from '@sanity/types' - -import {type SearchableType} from '../common' - -/** - * @internal - */ -export function getSearchTypesWithMaxDepth( - types: ObjectSchemaType[], - maxFieldDepth?: number, -): SearchableType[] { - return types.map((type) => ({ - title: type.title, - name: type.name, - // eslint-disable-next-line camelcase - __experimental_search: resolveSearchConfig(type, maxFieldDepth), - })) -} diff --git a/packages/sanity/src/core/search/common/getSearchableTypes.ts b/packages/sanity/src/core/search/common/getSearchableTypes.ts index a3014b4957a..58d1f3b033c 100644 --- a/packages/sanity/src/core/search/common/getSearchableTypes.ts +++ b/packages/sanity/src/core/search/common/getSearchableTypes.ts @@ -16,5 +16,5 @@ export const getSearchableTypes = (schema: Schema): ObjectSchemaType[] => .getTypeNames() .map((typeName) => schema.get(typeName)) .filter(isNonNullable) - .filter((schemaType) => isDocumentType(schemaType)) - .filter((type) => !isIgnoredType(type)) as ObjectSchemaType[] + .filter(isDocumentType) + .filter((type) => !isIgnoredType(type)) diff --git a/packages/sanity/src/core/search/common/index.ts b/packages/sanity/src/core/search/common/index.ts index 4e97de1802b..c8242bfd8a4 100644 --- a/packages/sanity/src/core/search/common/index.ts +++ b/packages/sanity/src/core/search/common/index.ts @@ -1,3 +1,3 @@ +export * from './deriveSearchWeightsFromType' export * from './getSearchableTypes' -export * from './getSearchTypesWithMaxDepth' export * from './types' diff --git a/packages/sanity/src/core/search/common/types.ts b/packages/sanity/src/core/search/common/types.ts index 1cc8ef64bcc..af8501323e5 100644 --- a/packages/sanity/src/core/search/common/types.ts +++ b/packages/sanity/src/core/search/common/types.ts @@ -1,24 +1,15 @@ import {type SanityClient} from '@sanity/client' -import {type ObjectSchemaType, type SanityDocumentLike} from '@sanity/types' +import {type CrossDatasetType, type SanityDocumentLike, type SchemaType} from '@sanity/types' import {type Observable} from 'rxjs' /** * @internal */ -export interface SearchTerms { +export interface SearchTerms { filter?: string params?: Record query: string - types: SearchableType[] -} - -/** - * @internal - */ -export interface SearchableType { - name: string - title?: string - __experimental_search: ObjectSchemaType['__experimental_search'] + types: Type[] } /** @@ -35,7 +26,7 @@ export interface SearchPath { */ export interface SearchSpec { typeName: string - paths?: SearchPath[] + paths: SearchPath[] } /** @@ -67,6 +58,7 @@ export interface WeightedHit extends SearchHit { * @internal */ export interface SearchFactoryOptions { + maxDepth?: number filter?: string params?: Record tag?: string @@ -97,7 +89,7 @@ export interface WeightedSearchResults { * @internal */ export type SearchStrategyFactory = ( - types: SearchableType[], + types: (SchemaType | CrossDatasetType)[], client: SanityClient, commonOpts: SearchFactoryOptions, ) => (searchTerms: string | SearchTerms, searchOpts?: SearchOptions) => Observable @@ -107,12 +99,14 @@ export type SearchStrategyFactory +} + +/** + * @internal + */ +export type TextSearchSort = Record + export type TextSearchParams = { query: { string: string @@ -170,6 +176,14 @@ export type TextSearchParams = { * parameter to paginate, keeping the query the same, but changing the cursor. */ fromCursor?: string + /** + * Configuration for individual document types. + */ + types?: Record + /** + * Result sorting. + */ + sort?: TextSearchSort[] } export type TextSearchResponse> = { diff --git a/packages/sanity/src/core/search/text-search/createTextSearch.test.ts b/packages/sanity/src/core/search/text-search/createTextSearch.test.ts new file mode 100644 index 00000000000..d93e1563f85 --- /dev/null +++ b/packages/sanity/src/core/search/text-search/createTextSearch.test.ts @@ -0,0 +1,227 @@ +import {describe, expect, it} from '@jest/globals' +import {Schema} from '@sanity/schema' +import {defineField, defineType} from '@sanity/types' + +import {getDocumentTypeConfiguration, getSort} from './createTextSearch' + +const testType = Schema.compile({ + types: [ + defineType({ + name: 'basic-schema-test', + type: 'document', + preview: { + select: { + title: 'title', + subtitle: 'subtitle', + description: 'description', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + }), + defineField({ + name: 'subtitle', + type: 'string', + }), + defineField({ + name: 'description', + type: 'string', + }), + ], + }), + defineType({ + name: 'basic-schema-test-preview-override', + type: 'document', + preview: { + select: { + title: 'title', + subtitle: 'subtitle', + description: 'description', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + options: { + search: { + weight: 2, + }, + }, + }), + defineField({ + name: 'subtitle', + type: 'string', + options: { + search: { + weight: 3, + }, + }, + }), + defineField({ + name: 'description', + type: 'string', + options: { + search: { + weight: 4, + }, + }, + }), + ], + }), + defineType({ + name: 'basic-schema-test-non-preview-fields', + type: 'document', + preview: { + select: { + title: 'title', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + }), + defineField({ + name: 'variety', + type: 'string', + options: { + search: { + weight: 2, + }, + }, + }), + ], + }), + defineType({ + name: 'basic-schema-test-hidden-fields', + type: 'document', + preview: { + select: { + title: 'title', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + }), + defineField({ + name: 'variety', + type: 'string', + hidden: true, + }), + ], + }), + ], +}) + +describe('getDocumentTypeConfiguration', () => { + it('includes default weights for the preview selection', () => { + expect( + getDocumentTypeConfiguration( + {}, + { + types: [testType.get('basic-schema-test')], + query: 'test', + }, + ), + ).toEqual({ + 'basic-schema-test': { + weights: { + title: 10, + subtitle: 5, + description: 1.5, + }, + }, + }) + }) + + it('includes custom search weight configuration for the preview selection', () => { + expect( + getDocumentTypeConfiguration( + {}, + { + types: [testType.get('basic-schema-test-preview-override')], + query: 'test', + }, + ), + ).toEqual({ + 'basic-schema-test-preview-override': { + weights: { + title: 2, + subtitle: 3, + description: 4, + }, + }, + }) + }) + + it('includes custom search weight configuration for non-preview fields', () => { + expect( + getDocumentTypeConfiguration( + {}, + { + types: [testType.get('basic-schema-test-non-preview-fields')], + query: 'test', + }, + ), + ).toEqual({ + 'basic-schema-test-non-preview-fields': { + weights: { + title: 10, + variety: 2, + }, + }, + }) + }) + + it('gives a zero weighting to hidden fields', () => { + expect( + getDocumentTypeConfiguration( + {}, + { + types: [testType.get('basic-schema-test-hidden-fields')], + query: 'test', + }, + ), + ).toEqual({ + 'basic-schema-test-hidden-fields': { + weights: { + title: 10, + variety: 0, + }, + }, + }) + }) +}) + +describe('getSort', () => { + it('transforms Studio sort options to valid Text Search sort options', () => { + expect( + getSort([ + { + field: 'title', + direction: 'desc', + }, + { + field: '_createdAt', + direction: 'asc', + }, + ]), + ).toEqual([ + { + title: { + order: 'desc', + }, + }, + { + _createdAt: { + order: 'asc', + }, + }, + ]) + }) +}) diff --git a/packages/sanity/src/core/search/text-search/createTextSearch.ts b/packages/sanity/src/core/search/text-search/createTextSearch.ts index c91bdfcd81f..2689c12908b 100644 --- a/packages/sanity/src/core/search/text-search/createTextSearch.ts +++ b/packages/sanity/src/core/search/text-search/createTextSearch.ts @@ -1,19 +1,28 @@ -import {type SanityDocumentLike} from '@sanity/types' +import {DEFAULT_MAX_FIELD_DEPTH} from '@sanity/schema/_internal' +import {type CrossDatasetType, type SanityDocumentLike, type SchemaType} from '@sanity/types' import {map} from 'rxjs/operators' import {removeDupes} from '../../util/draftUtils' import { - type SearchableType, + deriveSearchWeightsFromType, + type SearchOptions, + type SearchPath, + type SearchSort, type SearchStrategyFactory, type SearchTerms, + type TextSearchDocumentTypeConfiguration, type TextSearchParams, type TextSearchResponse, type TextSearchResults, + type TextSearchSort, } from '../common' const DEFAULT_LIMIT = 1000 -function normalizeSearchTerms(searchParams: string | SearchTerms, fallbackTypes: SearchableType[]) { +function normalizeSearchTerms( + searchParams: string | SearchTerms, + fallbackTypes: (SchemaType | CrossDatasetType)[], +) { if (typeof searchParams === 'string') { return { query: searchParams, @@ -27,6 +36,54 @@ function normalizeSearchTerms(searchParams: string | SearchTerms, fallbackTypes: } } +function optimizeSearchWeights(paths: SearchPath[]): SearchPath[] { + return paths.filter((path) => path.weight !== 1) +} + +export function getDocumentTypeConfiguration( + searchOptions: SearchOptions, + searchTerms: ReturnType, +): Record { + const specs = searchTerms.types + .map((schemaType) => + deriveSearchWeightsFromType({ + schemaType, + maxDepth: searchOptions.maxDepth || DEFAULT_MAX_FIELD_DEPTH, + processPaths: optimizeSearchWeights, + }), + ) + .filter(({paths}) => paths.length) + + return specs.reduce>((nextTypes, spec) => { + return { + ...nextTypes, + [spec.typeName]: spec.paths.reduce( + (nextType, {path, weight}) => { + return { + ...nextType, + weights: { + ...nextType.weights, + [path]: weight, + }, + } + }, + {}, + ), + } + }, {}) +} + +export function getSort(sort: SearchSort[] = []): TextSearchSort[] { + return sort.map( + ({field, direction}) => ({ + [field]: { + order: direction, + }, + }), + {}, + ) +} + /** * @internal */ @@ -52,10 +109,13 @@ export const createTextSearch: SearchStrategyFactory = ( query: {string: searchTerms.query}, filter: filters.join(' && '), params: { - __types: searchTerms.types.map((type) => type.name), + __types: searchTerms.types.map((type) => ('name' in type ? type.name : type.type)), ...factoryOptions.params, ...searchTerms.params, }, + types: getDocumentTypeConfiguration(searchOptions, searchTerms), + // TODO: `sort` is not supported by the Text Search API yet. + // sort: getSort(searchOptions.sort), includeAttributes: ['_id', '_type'], fromCursor: searchOptions.cursor, limit: searchOptions.limit ?? DEFAULT_LIMIT, diff --git a/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts b/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts index e15f435d5ed..7cd847d75dc 100644 --- a/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts +++ b/packages/sanity/src/core/search/weighted/createSearchQuery.test.ts @@ -1,7 +1,8 @@ /* eslint-disable camelcase */ import {describe, expect, it, test} from '@jest/globals' +import {Schema} from '@sanity/schema' +import {defineArrayMember, defineField, defineType} from '@sanity/types' -import {type SearchableType} from '../common' import {FINDABILITY_MVI} from '../constants' import { createSearchQuery, @@ -10,15 +11,30 @@ import { tokenize, } from './createSearchQuery' -const testType: SearchableType = { - name: 'basic-schema-test', - __experimental_search: [ - { - path: ['title'], - weight: 10, - }, +const testType = Schema.compile({ + types: [ + defineType({ + name: 'basic-schema-test', + type: 'document', + preview: { + select: { + title: 'title', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + options: { + search: { + weight: 10, + }, + }, + }), + ], + }), ], -} +}).get('basic-schema-test') describe('createSearchQuery', () => { describe('searchTerms', () => { @@ -30,10 +46,10 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + '| order(_id asc)' + '[0...$__limit]' + - '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": title })}', + '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ) expect(params).toEqual({ @@ -47,23 +63,51 @@ describe('createSearchQuery', () => { const {query} = createSearchQuery({ query: 'term0', types: [ - { - name: 'basic-schema-test', - __experimental_search: [ - { - path: ['title'], - weight: 10, - }, - { - path: ['object', 'field'], - weight: 5, - }, + Schema.compile({ + types: [ + defineType({ + name: 'basic-schema-test', + type: 'document', + preview: { + select: { + title: 'title', + }, + }, + fields: [ + defineField({ + name: 'title', + type: 'string', + options: { + search: { + weight: 10, + }, + }, + }), + defineField({ + name: 'object', + type: 'object', + fields: [ + defineField({ + name: 'field', + type: 'string', + options: { + search: { + weight: 5, + }, + }, + }), + ], + }), + ], + }), ], - }, + }).get('basic-schema-test'), ], }) - expect(query).toContain('*[_type in $__types && (title match $t0 || object.field match $t0)]') + expect(query).toContain( + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0 || object.field match $t0)]', + ) }) it('should have one match filter per term', () => { @@ -72,7 +116,9 @@ describe('createSearchQuery', () => { types: [testType], }) - expect(query).toContain('*[_type in $__types && (title match $t0) && (title match $t1)]') + expect(query).toContain( + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (_id match $t1 || _type match $t1 || title match $t1)]', + ) expect(params.t0).toEqual('term0*') expect(params.t1).toEqual('term1*') }) @@ -101,9 +147,9 @@ describe('createSearchQuery', () => { const result = [ `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (title match $t0)]{_type, _id, object{field}}', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]{_type, _id, object{field}}', '|order(_id asc)[0...$__limit]', - '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": title })}', + '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ].join('') expect(query).toBe(result) @@ -147,7 +193,7 @@ describe('createSearchQuery', () => { ) expect(query).toContain( - '*[_type in $__types && (title match $t0) && (randomCondition == $customParam)]', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0) && (randomCondition == $customParam)]', ) expect(params.customParam).toEqual('custom') }) @@ -195,10 +241,10 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + '| order(exampleField desc)' + '[0...$__limit]' + - '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": title })}', + '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ) }) @@ -229,9 +275,9 @@ describe('createSearchQuery', () => { const result = [ `// findability-mvi:${FINDABILITY_MVI}\n`, - '*[_type in $__types && (title match $t0)]| ', + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]| ', 'order(exampleField desc,anotherExampleField asc,lower(mapWithField) asc)', - '[0...$__limit]{_type, _id, ...select(_type == "basic-schema-test" => { "w0": title })}', + '[0...$__limit]{_type, _id, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ].join('') expect(query).toEqual(result) @@ -245,10 +291,10 @@ describe('createSearchQuery', () => { expect(query).toEqual( `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (title match $t0)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || title match $t0)]' + '| order(_id asc)' + '[0...$__limit]' + - '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": title })}', + '{_type, _id, ...select(_type == "basic-schema-test" => { "w0": _id,"w1": _type,"w2": title })}', ) }) @@ -274,18 +320,7 @@ describe('createSearchQuery', () => { const {searchSpec} = createSearchQuery( { query: 'term', - types: [ - { - name: 'basic-schema-test', - __experimental_search: [ - { - path: ['some', 'field'], - weight: 10, - mapWith: 'dateTime', - }, - ], - }, - ], + types: [testType], }, {tag: 'customTag'}, @@ -295,10 +330,17 @@ describe('createSearchQuery', () => { { typeName: testType.name, paths: [ + { + weight: 1, + path: '_id', + }, + { + weight: 1, + path: '_type', + }, { weight: 10, - path: 'some.field', - mapWith: 'dateTime', + path: 'title', }, ], }, @@ -306,20 +348,52 @@ describe('createSearchQuery', () => { }) }) - describe('__experimental_search', () => { + describe('search config', () => { it('should handle indexed array fields in an optimized manner', () => { const {query} = createSearchQuery({ query: 'term0 term1', types: [ - { - name: 'numbers-in-path', - __experimental_search: [ - { - path: ['cover', 0, 'cards', 0, 'title'], - weight: 1, - }, + Schema.compile({ + types: [ + defineType({ + name: 'numbers-in-path', + type: 'document', + fields: [ + defineField({ + name: 'cover', + type: 'array', + of: [ + defineArrayMember({ + type: 'object', + fields: [ + defineField({ + name: 'cards', + type: 'array', + of: [ + defineArrayMember({ + type: 'object', + fields: [ + defineField({ + name: 'title', + type: 'string', + options: { + search: { + weight: 1, + }, + }, + }), + ], + }), + ], + }), + ], + }), + ], + }), + ], + }), ], - }, + }).get('numbers-in-path'), ], }) @@ -329,37 +403,16 @@ describe('createSearchQuery', () => { * This is an improvement over before, where an illegal term was used (number-as-string, ala ["0"]), * which lead to no hits at all. */ `// findability-mvi:${FINDABILITY_MVI}\n` + - '*[_type in $__types && (cover[].cards[].title match $t0) && (cover[].cards[].title match $t1)]' + + '*[_type in $__types && (_id match $t0 || _type match $t0 || cover[].cards[].title match $t0) && (_id match $t1 || _type match $t1 || cover[].cards[].title match $t1)]' + '| order(_id asc)' + '[0...$__limit]' + // at this point we could refilter using cover[0].cards[0].title. // This solution was discarded at it would increase the size of the query payload by up to 50% // we still map out the path with number - '{_type, _id, ...select(_type == "numbers-in-path" => { "w0": cover[0].cards[0].title })}', + '{_type, _id, ...select(_type == "numbers-in-path" => { "w0": _id,"w1": _type,"w2": cover[].cards[].title })}', ) }) - - it('should use mapper function from __experimental_search', () => { - const {query} = createSearchQuery({ - query: 'test', - types: [ - { - name: 'type1', - __experimental_search: [ - { - path: ['pteField'], - weight: 1, - mapWith: 'pt::text', - }, - ], - }, - ], - }) - - expect(query).toContain('*[_type in $__types && (pt::text(pteField) match $t0)') - expect(query).toContain('...select(_type == "type1" => { "w0": pt::text(pteField) })') - }) }) }) diff --git a/packages/sanity/src/core/search/weighted/createSearchQuery.ts b/packages/sanity/src/core/search/weighted/createSearchQuery.ts index 325f96fa204..605660351ae 100644 --- a/packages/sanity/src/core/search/weighted/createSearchQuery.ts +++ b/packages/sanity/src/core/search/weighted/createSearchQuery.ts @@ -1,8 +1,9 @@ +import {DEFAULT_MAX_FIELD_DEPTH} from '@sanity/schema/_internal' +import {type CrossDatasetType, type SchemaType} from '@sanity/types' import {compact, flatten, flow, toLower, trim, union, uniq, words} from 'lodash' -import {joinPath} from '../../../core/util/searchUtils' import { - type SearchableType, + deriveSearchWeightsFromType, type SearchFactoryOptions, type SearchOptions, type SearchPath, @@ -30,44 +31,6 @@ export const DEFAULT_LIMIT = 1000 const combinePaths: (paths: string[][]) => string[] = flow([flatten, union, compact]) -/** - * Create an object containing all available document types and weighted paths, used to construct a GROQ query for search. - * System fields `_id` and `_type` are included by default. - * - * If `optimizeIndexPaths` is true, this will will convert all `__experimental_search` paths containing numbers - * into array syntax. E.g. ['cover', 0, 'cards', 0, 'title'] =\> "cover[].cards[].title" - * - * This optimization will yield more search results than may be intended, but offers better performance over arrays with indices. - * (which are currently unoptimizable by Content Lake) - */ -function createSearchSpecs(types: SearchableType[], optimizeIndexedPaths: boolean) { - let hasIndexedPaths = false - - const specs = types.map((type) => ({ - typeName: type.name, - paths: type.__experimental_search.map((config) => { - const path = config.path.map((p) => { - if (typeof p === 'number') { - hasIndexedPaths = true - if (optimizeIndexedPaths) { - return [] as [] - } - } - return p - }) - return { - weight: config.weight, - path: joinPath(path), - mapWith: config.mapWith, - } - }), - })) - return { - specs, - hasIndexedPaths, - } -} - const pathWithMapper = ({mapWith, path}: SearchPath): string => mapWith ? `${mapWith}(${path})` : path @@ -147,41 +110,34 @@ function toOrderClause(orderBy: SearchSort[]): string { * @internal */ export function createSearchQuery( - searchTerms: SearchTerms, + searchTerms: SearchTerms, searchOpts: SearchOptions & SearchFactoryOptions = {}, ): SearchQuery { const {filter, params, tag} = searchOpts - /** - * First pass: create initial search specs and determine if this subset of types contains - * any indexed paths in `__experimental_search`. - * e.g. "authors.0.title" or ["authors", 0, "title"] - */ - const {specs: exactSearchSpecs, hasIndexedPaths} = createSearchSpecs(searchTerms.types, false) + const specs = searchTerms.types + .map((schemaType) => + deriveSearchWeightsFromType({ + schemaType, + maxDepth: searchOpts.maxDepth || DEFAULT_MAX_FIELD_DEPTH, + isCrossDataset: searchOpts.isCrossDataset, + }), + ) + .filter(({paths}) => paths.length) // Extract search terms from string query, factoring in phrases wrapped in quotes const terms = extractTermsFromQuery(searchTerms.query) - /** - * Second pass: create an optimized spec (with array indices removed), but only if types with any - * indexed paths have been previously found. Otherwise, passthrough original search specs. - * - * These optimized specs are only used when building constraints in this search query. - */ - const optimizedSpecs = hasIndexedPaths - ? createSearchSpecs(searchTerms.types, true).specs - : exactSearchSpecs - // Construct search filters used in this GROQ query const filters = [ '_type in $__types', searchOpts.includeDrafts === false && `!(_id in path('drafts.**'))`, - ...createConstraints(terms, optimizedSpecs), + ...createConstraints(terms, specs), filter ? `(${filter})` : '', searchTerms.filter ? `(${searchTerms.filter})` : '', ].filter(Boolean) - const selections = exactSearchSpecs.map((spec) => { + const selections = specs.map((spec) => { const constraint = `_type == "${spec.typeName}" => ` const selection = `{ ${spec.paths.map((cfg, i) => `"w${i}": ${pathWithMapper(cfg)}`)} }` return `${constraint}${selection}` @@ -198,8 +154,6 @@ export function createSearchQuery( `*[${filters.join(' && ')}]` + `| order(${sortOrder})` + `[0...$__limit]` + - // the following would improve search quality for paths-with-numbers, but increases the size of the query by up to 50% - // `${hasIndexedPaths ? `[${createConstraints(terms, exactSearchSpec).join(' && ')}]` : ''}` + `{${finalProjection}}` // Optionally prepend our query with an 'extended' projection. @@ -228,12 +182,12 @@ export function createSearchQuery( query: updatedQuery, params: { ...toGroqParams(terms), - __types: exactSearchSpecs.map((spec) => spec.typeName), + __types: specs.map((spec) => spec.typeName), __limit: limit, ...(params || {}), }, options: {tag}, - searchSpec: exactSearchSpecs, + searchSpec: specs, terms, } } diff --git a/packages/sanity/src/core/search/weighted/createWeightedSearch.ts b/packages/sanity/src/core/search/weighted/createWeightedSearch.ts index 1c99ea14167..9e8b602e49f 100644 --- a/packages/sanity/src/core/search/weighted/createWeightedSearch.ts +++ b/packages/sanity/src/core/search/weighted/createWeightedSearch.ts @@ -1,18 +1,16 @@ -import {type SanityDocumentLike} from '@sanity/types' +import {type CrossDatasetType, type SanityDocumentLike, type SchemaType} from '@sanity/types' import {sortBy} from 'lodash' import {map, tap} from 'rxjs/operators' import {removeDupes} from '../../util/draftUtils' -import { - type SearchableType, - type SearchStrategyFactory, - type SearchTerms, - type WeightedSearchResults, -} from '../common' +import {type SearchStrategyFactory, type SearchTerms, type WeightedSearchResults} from '../common' import {applyWeights} from './applyWeights' import {createSearchQuery} from './createSearchQuery' -function getSearchTerms(searchParams: string | SearchTerms, types: SearchableType[]) { +function getSearchTerms( + searchParams: string | SearchTerms, + types: (SchemaType | CrossDatasetType)[], +) { if (typeof searchParams === 'string') { return { query: searchParams, diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/common/DocumentTypesPill.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/common/DocumentTypesPill.tsx index c3aeccf1809..7197609531c 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/common/DocumentTypesPill.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/common/DocumentTypesPill.tsx @@ -1,13 +1,13 @@ +import {type SchemaType} from '@sanity/types' import {Card, Text} from '@sanity/ui' import {useMemo} from 'react' import {useTranslation} from '../../../../../../i18n' -import {type SearchableType} from '../../../../../../search' import {documentTypesTruncated} from '../../utils/documentTypesTruncated' interface TypePillsProps { availableCharacters?: number - types: SearchableType[] + types: SchemaType[] } export function DocumentTypesPill({availableCharacters, types}: TypePillsProps) { diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/addFilter/createFilterMenuItems.ts b/packages/sanity/src/core/studio/components/navbar/search/components/filters/addFilter/createFilterMenuItems.ts index 47885f818e5..2b5846b4403 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/addFilter/createFilterMenuItems.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/addFilter/createFilterMenuItems.ts @@ -1,9 +1,8 @@ -import {type Schema} from '@sanity/types' +import {type Schema, type SchemaType} from '@sanity/types' import {type ButtonTone} from '@sanity/ui' import {difference, startCase} from 'lodash' import {type TFunction} from '../../../../../../../i18n' -import {type SearchableType} from '../../../../../../../search' import {isNonNullable} from '../../../../../../../util' import { type SearchFieldDefinition, @@ -40,7 +39,7 @@ export function createFilterMenuItems({ filterDefinitions: SearchFilterDefinitionDictionary schema: Schema titleFilter: string - types: SearchableType[] + types: SchemaType[] t: TFunction<'studio', undefined> }): FilterMenuItem[] { // Construct field filters based on available definitions and current title fitler @@ -148,7 +147,7 @@ function buildFieldMenuItemsNarrowed({ filterDefinitions: SearchFilterDefinitionDictionary filters: SearchFilter[] schema: Schema - types: SearchableType[] + types: SchemaType[] t: TFunction<'studio', undefined> }) { const sharedFilters = filters.filter((filter) => { diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/DocumentTypesPopoverContent.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/DocumentTypesPopoverContent.tsx index ad93bfe06ac..75fcd6ae8a1 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/DocumentTypesPopoverContent.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/DocumentTypesPopoverContent.tsx @@ -1,4 +1,4 @@ -import {type Schema} from '@sanity/types' +import {type Schema, type SchemaType} from '@sanity/types' import {Box, Flex, MenuDivider, Stack, Text} from '@sanity/ui' import {partition} from 'lodash' import {type KeyboardEvent, useCallback, useMemo, useRef, useState} from 'react' @@ -13,7 +13,6 @@ import { } from '../../../../../../../components' import {useSchema} from '../../../../../../../hooks' import {useTranslation} from '../../../../../../../i18n' -import {type SearchableType} from '../../../../../../../search' import {useSearchState} from '../../../contexts/search/useSearchState' import {type DocumentTypeMenuItem} from '../../../types' import {getSelectableOmnisearchTypes} from '../../../utils/selectors' @@ -173,13 +172,7 @@ export function DocumentTypesPopoverContent() { ) } -function ClearButton({ - onClick, - selectedTypes, -}: { - onClick: () => void - selectedTypes: SearchableType[] -}) { +function ClearButton({onClick, selectedTypes}: {onClick: () => void; selectedTypes: SchemaType[]}) { const {t} = useTranslation() return ( @@ -201,8 +194,8 @@ function ClearButton({ function useGetDocumentTypeItems( schema: Schema, - selectedTypes: SearchableType[], - selectedTypesSnapshot: SearchableType[], + selectedTypes: SchemaType[], + selectedTypesSnapshot: SchemaType[], typeFilter: string, ) { return useMemo(() => { diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/items/DocumentTypeFilterItem.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/items/DocumentTypeFilterItem.tsx index 00666c247ae..2d5e62901d9 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/items/DocumentTypeFilterItem.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/documentTypes/items/DocumentTypeFilterItem.tsx @@ -1,14 +1,14 @@ import {CheckmarkIcon} from '@sanity/icons' +import {type SchemaType} from '@sanity/types' import {Box, type ResponsiveMarginProps, type ResponsivePaddingProps} from '@sanity/ui' import {memo, useCallback} from 'react' import {Button} from '../../../../../../../../../ui-components' -import {type SearchableType} from '../../../../../../../../search' import {useSearchState} from '../../../../contexts/search/useSearchState' interface DocumentTypeFilterItemProps extends ResponsiveMarginProps, ResponsivePaddingProps { selected: boolean - type: SearchableType + type: SchemaType } export const DocumentTypeFilterItem = memo(function TypeFilterItem({ diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/Reference.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/Reference.tsx index 0ee21bf7365..592f806b3bb 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/Reference.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/Reference.tsx @@ -1,11 +1,15 @@ -import {isArraySchemaType, isReferenceSchemaType, type ReferenceValue} from '@sanity/types' +import { + isArraySchemaType, + isReferenceSchemaType, + type ReferenceValue, + type SchemaType, +} from '@sanity/types' import {Box, Card, Stack} from '@sanity/ui' import {useCallback, useMemo} from 'react' import {Button} from '../../../../../../../../../../ui-components' import {useSchema} from '../../../../../../../../../hooks' import {useTranslation} from '../../../../../../../../../i18n' -import {type SearchableType} from '../../../../../../../../../search' import {useSearchState} from '../../../../../contexts/search/useSearchState' import {type OperatorInputComponentProps} from '../../../../../definitions/operators/operatorTypes' import {getSchemaField} from '../../../../../utils/getSchemaField' @@ -50,9 +54,9 @@ export function SearchFilterReferenceInput({ } return [] }) - .reduce((acc, val) => { + .reduce((acc, val) => { if (acc.findIndex((v) => v.name === val?.name) < 0) { - acc.push(val as SearchableType) + acc.push(val as SchemaType) } return acc }, []) diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/ReferenceAutocomplete.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/ReferenceAutocomplete.tsx index 73c8a36d286..a54678dddf4 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/ReferenceAutocomplete.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/reference/ReferenceAutocomplete.tsx @@ -1,4 +1,4 @@ -import {type ReferenceValue} from '@sanity/types' +import {type ReferenceValue, type SchemaType} from '@sanity/types' import {Autocomplete, Box, Flex, Text} from '@sanity/ui' import { type ForwardedRef, @@ -15,7 +15,7 @@ import styled from 'styled-components' import {Popover} from '../../../../../../../../../../ui-components' import {useSchema} from '../../../../../../../../../hooks' import {Translate, useTranslation} from '../../../../../../../../../i18n' -import {type SearchableType, type SearchHit} from '../../../../../../../../../search' +import {type SearchHit} from '../../../../../../../../../search' import {getPublishedId} from '../../../../../../../../../util' import {POPOVER_RADIUS} from '../../../../../constants' import {useSearchState} from '../../../../../contexts/search/useSearchState' @@ -34,7 +34,7 @@ interface PopoverContentProps { interface ReferenceAutocompleteProps { onSelect?: (reference: ReferenceValue | null) => void - types?: SearchableType[] + types?: SchemaType[] value?: ReferenceValue | null } diff --git a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx index 037d8bdc39a..202894bc5e7 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/SearchProvider.tsx @@ -3,7 +3,7 @@ import {type ReactNode, useCallback, useEffect, useMemo, useReducer, useRef, use import {type CommandListHandle} from '../../../../../../components' import {useSchema} from '../../../../../../hooks' -import {type SearchableType, type SearchTerms} from '../../../../../../search' +import {type SearchTerms} from '../../../../../../search' import {useCurrentUser} from '../../../../../../store' import {useSource} from '../../../../../source' import {SEARCH_LIMIT} from '../../constants' @@ -83,9 +83,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) { const hasValidTerms = hasSearchableTerms({terms}) // Get a narrowed list of document types to search on based on any current active filters. - const documentTypes = documentTypesNarrowed.map( - (documentType) => schema.get(documentType) as SearchableType, - ) + const documentTypes = documentTypesNarrowed.map((documentType) => schema.get(documentType)!) // Get a list of 'complete' filters (filters that return valid values) const completeFilters = currentFilters.filter((filter) => diff --git a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.test.ts b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.test.ts index 9d8d6e81dcb..57265ecab5b 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.test.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.test.ts @@ -1,9 +1,8 @@ import {describe, expect, it} from '@jest/globals' -import {type CurrentUser} from '@sanity/types' +import {type CurrentUser, type SchemaType} from '@sanity/types' import {act, renderHook} from '@testing-library/react' import {useReducer} from 'react' -import {type SearchableType} from '../../../../../../search' import {type RecentSearch} from '../../datastores/recentSearches' import {type SearchOrdering} from '../../types' import {initialSearchState, searchReducer, type SearchReducerState} from './reducer' @@ -21,12 +20,10 @@ const mockOrdering: SearchOrdering = { titleKey: 'search.ordering.created-descending-label', } -const mockSearchableType: SearchableType = { - // eslint-disable-next-line camelcase - __experimental_search: [], +const mockSearchableType = { name: 'book', title: 'Book', -} +} as unknown as SchemaType const recentSearchTerms = { __recent: { diff --git a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.ts b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.ts index f06516dc1c0..00504877d94 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/contexts/search/reducer.ts @@ -1,6 +1,6 @@ -import {type CurrentUser} from '@sanity/types' +import {type CurrentUser, type SchemaType} from '@sanity/types' -import {type SearchableType, type SearchHit, type SearchTerms} from '../../../../../../search' +import {type SearchHit, type SearchTerms} from '../../../../../../search' import {getPublishedId} from '../../../../../../util' import {type RecentSearch} from '../../datastores/recentSearches' import {type SearchFieldDefinitionDictionary} from '../../definitions/fields' @@ -122,8 +122,8 @@ export type TermsFiltersSetValue = { } export type TermsQuerySet = {type: 'TERMS_QUERY_SET'; query: string} export type TermsSet = {type: 'TERMS_SET'; filters?: SearchFilter[]; terms: SearchTerms} -export type TermsTypeAdd = {type: 'TERMS_TYPE_ADD'; schemaType: SearchableType} -export type TermsTypeRemove = {type: 'TERMS_TYPE_REMOVE'; schemaType: SearchableType} +export type TermsTypeAdd = {type: 'TERMS_TYPE_ADD'; schemaType: SchemaType} +export type TermsTypeRemove = {type: 'TERMS_TYPE_REMOVE'; schemaType: SchemaType} export type TermsTypesClear = {type: 'TERMS_TYPES_CLEAR'} export type SearchAction = @@ -499,7 +499,7 @@ export function searchReducer(state: SearchReducerState, action: SearchAction): } } case 'TERMS_TYPES_CLEAR': { - const types: SearchableType[] = [] + const types: SchemaType[] = [] return { ...state, diff --git a/packages/sanity/src/core/studio/components/navbar/search/hooks/useSearch.ts b/packages/sanity/src/core/studio/components/navbar/search/hooks/useSearch.ts index c7f3c405875..653acdcc9d8 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/hooks/useSearch.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/hooks/useSearch.ts @@ -17,7 +17,6 @@ import { import {useClient} from '../../../../../hooks' import { createSearch, - getSearchTypesWithMaxDepth, type SearchHit, type SearchOptions, type SearchTerms, @@ -87,16 +86,13 @@ export function useSearch({ const search = useMemo( () => - createSearch( - getSearchTypesWithMaxDepth(getSearchableOmnisearchTypes(schema), maxFieldDepth), - client, - { - tag: 'search.global', - unique: true, - unstable_enableNewSearch, - }, - ), - [client, schema, maxFieldDepth, unstable_enableNewSearch], + createSearch(getSearchableOmnisearchTypes(schema), client, { + tag: 'search.global', + unique: true, + unstable_enableNewSearch, + maxDepth: maxFieldDepth, + }), + [client, maxFieldDepth, schema, unstable_enableNewSearch], ) const handleQueryChange = useObservableCallback( diff --git a/packages/sanity/src/core/studio/components/navbar/search/types.ts b/packages/sanity/src/core/studio/components/navbar/search/types.ts index 62f70d560f4..543ed71d250 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/types.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/types.ts @@ -1,7 +1,7 @@ +import {type SchemaType} from '@sanity/types' import {type ButtonTone, type CardTone} from '@sanity/ui' import { - type SearchableType, type SearchHit, type SearchOptions, type SearchSort, @@ -17,7 +17,7 @@ export type DocumentTypeMenuItem = interface DocumentTypeMenuItemType { selected: boolean - item: SearchableType + item: SchemaType type: 'item' } diff --git a/packages/sanity/src/core/studio/components/navbar/search/utils/documentTypesTruncated.ts b/packages/sanity/src/core/studio/components/navbar/search/utils/documentTypesTruncated.ts index 3c93ada8df5..b4cae22f980 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/utils/documentTypesTruncated.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/utils/documentTypesTruncated.ts @@ -1,5 +1,6 @@ +import {type SchemaType} from '@sanity/types' + import {type TFunction} from '../../../../../i18n' -import {type SearchableType} from '../../../../../search' const DEFAULT_AVAILABLE_CHARS = 40 // excluding "+x more" suffix @@ -20,7 +21,7 @@ export function getDocumentTypesTruncated({ types, }: { availableCharacters?: number - types: SearchableType[] + types: SchemaType[] }): {types: string[]; remainingCount: number} { if (types.length === 0) { return {remainingCount: 0, types: []} @@ -30,7 +31,7 @@ export function getDocumentTypesTruncated({ * Get the total number of visible document types whose titles fit within `availableCharacters` count. * The first document is always included, regardless of whether it fits within `availableCharacters` or not. */ - const visibleTypes = types.reduce( + const visibleTypes = types.reduce( (function () { let remaining = availableCharacters return function (acc, val, index) { @@ -71,7 +72,7 @@ export function documentTypesTruncated({ types, }: { availableCharacters?: number - types: SearchableType[] + types: SchemaType[] t: TFunction<'studio', undefined> }): string { if (types.length === 0) { @@ -94,6 +95,6 @@ export function documentTypesTruncated({ }) } -function typeTitle(schemaType: SearchableType) { +function typeTitle(schemaType: SchemaType) { return schemaType.title ?? schemaType.name } diff --git a/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.test.ts b/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.test.ts index eb3e9d85c6e..38008ca36be 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.test.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from '@jest/globals' import {Schema} from '@sanity/schema' +import {type SchemaType} from '@sanity/types' -import {type SearchableType} from '../../../../../search' import {filterDefinitions} from '../definitions/defaultFilters' import {createFieldDefinitionDictionary, createFieldDefinitions} from '../definitions/fields' import {createFilterDefinitionDictionary} from '../definitions/filters' @@ -88,20 +88,10 @@ describe('narrowDocumentTypes', () => { }) it('should create a list of narrowed document types based on selected types', () => { - const selectedTypes: SearchableType[] = [ - { - // eslint-disable-next-line camelcase - __experimental_search: [], - name: 'article', - title: 'Article', - }, - { - // eslint-disable-next-line camelcase - __experimental_search: [], - name: 'gallery', - title: 'Gallery', - }, - ] + const selectedTypes = [ + {name: 'article', title: 'Article'}, + {name: 'gallery', title: 'Gallery'}, + ] as SchemaType[] const narrowedDocumentTypes = narrowDocumentTypes({ fieldDefinitions: fieldDefinitionDictionary, diff --git a/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.ts b/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.ts index 82fc1e96351..5af14958dff 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/utils/filterUtils.ts @@ -1,7 +1,7 @@ +import {type SchemaType} from '@sanity/types' import intersection from 'lodash/intersection' import isEmpty from 'lodash/isEmpty' -import {type SearchableType} from '../../../../../search' import {isNonNullable} from '../../../../../util' import { type SearchFieldDefinition, @@ -82,7 +82,7 @@ export function narrowDocumentTypes({ }: { fieldDefinitions: SearchFieldDefinitionDictionary filters: SearchFilter[] - types: SearchableType[] + types: SchemaType[] }): string[] { // Get all 'manually' selected document types const selectedDocumentTypes = types.map((type) => type.name) diff --git a/packages/sanity/src/core/studio/components/navbar/search/utils/selectors.ts b/packages/sanity/src/core/studio/components/navbar/search/utils/selectors.ts index 2d43d01c460..6aea660504c 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/utils/selectors.ts +++ b/packages/sanity/src/core/studio/components/navbar/search/utils/selectors.ts @@ -1,12 +1,12 @@ import {type ObjectSchemaType, type Schema, type SchemaType} from '@sanity/types' -import {getSearchableTypes, type SearchableType} from '../../../../../search' +import {getSearchableTypes} from '../../../../../search' /** * Returns a list of all available document types filtered by a search string. * Types containing the search string in its `title` or `name` will be returned. */ -export function getSelectableOmnisearchTypes(schema: Schema, typeFilter: string): SearchableType[] { +export function getSelectableOmnisearchTypes(schema: Schema, typeFilter: string): SchemaType[] { return getSearchableOmnisearchTypes(schema) .filter((type) => inTypeFilter(type, typeFilter)) .sort(sortTypes) @@ -22,7 +22,7 @@ export function getSearchableOmnisearchTypes(schema: Schema): ObjectSchemaType[] ) } -export function sortTypes(a: SearchableType, b: SearchableType): number { +export function sortTypes(a: SchemaType, b: SchemaType): number { return (a.title ?? a.name).localeCompare(b.title ?? b.name) } diff --git a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts index 33ddb186d28..2ba93cdb9bd 100644 --- a/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts +++ b/packages/sanity/src/structure/panes/documentList/listenSearchQuery.ts @@ -16,7 +16,7 @@ import { } from 'rxjs' import {exhaustMapWithTrailing} from 'rxjs-exhaustmap-with-trailing' import {type SanityDocumentLike, type Schema} from 'sanity' -import {createSearch, getSearchableTypes, getSearchTypesWithMaxDepth} from 'sanity/_internalBrowser' +import {createSearch, getSearchableTypes} from 'sanity/_internalBrowser' import {getExtendedProjection} from '../../structureBuilder/util/getExtendedProjection' // FIXME @@ -97,24 +97,22 @@ export function listenSearchQuery(options: ListenQueryOptions): Observable { - const types = getSearchTypesWithMaxDepth( - getSearchableTypes(schema).filter((type) => { - if (typeNames.includes(type.name)) { - // make a call to getExtendedProjection in strict mode to verify that all fields are - // known. This method will throw an exception if there are any unknown fields specified - // in the sort by list - getExtendedProjection(type, sort.by, true) - return true - } - return false - }), - maxFieldDepth, - ) + const types = getSearchableTypes(schema).filter((type) => { + if (typeNames.includes(type.name)) { + // make a call to getExtendedProjection in strict mode to verify that all fields are + // known. This method will throw an exception if there are any unknown fields specified + // in the sort by list + getExtendedProjection(type, sort.by, true) + return true + } + return false + }) const search = createSearch(types, client, { filter, params, unstable_enableNewSearch, + maxDepth: maxFieldDepth, }) const doFetch = () => {