[ADF-3497] Facet intervals on search filter (#4255)

* [ADF-3497] allow facetIntervals on search

* [ADF-3497] update schema json

* [ADF-3497] update json

* [ADF-3497] documentation update

* [ADF-3497] specify that sets are not supported

* [ADF-3497] no spaces on labels - mention

* [ADF-3497] documentation update

* [ADF-3497] update examples & document label key

* [ADF-3497] tests added

* [ADF-3497] testRail id

* [ADF-3497] allow config of custom pageSize values

* [ADF-3497] support mincount filtering also for intervals

* [ADF-3497] support expanded property also for facetIntervals

* remove no longer needed info

- bcs. of PR #4322 fix
This commit is contained in:
Suzana Dirla
2019-02-18 20:51:15 +02:00
committed by Eugenio Romano
parent f20a71438c
commit e34f80aff7
11 changed files with 368 additions and 21 deletions

View File

@@ -122,7 +122,7 @@
"expanded": true, "expanded": true,
"mincount": 1, "mincount": 1,
"queries": [ "queries": [
{ "query": "created:2018", "label": "SEARCH.FACET_QUERIES.CREATED_THIS_YEAR" }, { "query": "created:2019", "label": "SEARCH.FACET_QUERIES.CREATED_THIS_YEAR" },
{ "query": "content.mimetype:text/html", "label": "SEARCH.FACET_QUERIES.MIMETYPE", "group":"Type facet queries" }, { "query": "content.mimetype:text/html", "label": "SEARCH.FACET_QUERIES.MIMETYPE", "group":"Type facet queries" },
{ "query": "content.size:[0 TO 10240]", "label": "SEARCH.FACET_QUERIES.XTRASMALL", "group":"Size facet queries"}, { "query": "content.size:[0 TO 10240]", "label": "SEARCH.FACET_QUERIES.XTRASMALL", "group":"Size facet queries"},
{ "query": "content.size:[10240 TO 102400]", "label": "SEARCH.FACET_QUERIES.SMALL", "group":"Size facet queries"}, { "query": "content.size:[10240 TO 102400]", "label": "SEARCH.FACET_QUERIES.SMALL", "group":"Size facet queries"},
@@ -132,6 +132,30 @@
{ "query": "content.size:[134217728 TO MAX]", "label": "SEARCH.FACET_QUERIES.XXTRALARGE", "group":"Size facet queries" } { "query": "content.size:[134217728 TO MAX]", "label": "SEARCH.FACET_QUERIES.XXTRALARGE", "group":"Size facet queries" }
] ]
}, },
"facetIntervals":{
"expanded": true,
"intervals":[
{
"label":"TheCreated",
"field":"cm:created",
"sets":[
{ "label":"lastYear", "start":"2018", "end":"2019", "endInclusive":false },
{ "label":"currentYear", "start":"NOW/YEAR", "end":"NOW/YEAR+1YEAR" },
{ "label":"earlier", "start":"*", "end":"2018", "endInclusive":false }
]
},
{
"label":"TheModified",
"field":"cm:modified",
"sets":[
{ "label":"2017", "start":"2017", "end":"2018", "endInclusive":false },
{ "label":"2017-2018", "start":"2017", "end":"2018", "endInclusive":true },
{ "label":"currentYear", "start":"NOW/YEAR", "end":"NOW/YEAR+1YEAR" },
{ "label":"earlierThan2017", "start":"*", "end":"2017", "endInclusive":false }
]
}
]
},
"categories": [ "categories": [
{ {
"id": "queryName", "id": "queryName",

View File

@@ -352,6 +352,56 @@ The default page size of 5 will be used if you set the value to 0 or omit it ent
![Facet Queries](../docassets/images/search-facet-queries.png) ![Facet Queries](../docassets/images/search-facet-queries.png)
### Facet Intervals
These provide custom categories based on admin defined ranges inside `intervals`. What is wanted for every interval can be specified exactly in the config file, and having overlapping ranges could also be possible.
#### FacetIntervals Properties
| Name | Type | Description |
| ---- | ---- | ----------- |
|intervals|array|Specifies the fields to facet by interval.|
|expanded|boolean|Toggles expanded state of the facet intervals.|
Note: `sets` parameter from Search API (Sets the intervals for all fields) is not yet supported.
```json
{
"search": {
"facetIntervals":{
"expanded": true,
"intervals":[
{
"label":"TheCreated",
"field":"cm:created",
"sets":[
{ "label":"lastYear", "start":"2017", "end":"2018", "endInclusive":false },
{ "label":"currentYear", "start":"NOW/YEAR", "end":"NOW/YEAR+1YEAR" },
{ "label":"earlier", "start":"*", "end":"2017", "endInclusive":false }
]
},
{
"label":"TheModified",
"field":"cm:modified",
"sets":[
{ "label":"2016", "start":"2017", "end":"2018", "endInclusive":false },
{ "label":"currentYear", "start":"NOW/YEAR", "end":"NOW/YEAR+1YEAR" },
{ "label":"earlierThan2017", "start":"*", "end":"2017", "endInclusive":false }
]
}
]
}
}
}
```
You can specify a value for the `mincount` property inside each `intervals` item to set the minimum count required for a facet interval to be displayed. By default, only the intervals that have 1 or more response entries are displayed at runtime.
Check the [schema.json](https://github.com/Alfresco/alfresco-ng2-components/blob/master/lib/core/app-config/schema.json)
for more details about what is the structure and the properties of `intervals` that you can set inside the configuration file.
Each `intervals` item defined is collected into its collapsible category identified uniquely by its `label`. The top code snippet will result in the following display of the facet intervals:
![Facet Intervals](../docassets/images/search-facet-intervals.png)
## See also ## See also
- [Search Query Builder service](search-query-builder.service.md) - [Search Query Builder service](search-query-builder.service.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -37,6 +37,8 @@ export class SearchFiltersPage {
'mat-expansion-panel[data-automation-id="expansion-panel-My facet queries"]')); 'mat-expansion-panel[data-automation-id="expansion-panel-My facet queries"]'));
facetQueriesTypeGroup = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-Type facet queries"]')); facetQueriesTypeGroup = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-Type facet queries"]'));
facetQueriesSizeGroup = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-Size facet queries"]')); facetQueriesSizeGroup = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-Size facet queries"]'));
facetIntervalsByCreated = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-TheCreated"]'));
facetIntervalsByModified = element(by.css('mat-expansion-panel[data-automation-id="expansion-panel-TheModified"]'));
checkSearchFiltersIsDisplayed() { checkSearchFiltersIsDisplayed() {
Util.waitUntilElementIsVisible(this.searchFilters); Util.waitUntilElementIsVisible(this.searchFilters);
@@ -110,6 +112,16 @@ export class SearchFiltersPage {
return this; return this;
} }
checkFacetIntervalsByCreatedIsDisplayed() {
this.searchCategoriesPage.checkFilterIsDisplayed(this.facetIntervalsByCreated);
return this;
}
checkFacetIntervalsByModifiedIsDisplayed() {
this.searchCategoriesPage.checkFilterIsDisplayed(this.facetIntervalsByModified);
return this;
}
isTypeFacetQueryGroupPresent() { isTypeFacetQueryGroupPresent() {
return this.facetQueriesTypeGroup.isPresent(); return this.facetQueriesTypeGroup.isPresent();
} }

View File

@@ -207,4 +207,12 @@ describe('Search Filters', () => {
expect(searchFiltersPage.isTypeFacetQueryGroupPresent()).toBe(false); expect(searchFiltersPage.isTypeFacetQueryGroupPresent()).toBe(false);
expect(searchFiltersPage.isSizeFacetQueryGroupPresent()).toBe(false); expect(searchFiltersPage.isSizeFacetQueryGroupPresent()).toBe(false);
}); });
it('[C297509] Should display search intervals under specified labels from config', () => {
browser.get(TestConfig.adf.url + '/search;q=*');
searchFiltersPage.checkFacetIntervalsByCreatedIsDisplayed()
.checkFacetIntervalsByModifiedIsDisplayed();
});
}); });

