Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WEBDEV-6174 Add models for collection page extra info #37

Merged
merged 5 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export {
export { SearchResponse } from './src/responses/search-response';
export { SearchResponseHeader } from './src/responses/search-response-header';
export { SearchResponseParams } from './src/responses/search-response-params';
export {
CollectionExtraInfo,
CollectionSearchDocs,
RelatedCollection,
UserDetails,
} from './src/responses/collection-extra-info';

export { MetadataSearchBackend } from './src/search-backend/metadata-search-backend';
export { FulltextSearchBackend } from './src/search-backend/fulltext-search-backend';
Expand Down
13 changes: 8 additions & 5 deletions src/models/hit-types/hit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,19 @@ import type { TextHit } from './text-hit';
export type HitType = 'item' | 'text';

/**
* Applying this to the Result type forces Intellisense to present Result as
* a type in its own right, and not as the underlying merge type it aliases.
* Really just to keep things clean at the call site.
* Additional information provided by the PPS about hits, separately from
* their fields map.
*/
interface PreserveAlias {} // eslint-disable-line @typescript-eslint/no-empty-interface
interface HitInfo {
hit_type?: HitType;
index?: string;
service_backend?: string;
}
latonv marked this conversation as resolved.
Show resolved Hide resolved

/**
* Result is an expansive type definition encompassing all the optional
* and required properties that may occur on any type of search result
* ('hit') returned by the various search backends. (Most metadata
* properties are optional anyway).
*/
export type SearchResult = Partial<ItemHit & TextHit> & PreserveAlias;
export type SearchResult = Partial<ItemHit & TextHit> & HitInfo;
55 changes: 55 additions & 0 deletions src/responses/collection-extra-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Metadata } from '../models/metadata';

/**
* Extra info about the target collection that is returned for
* the `collection_details` page type.
*/
export interface CollectionExtraInfo {
thumbnail_url?: string;
search_doc_fields?: CollectionSearchDocs;
has_items_with_searchable_text?: boolean;
forum_identifier?: string;
forum_count?: number;
review_count?: number;
related_collection_details?: RelatedCollection[];
uploader_details?: UserDetails;
contributors_details?: UserDetails[];
public_metadata?: typeof Metadata.prototype.rawMetadata;
}

/**
* Fields from the search docs returned for the `collection_details`
* page type.
*/
export interface CollectionSearchDocs {
item_count?: number;
files_count?: number;
collection_size?: number;
collection_files_count?: number;
month?: number;
week?: number;
downloads?: number;
num_favorites?: number;
title_message?: string | null;
primary_collection?: string | null;
}

/**
* Info about a related collection, as returned for the `collection_details`
* page type.
*/
export interface RelatedCollection {
identifier: string;
title?: string;
item_count?: number;
}

/**
* Info about a user (e.g., uploaders/contributors), as returned for
* the `collection_details` page type.
*/
export interface UserDetails {
screen_name?: string;
useritem?: string;
is_archivist?: boolean;
}
23 changes: 20 additions & 3 deletions src/responses/search-request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SearchRequestParams } from './search-request-params';

export type RequestKind = 'hits' | 'aggregations' | 'noop';

export interface BackendRequest {
/** The finalized client parameters used to query the backend service */
finalized_parameters: SearchRequestParams;
/** The backend request kind (which may differ from the overall request kind) */
kind: RequestKind;
/** Name of which other backend request spawned this one, if any */
parent?: string;
}

