Skip to content
Closed
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
46 changes: 46 additions & 0 deletions projects/contract/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,49 @@ export interface PathwayOfEntry {
name?: Highlighted;
hasEHLD?: boolean;
}

// -- The search answers -------------------------------------------------------
//
// Each takes its entry type as an argument: the service writes them with the
// entries it builds, a reader reads them with the view it needs.

/** One group of results: a type's, or "Results" for a search not grouped. */
export interface ResultGroup<Entry> {
/** Left out when the group has none (an ungrouped search that found none). */
entries?: Entry[];
/** Left out for a group Solr names with nothing. */
typeName?: string;
/** How many Solr found. */
entriesCount: number;
/** How many are written here. */
rowCount: number;
}

/** A term the search found nothing for, and whether it names a resource; an empty term or resource is left out. */
export interface TargetResult {
term?: string;
resource?: string | number | boolean;
target: boolean;
}

/** `/search/query`: the groups, the rows written, the groups and matches Solr counted. */
export interface GroupedResult<Entry> {
results: ResultGroup<Entry>[];
rowCount: number;
numberOfGroups: number;
numberOfMatches: number;
/** For a search that found nothing: the terms, and whether each is a target. */
targetResults?: TargetResult[];
}

/** `/search/diagram/{id}` and `/search/fireworks`: the entries, their type facets, Solr's count. */
export interface DiagramResult<Entry> {
/** Left out when there are none. */
entries?: Entry[];
/** Left out when there are none. */
facets?: FacetCount[];
found: number;
}

/** `/search/icon/query`: the icon library's page, one ungrouped group of icons. */
export type IconResult = ResultGroup<IconEntry>;
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<a [href]="`${CONTENT_DETAIL}/${icon().id}`" target="_blank" [matTooltip]="`Open origin of icon ${icon().name}`">
<a
[href]="`${CONTENT_DETAIL}/${icon().id}`"
target="_blank"
[matTooltip]="`Open origin of icon ${icon().iconName ?? icon().id}`"
>
<div [innerHTML]="iconSVG()! | safe: 'html' : true"></div>
</a>
37 changes: 12 additions & 25 deletions projects/pathway-browser/src/app/model/search.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import type { IconEntry, PathwayOfEntry, SearchEntryView } from '../../../../contract/search.ts';
import type {
DiagramResult,
FacetCount,
GroupedResult,
IconEntry,
PathwayOfEntry,
SearchEntryView,
} from '../../../../contract/search.ts';