View File

@@ -202,8 +202,8 @@ describe('SearchFilterComponent', () => {
queryBuilder.config = { queryBuilder.config = {
categories: [], categories: [],
facetFields: { fields: [ facetFields: { fields: [
{ label: 'f1', field: 'f1' }, { label: 'f1', field: 'f1', mincount: 0 },
{ label: 'f2', field: 'f2' } { label: 'f2', field: 'f2', mincount: 0 }
]}, ]},
facetQueries: { facetQueries: {
queries: [] queries: []
@@ -593,4 +593,92 @@ describe('SearchFilterComponent', () => {
expect(entry.checked).toBeFalsy(); expect(entry.checked).toBeFalsy();
} }
}); });
it('should fetch facet intervals from response payload', () => {
component.responseFacets = null;
queryBuilder.config = {
categories: [],
facetIntervals: {
intervals: [
{ label: 'test_intervals1', field: 'f1', sets: [
{ label: 'interval1', start: 's1', end: 'e1'},
{ label: 'interval2', start: 's2', end: 'e2'}
]},
{ label: 'test_intervals2', field: 'f2', sets: [
{ label: 'interval3', start: 's3', end: 'e3'},
{ label: 'interval4', start: 's4', end: 'e4'}
]}
]
}
};
const response1 = [
{ label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]},
{ label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]}
];
const response2 = [
{ label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]},
{ label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]}
];
const data = {
list: {
context: {
facets: [
{ type: 'interval', label: 'test_intervals1', buckets: response1 },
{ type: 'interval', label: 'test_intervals2', buckets: response2 }
]
}
}
};
component.onDataLoaded(data);
expect(component.responseFacets.length).toBe(2);
expect(component.responseFacets[0].buckets.length).toEqual(2);
expect(component.responseFacets[1].buckets.length).toEqual(2);
});
it('should filter out the fetched facet intervals that have bucket values less than their set mincount', () => {
component.responseFacets = null;
queryBuilder.config = {
categories: [],
facetIntervals: {
intervals: [
{ label: 'test_intervals1', field: 'f1', mincount: 2, sets: [
{ label: 'interval1', start: 's1', end: 'e1'},
{ label: 'interval2', start: 's2', end: 'e2'}
]},
{ label: 'test_intervals2', field: 'f2', mincount: 5, sets: [
{ label: 'interval3', start: 's3', end: 'e3'},
{ label: 'interval4', start: 's4', end: 'e4'}
]}
]
}
};
const response1 = [
{ label: 'interval1', filterQuery: 'query1', metrics: [{ value: { count: 1 }}]},
{ label: 'interval2', filterQuery: 'query2', metrics: [{ value: { count: 2 }}]}
];
const response2 = [
{ label: 'interval3', filterQuery: 'query3', metrics: [{ value: { count: 3 }}]},
{ label: 'interval4', filterQuery: 'query4', metrics: [{ value: { count: 4 }}]}
];
const data = {
list: {
context: {
facets: [
{ type: 'interval', label: 'test_intervals1', buckets: response1 },
{ type: 'interval', label: 'test_intervals2', buckets: response2 }
]
}
}
};
component.onDataLoaded(data);
expect(component.responseFacets.length).toBe(2);
expect(component.responseFacets[0].buckets.length).toEqual(1);
expect(component.responseFacets[1].buckets.length).toEqual(0);
});
}); });

