Skip to content

Commit

Permalink
feat(core): add Text Search API search strategy (#5785)
Browse files Browse the repository at this point in the history
* feat(core): add Text Search API search strategy

* feat(core): support multiple search strategies in `createSearch` function

* feat(core): use generic `createSearch` function for navbar search

* feat(core): use generic `createSearch` function for reference search

* feat(structure): very unsound prototype of Text Search API strategy for document lists

* feat(core): export Text Search API types

* feat(core): add type filtering to Text Search API request

* feat(core): remove unused search weighting for Text Search API search strategy

* feat(core): support filtering in Text Search API strategy

* refactor(core): specialise `createSearchQuery` function for Text Search API search strategy

* feat(core): add `TextSearchParams.params` type

* fix(core): allow `TextSearchParams.params` to include any single or array of primitives

* refactor(core): return `TextSearchParams` directly

* feat(core): add limit to Text Search API search strategy

* fix(core): allow non-weighted search hit

* refactor(core): remove unused function

* fix(core): allow non-weighted search hit

* refactor(core): remove unused function

* feat(core): scaffold hybrid search strategy

* refactor(core): rename for clarity

* chore(core): remove irrelevant comment

* feat(core): add `search.__experimental_strategy` option for controlling search strategy

* feat(test-studio): enable Text Search API search strategy

* feat: implement hybrid approach (messy)

* refactor(core): rename for clarity

* feat(core): add `useDocumentSearch` stub

* feat(core): prototype Text Search API pagination with global search

* feat(core): support new result data shape in weighted search strategy

* refactor(core): refine search result type

* fix(core): allow reference search to work with new data shape

* refactor(core): improve search strategy types

* refactor(core): improve search strategy types

* refactor: remove hybrid search and clean up

* refactor(core): rename binding

* refactor(core): remove unused offset-based pagination, fix pagination, handle next cursor in `SearchRequestComplete` action

* fix(core): use new data shape

* test(core): remove nonexistent option

* fix(core): deduplicate search results

* refactor(core): remove unused `useDocumentSearch` hook prototype

* test(core): global search pagination

* test(core): add assertion

* refactor(core): add type parameter for Text Search API hit attributes

* refactor(core): remove unused offset

* refactor(core): remove unused search offset handling

* feat(core): increase default search limit

* chore(core): add clarification

---------

Co-authored-by: Rico Kahler <[email protected]>
  • Loading branch information
juice49 and ricokahler authored Mar 5, 2024
1 parent 9fc34a2 commit 49fa240
Show file tree
Hide file tree
Showing 44 changed files with 826 additions and 533 deletions.
3 changes: 3 additions & 0 deletions dev/test-studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ const sharedSettings = definePlugin({
assetSources: [imageAssetSource],
},
},
search: {
unstable_enableNewSearch: true,
},

i18n: {
bundles: testStudioLocaleBundles,
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/types/src/reference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type ReferenceFilterSearchOptions = {
params?: Record<string, unknown>
tag?: string
maxFieldDepth?: number
unstable_enableNewSearch?: boolean
}

/** @public */
Expand Down
2 changes: 1 addition & 1 deletion packages/sanity/src/_internal/browser.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export {getSearchableTypes, getSearchTypesWithMaxDepth} from '../core/search'
export {createSearch, getSearchableTypes, getSearchTypesWithMaxDepth} from '../core/search'
export {useSearchMaxFieldDepth} from '../core/studio/components/navbar/search/hooks/useSearchMaxFieldDepth'
7 changes: 7 additions & 0 deletions packages/sanity/src/core/config/configPropertyReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,10 @@ export const partialIndexingEnabledReducer = (opts: {

return result
}

export const newSearchEnabledReducer: ConfigPropertyReducer<boolean, ConfigContext> = (
prev,
{search},
): boolean => {
return prev || search?.unstable_enableNewSearch || false
}
8 changes: 8 additions & 0 deletions packages/sanity/src/core/config/prepareConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
initialDocumentBadges,
initialLanguageFilter,
newDocumentOptionsResolver,
newSearchEnabledReducer,
partialIndexingEnabledReducer,
resolveProductionUrlReducer,
schemaTemplatesReducer,
Expand Down Expand Up @@ -569,6 +570,13 @@ function resolveSource({
initialValue: config.search?.unstable_partialIndexing?.enabled ?? false,
}),
},
unstable_enableNewSearch: resolveConfigProperty({
config,
context,
reducer: newSearchEnabledReducer,
propertyName: 'search.unstable_enableNewSearch',
initialValue: false,
}),
// we will use this when we add search config to PluginOptions
/*filters: resolveConfigProperty({
config,
Expand Down
12 changes: 12 additions & 0 deletions packages/sanity/src/core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ export interface PluginOptions {
unstable_partialIndexing?: {
enabled: boolean
}
/**
* Enables the experimental new search API as an opt-in feature. This flag
* allows you to test and provide feedback on the new search capabilities
* before they become the default search mechanism. It is part of an
* experimental set of features that are subject to change. Users should be
* aware that while this feature is in use, they may encounter
* inconsistencies or unexpected behavior compared to the stable search
* functionality.
*/
unstable_enableNewSearch?: boolean
}
}

