Skip to content

Commit

Permalink
feat: simplify search strategy configuration
Browse files Browse the repository at this point in the history
The search strategy was previously selected using the
`search.enableLegacySearch` boolean, which became confusing after we
reverted the default behaviour. Developers had to explicitly set
`search.enableLegacySearch` to `false` in order to enable the Text
Search API strategy.

This change deprecates `search.enableLegacySearch` in favour of
the `search.strategy` union, which is clearer and more flexible.

The current options are:

- `"groqLegacy"`: Legacy search strategy (the current default).
- `"textSearch"`: Search using the Text Search API.

In the near future, we will add a new option, allowing developers to
test the new GROQ search strategy.

If the project already sets `search.enableLegacySearch` to `false`,
this will automatically be mapped to the `"textSearch"` strategy.
If the project uses both `search.enableLegacySearch` and
`search.strategy`, `search.strategy` will take precedence.
  • Loading branch information
juice49 committed Nov 7, 2024
1 parent 1ad6d94 commit 315e45d
Show file tree
Hide file tree
Showing 21 changed files with 151 additions and 67 deletions.
1 change: 1 addition & 0 deletions packages/@sanity/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './paths'
export * from './portableText'
export * from './reference'
export * from './schema'
export * from './search'
export * from './slug'
export * from './transactionLog'
export * from './upload'
Expand Down
3 changes: 2 additions & 1 deletion packages/@sanity/types/src/reference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type SanityClient} from '@sanity/client'
import {type SanityDocument} from '../documents'
import {type Path} from '../paths'
import {type BaseSchemaTypeOptions} from '../schema'
import {type SearchStrategy} from '../search/types'

/** @public */
export interface Reference {
Expand All @@ -28,7 +29,7 @@ export type ReferenceFilterSearchOptions = {
params?: Record<string, unknown>
tag?: string
maxFieldDepth?: number
enableLegacySearch?: boolean
strategy?: SearchStrategy
}

/** @public */
Expand Down
10 changes: 10 additions & 0 deletions packages/@sanity/types/src/search/asserters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {searchStrategies, type SearchStrategy} from './types'

/**
* @internal
*/
export function isSearchStrategy(
maybeSearchStrategy: unknown,
): maybeSearchStrategy is SearchStrategy {
return searchStrategies.includes(maybeSearchStrategy as SearchStrategy)
}
2 changes: 2 additions & 0 deletions packages/@sanity/types/src/search/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './asserters'
export * from './types'
9 changes: 9 additions & 0 deletions packages/@sanity/types/src/search/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @public
*/
export const searchStrategies = ['groqLegacy', 'textSearch'] as const

/**
* @public
*/
export type SearchStrategy = (typeof searchStrategies)[number]
32 changes: 31 additions & 1 deletion packages/sanity/src/core/config/configPropertyReducers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import {type AssetSource, type SchemaTypeDefinition} from '@sanity/types'
import {
type AssetSource,
isSearchStrategy,
type SchemaTypeDefinition,
searchStrategies,
type SearchStrategy,
} from '@sanity/types'
import {type ErrorInfo, type ReactNode} from 'react'

import {type LocaleConfigContext, type LocaleDefinition, type LocaleResourceBundle} from '../i18n'
Expand Down Expand Up @@ -392,6 +398,30 @@ export const legacySearchEnabledReducer: ConfigPropertyReducer<boolean, ConfigCo
return prev
}

export const searchStrategyReducer: ConfigPropertyReducer<SearchStrategy, ConfigContext> = (
prev,
{search},
) => {
if (typeof search?.strategy === 'undefined') {
return prev
}

if (isSearchStrategy(search.strategy)) {
return search.strategy
}

const listFormatter = new Intl.ListFormat('en-US', {
type: 'disjunction',
})

const options = listFormatter.format(searchStrategies.map((value) => `"${value}"`))

const received =
typeof search.strategy === 'string' ? `"${search.strategy}"` : getPrintableType(search.strategy)

throw new Error(`Expected \`search.strategy\` to be ${options}, but received ${received}`)
}

