diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.test.tsx b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.test.tsx new file mode 100644 index 0000000000..3b5ea67786 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.test.tsx @@ -0,0 +1,84 @@ +import { calculate } from '../calculations' +import type { CalculatorInputs, ValidationResult } from '../types' +import { ResultsPanel } from './ResultsPanel' +import { EuiProvider } from '@elastic/eui' +import { render, screen } from '@testing-library/react' +import type { ComponentProps } from 'react' + +const inputs: CalculatorInputs = { + numVectors: 1_000_000, + numDimensions: 768, + elementType: 'float', + indexType: 'hnsw', + quantization: 'bbq', + replicas: 0, + hnswM: 16, + efConstruction: 100, + vectorsPerCluster: 384, + offHeapRamPercent: 10, +} + +const valid: ValidationResult = { valid: true } + +function renderPanel( + overrides: Partial> = {} +) { + const result = calculate(inputs) + return render( + + + + ) +} + +describe('ResultsPanel', () => { + it('labels RAM plainly and states disk relative to RAM needed for search', () => { + renderPanel() + + expect(screen.getByText('RAM per replica:')).toBeInTheDocument() + expect( + screen.getByText(/Disk is about .+ the RAM needed for search\./) + ).toBeInTheDocument() + expect(screen.queryByText(/Off-heap RAM/)).not.toBeInTheDocument() + expect( + screen.queryByText(/Disk : off-heap RAM/) + ).not.toBeInTheDocument() + }) + + it('calls out serverless on its own line and does not link away', () => { + renderPanel() + + expect( + screen.getByText('Does not apply to Elastic Cloud Serverless.') + ).toBeInTheDocument() + expect( + screen.getByText( + /self-managed Elasticsearch and Elastic Cloud Hosted/ + ) + ).toBeInTheDocument() + expect(screen.queryByText('Learn more')).not.toBeInTheDocument() + }) + + it('hides the compactness sentence and disclaimer when inputs are invalid', () => { + renderPanel({ result: null, inputsValid: false }) + + expect( + screen.queryByText(/RAM needed for search/) + ).not.toBeInTheDocument() + expect( + screen.queryByText(/Elastic Cloud Serverless/) + ).not.toBeInTheDocument() + expect(screen.getByText('RAM per replica:')).toBeInTheDocument() + }) +}) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.tsx b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.tsx index 44a873007b..0ae71e5f03 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/components/ResultsPanel.tsx @@ -1,8 +1,9 @@ import { formatBytesString } from '../calculations' -import { formatGroupedInteger } from '../formatNumbers' +import { formatDiskToRamSentence, formatGroupedInteger } from '../formatNumbers' import type { SizingResult, ValidationResult } from '../types' +import { CalcToolTip } from './CalcToolTip' import { HeroSizeLine } from './HeroSizeLine' -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui' +import { EuiHorizontalRule, EuiText } from '@elastic/eui' interface ResultsPanelProps { result: SizingResult | null @@ -12,11 +13,13 @@ interface ResultsPanelProps { validation: ValidationResult } -const KNN_MEMORY_DOC = - 'https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/approximate-knn-search#_ensure_data_nodes_have_enough_memory' +const SERVERLESS_NOTE = 'Does not apply to Elastic Cloud Serverless.' const SIZING_DISCLAIMER = - 'This calculator is a basic approximation of per-replica and cluster disk and RAM. Real requirements depend on data shape, indexing settings, query patterns, and how you deploy Elasticsearch.' + 'These estimates are disk and RAM per copy for self-managed Elasticsearch and Elastic Cloud Hosted. This RAM is for fast search, not the Java heap, and is not a full node size. Actual needs still depend on data shape, indexing settings, and query patterns.' + +const DISK_TO_RAM_TIP = + 'This compares how much the field stores on disk with how much RAM it wants in the filesystem cache for fast search. A high number, typical of DiskBBQ, means most of the index can stay on disk. It is not a node type, not Java heap, and not a serverless capacity number.' function clusterResourcesLabel(replicas: number): string { if (replicas === 0) { @@ -28,17 +31,6 @@ function clusterResourcesLabel(replicas: number): string { return `Cluster total (1 primary + ${formatGroupedInteger(replicas)} replicas):` } -function formatRatio(value: number): string { - return `${value.toFixed(value >= 10 ? 0 : 1)}×` -} - -function diskToRamRatioLabel(result: SizingResult): string { - if (result.diskToRamRatio <= 0) { - return '-' - } - return formatRatio(result.diskToRamRatio) -} - export function ResultsPanel({ result, inputsValid, @@ -47,6 +39,8 @@ export function ResultsPanel({ validation, }: ResultsPanelProps) { const showBody = inputsValid && result !== null && !validation.warning + const diskToRamSentence = + showBody && result ? formatDiskToRamSentence(result.diskToRamRatio) : '' return (
@@ -107,12 +101,6 @@ export function ResultsPanel({ > RAM per replica: - - Disk : off-heap RAM: -
- - {showBody && result - ? diskToRamRatioLabel(result) - : '-'} -
+ {diskToRamSentence && ( + + + {diskToRamSentence} + + + )} + + + {SERVERLESS_NOTE} + - {SIZING_DISCLAIMER}{' '} - - Learn more - + {SIZING_DISCLAIMER} )} diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.test.ts b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.test.ts new file mode 100644 index 0000000000..f2570788e1 --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.test.ts @@ -0,0 +1,29 @@ +import { formatDiskToRamSentence, formatTimes } from './formatNumbers' + +describe('formatTimes', () => { + it('uses no decimal places at 10× and above', () => { + expect(formatTimes(19.4)).toBe('19×') + expect(formatTimes(10)).toBe('10×') + }) + + it('keeps one decimal place below 10×', () => { + expect(formatTimes(1.54)).toBe('1.5×') + expect(formatTimes(2.3)).toBe('2.3×') + }) +}) + +describe('formatDiskToRamSentence', () => { + it('describes disk relative to the RAM needed for search', () => { + expect(formatDiskToRamSentence(19.4)).toBe( + 'Disk is about 19× the RAM needed for search.' + ) + expect(formatDiskToRamSentence(1.54)).toBe( + 'Disk is about 1.5× the RAM needed for search.' + ) + }) + + it('returns empty when the ratio is not meaningful', () => { + expect(formatDiskToRamSentence(0)).toBe('') + expect(formatDiskToRamSentence(-1)).toBe('') + }) +}) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.ts b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.ts index cba1b3df9d..84fcfddb71 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/formatNumbers.ts @@ -10,6 +10,22 @@ export function formatExactBytes(bytes: number): string { return `${formatGroupedInteger(bytes)} bytes` } +/** Compact multiplier, e.g. `19×` or `1.5×`. */ +export function formatTimes(value: number): string { + return `${value.toFixed(value >= 10 ? 0 : 1)}×` +} + +/** + * Compactness of this field: disk ÷ RAM needed for search. + * Empty when the ratio is not meaningful. + */ +export function formatDiskToRamSentence(ratio: number): string { + if (ratio <= 0) { + return '' + } + return `Disk is about ${formatTimes(ratio)} the RAM needed for search.` +} + /** Normalize user input: commas = thousands, dot = decimal; accepts legacy dot-grouping. */ export function normalizeGroupedNumberInput(raw: string): string { const trimmed = raw.trim() diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/types.ts b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/types.ts index eced103b85..59a21fe556 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/types.ts +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/types.ts @@ -55,7 +55,7 @@ export interface SizingResult { totalRam: number clusterDisk: number clusterRam: number - /** Disk ÷ off-heap RAM ratio (per replica). */ + /** Disk ÷ off-heap RAM working set (per replica). */ diskToRamRatio: number /** Index copies = 1 primary + replicas. */ totalCopies: number diff --git a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/vector-sizing-calculator.css b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/vector-sizing-calculator.css index 6945dc08b1..811cc0a0e6 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/vector-sizing-calculator.css +++ b/src/Elastic.Documentation.Site/Assets/web-components/VectorSizingCalculator/vector-sizing-calculator.css @@ -152,11 +152,40 @@ vector-sizing-calculator:not(:defined) { line-height: 20px !important; } +.vectorSizingCalc__diskRamSentence { + margin-top: 12px; +} + +.vectorSizingCalc__diskRamSentence .vectorSizingCalc__labelRow { + align-items: flex-start; + font-size: 14px; + line-height: 20px; + font-weight: 500; + color: #1d2a3e; + white-space: normal; +} + +.vectorSizingCalc__diskRamSentence + .vectorSizingCalc__labelRow + > span:first-child { + flex: 1 1 auto; + min-width: 0; +} + .vectorSizingCalc__disclaimerFooter { margin-top: auto; padding-top: 16px; } +.vectorSizingCalc__serverlessNote { + display: block; + margin-bottom: 8px; + font-size: 12px !important; + line-height: 16px !important; + font-weight: 600 !important; + color: #1d2a3e !important; +} + .vectorSizingCalc__byteSizeValue { display: inline-block; max-width: 100%; @@ -209,12 +238,6 @@ vector-sizing-calculator:not(:defined) { color: #798eaf !important; } -.vectorSizingCalc__disclaimer .euiLink { - font-size: 12px !important; - line-height: 16px !important; - font-weight: 400 !important; -} - .vectorSizingCalc__sectionTitle { font-size: 12px; line-height: 16px;