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 bf7e1a2
Show file tree
Hide file tree
Showing 22 changed files with 300 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]
130 changes: 130 additions & 0 deletions packages/sanity/src/core/config/__tests__/resolveConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {describe, expect, it} from 'vitest'
import {createMockAuthStore} from '../../store'
import {definePlugin} from '../definePlugin'
import {createSourceFromConfig, createWorkspaceFromConfig, resolveConfig} from '../resolveConfig'
import {type PluginOptions} from '../types'

describe('resolveConfig', () => {
it('throws on invalid tools property', async () => {
Expand Down Expand Up @@ -288,3 +289,132 @@ describe('createSourceFromConfig', () => {
})
})
})

describe('search strategy selection', () => {
const projectId = 'ppsg7ml5'
const dataset = 'production'

it('sets a default strategy', async () => {
const workspace = await createWorkspaceFromConfig({
projectId,
dataset,
})

expect(workspace.search.strategy).toBeTypeOf('string')
})

it('infers strategy based on `enableLegacySearch`', async () => {
const workspaceA = await createWorkspaceFromConfig({
projectId,
dataset,
search: {
enableLegacySearch: true,
},
})

expect(workspaceA.search.strategy).toBe('groqLegacy')

const workspaceB = await createWorkspaceFromConfig({
projectId,
dataset,
search: {
enableLegacySearch: false,
},
})

expect(workspaceB.search.strategy).toBe('textSearch')
})

it('gives precedence to `strategy`', async () => {
const workspaceA = await createWorkspaceFromConfig({
projectId,
dataset,
search: {
enableLegacySearch: true,
strategy: 'textSearch',
},
})

expect(workspaceA.search.strategy).toBe('textSearch')

const workspaceB = await createWorkspaceFromConfig({
projectId,
dataset,
search: {
enableLegacySearch: false,
strategy: 'groqLegacy',
},
})

expect(workspaceB.search.strategy).toBe('groqLegacy')
})

it('can be composed with other configurations', async () => {
const workspaceA = await createWorkspaceFromConfig({
projectId,
dataset,
plugins: [
getSearchOptionsPlugin({
enableLegacySearch: false,
}),
],
search: {
enableLegacySearch: true,
},
})

expect(workspaceA.search.strategy).toBe('groqLegacy')

const workspaceB = await createWorkspaceFromConfig({
projectId,
dataset,
plugins: [
getSearchOptionsPlugin({
enableLegacySearch: true,
}),
],
search: {
enableLegacySearch: false,
},
})

expect(workspaceB.search.strategy).toBe('textSearch')

const workspaceC = await createWorkspaceFromConfig({
projectId,
dataset,
plugins: [
getSearchOptionsPlugin({
enableLegacySearch: false,
}),
],
search: {
strategy: 'groqLegacy',
},
})

expect(workspaceC.search.strategy).toBe('groqLegacy')

const workspaceD = await createWorkspaceFromConfig({
projectId,
dataset,
plugins: [
getSearchOptionsPlugin({
strategy: 'textSearch',
}),
],
search: {
strategy: 'groqLegacy',
},
})

expect(workspaceD.search.strategy).toBe('groqLegacy')
})
})

function getSearchOptionsPlugin(options: PluginOptions['search']): PluginOptions {
return definePlugin({
name: 'sanity/search-options',
search: options,
})()
}
63 changes: 62 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,61 @@ export const legacySearchEnabledReducer: ConfigPropertyReducer<boolean, ConfigCo
return prev
}

/**
* Some projects may already be using the `enableLegacySearch` option. In order to gracefully
* migrate to the `strategy` option, this reducer produces a value that respects 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.
*
* Any explicitly defined `strategy` value will take precedence over the value inferred from
* `enableLegacySearch`.
*/
export const searchStrategyReducer = ({
config,
initialValue,
}: {
config: PluginOptions
initialValue: SearchStrategy
}): SearchStrategy => {
const flattenedConfig = flattenConfig(config, [])

type SearchStrategyReducerState = [
implicit: SearchStrategy | undefined,
explicit: SearchStrategy | undefined,
]

const [implicit, explicit] = flattenedConfig.reduce<SearchStrategyReducerState>(
([currentImplicit, currentExplicit], entry) => {
const {enableLegacySearch, strategy} = entry.config.search ?? {}

// The strategy has been explicitly defined.
if (typeof strategy !== 'undefined') {
if (!isSearchStrategy(strategy)) {
const listFormatter = new Intl.ListFormat('en-US', {type: 'disjunction'})
const options = listFormatter.format(searchStrategies.map((value) => `"${value}"`))
const received =
typeof strategy === 'string' ? `"${strategy}"` : getPrintableType(strategy)
throw new Error(`Expected \`search.strategy\` to be ${options}, but received ${received}`)
}

return [currentImplicit, strategy]
}

// The strategy has been implicitly defined.
if (typeof enableLegacySearch === 'boolean') {
return [enableLegacySearch ? 'groqLegacy' : 'textSearch', currentExplicit]
}

return [currentImplicit, currentExplicit]
},
[undefined, undefined],
)

return explicit ?? implicit ?? initialValue
}

export const startInCreateEnabledReducer = (opts: {
config: PluginOptions
initialValue: boolean
Expand Down
5 changes: 5 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,10 @@ function resolveSource({
initialValue: config.search?.unstable_partialIndexing?.enabled ?? false,
}),
},
strategy: searchStrategyReducer({
config,
initialValue: '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
Loading

0 comments on commit bf7e1a2

Please sign in to comment.