Classify a star for the rows that list it, not every star each time the index is built
The search index and the route index gave every one of the 455 571 stars a subtitle up front through spectralClassification, which filtered the dwarf table afresh on each call; the star field's tints read the same table once per BP−RP star. Both indices now carry the star itself and classify it only for the rows shown (entrySubtitle): eight search results, a few route options. The two filtered columns of the table are built once. Node, over the shipped catalogue, five runs: classifying every star 121-167 ms before, 56-62 after hoisting the table alone; reading the sequence at every BP−RP colour 187-216 ms, now 111-127. In the app on :4302, two cold loads each, the long task when the Search tab opens was 264-380 ms (median 280) before and 164-276 ms (median 171) after; the longest boot task 863 and 875 ms before, 654 and 741 after. Tests: the search spec checks its index holds no classification and the row still reads "Star · ~M8"; the scene spec now reads the route options, and checks its index holds none either. Controls: classifying every star in the search index, doing so in the route index, and route options printing the index's subtitle each fail the named test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -964,9 +964,20 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('offers a star for a route by its classification, and a star with none by its name alone', () => {
|
it('offers a star for a route by its classification, and a star with none by its name alone', () => {
|
||||||
const component = fixture.componentInstance as unknown as { starSearchIndex(): { entry: { starId?: number; subtitle: string } }[]; currentStarOption(): { subtitle: string } | null };
|
const component = fixture.componentInstance as unknown as {
|
||||||
expect(component.starSearchIndex().find(({ entry }) => entry.starId === LENS.id)?.entry.subtitle).toBe('');
|
starSearchIndex(): { entry: { starId?: number; subtitle: string } }[];
|
||||||
expect(component.starSearchIndex().find(({ entry }) => entry.starId === PROXIMA.id)?.entry.subtitle).toBe('M5V');
|
routeOptions(): { id: number; subtitle: string }[];
|
||||||
|
onRouteQuery(query: string): void;
|
||||||
|
currentStarOption(): { subtitle: string } | null;
|
||||||
|
};
|
||||||
|
const optionFor = (query: string, id: number): string | undefined => {
|
||||||
|
component.onRouteQuery(query);
|
||||||
|
return component.routeOptions().find((option) => option.id === id)?.subtitle;
|
||||||
|
};
|
||||||
|
expect(optionFor('KMT-2016', LENS.id)).toBe('');
|
||||||
|
expect(optionFor('Proxima', PROXIMA.id)).toBe('M5V');
|
||||||
|
// Classified for the options shown only: the index holds none, where all 455 571 stars cost 140-230 ms at boot.
|
||||||
|
expect(component.starSearchIndex().every(({ entry }) => entry.subtitle === '')).toBe(true);
|
||||||
navigationStore.selectStar(LENS.id);
|
navigationStore.selectStar(LENS.id);
|
||||||
expect(component.currentStarOption()?.subtitle).toBe('');
|
expect(component.currentStarOption()?.subtitle).toBe('');
|
||||||
navigationStore.selectStar(PROXIMA.id);
|
navigationStore.selectStar(PROXIMA.id);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import {
|
|||||||
HudReadout,
|
HudReadout,
|
||||||
} from '../hud/hud-dock.component';
|
} from '../hud/hud-dock.component';
|
||||||
import { RouteRequest, RouteResult, RouteStarOption } from '../hud/routes-panel.component';
|
import { RouteRequest, RouteResult, RouteStarOption } from '../hud/routes-panel.component';
|
||||||
import { buildSearchIndex, IndexedSearchEntry, rankSearchResults } from '../search/search-ranking';
|
import { buildSearchIndex, entrySubtitle, IndexedSearchEntry, rankSearchResults } from '../search/search-ranking';
|
||||||
import { StarmapHudComponent } from './starmap-hud.component';
|
import { StarmapHudComponent } from './starmap-hud.component';
|
||||||
import { SystemObjectCardComponent } from './system-object-card.component';
|
import { SystemObjectCardComponent } from './system-object-card.component';
|
||||||
import { RoutingClient } from './routing-client';
|
import { RoutingClient } from './routing-client';
|
||||||
@@ -526,7 +526,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
return rankSearchResults(index, query, ROUTE_OPTION_COUNT).flatMap((entry) =>
|
return rankSearchResults(index, query, ROUTE_OPTION_COUNT).flatMap((entry) =>
|
||||||
entry.starId === undefined
|
entry.starId === undefined
|
||||||
? []
|
? []
|
||||||
: [{ id: entry.starId, name: entry.name, subtitle: entry.subtitle }],
|
: [{ id: entry.starId, name: entry.name, subtitle: entrySubtitle(entry) }],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
private readonly routeQuery = signal('');
|
private readonly routeQuery = signal('');
|
||||||
@@ -733,7 +733,8 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
stars.map((star) => ({
|
stars.map((star) => ({
|
||||||
kind: 'star' as const,
|
kind: 'star' as const,
|
||||||
name: star.name,
|
name: star.name,
|
||||||
subtitle: spectralClassification(star),
|
subtitle: '',
|
||||||
|
star,
|
||||||
starId: star.id,
|
starId: star.id,
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { spectralClassification } from '../../shared/astro/spectral';
|
||||||
|
|
||||||
export type SearchResultKind = 'star' | 'body' | 'exoplanet';
|
export type SearchResultKind = 'star' | 'body' | 'exoplanet';
|
||||||
|
|
||||||
export interface SearchEntry {
|
export interface SearchEntry {
|
||||||
@@ -6,10 +8,21 @@ export interface SearchEntry {
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
/** HYG star id, for `kind: 'star'` results. */
|
/** HYG star id, for `kind: 'star'` results. */
|
||||||
starId?: number;
|
starId?: number;
|
||||||
|
/**
|
||||||
|
* The star behind a `kind: 'star'` entry, classified by {@link entrySubtitle} for the rows shown
|
||||||
|
* only. Classified up front, all 455 571 took 140-230 ms of the main thread at boot, for the
|
||||||
|
* route index, and again each time the search tab was opened.
|
||||||
|
*/
|
||||||
|
star?: Parameters<typeof spectralClassification>[0];
|
||||||
/** `bodies.json`/`exoplanets.json` id, for `kind: 'body' | 'exoplanet'` results. */
|
/** `bodies.json`/`exoplanets.json` id, for `kind: 'body' | 'exoplanet'` results. */
|
||||||
bodyId?: string;
|
bodyId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The line an entry is listed with: its subtitle, or a star's classification. */
|
||||||
|
export function entrySubtitle(entry: SearchEntry): string {
|
||||||
|
return entry.star ? spectralClassification(entry.star) : entry.subtitle;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How well a name matches, best first. The gaps are what matter: any exact match outranks every
|
* How well a name matches, best first. The gaps are what matter: any exact match outranks every
|
||||||
* prefix match, and so on, so a better kind of match can never be crowded out by a worse one.
|
* prefix match, and so on, so a better kind of match can never be crowded out by a worse one.
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ describe('SearchComponent', () => {
|
|||||||
expect(kindLine()).toBe('Star · ~M8');
|
expect(kindLine()).toBe('Star · ~M8');
|
||||||
await type('KMT-2016-BLG-1107L');
|
await type('KMT-2016-BLG-1107L');
|
||||||
expect(kindLine()).toBe('Star');
|
expect(kindLine()).toBe('Star');
|
||||||
|
// Classified for the rows shown only: over all 455 571 stars, each opening of the tab spent 140-230 ms on it.
|
||||||
|
const index = (fixture.componentInstance as unknown as { index(): { entry: { kind: string; subtitle: string } }[] }).index();
|
||||||
|
expect(index.filter(({ entry }) => entry.kind === 'star').every(({ entry }) => entry.subtitle === '')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows nothing for a query that matches nothing', async () => {
|
it('shows nothing for a query that matches nothing', async () => {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { Component, computed, ElementRef, output, signal, viewChild } from '@ang
|
|||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { DataLoaderService } from '../../core/data/data-loader.service';
|
import { DataLoaderService } from '../../core/data/data-loader.service';
|
||||||
import { spectralClassification } from '../../shared/astro/spectral';
|
|
||||||
import { NavigationStore } from '../../shared/state/navigation.store';
|
import { NavigationStore } from '../../shared/state/navigation.store';
|
||||||
import { ReticleIconComponent } from '../../shared/ui/reticle-icon.component';
|
import { ReticleIconComponent } from '../../shared/ui/reticle-icon.component';
|
||||||
import { buildSearchIndex, IndexedSearchEntry, rankSearchResults, SearchEntry, SearchResultKind } from './search-ranking';
|
import { buildSearchIndex, entrySubtitle, IndexedSearchEntry, rankSearchResults, SearchEntry, SearchResultKind } from './search-ranking';
|
||||||
|
|
||||||
const MAX_RESULTS = 8;
|
const MAX_RESULTS = 8;
|
||||||
const MIN_QUERY_LENGTH = 2;
|
const MIN_QUERY_LENGTH = 2;
|
||||||
@@ -108,7 +107,7 @@ export class SearchComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
readonly matchTotal = computed(() => this.matches().length);
|
readonly matchTotal = computed(() => this.matches().length);
|
||||||
readonly results = computed(() => this.matches().slice(0, MAX_RESULTS));
|
readonly results = computed(() => this.matches().slice(0, MAX_RESULTS).map((entry) => ({ ...entry, subtitle: entrySubtitle(entry) })));
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dataLoader: DataLoaderService,
|
private readonly dataLoader: DataLoaderService,
|
||||||
@@ -154,7 +153,7 @@ export class SearchComponent {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const entries: SearchEntry[] = [
|
const entries: SearchEntry[] = [
|
||||||
...stars.map((star): SearchEntry => ({ kind: 'star', name: star.name, subtitle: spectralClassification(star), starId: star.id })),
|
...stars.map((star): SearchEntry => ({ kind: 'star', name: star.name, subtitle: '', star, starId: star.id })),
|
||||||
...bodies.map((body): SearchEntry => ({ kind: 'body', name: body.name, subtitle: body.kind, bodyId: body.id })),
|
...bodies.map((body): SearchEntry => ({ kind: 'body', name: body.name, subtitle: body.kind, bodyId: body.id })),
|
||||||
...exoplanets.map((exoplanet): SearchEntry => ({ kind: 'exoplanet', name: exoplanet.name, subtitle: exoplanet.hostStarName, bodyId: exoplanet.id }))
|
...exoplanets.map((exoplanet): SearchEntry => ({ kind: 'exoplanet', name: exoplanet.name, subtitle: exoplanet.hostStarName, bodyId: exoplanet.id }))
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -158,6 +158,13 @@ const DWARF_SEQUENCE: readonly (readonly [string, number, number | null, number,
|
|||||||
['M8.5', 2.16, 5.1, 2420, -5.78, -3.09]
|
['M8.5', 2.16, 5.1, 2420, -5.78, -3.09]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The rows each colour is tabulated for, filtered once. Filtered on every call, the search index
|
||||||
|
* and the star field's tints, which read the table for each of the 455 571 stars, spent 140-230
|
||||||
|
* ms of the main thread on it at boot, and the search index again on each opening of its tab.
|
||||||
|
*/
|
||||||
|
const ROWS_WITH_COLOUR = { 1: DWARF_SEQUENCE.filter((row) => row[1] !== null), 2: DWARF_SEQUENCE.filter((row) => row[2] !== null) } as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The spectral type of the dwarf whose colour is nearest, for the stars no catalogue classified —
|
* The spectral type of the dwarf whose colour is nearest, for the stars no catalogue classified —
|
||||||
* every Gaia star, 83 % of the map. An estimate, and the caller must say so: it assumes a dwarf,
|
* every Gaia star, 83 % of the map. An estimate, and the caller must say so: it assumes a dwarf,
|
||||||
@@ -167,7 +174,7 @@ const DWARF_SEQUENCE: readonly (readonly [string, number, number | null, number,
|
|||||||
*/
|
*/
|
||||||
export function spectralTypeFromColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): string | null {
|
export function spectralTypeFromColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): string | null {
|
||||||
const column = system === 'B-V' ? 1 : 2;
|
const column = system === 'B-V' ? 1 : 2;
|
||||||
const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null);
|
const rows = ROWS_WITH_COLOUR[column];
|
||||||
if (colorIndex === null || !(colorIndex >= rows[0][column]! && colorIndex <= rows[rows.length - 1][column]!)) {
|
if (colorIndex === null || !(colorIndex >= rows[0][column]! && colorIndex <= rows[rows.length - 1][column]!)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -213,7 +220,7 @@ export interface DwarfSequencePoint {
|
|||||||
*/
|
*/
|
||||||
export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V', clampToTable = false): DwarfSequencePoint | null {
|
export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V', clampToTable = false): DwarfSequencePoint | null {
|
||||||
const column = system === 'B-V' ? 1 : 2;
|
const column = system === 'B-V' ? 1 : 2;
|
||||||
const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null);
|
const rows = ROWS_WITH_COLOUR[column];
|
||||||
const [bluest, reddest] = [rows[0][column]!, rows[rows.length - 1][column]!];
|
const [bluest, reddest] = [rows[0][column]!, rows[rows.length - 1][column]!];
|
||||||
if (colorIndex === null || !Number.isFinite(colorIndex) || (!clampToTable && !(colorIndex >= bluest && colorIndex <= reddest))) {
|
if (colorIndex === null || !Number.isFinite(colorIndex) || (!clampToTable && !(colorIndex >= bluest && colorIndex <= reddest))) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user