Six million tokens. That is what the Acme documentation corpus came to once Part 9 finished chunking it, and embedding every one of those tokens with text-embedding-3-small costs twelve cents. Once. Not twelve cents a month, not twelve cents a query, twelve cents in total for the whole index.
So embedding cost is not what this decision is about, whatever the pricing pages imply. What you are actually choosing is a vector width, and vector width sets how much RAM your search index needs for as long as the product exists. Pick 3072 dimensions and you have signed up for four times the memory of 768, forever, in exchange for a recall improvement you have not measured yet.
Key takeaways
Embedding a 50,000 chunk corpus costs 12 cents with text-embedding-3-small and 79 cents with text-embedding-3-large. Index memory is where the money goes: 307 MB against 614 MB for the same corpus, growing linearly forever.
On my 60 question eval set, recall@5 flattened at 1024 dimensions. Going from 1024 to 3072 bought 0.02 recall and tripled memory.
Truncating dimensions does not reduce your API bill by a cent, because billing is on input tokens. It only reduces storage and query time, which is exactly where your recurring cost lives.
MTEB rank is a poor selection signal. Scores are self reported with no independent verification step, and general retrieval performance does not predict performance on your corpus.
Project status, and why this choice is expensive to reverse
Last part the assistant learned to cut documents properly. Heading paths are prepended, tables survive intact, and 50,000 chunks now sit in a file with a mean length of 480 characters. This part turns each of those chunks into a vector and picks the model that does it. Part 11 puts those vectors into an actual store and compares pgvector, Chroma and Qdrant on the result.
Chunk size, as Part 9 noted, invalidates an index when you change it. Embedding model choice is worse. A query vector and a document vector only mean anything relative to each other if the same model produced both, so switching models means re-embedding every chunk, rebuilding every index, and, if the dimension count changed, altering your schema. There is no incremental migration. You run the old and new indexes side by side or you take an outage.
Which is why I want you to reason about three costs before you reason about quality at all. One time embedding cost, paid at ingestion. Recurring index memory, paid every hour the service runs. And query side embedding latency, paid on the critical path of every single search, because the user question has to become a vector before you can look anything up.
Arithmetic of dimensions, storage and query memory
Start by measuring rather than estimating. Below, one batch of 512 chunks goes through the embeddings endpoint and reports back everything you need to extrapolate: how many dimensions came out, how many tokens went in, and how long it took.
Three things fall straight out of that output. Mean chunk length is 120.8 tokens, so the full corpus is about 6.04 million tokens, which at two cents per million is 12 cents on text-embedding-3-small and 79 cents on text-embedding-3-large. Serial ingestion at 3.41 seconds per 512 chunks would take roughly five and a half minutes for the corpus, so parallelism is not urgent here but will be at ten times the size. And the returned vector has a norm of exactly 1.0000, meaning it is already unit length, so a dot product is a cosine similarity and you can skip a normalisation step.
Storage is simple arithmetic and worth doing on a napkin before you commit. Vectors are float32 by default, so bytes equals chunk count times dimensions times four. Graph indexes such as HNSW add their neighbour lists on top, which in my experience runs 30 to 50 percent above the raw vector bytes. Here is the table I keep open when someone asks which model to use.
| Dimensions | Raw vectors, 50k chunks | With HNSW, about 1.4x | Recall@5, my eval set | Where it fits |
|---|---|---|---|---|
| 384, bge-small self hosted | 76.8 MB | 108 MB | 0.76 | Data cannot leave the network, or cost per query must be zero |
| 512, 3-large truncated | 102 MB | 143 MB | 0.80 | Corpora above roughly 10 million chunks |
| 1024, 3-large truncated | 205 MB | 287 MB | 0.85 | My default once a corpus passes about 1 million chunks |
| 1536, 3-small full | 307 MB | 430 MB | 0.83 | Where I start every new project |
| 3072, 3-large full | 614 MB | 860 MB | 0.87 | Only with an eval set proving the last 0.02 matters |
Candidates worth benchmarking in 2026
Five candidates cover almost every real situation. I show the concept with OpenAI because its pricing and dimension controls are documented plainly, but the shape of the decision is identical on Cohere, Voyage and a self hosted model, and if you are already committed to a cloud, the AWS and Google Cloud series cover the vendor specific plumbing.
| Model | Dimensions | Max input | Price per 1M tokens | Hosting |
|---|---|---|---|---|
| text-embedding-3-small | 1536, reducible to 256 | 8,192 tokens | 0.02 USD, 0.01 on batch | API only |
| text-embedding-3-large | 3072, reducible to 256 | 8,192 tokens | 0.13 USD, 0.065 on batch | API only |
| Cohere embed v4 | 1024 default, Matryoshka options | long, check current docs | list price disputed across sources [VERIFY] | API, also via Bedrock and Azure |
| Voyage voyage-3 family | 1024 default, 256 to 2048 | long, check current docs | list price disputed across sources [VERIFY] | API |
| BAAI bge-small-en-v1.5 | 384, fixed | 512 tokens | no per token cost, you pay for compute | Self hosted, runs on CPU |
Note the fifth row carefully, because it is the one that catches teams out. A 512 token input limit means bge-small silently truncates anything longer, and the 480 character chunks from Part 9 are safely inside it while a 2,000 character chunk is not. Self hosted models tend to have short input windows, and a short input window is a constraint on your chunker, not a footnote.
Measuring recall on your own corpus
An eval set for this purpose is embarrassingly cheap to build and almost nobody builds one. Take 60 questions your support team actually gets, find the chunk that answers each, and store the pair. Two hours of work. That artifact then outlives every model you will ever try, and Part 20 makes the case for building it before the feature rather than after.
Recall@5 of 0.83 means the answering chunk appeared in the top five for 50 of 60 questions. Whether that is good depends entirely on what happens next, which is why Part 13 insists on separating retrieval evaluation from answer evaluation. For now it is a baseline, and a baseline is all you need to compare models against each other honestly.
Where common advice is wrong
Standard guidance is to open the MTEB leaderboard and take something near the top. Two problems. MTEB scores are submitted by model providers themselves and, although the evaluation code is open source, there is no independent verification step, so the leaderboard is closer to a self declaration register than a referee. And a Borda style aggregate across 100 plus tasks in dozens of languages tells you about general competence, not about English software documentation, which is the only distribution you care about.
Use the leaderboard to build a shortlist of four or five, then rank that shortlist on your own 60 questions. On the Acme corpus my shortlist reordered itself once I did that, and the model I would have picked from rank alone came third.
Matryoshka truncation, and where recall stops improving
Both text-embedding-3 models are trained so that the leading portion of the vector carries most of the signal, which means you can ask for a shorter vector and lose surprisingly little. Pass the dimensions parameter and the API returns a shorter, already renormalised vector. OpenAI reported that the large model shortened to 256 dimensions still outscores the older ada-002 model at its full 1536 on MTEB, which is the clearest possible statement that default width is not where the quality lives.
That last column surprises people, and it changes how you should think about the parameter. Truncation is not a cost optimisation on ingestion, it is purely a storage and latency optimisation. Which is fine, because storage and latency are the recurring costs and ingestion is not.
I learned to read that chart the hard way. On a support search system I shipped text-embedding-3-large at its full 3072 dimensions, because it won my comparison by three points and the whole embedding run cost 60 dollars, which felt like it settled the argument. Nine months later the pgvector table held 3.1 million ticket chunks at 38 GB, and once the index stopped fitting in the instance memory, p95 search latency drifted from 240 ms to 1.9 seconds over about ten days as traffic grew. No deploy had touched that code path. Re-embedding at 1024 dimensions over a weekend brought the index to 12.7 GB and p95 to 310 ms, and cost 0.02 of recall on the same eval set. I had bought three points of a benchmark and paid for it with a production incident.
One detail from that migration is worth stealing. If you truncate vectors yourself rather than asking the API for a shorter one, you must renormalise afterwards, because slicing a unit vector leaves you with something shorter than unit length and your cosine similarities quietly stop being cosine similarities. Passing the dimensions parameter avoids this entirely, which is the reason to prefer it over slicing in numpy.
Errors you will hit while embedding a corpus
Two of these will find you on your first full ingestion run, and both are loud, which is a mercy after Part 9 where every failure was silent.
Second one only appears when you change your mind, which you will. Dimension counts are baked into a pgvector column type, and no amount of hoping makes an existing column accept a different width.
A third failure deserves a mention even though it raises nothing. Query text and document text must go through the same model, and with some providers through the same input type flag as well, since Cohere distinguishes a search document from a search query and returns different vectors for identical text. Get that wrong and everything runs, nothing errors, and recall lands somewhere near random. Record model name, dimension count and input type together in one config object, and log all three with every ingestion run, in the same spirit as tracking model versions with MLflow.
Start on small at full width, move to large at 1024 only on evidence
My recommendation for the assistant is text-embedding-3-small at its full 1536 dimensions: 12 cents to build, 307 MB of vectors, recall@5 of 0.83, and no tuning to think about on day one. When an eval set shows that is not enough, move to text-embedding-3-large truncated to 1024, which on this corpus bought 0.02 recall for 100 MB less memory than the small model at full width. Self hosted bge-small is the right answer only when data cannot leave your network or when per query cost must be zero, and you should expect to give up around 0.07 recall for that.
What I would avoid is text-embedding-3-large at its default 3072, and I would avoid it specifically as a default rather than as a considered choice. Twice the memory of 1536 for 0.04 recall is a trade you might rationally make, but almost nobody making it has measured either side. If you are running it today, truncate to 1024 and re-run your eval before you assume you need the width.
On Monday, do one calculation. Multiply your chunk count by your dimension count by four bytes, add 40 percent for graph overhead, and hold that number up against the RAM on the machine your vector store runs on. If it is more than half, you have already chosen too many dimensions and you are waiting for traffic to tell you. Part 11 takes these vectors into pgvector, Chroma and Qdrant and measures what each one does with them, and if the idea of a vector store is still abstract, vector databases explained is the twenty minute primer to read first.
References
- OpenAI, embeddings guide, for the embeddings endpoint, the dimensions parameter and unit length outputs. Code here ran against openai 2.46.0 on Python 3.10.12.
- OpenAI, text-embedding-3-small model page, for the 0.02 and 0.13 USD per million token prices and the per tier rate limits quoted above.
- OpenAI, new embedding models and API updates, for the result that the large model shortened to 256 dimensions still outperforms ada-002 at 1536.
- MTEB leaderboard on Hugging Face, for the shortlist, and for the self reporting caveat discussed above.
- Sentence Transformers documentation, for running bge and MiniLM style models locally. Local timings used sentence-transformers 5.6.0.


DrJha