@internetarchive/collection-browser 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/app-root.js +1 -1
- package/dist/src/app-root.js.map +1 -1
- package/dist/src/collection-browser.d.ts +40 -3
- package/dist/src/collection-browser.js +214 -58
- package/dist/src/collection-browser.js.map +1 -1
- package/dist/src/collection-facets/more-facets-content.d.ts +3 -2
- package/dist/src/collection-facets/more-facets-content.js +6 -2
- package/dist/src/collection-facets/more-facets-content.js.map +1 -1
- package/dist/src/collection-facets.d.ts +3 -2
- package/dist/src/collection-facets.js +6 -2
- package/dist/src/collection-facets.js.map +1 -1
- package/dist/src/models.d.ts +9 -0
- package/dist/src/models.js +8 -0
- package/dist/src/models.js.map +1 -1
- package/dist/src/restoration-state-handler.d.ts +0 -1
- package/dist/src/restoration-state-handler.js +4 -4
- package/dist/src/restoration-state-handler.js.map +1 -1
- package/dist/src/sort-filter-bar/alpha-bar.d.ts +3 -0
- package/dist/src/sort-filter-bar/alpha-bar.js +32 -13
- package/dist/src/sort-filter-bar/alpha-bar.js.map +1 -1
- package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +2 -1
- package/dist/src/sort-filter-bar/sort-filter-bar.js +7 -0
- package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
- package/dist/src/tiles/image-block.js +0 -1
- package/dist/src/tiles/image-block.js.map +1 -1
- package/dist/test/collection-browser.test.js +81 -9
- package/dist/test/collection-browser.test.js.map +1 -1
- package/dist/test/collection-facets/more-facets-content.test.js +2 -2
- package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
- package/dist/test/mocks/mock-search-responses.d.ts +2 -0
- package/dist/test/mocks/mock-search-responses.js +70 -0
- package/dist/test/mocks/mock-search-responses.js.map +1 -1
- package/dist/test/mocks/mock-search-service.js +5 -1
- package/dist/test/mocks/mock-search-service.js.map +1 -1
- package/dist/test/restoration-state-handler.test.js +0 -1
- package/dist/test/restoration-state-handler.test.js.map +1 -1
- package/dist/test/sort-filter-bar/alpha-bar.test.d.ts +1 -0
- package/dist/test/sort-filter-bar/alpha-bar.test.js +44 -0
- package/dist/test/sort-filter-bar/alpha-bar.test.js.map +1 -0
- package/dist/test/sort-filter-bar/sort-filter-bar.test.js +32 -0
- package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
- package/package.json +3 -3
- package/src/app-root.ts +1 -1
- package/src/collection-browser.ts +273 -57
- package/src/collection-facets/more-facets-content.ts +6 -2
- package/src/collection-facets.ts +6 -2
- package/src/models.ts +15 -0
- package/src/restoration-state-handler.ts +7 -5
- package/src/sort-filter-bar/alpha-bar.ts +26 -9
- package/src/sort-filter-bar/sort-filter-bar.ts +9 -0
- package/src/tiles/image-block.ts +0 -1
- package/test/collection-browser.test.ts +90 -10
- package/test/collection-facets/more-facets-content.test.ts +2 -2
- package/test/mocks/mock-search-responses.ts +78 -0
- package/test/mocks/mock-search-service.ts +6 -0
- package/test/restoration-state-handler.test.ts +0 -3
- package/test/sort-filter-bar/alpha-bar.test.ts +52 -0
- package/test/sort-filter-bar/sort-filter-bar.test.ts +44 -0
|
@@ -17,6 +17,10 @@ import type {
|
|
|
17
17
|
} from '@internetarchive/infinite-scroller';
|
|
18
18
|
import {
|
|
19
19
|
Aggregation,
|
|
20
|
+
Bucket,
|
|
21
|
+
FilterConstraint,
|
|
22
|
+
FilterMap,
|
|
23
|
+
FilterMapBuilder,
|
|
20
24
|
SearchParams,
|
|
21
25
|
SearchResult,
|
|
22
26
|
SearchServiceInterface,
|
|
@@ -47,6 +51,9 @@ import {
|
|
|
47
51
|
CollectionDisplayMode,
|
|
48
52
|
FacetOption,
|
|
49
53
|
FacetBucket,
|
|
54
|
+
PrefixFilterType,
|
|
55
|
+
PrefixFilterCounts,
|
|
56
|
+
prefixFilterAggregationKeys,
|
|
50
57
|
} from './models';
|
|
51
58
|
import {
|
|
52
59
|
RestorationStateHandlerInterface,
|
|
@@ -92,8 +99,6 @@ export class CollectionBrowser
|
|
|
92
99
|
|
|
93
100
|
@property({ type: String }) sortDirection: SortDirection | null = null;
|
|
94
101
|
|
|
95
|
-
@property({ type: String }) dateRangeQueryClause?: string;
|
|
96
|
-
|
|
97
102
|
@property({ type: Number }) pageSize = 50;
|
|
98
103
|
|
|
99
104
|
@property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
|
|
@@ -177,6 +182,10 @@ export class CollectionBrowser
|
|
|
177
182
|
|
|
178
183
|
@state() private placeholderType: PlaceholderType = null;
|
|
179
184
|
|
|
185
|
+
@state() private prefixFilterCountMap: Partial<
|
|
186
|
+
Record<PrefixFilterType, PrefixFilterCounts>
|
|
187
|
+
> = {};
|
|
188
|
+
|
|
180
189
|
@query('#content-container') private contentContainer!: HTMLDivElement;
|
|
181
190
|
|
|
182
191
|
private languageCodeHandler = new LanguageCodeHandler();
|
|
@@ -323,6 +332,10 @@ export class CollectionBrowser
|
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
private get collectionBrowserTemplate() {
|
|
335
|
+
const shouldShowSearching =
|
|
336
|
+
this.searchResultsLoading || this.totalResults === undefined;
|
|
337
|
+
const resultsCount = this.totalResults?.toLocaleString();
|
|
338
|
+
const resultsLabel = this.totalResults === 1 ? 'Result' : 'Results';
|
|
326
339
|
return html`<div
|
|
327
340
|
id="left-column"
|
|
328
341
|
class="column${this.isResizeToMobile ? ' preload' : ''}"
|
|
@@ -331,12 +344,10 @@ export class CollectionBrowser
|
|
|
331
344
|
${this.mobileView ? this.mobileFacetsTemplate : nothing}
|
|
332
345
|
<div id="results-total">
|
|
333
346
|
<span id="big-results-count">
|
|
334
|
-
${
|
|
335
|
-
? this.totalResults.toLocaleString()
|
|
336
|
-
: '-'}
|
|
347
|
+
${shouldShowSearching ? html`Searching…` : resultsCount}
|
|
337
348
|
</span>
|
|
338
349
|
<span id="big-results-label">
|
|
339
|
-
${
|
|
350
|
+
${shouldShowSearching ? nothing : resultsLabel}
|
|
340
351
|
</span>
|
|
341
352
|
</div>
|
|
342
353
|
</div>
|
|
@@ -376,6 +387,7 @@ export class CollectionBrowser
|
|
|
376
387
|
.displayMode=${this.displayMode}
|
|
377
388
|
.selectedTitleFilter=${this.selectedTitleFilter}
|
|
378
389
|
.selectedCreatorFilter=${this.selectedCreatorFilter}
|
|
390
|
+
.prefixFilterCountMap=${this.prefixFilterCountMap}
|
|
379
391
|
.resizeObserver=${this.resizeObserver}
|
|
380
392
|
@sortChanged=${this.userChangedSort}
|
|
381
393
|
@displayModeChanged=${this.displayModeChanged}
|
|
@@ -423,6 +435,9 @@ export class CollectionBrowser
|
|
|
423
435
|
|
|
424
436
|
if (!sortField) return;
|
|
425
437
|
this.sortParam = { field: sortField, direction: this.sortDirection };
|
|
438
|
+
|
|
439
|
+
// Lazy-load the alphabet counts for title/creator sort bar as needed
|
|
440
|
+
this.updatePrefixFiltersForCurrentSort();
|
|
426
441
|
}
|
|
427
442
|
|
|
428
443
|
private displayModeChanged(
|
|
@@ -541,7 +556,8 @@ export class CollectionBrowser
|
|
|
541
556
|
.collectionNameCache=${this.collectionNameCache}
|
|
542
557
|
.languageCodeHandler=${this.languageCodeHandler}
|
|
543
558
|
.showHistogramDatePicker=${this.showHistogramDatePicker}
|
|
544
|
-
.
|
|
559
|
+
.query=${this.filteredQuery}
|
|
560
|
+
.filterMap=${this.filterMap}
|
|
545
561
|
.modalManager=${this.modalManager}
|
|
546
562
|
?collapsableFacets=${this.mobileView}
|
|
547
563
|
?facetsLoading=${this.facetDataLoading}
|
|
@@ -576,21 +592,6 @@ export class CollectionBrowser
|
|
|
576
592
|
`;
|
|
577
593
|
}
|
|
578
594
|
|
|
579
|
-
private get queryDebuggingTemplate() {
|
|
580
|
-
return html`
|
|
581
|
-
<div>
|
|
582
|
-
<ul>
|
|
583
|
-
<li>Base Query: ${this.baseQuery}</li>
|
|
584
|
-
<li>Facet Query: ${this.facetQuery}</li>
|
|
585
|
-
<li>Sort Filter Query: ${this.sortFilterQueries}</li>
|
|
586
|
-
<li>Date Range Query: ${this.dateRangeQueryClause}</li>
|
|
587
|
-
<li>Sort: ${this.sortParam?.field} ${this.sortParam?.direction}</li>
|
|
588
|
-
<li>Full Query: ${this.fullQuery}</li>
|
|
589
|
-
</ul>
|
|
590
|
-
</div>
|
|
591
|
-
`;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
595
|
private histogramDateRangeUpdated(
|
|
595
596
|
e: CustomEvent<{
|
|
596
597
|
minDate: string;
|
|
@@ -598,17 +599,21 @@ export class CollectionBrowser
|
|
|
598
599
|
}>
|
|
599
600
|
) {
|
|
600
601
|
const { minDate, maxDate } = e.detail;
|
|
601
|
-
|
|
602
602
|
[this.minSelectedDate, this.maxSelectedDate] = [minDate, maxDate];
|
|
603
|
-
this.dateRangeQueryClause = `year:[${minDate} TO ${maxDate}]`;
|
|
604
603
|
|
|
605
|
-
|
|
606
|
-
this.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
604
|
+
this.analyticsHandler?.sendEvent({
|
|
605
|
+
category: this.searchContext,
|
|
606
|
+
action: analyticsActions.histogramChanged,
|
|
607
|
+
label: this.dateRangeQueryClause,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
private get dateRangeQueryClause() {
|
|
612
|
+
if (!this.minSelectedDate || !this.maxSelectedDate) {
|
|
613
|
+
return undefined;
|
|
611
614
|
}
|
|
615
|
+
|
|
616
|
+
return `year:[${this.minSelectedDate} TO ${this.maxSelectedDate}]`;
|
|
612
617
|
}
|
|
613
618
|
|
|
614
619
|
firstUpdated(): void {
|
|
@@ -638,13 +643,22 @@ export class CollectionBrowser
|
|
|
638
643
|
changed.has('baseQuery') ||
|
|
639
644
|
changed.has('titleQuery') ||
|
|
640
645
|
changed.has('creatorQuery') ||
|
|
641
|
-
changed.has('
|
|
646
|
+
changed.has('minSelectedDate') ||
|
|
647
|
+
changed.has('maxSelectedDate') ||
|
|
642
648
|
changed.has('sortParam') ||
|
|
643
649
|
changed.has('selectedFacets') ||
|
|
644
650
|
changed.has('searchService')
|
|
645
651
|
) {
|
|
646
652
|
this.handleQueryChange();
|
|
647
653
|
}
|
|
654
|
+
if (
|
|
655
|
+
changed.has('baseQuery') ||
|
|
656
|
+
changed.has('minSelectedDate') ||
|
|
657
|
+
changed.has('maxSelectedDate') ||
|
|
658
|
+
changed.has('selectedFacets')
|
|
659
|
+
) {
|
|
660
|
+
this.refreshLetterCounts();
|
|
661
|
+
}
|
|
648
662
|
if (changed.has('selectedSort') || changed.has('sortDirection')) {
|
|
649
663
|
const prevSortDirection = changed.get('sortDirection') as SortDirection;
|
|
650
664
|
this.sendSortByAnalytics(prevSortDirection);
|
|
@@ -775,6 +789,9 @@ export class CollectionBrowser
|
|
|
775
789
|
this.previousQueryKey = this.pageFetchQueryKey;
|
|
776
790
|
|
|
777
791
|
this.dataSource = {};
|
|
792
|
+
this.totalResults = undefined;
|
|
793
|
+
this.aggregations = undefined;
|
|
794
|
+
this.fullYearsHistogramAggregation = undefined;
|
|
778
795
|
this.pageFetchesInProgress = {};
|
|
779
796
|
this.endOfDataReached = false;
|
|
780
797
|
this.pagesToRender = this.initialPageNumber;
|
|
@@ -830,7 +847,6 @@ export class CollectionBrowser
|
|
|
830
847
|
this.baseQuery = restorationState.baseQuery;
|
|
831
848
|
this.titleQuery = restorationState.titleQuery;
|
|
832
849
|
this.creatorQuery = restorationState.creatorQuery;
|
|
833
|
-
this.dateRangeQueryClause = restorationState.dateRangeQueryClause;
|
|
834
850
|
this.sortParam = restorationState.sortParam ?? null;
|
|
835
851
|
this.currentPage = restorationState.currentPage ?? 1;
|
|
836
852
|
this.minSelectedDate = restorationState.minSelectedDate;
|
|
@@ -850,7 +866,6 @@ export class CollectionBrowser
|
|
|
850
866
|
selectedFacets: this.selectedFacets ?? defaultSelectedFacets,
|
|
851
867
|
baseQuery: this.baseQuery,
|
|
852
868
|
currentPage: this.currentPage,
|
|
853
|
-
dateRangeQueryClause: this.dateRangeQueryClause,
|
|
854
869
|
titleQuery: this.titleQuery,
|
|
855
870
|
creatorQuery: this.creatorQuery,
|
|
856
871
|
minSelectedDate: this.minSelectedDate,
|
|
@@ -867,6 +882,84 @@ export class CollectionBrowser
|
|
|
867
882
|
this.searchResultsLoading = false;
|
|
868
883
|
}
|
|
869
884
|
|
|
885
|
+
private get filterMap(): FilterMap {
|
|
886
|
+
const builder = new FilterMapBuilder();
|
|
887
|
+
|
|
888
|
+
// Add the date range, if applicable
|
|
889
|
+
if (this.minSelectedDate) {
|
|
890
|
+
builder.addFilter(
|
|
891
|
+
'year',
|
|
892
|
+
this.minSelectedDate,
|
|
893
|
+
FilterConstraint.GREATER_OR_EQUAL
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (this.maxSelectedDate) {
|
|
897
|
+
builder.addFilter(
|
|
898
|
+
'year',
|
|
899
|
+
this.maxSelectedDate,
|
|
900
|
+
FilterConstraint.LESS_OR_EQUAL
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Add any selected facets
|
|
905
|
+
if (this.selectedFacets) {
|
|
906
|
+
for (const [facetName, facetValues] of Object.entries(
|
|
907
|
+
this.selectedFacets
|
|
908
|
+
)) {
|
|
909
|
+
const { name, values } = this.prepareFacetForFetch(
|
|
910
|
+
facetName,
|
|
911
|
+
facetValues
|
|
912
|
+
);
|
|
913
|
+
for (const [value, bucket] of Object.entries(values)) {
|
|
914
|
+
let constraint;
|
|
915
|
+
if (bucket.state === 'selected') {
|
|
916
|
+
constraint = FilterConstraint.INCLUDE;
|
|
917
|
+
} else if (bucket.state === 'hidden') {
|
|
918
|
+
constraint = FilterConstraint.EXCLUDE;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (constraint) {
|
|
922
|
+
builder.addFilter(name, value, constraint);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const filterMap = builder.build();
|
|
929
|
+
|
|
930
|
+
// TEMP: At present, the backend search engine incorrectly returns 0 results if
|
|
931
|
+
// the _first_ language filter contains a space, so let's try to avoid that if possible.
|
|
932
|
+
if (filterMap.language) {
|
|
933
|
+
for (const [value, constraint] of Object.entries(filterMap.language)) {
|
|
934
|
+
if (value.includes(' ')) {
|
|
935
|
+
// Delete and re-add this filter to make it the last one on the parent object
|
|
936
|
+
// (Technically this isn't in the standard, but most browser impls output
|
|
937
|
+
// object keys in the order they were added.)
|
|
938
|
+
delete filterMap.language[value];
|
|
939
|
+
filterMap.language[value] = constraint;
|
|
940
|
+
} else {
|
|
941
|
+
// As soon as we reach one without a space, we're done
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return filterMap;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** The base query joined with any title/creator letter filters */
|
|
951
|
+
private get filteredQuery(): string | undefined {
|
|
952
|
+
if (!this.baseQuery) return undefined;
|
|
953
|
+
let filteredQuery = this.baseQuery;
|
|
954
|
+
|
|
955
|
+
const { sortFilterQueries } = this;
|
|
956
|
+
if (sortFilterQueries) {
|
|
957
|
+
filteredQuery += ` AND ${sortFilterQueries}`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return filteredQuery;
|
|
961
|
+
}
|
|
962
|
+
|
|
870
963
|
/** The full query, including year facets and date range clauses */
|
|
871
964
|
private get fullQuery(): string | undefined {
|
|
872
965
|
if (!this.baseQuery) return undefined;
|
|
@@ -886,6 +979,22 @@ export class CollectionBrowser
|
|
|
886
979
|
return fullQuery;
|
|
887
980
|
}
|
|
888
981
|
|
|
982
|
+
/** The full query without any title/creator letter filters */
|
|
983
|
+
private get fullQueryWithoutAlphaFilters(): string | undefined {
|
|
984
|
+
if (!this.baseQuery) return undefined;
|
|
985
|
+
let fullQuery = this.baseQuery;
|
|
986
|
+
|
|
987
|
+
const { facetQuery, dateRangeQueryClause } = this;
|
|
988
|
+
|
|
989
|
+
if (facetQuery) {
|
|
990
|
+
fullQuery += ` AND ${facetQuery}`;
|
|
991
|
+
}
|
|
992
|
+
if (dateRangeQueryClause) {
|
|
993
|
+
fullQuery += ` AND ${dateRangeQueryClause}`;
|
|
994
|
+
}
|
|
995
|
+
return fullQuery;
|
|
996
|
+
}
|
|
997
|
+
|
|
889
998
|
/** The full query without any year facets or date range clauses */
|
|
890
999
|
private get fullQueryWithoutDates(): string | undefined {
|
|
891
1000
|
if (!this.baseQuery) return undefined;
|
|
@@ -951,29 +1060,57 @@ export class CollectionBrowser
|
|
|
951
1060
|
facetName: string,
|
|
952
1061
|
facetValues: Record<string, FacetBucket>
|
|
953
1062
|
): string {
|
|
954
|
-
const
|
|
1063
|
+
const { name: facetQueryName, values } = this.prepareFacetForFetch(
|
|
1064
|
+
facetName,
|
|
1065
|
+
facetValues
|
|
1066
|
+
);
|
|
1067
|
+
const facetEntries = Object.entries(values);
|
|
955
1068
|
if (facetEntries.length === 0) return '';
|
|
956
1069
|
|
|
957
|
-
const facetQueryName =
|
|
958
|
-
facetName === 'lending' ? 'lending___status' : facetName;
|
|
959
1070
|
const facetValuesArray: string[] = [];
|
|
960
|
-
|
|
961
1071
|
for (const [key, facetData] of facetEntries) {
|
|
962
1072
|
const plusMinusPrefix = facetData.state === 'hidden' ? '-' : '';
|
|
1073
|
+
facetValuesArray.push(`${plusMinusPrefix}"${key}"`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const valueQuery = facetValuesArray.join(` OR `);
|
|
1077
|
+
return `${facetQueryName}:(${valueQuery})`;
|
|
1078
|
+
}
|
|
963
1079
|
|
|
964
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* Handles some special pre-request normalization steps for certain facet types
|
|
1082
|
+
* that require them.
|
|
1083
|
+
*
|
|
1084
|
+
* @param facetName The name of the facet type (e.g., 'language')
|
|
1085
|
+
* @param facetValues An array of values for that facet type
|
|
1086
|
+
*/
|
|
1087
|
+
private prepareFacetForFetch(
|
|
1088
|
+
facetName: string,
|
|
1089
|
+
facetValues: Record<string, FacetBucket>
|
|
1090
|
+
): { name: string; values: Record<string, FacetBucket> } {
|
|
1091
|
+
let [normalizedName, normalizedValues] = [facetName, facetValues];
|
|
1092
|
+
|
|
1093
|
+
// The full "search engine" name of the lending field is "lending___status"
|
|
1094
|
+
if (facetName === 'lending') {
|
|
1095
|
+
normalizedName = 'lending___status';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Language codes like "en-US|en-GB|en" need to be broken apart into individual values
|
|
1099
|
+
if (facetName === 'language') {
|
|
1100
|
+
normalizedValues = {};
|
|
1101
|
+
for (const [facetValue, facetData] of Object.entries(facetValues)) {
|
|
965
1102
|
const languages =
|
|
966
|
-
this.languageCodeHandler.getCodeArrayFromCodeString(
|
|
967
|
-
for (const
|
|
968
|
-
|
|
1103
|
+
this.languageCodeHandler.getCodeArrayFromCodeString(facetValue);
|
|
1104
|
+
for (const lang of languages) {
|
|
1105
|
+
normalizedValues[lang] = { ...facetData };
|
|
969
1106
|
}
|
|
970
|
-
} else {
|
|
971
|
-
facetValuesArray.push(`${plusMinusPrefix}"${key}"`);
|
|
972
1107
|
}
|
|
973
1108
|
}
|
|
974
1109
|
|
|
975
|
-
|
|
976
|
-
|
|
1110
|
+
return {
|
|
1111
|
+
name: normalizedName,
|
|
1112
|
+
values: normalizedValues,
|
|
1113
|
+
};
|
|
977
1114
|
}
|
|
978
1115
|
|
|
979
1116
|
/**
|
|
@@ -1018,11 +1155,12 @@ export class CollectionBrowser
|
|
|
1018
1155
|
}
|
|
1019
1156
|
|
|
1020
1157
|
private async fetchFacets() {
|
|
1021
|
-
if (!this.
|
|
1158
|
+
if (!this.filteredQuery) return;
|
|
1022
1159
|
|
|
1023
1160
|
const params: SearchParams = {
|
|
1024
|
-
query: this.
|
|
1161
|
+
query: this.filteredQuery,
|
|
1025
1162
|
rows: 0,
|
|
1163
|
+
filters: this.filterMap,
|
|
1026
1164
|
// Fetch a few extra buckets beyond the 6 we show, in case some get suppressed
|
|
1027
1165
|
aggregationsSize: 10,
|
|
1028
1166
|
// Note: we don't need an aggregations param to fetch the default aggregations from the PPS.
|
|
@@ -1132,7 +1270,14 @@ export class CollectionBrowser
|
|
|
1132
1270
|
}
|
|
1133
1271
|
|
|
1134
1272
|
/**
|
|
1135
|
-
* The query key is a string that uniquely identifies the current
|
|
1273
|
+
* The query key is a string that uniquely identifies the current search.
|
|
1274
|
+
* It consists of:
|
|
1275
|
+
* - The current base query
|
|
1276
|
+
* - The current search type
|
|
1277
|
+
* - Any currently-applied facets
|
|
1278
|
+
* - Any currently-applied date range
|
|
1279
|
+
* - Any currently-applied prefix filters
|
|
1280
|
+
* - The current sort options
|
|
1136
1281
|
*
|
|
1137
1282
|
* This lets us keep track of queries so we don't persist data that's
|
|
1138
1283
|
* no longer relevant.
|
|
@@ -1145,7 +1290,7 @@ export class CollectionBrowser
|
|
|
1145
1290
|
private pageFetchesInProgress: Record<string, Set<number>> = {};
|
|
1146
1291
|
|
|
1147
1292
|
async fetchPage(pageNumber: number) {
|
|
1148
|
-
if (!this.
|
|
1293
|
+
if (!this.filteredQuery) return;
|
|
1149
1294
|
|
|
1150
1295
|
// if we already have data, don't fetch again
|
|
1151
1296
|
if (this.dataSource[pageNumber]) return;
|
|
@@ -1162,10 +1307,11 @@ export class CollectionBrowser
|
|
|
1162
1307
|
|
|
1163
1308
|
const sortParams = this.sortParam ? [this.sortParam] : [];
|
|
1164
1309
|
const params: SearchParams = {
|
|
1165
|
-
query: this.
|
|
1310
|
+
query: this.filteredQuery,
|
|
1166
1311
|
page: pageNumber,
|
|
1167
1312
|
rows: this.pageSize,
|
|
1168
1313
|
sort: sortParams,
|
|
1314
|
+
filters: this.filterMap,
|
|
1169
1315
|
aggregations: { omit: true },
|
|
1170
1316
|
};
|
|
1171
1317
|
const searchResponse = await this.searchService?.search(
|
|
@@ -1203,7 +1349,7 @@ export class CollectionBrowser
|
|
|
1203
1349
|
}
|
|
1204
1350
|
}
|
|
1205
1351
|
const queryChangedSinceFetch =
|
|
1206
|
-
searchQuery !== this.
|
|
1352
|
+
searchQuery !== this.filteredQuery || sortChanged;
|
|
1207
1353
|
if (queryChangedSinceFetch) return;
|
|
1208
1354
|
|
|
1209
1355
|
const { results } = success.response;
|
|
@@ -1211,12 +1357,13 @@ export class CollectionBrowser
|
|
|
1211
1357
|
this.preloadCollectionNames(results);
|
|
1212
1358
|
this.updateDataSource(pageNumber, results);
|
|
1213
1359
|
}
|
|
1360
|
+
|
|
1361
|
+
// When we reach the end of the data, we can set the infinite scroller's
|
|
1362
|
+
// item count to the real total number of results (rather than the
|
|
1363
|
+
// temporary estimates based on pages rendered so far).
|
|
1214
1364
|
if (results.length < this.pageSize) {
|
|
1215
1365
|
this.endOfDataReached = true;
|
|
1216
|
-
|
|
1217
|
-
if (this.infiniteScroller) {
|
|
1218
|
-
this.infiniteScroller.itemCount = this.actualTileCount;
|
|
1219
|
-
}
|
|
1366
|
+
this.infiniteScroller.itemCount = this.totalResults;
|
|
1220
1367
|
}
|
|
1221
1368
|
this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
|
|
1222
1369
|
this.searchResultsLoading = false;
|
|
@@ -1320,6 +1467,73 @@ export class CollectionBrowser
|
|
|
1320
1467
|
}
|
|
1321
1468
|
}
|
|
1322
1469
|
|
|
1470
|
+
/** Fetches the aggregation buckets for the given prefix filter type. */
|
|
1471
|
+
private async fetchPrefixFilterBuckets(
|
|
1472
|
+
filterType: PrefixFilterType
|
|
1473
|
+
): Promise<Bucket[]> {
|
|
1474
|
+
if (!this.fullQueryWithoutAlphaFilters) return [];
|
|
1475
|
+
|
|
1476
|
+
const filterAggregationKey = prefixFilterAggregationKeys[filterType];
|
|
1477
|
+
const params: SearchParams = {
|
|
1478
|
+
query: this.fullQueryWithoutAlphaFilters,
|
|
1479
|
+
rows: 0,
|
|
1480
|
+
// Only fetch the firstTitle or firstCreator aggregation
|
|
1481
|
+
aggregations: { simpleParams: [filterAggregationKey] },
|
|
1482
|
+
// Fetch all 26 letter buckets
|
|
1483
|
+
aggregationsSize: 26,
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const searchResponse = await this.searchService?.search(
|
|
1487
|
+
params,
|
|
1488
|
+
this.searchType
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
return (searchResponse?.success?.response?.aggregations?.[
|
|
1492
|
+
filterAggregationKey
|
|
1493
|
+
]?.buckets ?? []) as Bucket[];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** Fetches and caches the prefix filter counts for the given filter type. */
|
|
1497
|
+
private async updatePrefixFilterCounts(
|
|
1498
|
+
filterType: PrefixFilterType
|
|
1499
|
+
): Promise<void> {
|
|
1500
|
+
const buckets = await this.fetchPrefixFilterBuckets(filterType);
|
|
1501
|
+
// Unpack the aggregation buckets into a simple map like { 'A': 50, 'B': 25, ... }
|
|
1502
|
+
this.prefixFilterCountMap = { ...this.prefixFilterCountMap }; // Clone the object to trigger an update
|
|
1503
|
+
this.prefixFilterCountMap[filterType] = buckets.reduce(
|
|
1504
|
+
(acc: Record<string, number>, bucket: Bucket) => {
|
|
1505
|
+
acc[(bucket.key as string).toUpperCase()] = bucket.doc_count;
|
|
1506
|
+
return acc;
|
|
1507
|
+
},
|
|
1508
|
+
{}
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* Fetches and caches the prefix filter counts for the current sort type,
|
|
1514
|
+
* provided it is one that permits prefix filtering. (If not, this does nothing).
|
|
1515
|
+
*/
|
|
1516
|
+
private async updatePrefixFiltersForCurrentSort(): Promise<void> {
|
|
1517
|
+
if (['title', 'creator'].includes(this.selectedSort)) {
|
|
1518
|
+
const filterType = this.selectedSort as PrefixFilterType;
|
|
1519
|
+
if (!this.prefixFilterCountMap[filterType]) {
|
|
1520
|
+
this.updatePrefixFilterCounts(filterType);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Clears the cached letter counts for both title and creator, and
|
|
1527
|
+
* fetches a new set of counts for whichever of them is the currently
|
|
1528
|
+
* selected sort option (which may be neither).
|
|
1529
|
+
*
|
|
1530
|
+
* Call this whenever the counts are invalidated (e.g., by a query change).
|
|
1531
|
+
*/
|
|
1532
|
+
private refreshLetterCounts(): void {
|
|
1533
|
+
this.prefixFilterCountMap = {};
|
|
1534
|
+
this.updatePrefixFiltersForCurrentSort();
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1323
1537
|
/*
|
|
1324
1538
|
* Convert etree titles
|
|
1325
1539
|
* "[Creator] Live at [Place] on [Date]" => "[Date]: [Place]"
|
|
@@ -1385,8 +1599,10 @@ export class CollectionBrowser
|
|
|
1385
1599
|
* increase the number of pages to render and start fetching data for the new page
|
|
1386
1600
|
*/
|
|
1387
1601
|
private scrollThresholdReached() {
|
|
1388
|
-
this.
|
|
1389
|
-
|
|
1602
|
+
if (!this.endOfDataReached) {
|
|
1603
|
+
this.pagesToRender += 1;
|
|
1604
|
+
this.fetchPage(this.pagesToRender);
|
|
1605
|
+
}
|
|
1390
1606
|
}
|
|
1391
1607
|
|
|
1392
1608
|
static styles = css`
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
SearchParams,
|
|
18
18
|
SearchType,
|
|
19
19
|
AggregationSortType,
|
|
20
|
+
FilterMap,
|
|
20
21
|
} from '@internetarchive/search-service';
|
|
21
22
|
import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
|
|
22
23
|
import type { ModalManagerInterface } from '@internetarchive/modal-manager';
|
|
@@ -44,7 +45,9 @@ export class MoreFacetsContent extends LitElement {
|
|
|
44
45
|
|
|
45
46
|
@property({ type: String }) facetAggregationKey?: FacetOption;
|
|
46
47
|
|
|
47
|
-
@property({ type: String })
|
|
48
|
+
@property({ type: String }) query?: string;
|
|
49
|
+
|
|
50
|
+
@property({ type: Object }) filterMap?: FilterMap;
|
|
48
51
|
|
|
49
52
|
@property({ type: Object }) modalManager?: ModalManagerInterface;
|
|
50
53
|
|
|
@@ -127,7 +130,8 @@ export class MoreFacetsContent extends LitElement {
|
|
|
127
130
|
const aggregationsSize = 65535; // todo - do we want to have all the records at once?
|
|
128
131
|
|
|
129
132
|
const params: SearchParams = {
|
|
130
|
-
query: this.
|
|
133
|
+
query: this.query as string,
|
|
134
|
+
filters: this.filterMap,
|
|
131
135
|
aggregations,
|
|
132
136
|
aggregationsSize,
|
|
133
137
|
rows: 0, // todo - do we want server-side pagination with offset/page/limit flag?
|
package/src/collection-facets.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { map } from 'lit/directives/map.js';
|
|
|
12
12
|
import type {
|
|
13
13
|
Aggregation,
|
|
14
14
|
Bucket,
|
|
15
|
+
FilterMap,
|
|
15
16
|
SearchServiceInterface,
|
|
16
17
|
SearchType,
|
|
17
18
|
} from '@internetarchive/search-service';
|
|
@@ -72,7 +73,9 @@ export class CollectionFacets extends LitElement {
|
|
|
72
73
|
|
|
73
74
|
@property({ type: Boolean }) showHistogramDatePicker = false;
|
|
74
75
|
|
|
75
|
-
@property({ type: String })
|
|
76
|
+
@property({ type: String }) query?: string;
|
|
77
|
+
|
|
78
|
+
@property({ type: Object }) filterMap?: FilterMap;
|
|
76
79
|
|
|
77
80
|
@property({ type: Object, attribute: false })
|
|
78
81
|
modalManager?: ModalManagerInterface;
|
|
@@ -491,7 +494,8 @@ export class CollectionFacets extends LitElement {
|
|
|
491
494
|
.analyticsHandler=${this.analyticsHandler}
|
|
492
495
|
.facetKey=${facetGroup.key}
|
|
493
496
|
.facetAggregationKey=${facetAggrKey}
|
|
494
|
-
.
|
|
497
|
+
.query=${this.query}
|
|
498
|
+
.filterMap=${this.filterMap}
|
|
495
499
|
.modalManager=${this.modalManager}
|
|
496
500
|
.searchService=${this.searchService}
|
|
497
501
|
.searchType=${this.searchType}
|
package/src/models.ts
CHANGED
|
@@ -119,6 +119,21 @@ export const MetadataFieldToSortField: {
|
|
|
119
119
|
creatorSorter: SortField.creator,
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
+
/** A union of the fields that permit prefix filtering (e.g., alphabetical filtering) */
|
|
123
|
+
export type PrefixFilterType = 'title' | 'creator';
|
|
124
|
+
|
|
125
|
+
/** A map from prefixes (e.g., initial letters) to the number of items matching that prefix */
|
|
126
|
+
export type PrefixFilterCounts = Record<string, number>;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A map from prefix filter types to the corresponding aggregation keys
|
|
130
|
+
* that are needed to fetch the filter counts from the backend.
|
|
131
|
+
*/
|
|
132
|
+
export const prefixFilterAggregationKeys: Record<PrefixFilterType, string> = {
|
|
133
|
+
title: 'firstTitle',
|
|
134
|
+
creator: 'firstCreator',
|
|
135
|
+
};
|
|
136
|
+
|
|
122
137
|
export type FacetOption =
|
|
123
138
|
| 'subject'
|
|
124
139
|
| 'lending'
|
|
@@ -25,7 +25,6 @@ export interface RestorationState {
|
|
|
25
25
|
selectedFacets: SelectedFacets;
|
|
26
26
|
baseQuery?: string;
|
|
27
27
|
currentPage?: number;
|
|
28
|
-
dateRangeQueryClause?: string;
|
|
29
28
|
titleQuery?: string;
|
|
30
29
|
creatorQuery?: string;
|
|
31
30
|
minSelectedDate?: string;
|
|
@@ -142,8 +141,11 @@ export class RestorationStateHandler
|
|
|
142
141
|
}
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
if (state.
|
|
146
|
-
searchParams.append(
|
|
144
|
+
if (state.minSelectedDate && state.maxSelectedDate) {
|
|
145
|
+
searchParams.append(
|
|
146
|
+
'and[]',
|
|
147
|
+
`year:[${state.minSelectedDate} TO ${state.maxSelectedDate}]`
|
|
148
|
+
);
|
|
147
149
|
}
|
|
148
150
|
if (state.titleQuery) {
|
|
149
151
|
searchParams.append('and[]', state.titleQuery);
|
|
@@ -159,7 +161,8 @@ export class RestorationStateHandler
|
|
|
159
161
|
page: state.currentPage,
|
|
160
162
|
and: state.selectedFacets,
|
|
161
163
|
not: state.selectedFacets,
|
|
162
|
-
|
|
164
|
+
minDate: state.minSelectedDate,
|
|
165
|
+
maxDate: state.maxSelectedDate,
|
|
163
166
|
},
|
|
164
167
|
'',
|
|
165
168
|
url
|
|
@@ -244,7 +247,6 @@ export class RestorationStateHandler
|
|
|
244
247
|
0,
|
|
245
248
|
maxDate.length - 1
|
|
246
249
|
);
|
|
247
|
-
restorationState.dateRangeQueryClause = `year:${value}`;
|
|
248
250
|
} else {
|
|
249
251
|
this.setSelectedFacetState(
|
|
250
252
|
restorationState.selectedFacets,
|