fix RED-2675: search screen not ordering by score
This commit is contained in:
parent
b67130039e
commit
3d0999bd3e
@ -4,7 +4,7 @@ import { Dossier, DossierAttributeWithValue, DossierTemplate } from '@red/domain
|
|||||||
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
import { DossiersDialogService } from '../../../../services/dossiers-dialog.service';
|
||||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||||
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
import { DossierTemplatesService } from '@services/entity-services/dossier-templates.service';
|
||||||
import { FilesService } from '../../../../../../services/entity-services/files.service';
|
import { FilesService } from '@services/entity-services/files.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
|||||||
@ -47,9 +47,7 @@
|
|||||||
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
|
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
|
||||||
> <s>{{ term }}</s></span
|
> <s>{{ term }}</s></span
|
||||||
>. {{ 'search-screen.must-contain' | translate }}:
|
>. {{ 'search-screen.must-contain' | translate }}:
|
||||||
<span
|
<span (click)="$event.stopPropagation(); mustContain(term)" *ngFor="let term of unmatched"
|
||||||
(click)="$event.stopPropagation(); updateNavigation(search$.getValue().query, term)"
|
|
||||||
*ngFor="let term of unmatched"
|
|
||||||
> <u>{{ term }}</u></span
|
> <u>{{ term }}</u></span
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Component, forwardRef, Injector, OnDestroy } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, forwardRef, Injector, OnDestroy } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
DefaultListingServices,
|
DefaultListingServices,
|
||||||
IListable,
|
|
||||||
keyChecker,
|
keyChecker,
|
||||||
List,
|
List,
|
||||||
ListingComponent,
|
ListingComponent,
|
||||||
@ -10,8 +9,8 @@ import {
|
|||||||
SearchPositions,
|
SearchPositions,
|
||||||
TableColumnConfig,
|
TableColumnConfig,
|
||||||
} from '@iqser/common-ui';
|
} from '@iqser/common-ui';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { merge, Observable } from 'rxjs';
|
||||||
import { debounceTime, map, skip, switchMap, tap } from 'rxjs/operators';
|
import { debounceTime, map, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
import { fileStatusTranslations } from '../../translations/file-status-translations';
|
||||||
@ -19,44 +18,38 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import { RouterHistoryService } from '@services/router-history.service';
|
import { RouterHistoryService } from '@services/router-history.service';
|
||||||
import { DossiersService } from '@services/entity-services/dossiers.service';
|
import { DossiersService } from '@services/entity-services/dossiers.service';
|
||||||
import { PlatformSearchService } from '../../shared/services/platform-search.service';
|
import { PlatformSearchService } from '../../shared/services/platform-search.service';
|
||||||
import { IMatchedDocument, ISearchResponse } from '@red/domain';
|
import { IMatchedDocument, ISearchInput, ISearchListItem, ISearchResponse } from '@red/domain';
|
||||||
|
|
||||||
interface ListItem extends IListable {
|
function toSearchInput(query: string, dossierIds: List | string): ISearchInput {
|
||||||
readonly dossierId: string;
|
return {
|
||||||
readonly filename: string;
|
query,
|
||||||
readonly unmatched: List | null;
|
dossierIds: dossierIds ? (typeof dossierIds === 'string' ? [dossierIds] : dossierIds) : [],
|
||||||
readonly highlights: Record<string, List>;
|
};
|
||||||
readonly routerLink: string;
|
|
||||||
readonly status: string;
|
|
||||||
readonly dossierName: string;
|
|
||||||
readonly numberOfPages: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchInput {
|
|
||||||
readonly query: string;
|
|
||||||
readonly dossierIds?: List;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './search-screen.component.html',
|
templateUrl: './search-screen.component.html',
|
||||||
styleUrls: ['./search-screen.component.scss'],
|
styleUrls: ['./search-screen.component.scss'],
|
||||||
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => SearchScreenComponent) }],
|
providers: [...DefaultListingServices, { provide: ListingComponent, useExisting: forwardRef(() => SearchScreenComponent) }],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class SearchScreenComponent extends ListingComponent<ListItem> implements OnDestroy {
|
export class SearchScreenComponent extends ListingComponent<ISearchListItem> implements OnDestroy {
|
||||||
readonly fileStatusTranslations = fileStatusTranslations;
|
readonly fileStatusTranslations = fileStatusTranslations;
|
||||||
readonly searchPositions = SearchPositions;
|
readonly searchPositions = SearchPositions;
|
||||||
|
|
||||||
readonly tableHeaderLabel = _('search-screen.table-header');
|
readonly tableHeaderLabel = _('search-screen.table-header');
|
||||||
readonly tableColumnConfigs: TableColumnConfig<ListItem>[] = [
|
readonly tableColumnConfigs: TableColumnConfig<ISearchListItem>[] = [
|
||||||
{ label: _('search-screen.cols.document'), width: '2fr' },
|
{ label: _('search-screen.cols.document'), width: '2fr' },
|
||||||
{ label: _('search-screen.cols.status') },
|
{ label: _('search-screen.cols.status') },
|
||||||
{ label: _('search-screen.cols.dossier') },
|
{ label: _('search-screen.cols.dossier') },
|
||||||
{ label: _('search-screen.cols.pages'), width: 'auto' },
|
{ label: _('search-screen.cols.pages'), width: 'auto' },
|
||||||
];
|
];
|
||||||
readonly search$ = new BehaviorSubject<SearchInput>(null);
|
|
||||||
readonly searchResults$: Observable<ListItem[]> = this.search$.asObservable().pipe(
|
readonly searchResults$ = merge(this._searchChanged$, this._filtersChanged$).pipe(
|
||||||
|
startWith(this._routeQuery),
|
||||||
tap(() => this._loadingService.start()),
|
tap(() => this._loadingService.start()),
|
||||||
switchMap(query => this._search(query)),
|
tap(value => this.updateNavigation(value.query)),
|
||||||
|
switchMap(query => this._platformSearchService.search(query)),
|
||||||
map(searchResult => this._toMatchedDocuments(searchResult)),
|
map(searchResult => this._toMatchedDocuments(searchResult)),
|
||||||
map(docs => this._toListItems(docs)),
|
map(docs => this._toListItems(docs)),
|
||||||
tap(result => this.entitiesService.setEntities(result)),
|
tap(result => this.entitiesService.setEntities(result)),
|
||||||
@ -76,6 +69,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
|||||||
super(_injector);
|
super(_injector);
|
||||||
this.searchService.skip = true;
|
this.searchService.skip = true;
|
||||||
|
|
||||||
|
const dossierId = _activatedRoute.snapshot.queryParamMap.get('dossierId');
|
||||||
this.filterService.addFilterGroups([
|
this.filterService.addFilterGroups([
|
||||||
{
|
{
|
||||||
slug: 'dossiers',
|
slug: 'dossiers',
|
||||||
@ -87,57 +81,53 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
|||||||
new NestedFilter({
|
new NestedFilter({
|
||||||
id: dossier.id,
|
id: dossier.id,
|
||||||
label: dossier.dossierName,
|
label: dossier.dossierName,
|
||||||
|
checked: dossier.id === dossierId,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
checker: keyChecker('dossierId'),
|
checker: keyChecker('dossierId'),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.addSubscription = _activatedRoute.queryParamMap
|
|
||||||
.pipe(map(value => ({ query: value.get('query'), dossierId: value.get('dossierId') })))
|
|
||||||
.subscribe(mappedValue => this._updateValues(mappedValue));
|
|
||||||
|
|
||||||
this.addSubscription = this.searchService.valueChanges$.pipe(debounceTime(300)).subscribe(value => this.updateNavigation(value));
|
|
||||||
|
|
||||||
this.addSubscription = this.filterService.filterGroups$.pipe(skip(1)).subscribe(group => {
|
|
||||||
const dossierIds = group[0].filters.filter(v => v.checked).map(v => v.id);
|
|
||||||
this.search$.next({ query: this.searchService.searchValue, dossierIds: dossierIds });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNavigation(query: string, mustContain?: string): void {
|
private get _searchChanged$(): Observable<ISearchInput> {
|
||||||
const newQuery = query?.replace(mustContain, `"${mustContain}"`);
|
return this.searchService.valueChanges$.pipe(
|
||||||
const queryParams = newQuery && newQuery !== '' ? { query: newQuery } : {};
|
debounceTime(300),
|
||||||
this._router.navigate([], { queryParams }).then();
|
map(value => ({ query: value, dossierIds: [] })),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _search(searchInput: SearchInput): Observable<ISearchResponse> {
|
private get _filtersChanged$() {
|
||||||
return this._platformSearchService.search({
|
return this.filterService.filterGroups$.pipe(
|
||||||
dossierIds: [...searchInput.dossierIds],
|
map(groups => groups[0].filters.filter(v => v.checked).map(v => v.id)),
|
||||||
queryString: searchInput.query ?? '',
|
map(dossierIds => toSearchInput(this.searchService.searchValue, dossierIds)),
|
||||||
page: 1,
|
);
|
||||||
returnSections: false,
|
|
||||||
pageSize: 300,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateValues({ query, dossierId }: { readonly query: string; readonly dossierId: string }) {
|
private get _routeQuery(): ISearchInput {
|
||||||
if (dossierId) {
|
const query = this._activatedRoute.snapshot.queryParamMap.get('query');
|
||||||
this.filterService.toggleFilter('dossiers', dossierId);
|
const dossierId = this._activatedRoute.snapshot.queryParamMap.get('dossierId');
|
||||||
}
|
|
||||||
this.searchService.searchValue = query;
|
this.searchService.searchValue = query;
|
||||||
this.search$.next({ query, dossierIds: dossierId ? [dossierId] : [] });
|
return { query, dossierIds: dossierId ? [dossierId] : [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNavigation(query: string) {
|
||||||
|
return this._router.navigate([], { queryParams: { query } });
|
||||||
|
}
|
||||||
|
|
||||||
|
mustContain(value: string) {
|
||||||
|
const newQuery = this.searchService.searchValue.replace(value, `"${value}"`);
|
||||||
|
this.searchService.searchValue = newQuery ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toMatchedDocuments({ matchedDocuments }: ISearchResponse): IMatchedDocument[] {
|
private _toMatchedDocuments({ matchedDocuments }: ISearchResponse): IMatchedDocument[] {
|
||||||
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
|
return matchedDocuments.filter(doc => doc.score > 0 && doc.matchedTerms.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toListItems(matchedDocuments: IMatchedDocument[]): ListItem[] {
|
private _toListItems(matchedDocuments: IMatchedDocument[]): ISearchListItem[] {
|
||||||
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
|
return matchedDocuments.map(document => this._toListItem(document)).filter(value => value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights }: IMatchedDocument): ListItem {
|
private _toListItem({ dossierId, fileId, unmatchedTerms, highlights, score }: IMatchedDocument): ISearchListItem {
|
||||||
const file = this._dossiersService.find(dossierId, fileId);
|
const file = this._dossiersService.find(dossierId, fileId);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -152,7 +142,7 @@ export class SearchScreenComponent extends ListingComponent<ListItem> implements
|
|||||||
numberOfPages: file.numberOfPages,
|
numberOfPages: file.numberOfPages,
|
||||||
dossierName: this._dossiersService.find(dossierId).dossierName,
|
dossierName: this._dossiersService.find(dossierId).dossierName,
|
||||||
filename: file.filename,
|
filename: file.filename,
|
||||||
searchKey: file.filename,
|
searchKey: score.toString(),
|
||||||
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`,
|
routerLink: `/main/dossiers/${dossierId}/file/${fileId}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable, Injector } from '@angular/core';
|
import { Injectable, Injector } from '@angular/core';
|
||||||
import { GenericService } from '@iqser/common-ui';
|
import { GenericService } from '@iqser/common-ui';
|
||||||
import { ISearchRequest, ISearchResponse } from '@red/domain';
|
import { ISearchInput, ISearchRequest, ISearchResponse } from '@red/domain';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PlatformSearchService extends GenericService<ISearchResponse> {
|
export class PlatformSearchService extends GenericService<ISearchResponse> {
|
||||||
@ -8,7 +8,13 @@ export class PlatformSearchService extends GenericService<ISearchResponse> {
|
|||||||
super(_injector, 'search');
|
super(_injector, 'search');
|
||||||
}
|
}
|
||||||
|
|
||||||
search(body: ISearchRequest) {
|
search({ dossierIds, query }: ISearchInput) {
|
||||||
return this._post(body);
|
return this._post({
|
||||||
|
dossierIds,
|
||||||
|
queryString: query ?? '',
|
||||||
|
page: 1,
|
||||||
|
returnSections: false,
|
||||||
|
pageSize: 300,
|
||||||
|
} as ISearchRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,3 +2,5 @@ export * from './matched-document';
|
|||||||
export * from './matched-section';
|
export * from './matched-section';
|
||||||
export * from './search.request';
|
export * from './search.request';
|
||||||
export * from './search.response';
|
export * from './search.response';
|
||||||
|
export * from './search-list-item';
|
||||||
|
export * from './search-input';
|
||||||
|
|||||||
6
libs/red-domain/src/lib/search/search-input.ts
Normal file
6
libs/red-domain/src/lib/search/search-input.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { List } from '@iqser/common-ui';
|
||||||
|
|
||||||
|
export interface ISearchInput {
|
||||||
|
readonly query: string;
|
||||||
|
readonly dossierIds?: List;
|
||||||
|
}
|
||||||
12
libs/red-domain/src/lib/search/search-list-item.ts
Normal file
12
libs/red-domain/src/lib/search/search-list-item.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { IListable, List } from '@iqser/common-ui';
|
||||||
|
|
||||||
|
export interface ISearchListItem extends IListable {
|
||||||
|
readonly dossierId: string;
|
||||||
|
readonly filename: string;
|
||||||
|
readonly unmatched: List | null;
|
||||||
|
readonly highlights: Record<string, List>;
|
||||||
|
readonly routerLink: string;
|
||||||
|
readonly status: string;
|
||||||
|
readonly dossierName: string;
|
||||||
|
readonly numberOfPages: number;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user