Expand Down Expand Up @@ -707,6 +717,8 @@ export interface Source {
unstable_partialIndexing?: {
enabled: boolean
}

unstable_enableNewSearch?: boolean
}

/** @internal */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
type Path,
type Reference,
type ReferenceFilterSearchOptions,
type ReferenceOptions,
type ReferenceSchemaType,
type SanityDocument,
} from '@sanity/types'
import {type Path, type Reference, type ReferenceSchemaType} from '@sanity/types'
import * as PathUtils from '@sanity/util/paths'
import {
type ComponentProps,
Expand All @@ -15,16 +8,10 @@ import {
useMemo,
useRef,
} from 'react'
import {from, throwError} from 'rxjs'
import {catchError, mergeMap} from 'rxjs/operators'

import {type Source} from '../../../config'
import {type FIXME} from '../../../FIXME'
import {useSchema} from '../../../hooks'
import {useDocumentPreviewStore} from '../../../store'
import {useSource} from '../../../studio'
import {useSearchMaxFieldDepth} from '../../../studio/components/navbar/search/hooks/useSearchMaxFieldDepth'
import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient'
import {isNonNullable} from '../../../util'
import {useFormValue} from '../../contexts/FormValue'
import {useReferenceInputOptions} from '../../studio'
Expand All @@ -37,37 +24,6 @@ function useValueRef<T>(value: T): {current: T} {
return ref
}

interface SearchError {
message: string
details?: {
type: string
description: string
}
}

// eslint-disable-next-line require-await
async function resolveUserDefinedFilter(
options: ReferenceOptions | undefined,
document: SanityDocument,
valuePath: Path,
getClient: Source['getClient'],
): Promise<ReferenceFilterSearchOptions> {
if (!options) {
return {}
}

if (typeof options.filter === 'function') {
const parentPath = valuePath.slice(0, -1)
const parent = PathUtils.get(document, parentPath) as Record<string, unknown>
return options.filter({document, parentPath, parent, getClient})
}

return {
filter: options.filter,
params: 'filterParams' in options ? options.filterParams : undefined,
}
}

interface Options {
path: Path
schemaType: ReferenceSchemaType
Expand All @@ -76,12 +32,8 @@ interface Options {

export function useReferenceInput(options: Options) {
const {path, schemaType} = options
const source = useSource()
const client = source.getClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
const schema = useSchema()
const documentPreviewStore = useDocumentPreviewStore()
const maxFieldDepth = useSearchMaxFieldDepth()
const searchClient = useMemo(() => client.withConfig({apiVersion: '2021-03-25'}), [client])
const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
useReferenceInputOptions()

Expand All @@ -95,32 +47,6 @@ export function useReferenceInput(options: Options) {
}, [documentTypeName, schema])

const disableNew = schemaType.options?.disableNew === true
const getClient = source.getClient

const handleSearch = useCallback(
(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,
}),
),

catchError((err: SearchError) => {
const isQueryError = err.details && err.details.type === 'queryParseError'
if (schemaType.options?.filter && isQueryError) {
err.message = `Invalid reference filter, please check the custom "filter" option`
}
return throwError(err)
}),
),

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

const template = options.value?._strengthenOnPublish?.template
const EditReferenceLink = useMemo(
Expand Down Expand Up @@ -193,7 +119,6 @@ export function useReferenceInput(options: Options) {

return {
selectedState,
handleSearch,
isCurrentDocumentLiveEdit,
handleEditReference,
EditReferenceLink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {combineLatest, type Observable, of} from 'rxjs'
import {map, mergeMap, startWith, switchMap} from 'rxjs/operators'

import {type DocumentPreviewStore, getPreviewPaths, prepareForPreview} from '../../../../preview'
import {createWeightedSearch, getSearchTypesWithMaxDepth} from '../../../../search'
import {createSearch, getSearchTypesWithMaxDepth} from '../../../../search'
import {collate, type CollatedHit, getDraftId, getIdPair, isRecord} from '../../../../util'
import {type ReferenceInfo, type ReferenceSearchHit} from '../../../inputs/ReferenceInput/types'

Expand Down Expand Up @@ -191,14 +191,14 @@ export function referenceSearch(
textTerm: string,
type: ReferenceSchemaType,
options: ReferenceFilterSearchOptions,
unstable_enableNewSearch: boolean,
): Observable<ReferenceSearchHit[]> {
const searchWeighted = createWeightedSearch(
getSearchTypesWithMaxDepth(type.to, options.maxFieldDepth),
client,
options,
)
return searchWeighted(textTerm, {includeDrafts: true}).pipe(
map((results) => results.map((result) => result.hit)),
const search = createSearch(getSearchTypesWithMaxDepth(type.to, options.maxFieldDepth), client, {
...options,
unstable_enableNewSearch,
})
return search(textTerm, {includeDrafts: true}).pipe(
map(({hits}) => hits.map(({hit}) => hit)),
map(collate),
// pick the 100 best matches
map((collated) => collated.slice(0, 100)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function StudioCrossDatasetReferenceInput(props: StudioCrossDatasetRefere
const client = source.getClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
const documentPreviewStore = useDocumentPreviewStore()
const getClient = source.getClient
const {unstable_enableNewSearch = false} = source.search

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

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

[crossDatasetClient, documentRef, path, schemaType, maxFieldDepth, getClient],
[
schemaType,
documentRef,
path,
getClient,
crossDatasetClient,
maxFieldDepth,
unstable_enableNewSearch,
],
)

const getReferenceInfo = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import {type Observable} from 'rxjs'
import {map} from 'rxjs/operators'

import {createWeightedSearch} from '../../../../../search'
import {createSearch} from '../../../../../search'
import {collate} from '../../../../../util'

interface SearchHit {
Expand All @@ -22,7 +22,7 @@ export function search(
type: CrossDatasetReferenceSchemaType,
options: ReferenceFilterSearchOptions,
): Observable<SearchHit[]> {
const searchWeighted = createWeightedSearch(
const searchWeighted = createSearch(
type.to.map((crossDatasetType) => ({
name: crossDatasetType.type,
// eslint-disable-next-line camelcase
Expand All @@ -31,14 +31,12 @@ export function search(
options.maxFieldDepth,
),
})),

client,
options,
)

return searchWeighted(textTerm, {includeDrafts: false}).pipe(
// pick the 100 best matches
map((results) => results.map((result) => result.hit)),
map(({hits}) => hits.map(({hit}) => hit)),
map(collate),
map((collated) =>
collated.map((entry) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function StudioReferenceInput(props: StudioReferenceInputProps) {
const {path, schemaType} = props
const {EditReferenceLinkComponent, onEditReference, activePath, initialValueTemplateItems} =
useReferenceInputOptions()
const {unstable_enableNewSearch = false} = source.search

const documentValue = useFormValue([]) as FIXME
const documentRef = useValueRef(documentValue)
Expand All @@ -110,13 +111,19 @@ 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,
}),
adapter.referenceSearch(
searchClient,
searchString,
schemaType,
{
...schemaType.options,
filter,
params,
tag: 'search.reference',
maxFieldDepth,
},
unstable_enableNewSearch,
),
),

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

[documentRef, path, searchClient, schemaType, maxFieldDepth, getClient],
[
schemaType,
documentRef,
path,
getClient,
searchClient,
maxFieldDepth,
unstable_enableNewSearch,
],
)

const template = props.value?._strengthenOnPublish?.template
Expand Down
3 changes: 1 addition & 2 deletions packages/sanity/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ export * from './preview'
export * from './schema'
export type {
SearchableType,
SearchFactoryOptions,
SearchOptions,
SearchSort,
SearchTerms,
WeightedSearchOptions,
} from './search'
export {createSearchQuery} from './search'
export * from './store'
export * from './studio'
export * from './studioClient'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {describe, expect, it} from '@jest/globals'
import {Schema} from '@sanity/schema'

import {getSearchableTypes} from '../common/utils'
import {getSearchableTypes} from '../common'
import {getSearchTypesWithMaxDepth} from './getSearchTypesWithMaxDepth'

const mockSchema = Schema.compile({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {resolveSearchConfig} from '@sanity/schema/_internal'
import {type ObjectSchemaType} from '@sanity/types'

import {type SearchableType} from './types'
import {type SearchableType} from '../common'

/**
* @internal
Expand Down
3 changes: 3 additions & 0 deletions packages/sanity/src/core/search/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './getSearchableTypes'
export * from './getSearchTypesWithMaxDepth'
export * from './types'
Loading

0 comments on commit 49fa240

Please sign in to comment.