/**
* A model for the request parameters returned with each search response.
*/
Expand All @@ -11,12 +22,18 @@ export class SearchRequest {
clientParameters: SearchRequestParams;

/**
* The finalized request parameters as determined by the backend
* Details about the actual backend requests made
*/
backendRequests: Record<string, BackendRequest>;

/**
* The overall 'kind' of request being made (e.g., for hits vs. aggregations)
*/
finalizedParameters: SearchRequestParams;
kind: RequestKind;

constructor(json: Record<string, any>) {
this.clientParameters = json.client_parameters;
this.finalizedParameters = json.finalized_parameters;
this.backendRequests = json.backend_requests;
this.kind = json.kind;
}
}
20 changes: 16 additions & 4 deletions src/responses/search-response-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Aggregation } from '../models/aggregation';
import { SearchResult, HitType } from '../models/hit-types/hit';
import { ItemHit } from '../models/hit-types/item-hit';
import { TextHit } from '../models/hit-types/text-hit';
import { CollectionExtraInfo } from './collection-extra-info';
import type { SearchHitSchema } from './search-hit-schema';

/**
Expand All @@ -12,6 +13,7 @@ export interface SearchResponseBody {
hits: SearchResponseHits;
aggregations?: Record<string, Aggregation>;
collection_titles?: Record<string, string>;
collection_extra_info?: CollectionExtraInfo;
}

/**
Expand Down Expand Up @@ -59,20 +61,26 @@ export class SearchResponseDetails {
*/
collectionTitles?: Record<string, string>;

/**
* Extra info about the target collection, returned when the page type is
* `collection_details`.
*/
collectionExtraInfo?: CollectionExtraInfo;

/**
* The hit schema for this response
*/
schema?: SearchHitSchema;

