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
1 change: 1 addition & 0 deletions src/Elastic.Documentation.Site/Assets/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import('./web-components/VersionDropdown')
import('./web-components/AppliesToPopover')
import('./web-components/FullPageSearch/FullPageSearchComponent')
import('./web-components/Diagnostics/DiagnosticsComponent')
import('./web-components/VectorSizingCalculator/VectorSizingCalculatorComponent')

const { getOS } = new UAParser()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { EuiSpacer, EuiText } from '@elastic/eui';
import type {
ElementType,
IndexType,
Quantization,
CalculatorInputs,
} from './types';
import {
calculate,
validate,
getAvailableQuantizations,
} from './calculations';
import { ConfigurationPanel } from './components/ConfigurationPanel';
import { ResultsPanel } from './components/ResultsPanel';
import { ClusterTotals } from './components/ClusterTotals';
import { FormulasPanel } from './components/FormulasPanel';

function getAvailableIndexTypes(elementType: ElementType) {
const options = [
{ value: 'hnsw', text: 'HNSW' },
{ value: 'flat', text: 'Flat (brute-force)' },
];
if (elementType === 'float' || elementType === 'bfloat16') {
options.push({ value: 'disk_bbq', text: 'DiskBBQ' });
}
return options;
}

function parseVectorCount(s: string): number {
if (!s) return NaN;
const clean = s.trim().replace(/,/g, '');
const multipliers: Record<string, number> = {
k: 1e3, K: 1e3, m: 1e6, M: 1e6, b: 1e9, B: 1e9,
};
const match = clean.match(/^(\d+\.?\d*)\s*([kKmMbB])?$/);
if (!match) return parseInt(clean, 10);
return Math.round(
parseFloat(match[1]) * (match[2] ? multipliers[match[2]] : 1)
);
}

export function Calculator() {
const [vectorsText, setVectorsText] = useState('');
const [numDimensions, setNumDimensions] = useState<number | string>('');
const [elementType, setElementType] = useState<ElementType>('float');
const [indexType, setIndexType] = useState<IndexType>('hnsw');
const [quantization, setQuantization] = useState<Quantization>('none');
const [replicas, setReplicas] = useState(1);
const [hnswM, setHnswM] = useState(16);
const [efConstruction, setEfConstruction] = useState(100);
const [vectorsPerCluster, setVectorsPerCluster] = useState(384);

const indexTypeOptions = useMemo(() => getAvailableIndexTypes(elementType), [elementType]);
const quantOptions = useMemo(() => getAvailableQuantizations(elementType, indexType), [elementType, indexType]);

useEffect(() => {
const available = indexTypeOptions.map((o) => o.value);
if (!available.includes(indexType)) {
setIndexType(available[0] as IndexType);
}
}, [indexTypeOptions, indexType]);

useEffect(() => {
const available = quantOptions.map((o) => o.value);
if (!available.includes(quantization)) {
setQuantization(available[0] as Quantization);
}
}, [quantOptions, quantization]);

const inputs: CalculatorInputs = useMemo(() => ({
numVectors: parseVectorCount(vectorsText),
numDimensions: typeof numDimensions === 'number' ? numDimensions : NaN,
elementType,
indexType,
quantization,
replicas,
hnswM,
efConstruction,
vectorsPerCluster,
}), [vectorsText, numDimensions, elementType, indexType, quantization, replicas, hnswM, efConstruction, vectorsPerCluster]);

const validation = useMemo(() => validate(inputs), [inputs]);
const result = useMemo(() => (validation.valid ? calculate(inputs) : null), [inputs, validation]);

const handleVectorsBlur = useCallback(() => {
const v = parseVectorCount(vectorsText);
if (!isNaN(v) && v > 0) {
setVectorsText(v.toLocaleString('en-US'));
}
}, [vectorsText]);

return (
<>
<ConfigurationPanel
vectorsText={vectorsText}
onVectorsChange={setVectorsText}
onVectorsBlur={handleVectorsBlur}
numDimensions={numDimensions}
onDimensionsChange={setNumDimensions}
elementType={elementType}
onElementTypeChange={setElementType}
indexType={indexType}
onIndexTypeChange={setIndexType}
indexTypeOptions={indexTypeOptions}
quantization={quantization}
onQuantizationChange={setQuantization}
quantOptions={quantOptions}
replicas={replicas}
onReplicasChange={setReplicas}
hnswM={hnswM}
onHnswMChange={setHnswM}
efConstruction={efConstruction}
onEfConstructionChange={setEfConstruction}
vectorsPerCluster={vectorsPerCluster}
onVectorsPerClusterChange={setVectorsPerCluster}
validation={validation}
/>

<EuiSpacer size="m" />
<ResultsPanel result={result} />

{result && replicas > 0 && (
<>
<EuiSpacer size="m" />
<ClusterTotals result={result} replicas={replicas} />
</>
)}

{result && (
<>
<EuiSpacer size="m" />
<FormulasPanel formulas={result.formulas} />
</>
)}

<EuiSpacer size="s" />
<EuiText size="xs" color="subdued">
<em>
Estimates are approximate — run benchmarks with your specific
dataset for production sizing.
</em>
</EuiText>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '../../eui-icons-cache'
import { Calculator } from './Calculator'
import { EuiProvider } from '@elastic/eui'
import r2wc from '@r2wc/react-to-web-component'
import * as React from 'react'
import { StrictMode } from 'react'

const VectorSizingCalculatorWrapper = () => {
return (
<StrictMode>
<EuiProvider
colorMode="light"
globalStyles={false}
utilityClasses={false}
>
<Calculator />
</EuiProvider>
</StrictMode>
)
}

if (!customElements.get('vector-sizing-calculator')) {
customElements.define(
'vector-sizing-calculator',
r2wc(VectorSizingCalculatorWrapper)
)
}
Loading
Loading