Where Vectors Live: Parquet, NPY and Vector Database Storage

Embeddings are just arrays of numbers, which makes storing them sound like a solved problem. It is, but the default choices are wrong by an order of magnitude — and the mistake is almost always the same one: treating a dense numeric array as though it were a document.

The short version

An embedding is dimensions × 4 bytes in float32. A 1,536-dimension vector is ~6KB; a million of them ~6GB.

Store them binary and columnar — Parquet with metadata, NPY without. Never JSON, which inflates the same data three to four times and has to be parsed on every read.

The size arithmetic

Everything downstream follows from one multiplication, so it is worth having the numbers to hand.

PrecisionBytes/dim1,536-dim vector1 million vectors
float3246.1 KB6.1 GB
float1623.1 KB3.1 GB
int811.5 KB1.5 GB
binary (1-bit)0.125192 B0.2 GB
JSON text~13~20 KB~20 GB

The last row is the one that ruins projects, and it is worth seeing why rather than taking it on faith.

// The same single float, two ways binary float32: 4 bytes JSON text: "-0.023847291946411133"21 bytes // Plus a comma between every pair, plus brackets. // And every read must parse text back into numbers.

So JSON costs you roughly four times the disk, a great deal more memory during parsing, and load times measured in minutes rather than seconds. It is the natural choice if you think of an embedding as data to be transmitted, and the wrong one because an embedding is a fixed-width numeric array that never needs to be human-readable.

The formats, by situation

FormatGood forWeakness
NPY / NPZOne array, one processNo metadata, no filtering
ParquetVectors + metadata at scaleNot a live index
Arrow / FeatherFast interprocess sharingLess compression
HDF5Huge scientific arraysHeavy, fiddly concurrency
SafetensorsModel weights specificallyNot built for row data
CSVNothing hereText overhead, no types
JSONNothing hereSee above

NPY is the right answer when you have one array and one program. It is a tiny header followed by the raw bytes, it memory-maps cleanly, and loading is effectively instant because there is nothing to decode.

Parquet is the default for anything real, because real systems never have only vectors. You have a document ID, a source, a timestamp, a chunk index — and Parquet stores those in columns alongside the embedding.

// Parquet: columnar, so you can read metadata without // touching the vector column at all id │ source │ created │ embedding ────────┼────────────┼────────────┼────────────────── doc_001 │ manual.pdf │ 2026-07-14 │ [1536 floats] doc_002 │ manual.pdf │ 2026-07-14 │ [1536 floats] // "How many chunks from manual.pdf?" reads one column. // In a row format you would read every vector to answer it.

That columnar property is the practical advantage. Filtering, counting and inspecting your corpus are common operations, and in Parquet they never touch the expensive column.

⚠️ Set the float type explicitly

Many pipelines produce float64 by default — double the necessary size for zero retrieval benefit, because the embedding model did not generate that precision in the first place. You are storing rounding noise.

Cast to float32 as a minimum, and to float16 unless you have measured a reason not to. This is a one-line change that halves or quarters your storage, and it is the most commonly missed easy win in embedding pipelines.

Trading precision for space

Embeddings tolerate precision loss far better than intuition suggests, because retrieval depends on the relative ordering of similarity scores rather than their exact values. Small perturbations rarely reorder the top results.

float32 → float16 // 50% smaller, recall impact negligible float32 → int8 // 75% smaller, small measurable loss float32 → binary // 97% smaller, real loss — needs rescoring

The standard pattern at scale is two-stage retrieval: search a heavily quantised index to get a few hundred candidates fast, then rescore just those against full-precision vectors. You get most of the speed and storage benefit of quantisation with accuracy close to the full-precision baseline, because the aggressive stage only has to be good enough to keep the right answers in the candidate set.

When you actually need a vector database

Vector databases are excellent and frequently adopted a year before they are needed. The honest threshold is higher than the marketing implies.

