sanity
Version:
Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches
146 lines (132 loc) • 4.23 kB
text/typescript
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 {
deriveSearchWeightsFromType,
type SearchOptions,
type SearchPath,
type SearchSort,
type SearchStrategyFactory,
type SearchTerms,
type TextSearchDocumentTypeConfiguration,
type TextSearchOrder,
type TextSearchParams,
type TextSearchResponse,
type TextSearchResults,
} from '../common'
const DEFAULT_LIMIT = 1000
function normalizeSearchTerms(
searchParams: string | SearchTerms,
fallbackTypes: (SchemaType | CrossDatasetType)[],
) {
if (typeof searchParams === 'string') {
return {
query: searchParams,
types: fallbackTypes,
}
}
return {
...searchParams,
types: searchParams.types.length ? searchParams.types : fallbackTypes,
}
}
function optimizeSearchWeights(paths: SearchPath[]): SearchPath[] {
return paths.filter((path) => path.weight !== 1)
}
export function getDocumentTypeConfiguration(
searchOptions: SearchOptions,
searchTerms: ReturnType<typeof normalizeSearchTerms>,
): Record<string, TextSearchDocumentTypeConfiguration> {
const specs = searchTerms.types
.map((schemaType) =>
deriveSearchWeightsFromType({
schemaType,
maxDepth: searchOptions.maxDepth || DEFAULT_MAX_FIELD_DEPTH,
processPaths: optimizeSearchWeights,
}),
)
.filter(({paths}) => paths.length)
return specs.reduce<Record<string, TextSearchDocumentTypeConfiguration>>((nextTypes, spec) => {
return {
...nextTypes,
[spec.typeName]: spec.paths.reduce<TextSearchDocumentTypeConfiguration>(
(nextType, {path, weight}) => {
return {
...nextType,
weights: {
...nextType.weights,
[path]: weight,
},
}
},
{},
),
}
}, {})
}
export function getOrder(sort: SearchSort[] = []): TextSearchOrder[] {
return sort.map<TextSearchOrder>(
({field, direction}) => ({
attribute: field,
direction,
}),
{},
)
}
/**
* @internal
*/
export const createTextSearch: SearchStrategyFactory<TextSearchResults> = (
typesFromFactory,
client,
factoryOptions,
) => {
// Search currently supports both strings (reference + cross dataset reference inputs)
// or a SearchTerms object (omnisearch).
return function search(searchParams, searchOptions = {}) {
const searchTerms = normalizeSearchTerms(searchParams, typesFromFactory)
// Construct search filters used in this GROQ query
const filters = [
'_type in $__types',
searchOptions.includeDrafts === false && "!(_id in path('drafts.**'))",
factoryOptions.filter ? `(${factoryOptions.filter})` : false,
searchTerms.filter ? `(${searchTerms.filter})` : false,
].filter((baseFilter): baseFilter is string => Boolean(baseFilter))
const textSearchParams: TextSearchParams = {
query: {string: searchTerms.query},
filter: filters.join(' && '),
params: {
__types: searchTerms.types.map((type) => ('name' in type ? type.name : type.type)),
...factoryOptions.params,
...searchTerms.params,
},
types: getDocumentTypeConfiguration(searchOptions, searchTerms),
order: getOrder(searchOptions.sort),
includeAttributes: ['_id', '_type'],
fromCursor: searchOptions.cursor,
limit: searchOptions.limit ?? DEFAULT_LIMIT,
}
return client.observable
.request<TextSearchResponse<SanityDocumentLike>>({
uri: `/data/textsearch/${client.config().dataset}`,
method: 'POST',
json: true,
body: textSearchParams,
tag: factoryOptions.tag,
})
.pipe(
map((response) => {
let documents = response.hits.map((hit) => hit.attributes)
if (factoryOptions.unique) {
documents = removeDupes(documents)
}
return {
type: 'text',
hits: documents.map((hit) => ({hit})),
nextCursor: response.nextCursor,
}
}),
)
}
}