Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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