export namespace Search {
export const EMPTY_RESULTS = { found: 0, facets: [], entries: [] } as Search.Result;
Expand All @@ -21,17 +28,9 @@ export namespace Search {
export type Entry = SearchEntryView;
export type PathwayOf = PathwayOfEntry;

export interface Facet {
name: string;
count: number;
}

// firework and diagram
export interface Result {
entries: Entry[];
facets: Facet[];
found: number;
}
// A diagram's or the species overview's search: the contract's (spec 016).
export type Facet = FacetCount;
export type Result = DiagramResult<Entry>;

// Add a sub-namespace for Icon-specific results
export namespace Icon {
Expand All @@ -40,18 +39,6 @@ export namespace Search {

// A query for icons answers whatever its groups hold: a query matching
// no icon answers proteins, compounds and the like, any kind.
export interface EntryResult {
entries: Search.Entry[];
typeName: string;
entriesCount: number;
rowCount: number;
}

export interface Result {
results: EntryResult[];
rowCount: number;
numberOfGroups: number;
numberOfMatches: number;
}
export type Result = GroupedResult<Search.Entry>;
}
}
2 changes: 1 addition & 1 deletion projects/pathway-browser/src/app/services/icon.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export class IconService {
// No result group at all is no icon, not an error.
map((response) => {
const [group] = response.results;
const [entry] = group?.typeName === 'Icon' ? group.entries : [];
const [entry] = group?.typeName === 'Icon' ? (group.entries ?? []) : [];
return entry && isIcon(entry) ? entry : null;
}),
switchMap((entry) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,13 @@ export class SearchComponent {
// TODO find a way to avoid the break in scroll when loading a new page
export class SearchDataSource extends DataSource<Search.Entry | undefined> {
private readonly _result = signal<Search.Result | undefined>(undefined);
readonly facets = computed(() => this._result()?.facets || []);
// A count Solr gives under no name is written without one: nothing to show
// or filter by, as on the site's search page.
readonly facets = computed(() =>
(this._result()?.facets || []).filter(
(facet): facet is Search.Facet & { name: string } => !!facet.name
)
);
readonly found = computed(() => this._result()?.found || 0);
readonly isLoading = signal<boolean>(false);
/** Pages that could not be loaded, until asked for again. */
Expand Down Expand Up @@ -484,7 +490,7 @@ export class SearchDataSource extends DataSource<Search.Entry | undefined> {
if (!current()) return;
this._result.set(result);
if (this.cachedEntries.length !== result.found) this.cachedEntries.length = result.found;
this.cachedEntries.splice(page * this.pageSize, this.pageSize, ...result.entries);
this.cachedEntries.splice(page * this.pageSize, this.pageSize, ...(result.entries ?? []));
this.dataStream.next(this.cachedEntries);
markFailed(false);
this.isLoading.set(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class InteractorDetailComponent implements OnInit {
// The first hit that is a reference: when nothing matches as an
// interactor the search answers unfiltered, and an Icon can lead.
const entry = search?.results
.flatMap((group) => group.entries)
.flatMap((group) => group.entries ?? [])
.find((hit) => hit.databaseName && hit.referenceIdentifier);
if (entry) {
// The search is highlighted: an identifier or name that matched
Expand Down
30 changes: 20 additions & 10 deletions projects/website-angular/src/app/search/search.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
SearchFilters,
Facet,
NamedFacetCount,
ResultGroup,
ShownGroup,
ShownResult,
} from '../../model/search.model';
import {
Component,
Expand Down Expand Up @@ -108,7 +109,7 @@ export class SearchComponent implements OnInit, OnDestroy {
this.cdr.markForCheck();
}
suggestedTerms: string[] = [];
results: SearchResult | null = null;
results: ShownResult | null = null;
facets: FacetResponse | null = null;
loading = false;
error = '';
Expand Down Expand Up @@ -310,7 +311,15 @@ export class SearchComponent implements OnInit, OnDestroy {
this.hasNoResults = false;
} else {
// Successful API response - check if we have results
const res = results as SearchResult;
const answer = results as SearchResult;
const res: ShownResult = {
...answer,
results: answer.results.map((group) => ({
...group,
entries: group.entries ?? [],
typeName: group.typeName ?? '',
})),
};
// Solr's true total across the whole result set; per-group
// pagination chops each `group.entries` down to ~30, so we
// must NOT recompute this from entries.length later.
Expand Down Expand Up @@ -391,7 +400,7 @@ export class SearchComponent implements OnInit, OnDestroy {
typeName: 'Pages',
entries: pageEntries,
entriesCount: pageEntries.length,
} as ResultGroup,
} as ShownGroup,
];
}
// Keep Solr's biology total; add Pages count (when shown); zero
Expand Down Expand Up @@ -445,6 +454,7 @@ export class SearchComponent implements OnInit, OnDestroy {
return of({
results: [],
rowCount: 0,
numberOfGroups: 0,
numberOfMatches: 0,
});
}
Expand Down Expand Up @@ -479,7 +489,7 @@ export class SearchComponent implements OnInit, OnDestroy {
for (const result of results) {
if (!result?.results.length) continue;
const group = result.results.find((g) => g.typeName === 'Protein');
if (group) allProteins.push(...group.entries);
if (group) allProteins.push(...(group.entries ?? []));
}

// Group by referenceIdentifier, else the entry's own id (its stId, or
Expand Down Expand Up @@ -700,18 +710,18 @@ export class SearchComponent implements OnInit, OnDestroy {
}

// Per-group pagination
getGroupPage(group: ResultGroup): number {
getGroupPage(group: ShownGroup): number {
return this.groupPages[group.typeName] || 0;
}

getGroupTotalPages(group: ResultGroup): number {
getGroupTotalPages(group: ShownGroup): number {
if (group.typeName === 'Protein') {
return Math.ceil(this.uniqueProteins.length / this.groupPageSize) || 1;
}
return Math.ceil(group.entriesCount / this.groupPageSize);
}

getGroupPageEntries(group: ResultGroup): SearchEntry[] {
getGroupPageEntries(group: ShownGroup): SearchEntry[] {
if (group.typeName === 'Protein') {
// While still loading all proteins, show nothing (loading message is displayed)
if (this.proteinLoading) return [];
Expand All @@ -726,7 +736,7 @@ export class SearchComponent implements OnInit, OnDestroy {
return this.groupPageEntries[group.typeName] || group.entries.slice(0, this.groupPageSize);
}

goToGroupPage(group: ResultGroup, page: number): void {
goToGroupPage(group: ShownGroup, page: number): void {
const total = this.getGroupTotalPages(group);
if (page < 0 || page >= total) return;

Expand Down Expand Up @@ -760,7 +770,7 @@ export class SearchComponent implements OnInit, OnDestroy {
});
}

getGroupPageNumbers(group: ResultGroup): (number | '...')[] {
getGroupPageNumbers(group: ShownGroup): (number | '...')[] {
return this.buildPageNumbers(this.getGroupPage(group), this.getGroupTotalPages(group));
}

Expand Down
7 changes: 1 addition & 6 deletions projects/website-angular/src/model/icon.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,5 @@ export type { IconCategory, IconFacetResponse } from '../../../contract/search.t

// One icon: the contract's (spec 016).
export type { IconEntry } from '../../../contract/search.ts';
import type { IconEntry } from '../../../contract/search.ts';

export interface IconResult {
/** Left out when nothing matches. */
entries?: IconEntry[];
entriesCount: number;
}
export type { IconResult } from '../../../contract/search.ts';
23 changes: 12 additions & 11 deletions projects/website-angular/src/model/search.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
export type { SearchEntryView as SearchEntry } from '../../../contract/search.ts';
import type { SearchEntryView } from '../../../contract/search.ts';

export interface ResultGroup {
typeName: string;
entries: SearchEntryView[];
entriesCount: number;
rowCount: number;
}
// A search's answer, as the contract has it, with the view's entries.
import type { GroupedResult, ResultGroup as Group } from '../../../contract/search.ts';
type ResultGroup = Group<SearchEntryView>;
export type SearchResult = GroupedResult<SearchEntryView>;

export interface SearchResult {
results: ResultGroup[];
rowCount: number;
numberOfMatches: number;
}
/**
* A group as the search page keeps it: its entries and name settled when the
* answer arrives, an absent list as none and an absent name as empty text.
* The answer leaves both out at times (an ungrouped page past the end, a
* group Solr names with nothing); the page reads them everywhere.
*/
export type ShownGroup = ResultGroup & Required<Pick<ResultGroup, 'entries' | 'typeName'>>;
export type ShownResult = Omit<SearchResult, 'results'> & { results: ShownGroup[] };

export type { Facet, FacetResponse, NamedFacetCount } from '../../../contract/search.ts';

Expand Down
5 changes: 3 additions & 2 deletions specs/016-api-contract/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ description: 'Tasks for spec 016: one API contract for the content service and t
- [x] T010 [US2] Make the site's `SearchEntry` and `IconEntry` re-export the contract's, and narrow by kind where `search.component.ts`, `interactor-detail.component.ts`, `icon-lib.component.ts` and `icon-detail.component.ts` read kind-specific fields.
- Escape or render `Highlighted` fields as each reader does today, with no change visible on the page.
- Check the search page and an icon page in a browser.
- Done in #691. The site's `SearchEntry` is the contract's `SearchEntryView`: any kind, what every kind carries required, the rest optional (FR-003's view, since the search page shows every kind side by side and a template cannot read a field one kind lacks); `IconEntry` is the contract's kind. Two readers changed: the protein forms' key drops a fallback `id` already covers (`id` is the stId, or the dbId, never empty), and an icon's image is found by `id`, the same value as `stId` but never highlighted. From #690's review, the icon library too (#691): its search is highlighted, and asking beta's service for `R-ICO-012449` there answers `stId` as `<span class="highlighting" >R-ICO-012449</span>`, `id` plain, so its image, link, fetch and shown id were built from markup; they take `id` now, as does the pathway browser's icon load. From #691's review (#697): the search page printed a highlighted reference identifier as text, `PTEN (<span class="highlighting" >P60484</span>)` for a search of P60484, seen in the browser against beta's services; it is rendered as its neighbours are (`[innerHTML]`) and reads `PTEN (P60484)`. An icon's alt text and the pathway browser's icon link take `id`, and the interactor page strips a highlighted name as it strips the identifier. Nothing else visible changes; the search, search-error and icon-library end-to-end specs pass (46 and 78 tests with the specs below). The single icon page reads only type changes; no recording opens one (the `gap` T014 closes).
- Done in #691. The site's `SearchEntry` is the contract's `SearchEntryView`: any kind, what every kind carries required, the rest optional (FR-003's view, since the search page shows every kind side by side and a template cannot read a field one kind lacks); `IconEntry` is the contract's kind. Two readers changed: the protein forms' key drops a fallback `id` already covers (`id` is the stId, or the dbId, never empty), and an icon's image is found by `id`, the same value as `stId` but never highlighted. From #690's review, the icon library too (#691): its search is highlighted, and asking beta's service for `R-ICO-012449` there answers `stId` as `<span class="highlighting" >R-ICO-012449</span>`, `id` plain, so its image, link, fetch and shown id were built from markup; they take `id` now, as does the pathway browser's icon load. From #691's review (#697): the search page printed a highlighted reference identifier as text, `PTEN (<span class="highlighting" >P60484</span>)` for a search of P60484, seen in the browser against beta's services; it is rendered as its neighbours are (`[innerHTML]`) and reads `PTEN (P60484)`. An icon's alt text and the pathway browser's icon link take `id`, and the interactor page strips a highlighted name as it strips the identifier. Nothing else visible changes; the search, search-error and icon-library end-to-end specs passed for #691 (46 and 78 tests with the specs below). The single icon page reads only type changes; no recording opens one (the `gap` T014 closes).
- [x] T011 [US2] Make the pathway browser's `Search.Entry` and `Search.Icon.Entry` re-export the contract's, keeping the value `EMPTY_RESULTS` in the namespace. Adjust `viewport/search/search.component.ts`, `services/icon.service.ts` and `details/tabs/description-tab/icon/icon.component.ts`, and check the diagram search in a browser.
- Done in #691. `Search.Entry` is the view, `Search.Icon.Entry` the icon kind, and an icon query's groups hold any kind (three of the five recorded answers are proteins, compounds or drugs), so `fetchIcon` takes an entry only when it is an icon, not deleted, besides its group's name. Its unit spec's icon is now one as the search writes it, and a deleted icon in an icon group gives none (failing without the check). The flag button takes `id`. The in-diagram search, flagging, pathway-browser, entity popup, structures and pathway-data specs pass; their recordings answer the icon query with icons (R-ICO-013990, R-ICO-012536) and with other kinds.
- [x] T012 [US2] Recount `tools/api-types/known-problems.json`. Record what remains on the search routes, each item with its reason, in this task's PR and here.
Expand All @@ -72,7 +72,8 @@ description: 'Tasks for spec 016: one API contract for the content service and t

**Independent test**: every search route's known problems are at zero or explained.

- [ ] T013 [US3] Write `ResultGroup<E>`, `GroupedResult` (with `numberOfGroups`), the diagram and species-overview result types and `IconResult` (with `rowCount` and `typeName`) in `projects/contract/search.ts`, and point their writers in `search-endpoints.ts` and `search-diagram-endpoints.ts`, and their readers, at them.
- [x] T013 [US3] Write `ResultGroup<E>`, `GroupedResult` (with `numberOfGroups`), the diagram and species-overview result types and `IconResult` (with `rowCount` and `typeName`) in `projects/contract/search.ts`, and point their writers in `search-endpoints.ts` and `search-diagram-endpoints.ts`, and their readers, at them.
- Done in #702. The contract's `ResultGroup<Entry>`, `GroupedResult<Entry>`, `DiagramResult<Entry>`, `TargetResult` and `IconResult`, each taking its entry type as a named argument: the service writes them with its `WrittenEntry`, a reader reads them with the view. A group's `entries` is optional (an ungrouped page past the end has none) and so is its `typeName` (a group Solr names with nothing); the search page settles both once when the answer arrives (`ShownGroup`), and the pathway browser's search shows only named type facets, as the site's does. A target's empty term or resource is left out, so both are optional. A grouping without `matches` is skipped, as SolrJ's `QueryResponse.extractGroupedInfo` skips it (read from its bytecode, from #700's review): the first grouping that has its matches is read, and none is no result, Java's 500 (a spec of two groupings, the first without matches, fails on the first-grouping reading). The search routes' known problems are 0, the whole 12,164. From #697's review, the pathway browser's icon tooltip takes `iconName`, never highlighted.
- [ ] T014 [US3] Record an answer for `/search/icon/{id}` (already in `tools/api-types/calls.ts` as `IconEntry`, with a `gap`) through the e2e suite and remove the `gap` (record rewrites three HARs: check the diff). Type its writer.
- [ ] T015 [US3] Tick spec 013 T019 in `specs/013-server-types/tasks.md` with T002–T014's PR numbers.

Expand Down
2 changes: 0 additions & 2 deletions tools/api-types/known-problems.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
"GET /data/query/{id} as { displayName?: string; name?: string[] }": 221,
"GET /data/species/main as Species[]": 16,
"GET /exporter/reaction/{id}/diagram as ReactionJson": 704,
"GET /search/icon/query as IconResult": 4,
"GET /search/query as SearchResult": 12,
"POST /data/orthologies/ids/species/{dbId} as { [p: string]: SelectableObject }": 3,
"POST /interactors/psicquic/molecules/{resource}/details as Interactors": 1560
}
Loading
Loading