constructor(body: SearchResponseBody, schema: SearchHitSchema) {
this.schema = schema;
const hitType = schema?.hit_type;
const schemaHitType = schema?.hit_type;

this.totalResults = body?.hits?.total ?? 0;
this.returnedCount = body?.hits?.returned ?? 0;
this.results =
body?.hits?.hits?.map((hit: SearchResult) =>
SearchResponseDetails.createResult(hitType, hit)
SearchResponseDetails.createResult(hit.hit_type ?? schemaHitType, hit)
) ?? [];

// Construct Aggregation objects
Expand All @@ -89,6 +97,10 @@ export class SearchResponseDetails {
if (body?.collection_titles) {
this.collectionTitles = body.collection_titles;
}

if (body?.collection_extra_info) {
this.collectionExtraInfo = body.collection_extra_info;
}
}

/**
Expand All @@ -104,8 +116,8 @@ export class SearchResponseDetails {
case 'text':
return new TextHit(result);
default:
// The hit type doesn't tell us what to construct, so just return the input as-is
return result;
// The hit type doesn't tell us what to construct, so just construct an ItemHit
return new ItemHit(result);
}
}
}
80 changes: 44 additions & 36 deletions test/mock-response-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,28 @@ export class MockResponseGenerator {
hits_per_page: params.rows ?? 0,
fields: params.fields ?? [],
},
finalized_parameters: {
client: 'page_production_service_endpoint',
user_query: params.query,
service_backend: 'metadata',
page: params.page ?? 0,
page_type: 'search_results',
hits_per_page: 50,
fields: ['_tile_'].concat(params.fields ?? []),
sort: ['_score'],
aggregations: [
'mediatype',
'year',
'subject',
'collection',
'creator',
'language',
],
aggregations_size: 6,
backend_requests: {
primary: {
finalized_parameters: {
client: 'page_production_service_endpoint',
user_query: params.query,
service_backend: 'metadata',
page: params.page ?? 0,
page_type: 'search_results',
hits_per_page: 50,
fields: ['_tile_'].concat(params.fields ?? []),
sort: ['_score'],
aggregations: [
'mediatype',
'year',
'subject',
'collection',
'creator',
'language',
],
aggregations_size: 6,
},
},
},
},
responseHeader: {
Expand Down Expand Up @@ -76,24 +80,28 @@ export class MockResponseGenerator {
hits_per_page: params.rows ?? 0,
fields: params.fields ?? [],
},
finalized_parameters: {
client: 'page_production_service_endpoint',
user_query: params.query,
service_backend: 'fts',
page: params.page ?? 0,
page_type: 'search_results',
hits_per_page: 50,
fields: ['_tile_'].concat(params.fields ?? []),
sort: ['_score'],
aggregations: [
'mediatype',
'year',
'subject',
'collection',
'creator',
'language',
],
aggregations_size: 6,
backend_requests: {
primary: {
finalized_parameters: {
client: 'page_production_service_endpoint',
user_query: params.query,
service_backend: 'fts',
page: params.page ?? 0,
page_type: 'search_results',
hits_per_page: 50,
fields: ['_tile_'].concat(params.fields ?? []),
sort: ['_score'],
aggregations: [
'mediatype',
'year',
'subject',
'collection',
'creator',
'language',
],
aggregations_size: 6,
},
},
},
},
responseHeader: {
Expand Down
78 changes: 74 additions & 4 deletions test/responses/search-response-details.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const responseBody: SearchResponseBody = {
collection_titles: {
baz: 'Baz Collection',
},
collection_extra_info: {
thumbnail_url: 'foo',
latonv marked this conversation as resolved.
Show resolved Hide resolved
},
};

describe('SearchResponseDetails', () => {
Expand Down Expand Up @@ -63,6 +66,46 @@ describe('SearchResponseDetails', () => {
expect(details.results[1].collection?.value).to.equal('baz');
});

it('prefers hit-type specified on hit itself over schema hit-type', () => {
const responseBodyWithTextHit = { ...responseBody } as SearchResponseBody;
responseBodyWithTextHit.hits = {
total: 2,
returned: 2,
hits: [
{
hit_type: 'text',
fields: {
identifier: 'foo',
mediatype: 'texts',
},
},
{
fields: {
identifier: 'bar',
collection: ['baz'],
},
},
],
};

const responseSchema = {
hit_type: 'item' as HitType,
field_properties: {},
};

const details = new SearchResponseDetails(
responseBodyWithTextHit,
responseSchema
);
expect(details.results[0]).to.be.instanceOf(TextHit); // From hit, not schema
expect(details.results[0].identifier).to.equal('foo');
expect(details.results[0].mediatype?.value).to.equal('texts');
expect(details.results[0].creator?.value).to.be.undefined;
expect(details.results[1]).to.be.instanceOf(ItemHit); // From schema
expect(details.results[1].identifier).to.equal('bar');
expect(details.results[1].collection?.value).to.equal('baz');
});

it('includes aggregations', () => {
const aggsResponseBody: SearchResponseBody = {
hits: {
Expand Down Expand Up @@ -113,10 +156,7 @@ describe('SearchResponseDetails', () => {
});

it('collection titles map is optional', () => {
const responseBodyWithoutTitles = Object.assign(
{},
responseBody
) as SearchResponseBody;
const responseBodyWithoutTitles = { ...responseBody } as SearchResponseBody;
delete responseBodyWithoutTitles.collection_titles;

const responseSchema = {
Expand All @@ -131,4 +171,34 @@ describe('SearchResponseDetails', () => {
expect(details.results.length).to.equal(2);
expect(details.collectionTitles).to.be.undefined;
});

it('provides access to collection extra info', () => {
const responseSchema = {
hit_type: 'item' as HitType,
field_properties: {},
};

const details = new SearchResponseDetails(responseBody, responseSchema);
expect(details.results.length).to.equal(2);
expect(details.collectionExtraInfo).to.deep.equal({ thumbnail_url: 'foo' });
});

it('collection extra info is optional', () => {
const responseBodyWithoutExtraInfo = {
...responseBody,
} as SearchResponseBody;
delete responseBodyWithoutExtraInfo.collection_extra_info;

const responseSchema = {
hit_type: 'item' as HitType,
field_properties: {},
};

const details = new SearchResponseDetails(
responseBodyWithoutExtraInfo,
responseSchema
);
expect(details.results.length).to.equal(2);
expect(details.collectionExtraInfo).to.be.undefined;
});
latonv marked this conversation as resolved.
Show resolved Hide resolved
});
Loading