export const startInCreateEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
Expand Down
17 changes: 17 additions & 0 deletions packages/sanity/src/core/config/prepareConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
partialIndexingEnabledReducer,
resolveProductionUrlReducer,
schemaTemplatesReducer,
searchStrategyReducer,
startInCreateEnabledReducer,
toolsReducer,
} from './configPropertyReducers'
Expand Down Expand Up @@ -608,6 +609,22 @@ function resolveSource({
initialValue: config.search?.unstable_partialIndexing?.enabled ?? false,
}),
},
strategy: resolveConfigProperty({
config,
context,
reducer: searchStrategyReducer,
propertyName: 'search.strategy',
// Some projects may already be using the `enableLegacySearch` option. In order to
// gracefully migrate to the `strategy` option, the next line sets its initial value
// according to any existing `enableLegacySearch` option.
//
// If the project currently enables the Text Search API search strategy by setting
// `enableLegacySearch` to `false`, this is mapped to the `textSearch` strategy.
//
// The project may override any existing `enableLegacySearch` option by setting the
// `strategy` value.
initialValue: config.search?.enableLegacySearch === false ? 'textSearch' : 'groqLegacy',
}),
enableLegacySearch: resolveConfigProperty({
config,
context,
Expand Down
19 changes: 19 additions & 0 deletions packages/sanity/src/core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type Schema,
type SchemaType,
type SchemaTypeDefinition,
type SearchStrategy,
} from '@sanity/types'
import {type i18n} from 'i18next'
import {type ComponentType, type ErrorInfo, type ReactNode} from 'react'
Expand Down Expand Up @@ -383,8 +384,25 @@ export interface PluginOptions {
unstable_partialIndexing?: {
enabled: boolean
}

/**
* Control the strategy used for searching documents. This should generally only be used if you
* wish to try experimental search strategies.
*
* This option takes precedence over the deprecated `search.enableLegacySearch` option.
*
* Can be one of:
*
* - `"groqLegacy"` (default): Use client-side tokenization and schema introspection to search
* using the GROQ Query API.
* - `"textSearch"` (deprecated): Perform full text searching using the Text Search API.
*/
strategy?: SearchStrategy

/**
* Enables the legacy Query API search strategy.
*
* @deprecated Use `search.strategy` instead.
*/
enableLegacySearch?: boolean
}
Expand Down Expand Up @@ -759,6 +777,7 @@ export interface Source {
}

enableLegacySearch?: boolean
strategy?: SearchStrategy
}

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,9 @@ export function referenceSearch(
textTerm: string,
type: ReferenceSchemaType,
options: ReferenceFilterSearchOptions,
enableLegacySearch: boolean,
): Observable<ReferenceSearchHit[]> {
const search = createSearch(type.to, client, {
...options,
enableLegacySearch,
maxDepth: options.maxFieldDepth || DEFAULT_MAX_FIELD_DEPTH,
})
return search(textTerm, {includeDrafts: true}).pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function StudioCrossDatasetReferenceInput(props: StudioCrossDatasetRefere
const client = source.getClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
const documentPreviewStore = useDocumentPreviewStore()
const getClient = source.getClient
const {enableLegacySearch = false} = source.search
const {strategy: searchStrategy} = source.search

const crossDatasetClient = useMemo(() => {
return (
Expand Down Expand Up @@ -110,7 +110,7 @@ export function StudioCrossDatasetReferenceInput(props: StudioCrossDatasetRefere
params,
tag: 'search.cross-dataset-reference',
maxFieldDepth,
enableLegacySearch,
strategy: searchStrategy,
}),
),

Expand All @@ -123,15 +123,7 @@ export function StudioCrossDatasetReferenceInput(props: StudioCrossDatasetRefere
}),
),

[
schemaType,
documentRef,
path,
getClient,
crossDatasetClient,
maxFieldDepth,
enableLegacySearch,
],
[schemaType, documentRef, path, getClient, crossDatasetClient, maxFieldDepth, searchStrategy],
)

const getReferenceInfo = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
const {path, schemaType} = props
const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
useReferenceInputOptions()
const {enableLegacySearch = false} = source.search
const {strategy: searchStrategy} = source.search

const documentValue = useFormValue([]) as FIXME
const documentRef = useValueRef(documentValue)
Expand All @@ -80,19 +80,14 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
(searchString: string) =>
from(resolveUserDefinedFilter(schemaType.options, documentRef.current, path, getClient)).pipe(
mergeMap(({filter, params}) =>
adapter.referenceSearch(
searchClient,
searchString,
schemaType,
{
...schemaType.options,
filter,
params,
tag: 'search.reference',
maxFieldDepth,
},
enableLegacySearch,
),
adapter.referenceSearch(searchClient, searchString, schemaType, {
...schemaType.options,
filter,
params,
tag: 'search.reference',
maxFieldDepth,
strategy: searchStrategy,
}),
),

catchError((err: SearchError) => {
Expand All @@ -104,7 +99,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
}),
),

