fix RED-2675: search screen not ordering by score

This commit is contained in:
Dan Percic 2021-11-08 19:22:32 +02:00
parent b67130039e
commit 3d0999bd3e
7 changed files with 76 additions and 62 deletions

View File

@ -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';

View File

@ -47,9 +47,7 @@
{{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched" {{ 'search-screen.missing' | translate }}:<span *ngFor="let term of unmatched"
>&nbsp;<s>{{ term }}</s></span >&nbsp;<s>{{ term }}</s></span
>.&nbsp;{{ 'search-screen.must-contain' | translate }}: >.&nbsp;{{ '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"
>&nbsp;<u>{{ term }}</u></span >&nbsp;<u>{{ term }}</u></span
> >
</span> </span>

View File

@ -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}`,
}; };
} }

View File

@ -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);
} }
} }

View File

@ -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';

View File

@ -0,0 +1,6 @@
import { List } from '@iqser/common-ui';
export interface ISearchInput {
readonly query: string;
readonly dossierIds?: List;
}

View 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;
}