Skip to content

Commit

Permalink
wip(sanity): avoid search scoring when possible and implement cursors…
Browse files Browse the repository at this point in the history
… that support multiple orderings
  • Loading branch information
juice49 committed Nov 25, 2024
1 parent 5b7a72a commit f9a65fb
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 13 deletions.
61 changes: 53 additions & 8 deletions packages/sanity/src/core/search/groq2024/createGroq2024Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {map} from 'rxjs'

import {
type Groq2024SearchResults,
type SearchSort,
type SearchStrategyFactory,
type SearchTerms,
type SortDirection,
} from '../common/types'
import {createSearchQuery} from './createSearchQuery'

Expand Down Expand Up @@ -32,10 +34,16 @@ export const createGroq2024Search: SearchStrategyFactory<Groq2024SearchResults>
return function search(searchParams, searchOptions = {}) {
const searchTerms = getSearchTerms(searchParams, typesFromFactory)

const {query, params, options} = createSearchQuery(searchTerms, searchParams, {
const mergedOptions = {
...factoryOptions,
...searchOptions,
})
}

const {query, params, options, sortOrder} = createSearchQuery(
searchTerms,
searchParams,
mergedOptions,
)

return client.observable
.withConfig({
Expand All @@ -53,19 +61,56 @@ export const createGroq2024Search: SearchStrategyFactory<Groq2024SearchResults>
// the penultimate result must be used to determine the start of the next page.
const lastResult = hasNextPage ? hits.at(-2) : hits.at(-1)

// TODO: This cursor must change if not sorting by `_score`.
const nextCursor = hasNextPage
? `(_score < ${lastResult?._score} || (_score == ${lastResult?._score} && _id > "${lastResult?._id}"))`
: undefined

return {
type: 'groq2024',
// Search overfetches by 1 to determine whether there is another page to fetch. Therefore,
// exclude the final result if it's beyond the limit.
hits: hits.map((hit) => ({hit})).slice(0, searchOptions.limit),
nextCursor,
nextCursor: hasNextPage ? getNextCursor({lastResult, sortOrder}) : undefined,
}
}),
)
}
}

function getNextCursor({
lastResult,
sortOrder,
}: {
lastResult?: SanityDocumentLike
sortOrder: SearchSort[]
}): string | undefined {
if (!lastResult) {
return undefined
}

return (
sortOrder
// Content Lake always orders by `_id asc` as a tiebreaker.
.concat({field: '_id', direction: 'asc'})
.reduce<string | undefined>((cursor, sortEntry, index) => {
const nextPredicate = sortOrder
.slice(0, index)
.map((previousSortEntry) => getCursorPredicate(previousSortEntry, lastResult, '=='))
.concat(getCursorPredicate(sortEntry, lastResult))
.join(' && ')

return [cursor, `(${nextPredicate})`]
.filter((segment) => typeof segment !== 'undefined')
.join(' || ')
}, undefined)
)
}

const sortComparators: Record<SortDirection, '>' | '<'> = {
asc: '>',
desc: '<',
}

function getCursorPredicate(
sort: SearchSort,
lastEntry: SanityDocumentLike,
comparator: '>' | '<' | '==' = sortComparators[sort.direction],
): string {
return [sort.field, comparator, JSON.stringify(lastEntry[sort.field])].join(' ')
}
20 changes: 15 additions & 5 deletions packages/sanity/src/core/search/groq2024/createSearchQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface SearchQuery {
query: string
params: SearchParams
options: Record<string, unknown>
sortOrder: SearchSort[]
}

function isSchemaType(
Expand Down Expand Up @@ -65,16 +66,19 @@ export function createSearchQuery(
)
.filter(({paths}) => paths.length !== 0)

// TODO: Unnecessary when `!isScored`.
const flattenedSpecs = specs
.map(({typeName, paths}) => paths.map((path) => ({...path, typeName})))
.flat()
.filter(({weight}) => weight !== 0)

// TODO: Unnecessary when `!isScored`.
const groupedSpecs = groupBy(
flattenedSpecs,
(entry) => `text::matchQuery(${entry.path}, $__query), ${entry.weight}`,
)

// TODO: Unnecessary when `!isScored`.
const score = Object.entries(groupedSpecs)
.map(([args, entries]) => {
if (entries.length === 1) {
Expand All @@ -84,23 +88,28 @@ export function createSearchQuery(
})
.concat([`text::matchQuery(@, $__query)`])

const sortOrder = options?.sort ?? [{field: '_score', direction: 'desc'}]
const isScored = sortOrder.some(({field}) => field === '_score')

const filters = [
'_type in $__types',
// TODO: It will be necessary to omit zero-weighted paths when `!isScored`.
isScored ? false : `text::matchQuery(@, $__query)`,
options.filter ? `(${options.filter})` : false,
searchTerms.filter ? `(${searchTerms.filter})` : false,
'!(_id in path("versions.**"))',
options.cursor,
].filter((baseFilter) => typeof baseFilter === 'string')

const sortOrder = toOrderClause(options?.sort ?? [{field: '_score', direction: 'desc'}])
const projectionFields = ['_type', '_id', '_score']
// TODO: Any field used in `sortOrder` must be projected in order to create a cursor.
const projectionFields = ['_type', '_id', ...(isScored ? ['_score'] : [])]
const projection = projectionFields.join(', ')

const query = [
`*[${filters.join(' && ')}]`,
['|', `score(${score.join(', ')})`],
['|', `order(${sortOrder})`],
`[_score > 0]`,
isScored ? ['|', `score(${score.join(', ')})`] : [],
['|', `order(${toOrderClause(sortOrder)})`],
isScored ? `[_score > 0]` : [],
`[0...$__limit]`,
`{${projection}}`,
]
Expand Down Expand Up @@ -129,5 +138,6 @@ export function createSearchQuery(
query: [pragma, query].join('\n'),
options: pick(options, ['tag']),
params,
sortOrder,
}
}

0 comments on commit f9a65fb

Please sign in to comment.