[schemaType, documentRef, path, getClient, searchClient, maxFieldDepth, enableLegacySearch],
[schemaType, documentRef, path, getClient, searchClient, maxFieldDepth, searchStrategy],
)

const template = props.value?._strengthenOnPublish?.template
Expand Down
9 changes: 7 additions & 2 deletions packages/sanity/src/core/search/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {type SanityClient} from '@sanity/client'
import {type CrossDatasetType, type SanityDocumentLike, type SchemaType} from '@sanity/types'
import {
type CrossDatasetType,
type SanityDocumentLike,
type SchemaType,
type SearchStrategy,
} from '@sanity/types'
import {type Observable} from 'rxjs'

/**
Expand Down Expand Up @@ -64,7 +69,7 @@ export interface SearchFactoryOptions {
tag?: string
/* only return unique documents (e.g. not both draft and published) */
unique?: boolean
enableLegacySearch?: boolean
strategy?: SearchStrategy
}

/**
Expand Down
11 changes: 10 additions & 1 deletion packages/sanity/src/core/search/search.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {type SearchStrategy} from '@sanity/types'

import {
type SearchStrategyFactory,
type TextSearchResults,
Expand All @@ -6,12 +8,19 @@ import {
import {createTextSearch} from './text-search'
import {createWeightedSearch} from './weighted'

const searchStrategies = {
groqLegacy: createWeightedSearch,
textSearch: createTextSearch,
} satisfies Record<SearchStrategy, SearchStrategyFactory<TextSearchResults | WeightedSearchResults>>

const DEFAULT_SEARCH_STRATEGY: SearchStrategy = 'groqLegacy'

/** @internal */
export const createSearch: SearchStrategyFactory<TextSearchResults | WeightedSearchResults> = (
searchableTypes,
client,
options,
) => {
const factory = options.enableLegacySearch ? createWeightedSearch : createTextSearch
const factory = searchStrategies[options.strategy ?? DEFAULT_SEARCH_STRATEGY]
return factory(searchableTypes, client, options)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ function CustomMenuItem({ordering}: {ordering: SearchOrdering}) {

export function SortMenu() {
const {t} = useTranslation()
const {enableLegacySearch = false} = useWorkspace().search
const {strategy: searchStrategy} = useWorkspace().search
const {
state: {ordering},
} = useSearchState()

const menuButtonId = useId()

const menuOrderings: (SearchDivider | SearchOrdering)[] = useMemo(() => {
const orderings = getOrderings({enableLegacySearch})
const orderings = getOrderings({searchStrategy})
return [
orderings.relevance,
{type: 'divider'},
Expand All @@ -66,7 +66,7 @@ export function SortMenu() {
orderings.updatedAsc,
orderings.updatedDesc,
]
}, [enableLegacySearch])
}, [searchStrategy])

const currentMenuItem = menuOrderings.find(
(item): item is SearchOrdering => isEqual(ordering, item) && !isSearchDivider(item),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
const schema = useSchema()
const currentUser = useCurrentUser()
const {
search: {operators, filters, enableLegacySearch},
search: {operators, filters, strategy},
} = useSource()

// Create field, filter and operator dictionaries
Expand All @@ -60,16 +60,9 @@ export function SearchProvider({children, fullscreen}: SearchProviderProps) {
cursor: null,
nextCursor: null,
},
enableLegacySearch,
strategy,
}),
[
currentUser,
fieldDefinitions,
filterDefinitions,
fullscreen,
operatorDefinitions,
enableLegacySearch,
],
[currentUser, fullscreen, fieldDefinitions, operatorDefinitions, filterDefinitions, strategy],
)
const [state, dispatch] = useReducer(searchReducer, initialState)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ describe('searchReducer', () => {
searchReducer,
initialSearchState({
...initialStateContext,
enableLegacySearch: false,
strategy: 'textSearch',
}),
),
)
Expand All @@ -150,7 +150,7 @@ describe('searchReducer', () => {
searchReducer,
initialSearchState({
...initialStateContext,
enableLegacySearch: true,
strategy: 'groqLegacy',
}),
),
)
Expand Down
Loading

0 comments on commit 315e45d

Please sign in to comment.