View File

@@ -41,8 +41,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE; private facetQueriesPageSize = this.DEFAULT_PAGE_SIZE;
facetQueriesLabel: string = 'Facet Queries'; facetQueriesLabel: string = 'Facet Queries';
facetQueriesExpanded = false; facetExpanded = {
facetFieldsExpanded = false; 'default': false
};
selectedBuckets: Array<{ field: FacetField, bucket: FacetFieldBucket }> = []; selectedBuckets: Array<{ field: FacetField, bucket: FacetFieldBucket }> = [];
@@ -52,10 +53,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
if (queryBuilder.config && queryBuilder.config.facetQueries) { if (queryBuilder.config && queryBuilder.config.facetQueries) {
this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries'; this.facetQueriesLabel = queryBuilder.config.facetQueries.label || 'Facet Queries';
this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE; this.facetQueriesPageSize = queryBuilder.config.facetQueries.pageSize || this.DEFAULT_PAGE_SIZE;
this.facetQueriesExpanded = queryBuilder.config.facetQueries.expanded; this.facetExpanded['query'] = queryBuilder.config.facetQueries.expanded;
} }
if (queryBuilder.config && queryBuilder.config.facetFields) { if (queryBuilder.config && queryBuilder.config.facetFields) {
this.facetFieldsExpanded = queryBuilder.config.facetFields.expanded; this.facetExpanded['field'] = queryBuilder.config.facetFields.expanded;
}
if (queryBuilder.config && queryBuilder.config.facetIntervals) {
this.facetExpanded['interval'] = queryBuilder.config.facetIntervals.expanded;
} }
this.queryBuilder.updated.pipe( this.queryBuilder.updated.pipe(
@@ -146,7 +150,7 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
} }
shouldExpand(field: FacetField): boolean { shouldExpand(field: FacetField): boolean {
return field.type === 'query' ? this.facetQueriesExpanded : this.facetFieldsExpanded; return this.facetExpanded[field.type] || this.facetExpanded['default'];
} }
onDataLoaded(data: any) { onDataLoaded(data: any) {
@@ -162,8 +166,9 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
private parseFacets(context: ResultSetContext) { private parseFacets(context: ResultSetContext) {
if (!this.responseFacets) { if (!this.responseFacets) {
const responseFacetFields = this.parseFacetFields(context); const responseFacetFields = this.parseFacetFields(context);
const responseFacetIntervals = this.parseFacetIntervals(context);
const responseGroupedFacetQueries = this.parseFacetQueries(context); const responseGroupedFacetQueries = this.parseFacetQueries(context);
this.responseFacets = responseFacetFields.concat(...responseGroupedFacetQueries); this.responseFacets = responseFacetFields.concat(...responseGroupedFacetQueries, ...responseFacetIntervals);
} else { } else {
this.responseFacets = this.responseFacets this.responseFacets = this.responseFacets
@@ -184,12 +189,11 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
} }
} }
private parseFacetFields(context: ResultSetContext): FacetField[] { private parseFacetItems(context: ResultSetContext, configFacetFields, itemType): FacetField[] {
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
return configFacetFields.map((field) => { return configFacetFields.map((field) => {
const responseField = (context.facets || []).find((response) => response.type === 'field' && response.label === field.label) || {}; const responseField = (context.facets || []).find((response) => response.type === itemType && response.label === field.label) || {};
const responseBuckets = this.getResponseBuckets(responseField); const responseBuckets = this.getResponseBuckets(responseField)
.filter(this.getFilterByMinCount(field.mincount));
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, field.pageSize); const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, field.pageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => { bucketList.filter = (bucket: FacetFieldBucket): boolean => {
@@ -212,6 +216,16 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}); });
} }
private parseFacetFields(context: ResultSetContext): FacetField[] {
const configFacetFields = this.queryBuilder.config.facetFields && this.queryBuilder.config.facetFields.fields || [];
return this.parseFacetItems(context, configFacetFields, 'field');
}
private parseFacetIntervals(context: ResultSetContext): FacetField[] {
const configFacetIntervals = this.queryBuilder.config.facetIntervals && this.queryBuilder.config.facetIntervals.intervals || [];
return this.parseFacetItems(context, configFacetIntervals, 'interval');
}
private parseFacetQueries(context: ResultSetContext): FacetField[] { private parseFacetQueries(context: ResultSetContext): FacetField[] {
const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || []; const configFacetQueries = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.queries || [];
const configGroups = configFacetQueries.reduce((acc, query) => { const configGroups = configFacetQueries.reduce((acc, query) => {
@@ -225,10 +239,13 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
}, []); }, []);
const result = []; const result = [];
const mincount = this.queryBuilder.config.facetQueries && this.queryBuilder.config.facetQueries.mincount;
const mincountFilter = this.getFilterByMinCount(mincount);
Object.keys(configGroups).forEach((group) => { Object.keys(configGroups).forEach((group) => {
const responseField = (context.facets || []).find((response) => response.type === 'query' && response.label === group) || {}; const responseField = (context.facets || []).find((response) => response.type === 'query' && response.label === group) || {};
const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group]); const responseBuckets = this.getResponseQueryBuckets(responseField, configGroups[group])
.filter(mincountFilter);
const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, this.facetQueriesPageSize); const bucketList = new SearchFilterList<FacetFieldBucket>(responseBuckets, this.facetQueriesPageSize);
bucketList.filter = (bucket: FacetFieldBucket): boolean => { bucketList.filter = (bucket: FacetFieldBucket): boolean => {
@@ -278,12 +295,6 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
display: respBucket.display, display: respBucket.display,
label: respBucket.label label: respBucket.label
}; };
}).filter((bucket) => {
let mincount = this.queryBuilder.config.facetQueries.mincount;
if (mincount === undefined) {
mincount = 1;
}
return bucket.count >= mincount;
}); });
} }
@@ -291,4 +302,14 @@ export class SearchFilterComponent implements OnInit, OnDestroy {
return (!!bucket && !!bucket.metrics && bucket.metrics[0] && bucket.metrics[0].value && bucket.metrics[0].value.count) return (!!bucket && !!bucket.metrics && bucket.metrics[0] && bucket.metrics[0].value && bucket.metrics[0].value.count)
|| 0; || 0;
} }
private getFilterByMinCount(mincountInput: number) {
return (bucket) => {
let mincount = mincountInput;
if (mincount === undefined) {
mincount = 1;
}
return bucket.count >= mincount;
};
}
} }

