Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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<ComponentProps<typeof ResultsPanel>> = {}
) {
const result = calculate(inputs)
return render(
<EuiProvider
colorMode="light"
globalStyles={false}
utilityClasses={false}
>
<ResultsPanel
result={result}
inputsValid
quantizationLabel="BBQ"
replicas={0}
validation={valid}
{...overrides}
/>
</EuiProvider>
)
}

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()
})
})
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -47,6 +39,8 @@ export function ResultsPanel({
validation,
}: ResultsPanelProps) {
const showBody = inputsValid && result !== null && !validation.warning
const diskToRamSentence =
showBody && result ? formatDiskToRamSentence(result.diskToRamRatio) : ''

return (
<div className="vectorSizingCalc__panel vectorSizingCalc__panel--right">
Expand Down Expand Up @@ -107,12 +101,6 @@ export function ResultsPanel({
>
RAM per replica:
</EuiText>
<EuiText
size="s"
className="vectorSizingCalc__detailLabel"
>
Disk : off-heap RAM:
</EuiText>
</div>
<div className="vectorSizingCalc__resultsDetailValues">
<EuiText
Expand Down Expand Up @@ -145,17 +133,24 @@ export function ResultsPanel({
? formatBytesString(result.totalRam)
: '0 MiB'}
</EuiText>
<EuiText
size="s"
className="vectorSizingCalc__detailValue"
>
{showBody && result
? diskToRamRatioLabel(result)
: '-'}
</EuiText>
</div>
</div>

{diskToRamSentence && (
<EuiText
size="s"
className="vectorSizingCalc__diskRamSentence"
>
<CalcToolTip
content={DISK_TO_RAM_TIP}
position="left"
repositionOnScroll
>
{diskToRamSentence}
</CalcToolTip>
</EuiText>
)}

<EuiHorizontalRule margin="l" />

<EuiText
Expand All @@ -168,18 +163,17 @@ export function ResultsPanel({

{showBody && (
<div className="vectorSizingCalc__disclaimerFooter">
<EuiText
size="xs"
className="vectorSizingCalc__serverlessNote"
>
{SERVERLESS_NOTE}
</EuiText>
<EuiText
size="xs"
className="vectorSizingCalc__disclaimer"
>
{SIZING_DISCLAIMER}{' '}
<EuiLink
href={KNN_MEMORY_DOC}
target="_blank"
external
>
Learn more
</EuiLink>
{SIZING_DISCLAIMER}
</EuiText>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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('')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down Expand Up @@ -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;
Expand Down
Loading