// Brute force over a numpy array — the whole search scores = embeddings @ query // one matrix multiply top = np.argpartition(scores, -k)[-k:] // 100k vectors × 1,536 dims: a few milliseconds. // Exact results — no approximation, no index to tune.

Below roughly 100,000 vectors this is genuinely the right answer, and it has properties an approximate index does not: results are exact, there is nothing to tune, and there is no service to operate.

ScaleApproach
< 10kArray in memory, brute force
10k – 100kArray or an embedded index
100k – 10MLocal ANN index, or a vector DB
> 10MVector database
Any scale, live writesVector database

That last row matters more than the size thresholds. Static corpora are easy at any scale — build the index offline and serve it. Corpora with constant inserts, updates and deletes are where a database earns its keep, because maintaining a mutable approximate index correctly is genuinely hard and not something to build yourself.

🚨 Keep the source text, and keep it linked

An embedding is a one-way transformation. You cannot recover the text from the vector, so a store of vectors without their source text is a store of numbers nobody can interpret.

Worse, you cannot re-embed. When you change embedding model — and you will, they improve — every existing vector becomes incompatible and the entire corpus must be regenerated from the original text. Store the text, the chunk boundaries, and which model and version produced each vector. That last field is the one everyone omits and everyone later needs.

A storage layout that survives contact with reality

corpus/ documents.parquet // id, path, hash, ingested_at chunks.parquet // id, doc_id, text, start, end embeddings_v2.parquet // chunk_id, vector, model, dims index/ // built artefacts — disposable

Three properties make this hold up over time:

  • Text and vectors are separate. Re-embedding writes a new file; nothing else changes.
  • The model version is a column, so mixed-generation corpora are detectable rather than silently broken.
  • The index is disposable. Anything you can rebuild from the parquet files should not be your source of truth, and treating an index as durable state is how corpora become unreproducible.

If you are moving between formats during a migration, our JSON to CSV converter handles the metadata side in the browser — though for the vector columns themselves, a binary format is the destination you want.

Working with the metadata around your vectors?

Convert and validate JSON and CSV entirely in your browser — nothing is uploaded to a server.

Open JSON to CSV →

Summary

  • dimensions × 4 bytes is the float32 size. 1M × 1,536-dim ≈ 6GB.
  • JSON inflates vectors three to four times and must be parsed on every read.
  • Parquet for vectors plus metadata; NPY for a bare array.
  • Cast to float32 or float16 explicitly — float64 defaults store rounding noise.
  • Quantisation is cheap accuracy-wise; pair aggressive levels with a rescoring pass.
  • Under ~100k vectors, brute force is exact and fast enough.
  • Live writes justify a vector database more than raw scale does.
  • Always keep the source text and the model version. You will re-embed.

Frequently Asked Questions

How much disk space do embeddings take?

As raw float32, it is dimensions times four bytes per vector — so a 1,536-dimension embedding is about 6KB, and a million of them is roughly 6GB. Halving precision to float16 halves that with negligible retrieval impact, and quantising to 8-bit integers cuts it to a quarter.

Why shouldn't I store embeddings as JSON?

Because every float becomes a text string of up to 20 characters instead of 4 bytes, inflating storage roughly three to four times, and every read has to parse text back into numbers. A million vectors that occupy 6GB in binary can exceed 20GB as JSON, and load times go from seconds to minutes.

What is the best file format for storing embeddings?

For a single array you control, NPY is simple and fast. For anything with metadata alongside the vectors, Parquet is the practical default — it is columnar, compressed, widely supported and lets you filter without reading the vectors. Both beat JSON and CSV by a wide margin.

Do I need a vector database?

Below roughly 100,000 vectors, usually not — a brute-force search over an in-memory array is fast enough and far simpler to operate. Vector databases earn their complexity at larger scale, or when you need live inserts, metadata filtering and persistence without building those yourself.

Does reducing float precision hurt search quality?

Very little for float16, which is why it is a common default. More aggressive quantisation to 8-bit or binary trades measurable recall for large storage and speed gains, and is usually paired with a rescoring pass over full-precision vectors for the top candidates.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.