View File

@@ -38,6 +38,10 @@ export interface SearchConfiguration {
expanded?: boolean; expanded?: boolean;
fields: Array<FacetField>; fields: Array<FacetField>;
}; };
facetIntervals?: {
expanded?: boolean;
intervals: Array<any>;
};
sorting?: { sorting?: {
options: Array<SearchSortingDefinition>; options: Array<SearchSortingDefinition>;
defaults: Array<SearchSortingDefinition>; defaults: Array<SearchSortingDefinition>;

View File

@@ -371,6 +371,39 @@ describe('SearchQueryBuilder', () => {
expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields.fields)); expect(compiled.facetFields.facets).toEqual(jasmine.objectContaining(config.facetFields.fields));
}); });
it('should build query with custom facet intervals', () => {
const config: SearchConfiguration = {
categories: [
<any> { id: 'cat1', enabled: true }
],
facetIntervals: {
intervals: [
{
label: 'test_intervals1',
field: 'f1',
sets: [
{ label: 'interval1', start: 's1', end: 'e1' },
{ label: 'interval2', start: 's2', end: 'e2' }
]
},
{
label: 'test_intervals2',
field: 'f2',
sets: [
{ label: 'interval3', start: 's3', end: 'e3' },
{ label: 'interval4', start: 's4', end: 'e4' }
]
}
]
}
};
const builder = new SearchQueryBuilderService(buildConfig(config), null);
builder.queryFragments['cat1'] = 'cm:name:test';
const compiled = builder.buildQuery();
expect(compiled.facetIntervals).toEqual(jasmine.objectContaining(config.facetIntervals));
});
it('should build query with sorting', () => { it('should build query with sorting', () => {
const config: SearchConfiguration = { const config: SearchConfiguration = {
fields: [], fields: [],

View File

@@ -224,6 +224,7 @@ export class SearchQueryBuilderService {
fields: this.config.fields, fields: this.config.fields,
filterQueries: this.filterQueries, filterQueries: this.filterQueries,
facetQueries: this.facetQueries, facetQueries: this.facetQueries,
facetIntervals: this.facetIntervals,
facetFields: this.facetFields, facetFields: this.facetFields,
sort: this.sort sort: this.sort
}; };
@@ -280,6 +281,20 @@ export class SearchQueryBuilderService {
return false; return false;
} }
/**
* Checks if FacetIntervals has been defined
* @returns True if defined, false otherwise
*/
get hasFacetIntervals(): boolean {
if (this.config
&& this.config.facetIntervals
&& this.config.facetIntervals.intervals
&& this.config.facetIntervals.intervals.length > 0) {
return true;
}
return false;
}
protected get sort(): RequestSortDefinitionInner[] { protected get sort(): RequestSortDefinitionInner[] {
return this.sorting.map((def) => { return this.sorting.map((def) => {
return new RequestSortDefinitionInner({ return new RequestSortDefinitionInner({
@@ -301,6 +316,22 @@ export class SearchQueryBuilderService {
return null; return null;
} }
protected get facetIntervals(): any {
if (this.hasFacetIntervals) {
const configIntervals = this.config.facetIntervals;
return {
intervals: configIntervals.intervals.map((interval) => <any> {
label: interval.label,
field: interval.field,
sets: interval.sets
})
};
}
return null;
}
protected getFinalQuery(): string { protected getFinalQuery(): string {
let query = ''; let query = '';

View File

@@ -980,6 +980,81 @@
} }
} }
}, },
"facetIntervals": {
"type": "object",
"required": [
"intervals"
],
"properties": {
"intervals": {
"description": "List of facet intervals",
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"field",
"sets"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the field facet.",
"type": "string"
},
"field": {
"description": "This specifies the field to facet on.",
"type": "string"
},
"sets": {
"type": "array",
"items": {
"type": "object",
"required": [
"label",
"start",
"end"
],
"properties": {
"label": {
"description": "This specifies the label to use to identify the set.",
"type": "string"
},
"start": {
"description": "This specifies the start of the range.",
"type": "string"
},
"end": {
"description": "This specifies the end of the range.",
"type": "string"
},
"startInclusive": {
"description": "When true, the set will include values greater or equal to 'start'. The default value is true.",
"type": "boolean"
},
"endInclusive": {
"description": "When true, the set will include values less than or equal to 'end'. The default value is true.",
"type": "boolean"
}
}
}
},
"pageSize": {
"type": "number",
"description": "Display page size"
},
"mincount": {
"type": "number",
"description": "This specifies the minimum count required for a facet interval to be displayed. The default value is 1."
}
}
}
},
"expanded": {
"description": "Toggles expanded state of the facet intervals",
"type": "boolean"
}
}
},
"facetQueries": { "facetQueries": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -1017,6 +1092,7 @@
"type": "string" "type": "string"
}, },
"label": { "label": {
"description": "Unique identifier for the query",
"type": "string" "type": "string"
}, },
"group": { "group": {