From c867ae1231b9cd0c54cc90a7e9647a16123474ce Mon Sep 17 00:00:00 2001 From: Roman Kalyakin Date: Fri, 8 Nov 2024 14:38:58 +0100 Subject: [PATCH] search facets --- src/hooks/transformation.ts | 4 +- src/models/generated/schemasPublic.d.ts | 24 +++-- .../schemasPublic/SearchFacetBucket.json | 20 ++--- .../search-facets/search-facets.hooks.ts | 5 +- .../search-facets/search-facets.schema.ts | 10 ++- .../search-facets/search-facets.service.ts | 27 +++--- src/transformers/contentItem.ts | 6 +- src/transformers/searchFacet.ts | 89 +++++++++++++++++++ 8 files changed, 141 insertions(+), 44 deletions(-) create mode 100644 src/transformers/searchFacet.ts diff --git a/src/hooks/transformation.ts b/src/hooks/transformation.ts index 99423455..73625aa3 100644 --- a/src/hooks/transformation.ts +++ b/src/hooks/transformation.ts @@ -2,7 +2,7 @@ import { HookContext, HookFunction } from '@feathersjs/feathers' import { ImpressoApplication } from '../types' export const transformResponse = ( - transformer: (item: I) => O, + transformer: ((item: I) => O) | ((item: I, context: HookContext) => O), condition?: (context: HookContext) => boolean ): HookFunction => { return context => { @@ -11,7 +11,7 @@ export const transformResponse = ( if (context.result != null) { const ctx = context as any - ctx.result = transformer(context.result as I) + ctx.result = transformer(context.result as I, context) } return context } diff --git a/src/models/generated/schemasPublic.d.ts b/src/models/generated/schemasPublic.d.ts index b7e32cfb..eab7586e 100644 --- a/src/models/generated/schemasPublic.d.ts +++ b/src/models/generated/schemasPublic.d.ts @@ -243,19 +243,17 @@ export interface SearchFacetBucket { */ count: number; /** - * Value of the 'type' element + * Value that represents the bucket. */ - val: string; + value: string | number; /** - * UID of the 'type' element. Same as 'val' + * Unique ID of the value, if relevant and different from the value itself. */ uid?: string; /** - * The item in the bucket. Particular objct schema depends on the facet type + * Label of the value, if relevant. */ - item?: { - [k: string]: unknown; - }; + label?: string; } /** * Facet bucket @@ -289,19 +287,17 @@ export interface SearchFacetBucket { */ count: number; /** - * Value of the 'type' element + * Value that represents the bucket. */ - val: string; + value: string | number; /** - * UID of the 'type' element. Same as 'val' + * Unique ID of the value, if relevant and different from the value itself. */ uid?: string; /** - * The item in the bucket. Particular objct schema depends on the facet type + * Label of the value, if relevant. */ - item?: { - [k: string]: unknown; - }; + label?: string; } diff --git a/src/schema/schemasPublic/SearchFacetBucket.json b/src/schema/schemasPublic/SearchFacetBucket.json index e16fc476..f64d3fb2 100644 --- a/src/schema/schemasPublic/SearchFacetBucket.json +++ b/src/schema/schemasPublic/SearchFacetBucket.json @@ -4,22 +4,20 @@ "title": "Search Facet Bucket", "description": "Facet bucket", "additionalProperties": false, - "required": ["count", "val"], "properties": { "count": { "type": "integer", - "description": "Number of items in the bucket" + "description": "Number of items in the bucket", + "minimum": 0 }, - "val": { - "type": "string", - "description": "Value of the 'type' element" + "value": { + "anyOf": [{ "type": "string" }, { "type": "number" }, { "type": "integer" }], + "description": "Value that represents the bucket." }, - "uid": { + "label": { "type": "string", - "description": "UID of the 'type' element. Same as 'val'" - }, - "item": { - "description": "The item in the bucket. Particular objct schema depends on the facet type" + "description": "Label of the value, if relevant." } - } + }, + "required": ["count", "value"] } diff --git a/src/services/search-facets/search-facets.hooks.ts b/src/services/search-facets/search-facets.hooks.ts index 59ee9e6e..17b4d29d 100644 --- a/src/services/search-facets/search-facets.hooks.ts +++ b/src/services/search-facets/search-facets.hooks.ts @@ -9,6 +9,9 @@ import { eachFilterValidator, paramsValidator } from '../search/search.validator import { getIndexMeta } from './search-facets.class' import { IndexId, OrderByChoices, facetTypes } from './search-facets.schema' import { parseFilters } from '../../util/queryParameters' +import { transformResponse } from '../../hooks/transformation' +import { inPublicApi } from '../../hooks/redaction' +import { transformSearchFacet } from '../../transformers/searchFacet' const getAndFindHooks = (index: IndexId) => [ validate({ @@ -116,6 +119,6 @@ export const getHooks = (index: IndexId) => ({ after: { find: [resolveCollections(), resolveTextReuseClusters()], - get: [resolveCollections(), resolveTextReuseClusters()], + get: [resolveCollections(), resolveTextReuseClusters(), transformResponse(transformSearchFacet, inPublicApi)], }, }) diff --git a/src/services/search-facets/search-facets.schema.ts b/src/services/search-facets/search-facets.schema.ts index 138b5399..853f8ecc 100644 --- a/src/services/search-facets/search-facets.schema.ts +++ b/src/services/search-facets/search-facets.schema.ts @@ -104,8 +104,11 @@ const toPascalCase = (s: string) => { export const getDocs = (index: IndexId): ServiceSwaggerOptions => ({ description: `${facetNames[index]} facets`, - securities: ['get', 'find'], + securities: ['get' /*, 'find' */], operations: { + // RK: I disabled the find operation because it can be entirely replaced by individual get operations, + // not used in impresso-py and adds extra maintenance burden. + /* find: { operationId: `find${toPascalCase(index)}Facets`, description: `Get mutliple ${facetNames[index]} facets`, @@ -119,6 +122,7 @@ export const getDocs = (index: IndexId): ServiceSwaggerOptions => ({ schema: 'SearchFacet', }), }, + */ get: { operationId: `get${toPascalCase(index)}Facet`, description: `Get a single ${facetNames[index]} facet`, @@ -137,8 +141,8 @@ export const getDocs = (index: IndexId): ServiceSwaggerOptions => ({ ...getStandardParameters({ method: 'find' }), ], responses: getStandardResponses({ - method: 'get', - schema: 'SearchFacet', + method: 'find', + schema: 'SearchFacetBucket', }), }, }, diff --git a/src/services/search-facets/search-facets.service.ts b/src/services/search-facets/search-facets.service.ts index b0ca7de8..bf795b52 100644 --- a/src/services/search-facets/search-facets.service.ts +++ b/src/services/search-facets/search-facets.service.ts @@ -10,20 +10,23 @@ const SupportedIndexes: IndexId[] = Object.keys(SolrMappings).map(key => key.rep export default (app: ImpressoApplication) => { // Initialize our service with any options it requires + const isPublicApi = app.get('isPublicApi') SupportedIndexes.forEach(index => { - app.use( - `search-facets/${index}`, - new Service({ - app, - index, - name: `search-facets-${index}`, - }), - { - events: [], - docs: createSwaggerServiceOptions({ schemas: {}, docs: getDocs(index) }), - } as ServiceOptions - ) + const svc = new Service({ + app, + index, + name: `search-facets-${index}`, + }) + // not exposing find method in public API + if (isPublicApi) { + ;(svc as any).find = undefined + } + + app.use(`search-facets/${index}`, svc, { + events: [], + docs: createSwaggerServiceOptions({ schemas: {}, docs: getDocs(index) }), + } as ServiceOptions) // Get our initialized service so that we can register hooks const service = app.service(`search-facets/${index}`) service.hooks(getHooks(index)) diff --git a/src/transformers/contentItem.ts b/src/transformers/contentItem.ts index 26eeddd4..7498f1f9 100644 --- a/src/transformers/contentItem.ts +++ b/src/transformers/contentItem.ts @@ -27,7 +27,11 @@ export const transformContentItem = (input: ContentItemPrivate): ContentItemPubl transcript: input.content ?? '', locations: input.locations?.map(toEntityMention) ?? [], persons: input.persons?.map(toEntityMention) ?? [], - topics: input.topics?.map(toTopicMention)?.filter(v => v != null) ?? [], + topics: + input.topics + ?.map(toTopicMention) + ?.filter(v => v != null) + .map(v => v as TopicMention) ?? [], transcriptLength: input.size ?? 0, totalPages: input.nbPages, languageCode: input.language?.toLowerCase(), diff --git a/src/transformers/searchFacet.ts b/src/transformers/searchFacet.ts new file mode 100644 index 00000000..94838a1e --- /dev/null +++ b/src/transformers/searchFacet.ts @@ -0,0 +1,89 @@ +import { HookContext } from '@feathersjs/feathers' +import { ImpressoApplication } from '../types' + +import { + SearchFacet, + BaseFind, + SearchFacetBucket as SearchFacetBucketInternal, + SearchFacetRangeBucket, +} from '../models/generated/schemas' +import { SearchFacetBucket } from '../models/generated/schemasPublic' +import Collection from '../models/collections.model' +import Newspaper from '../models/newspapers.model' +import Entity from '../models/entities.model' +import Topic from '../models/topics.model' + +interface FacetContainer extends BaseFind { + data: SearchFacetBucket[] +} + +const transformBucket = ( + input: SearchFacetBucketInternal | SearchFacetRangeBucket, + facetType: string +): SearchFacetBucket => { + switch (facetType) { + case 'contentLength': + case 'month': + case 'textReuseClusterSize': + case 'textReuseClusterLexicalOverlap': + case 'textReuseClusterDayDelta': + return { + count: input.count, + value: typeof input.val === 'string' ? parseInt(input.val) : input.val, + } + case 'country': + case 'type': + case 'language': + case 'accessRight': + return { + count: input.count, + value: String(input.val), + } + case 'topic': + const topicItem = (input as any)?.item as Topic + return { + count: input.count, + value: String(input.val), + label: topicItem.words.map(({ w, p }) => `${w} (${p})`).join(', '), + } + case 'collection': + const collectionItem = (input as any)?.item as Collection + return { + count: input.count, + value: String(input.val), + label: collectionItem != null ? collectionItem.name : undefined, + } + case 'newspaper': + const newspaperItem = (input as any)?.item as Newspaper + return { + count: input.count, + value: String(input.val), + label: newspaperItem.name, + } + case 'person': + case 'location': + const entityItem = (input as any)?.item as Entity + return { + count: input.count, + value: String(input.val), + label: entityItem.name, + } + default: + return { + count: input.count, + value: input.val, + } + } +} + +export const transformSearchFacet = (input: SearchFacet, context: HookContext): FacetContainer => { + console.log('III', input, context.id) + + return { + data: input.buckets.map(b => transformBucket(b, context.id as string)), + total: input.numBuckets, + limit: context.params?.query?.limit ?? input.buckets.length, + offset: context.params?.query?.offset ?? 0, + info: {}, + } +}