Generating and Storing Embeddings

Ma Mahalakshmi V Updated 16 Sep 2026
7 min read ·Lesson 43 of 72

From a Single Vector to a Reusable Collection

Lesson 1 covered what an embedding is and why similar meanings end up close together in the embedding space. This lesson covers the practical mechanics of actually building a collection of embeddings you can search against later: how to generate them efficiently, what to store alongside each one, how storage format and cost scale with collection size, and how to control a vector's dimensionality when a model supports it.

Embeddings Are Usually Computed Once and Reused

Embedding generation, like every other API call this course has covered, has a real cost per request, though it is typically much cheaper per unit of text than text generation, since the model only needs to produce a fixed-length vector rather than generate new text token by token. The practical implication is that embeddings are usually computed once, when a piece of text is first added to a collection, and reused across every subsequent search against that collection — following exactly the same one-time-indexing-versus-per-query-search separation Unit 9, Lesson 2 established for vector stores, since a vector store is, under the hood, a system for storing and efficiently searching over exactly the kind of embeddings this unit introduces.

def embed_and_store(client, texts: list[str]) -> list[dict]:
    response = client.embeddings.create(model="text-embedding-4", input=texts)
    return [
        {"text": text, "embedding": data.embedding}
        for text, data in zip(texts, response.data)
    ]

documents = [
    "Our return policy allows returns within 30 days of purchase.",
    "Shipping typically takes 3 to 5 business days.",
    "Refunds are processed within 5 to 7 business days after we receive the item.",
]

stored_documents = embed_and_store(client, documents)
print(f"Stored {len(stored_documents)} documents with embeddings.")

This function computes each document's embedding exactly once and stores it alongside the original text — a pattern worth internalizing before Lesson 3, which builds directly on having a collection of pre-computed, stored embeddings ready to search against, rather than recomputing an embedding for every stored document on every single search query, which would be needlessly expensive and slow. Storing the original text alongside its embedding matters just as much as the vector itself: a search that finds the closest matching vector is useless if there's no way to retrieve what that vector actually represents.

Deciding What to Store Alongside Each Embedding

A real application almost always needs more than just text and a vector — a document's source, its category, the date it was added, an identifier linking it back to a database record. Nothing about client.embeddings.create() handles this for you; it is entirely the calling code's responsibility to decide what metadata travels alongside each embedding and how it is stored.

def embed_and_store_with_metadata(client, records: list[dict]) -> list[dict]:
    texts = [record["text"] for record in records]
    response = client.embeddings.create(model="text-embedding-4", input=texts)
    return [
        {
            "text": record["text"],
            "embedding": data.embedding,
            "source_id": record["source_id"],
            "category": record.get("category"),
        }
        for record, data in zip(records, response.data)
    ]

records = [
    {"text": "Our return policy allows returns within 30 days.", "source_id": "policy-001", "category": "returns"},
    {"text": "Shipping typically takes 3 to 5 business days.", "source_id": "policy-002", "category": "shipping"},
]

stored = embed_and_store_with_metadata(client, records)

Attaching source_id and category to each stored embedding here is what makes the resulting collection actually usable in a real application: source_id lets a search result be traced back to the original record it came from (for citing a source, or for updating that specific record's embedding later without touching any others), and category enables the kind of pre-filtering Lesson 3 covers, where a hard structural filter narrows the search space before ranking by similarity.

Where Embeddings Actually Get Stored

For a small collection — a few hundred to a few thousand documents — storing embeddings as an in-memory Python list of dictionaries, as shown above, or serialized to a JSON file on disk, is entirely reasonable and requires no additional infrastructure. For anything larger, or anything that needs to persist reliably and be queried efficiently across multiple runs of an application, a proper storage layer becomes necessary: a plain relational database with an extra column holding the serialized vector (as a JSON array or a binary blob, depending on the database), a NumPy array saved to disk for fast bulk loading, or a dedicated vector database designed specifically to store and search embeddings efficiently at scale.

import json

def save_embeddings_to_disk(stored_documents: list[dict], filepath: str) -> None:
    with open(filepath, "w") as f:
        json.dump(stored_documents, f)

def load_embeddings_from_disk(filepath: str) -> list[dict]:
    with open(filepath, "r") as f:
        return json.load(f)

save_embeddings_to_disk(stored_documents, "embeddings_cache.json")
reloaded = load_embeddings_from_disk("embeddings_cache.json")
print(f"Reloaded {len(reloaded)} stored documents from disk.")

Persisting embeddings to disk (or a database) between runs, rather than recomputing them every time an application starts, is a direct extension of the one-time-computation principle established above: if the underlying text hasn't changed, there is no reason to pay for and wait on a new embedding call just because the application process restarted. This becomes increasingly important as a collection grows — recomputing embeddings for thousands of documents on every application startup is both slow and unnecessarily expensive.

Controlling Vector Dimensionality

Some embedding models support requesting a shorter vector than their default output length, trading a small amount of representational precision for a smaller, cheaper-to-store, and faster-to-compare vector.

response_full = client.embeddings.create(model="text-embedding-4", input="Return policy inquiry")
response_short = client.embeddings.create(model="text-embedding-4", input="Return policy inquiry", dimensions=256)

print(f"Default length: {len(response_full.data[0].embedding)}")
print(f"Requested shorter length: {len(response_short.data[0].embedding)}")

Note: Whether a given embedding model supports the dimensions parameter, and what range of values it accepts, depends on the specific model — not every embedding model supports shortening its output vector this way. Confirm current support and valid ranges against your installed SDK version's documentation.

This matters practically for large-scale storage and search: a vector store holding embeddings for millions of documents pays a real cost, in both storage space and per-comparison compute time, that scales directly with vector length, so a model supporting a shorter output length offers a genuine trade-off worth considering deliberately rather than always defaulting to the maximum available dimensionality — mirroring, in spirit, the same "match the setting to the actual need" theme this course has applied repeatedly to image resolution (Unit 7, Lesson 3) and reasoning effort (Unit 3): more dimensions are not automatically better if the application's actual retrieval quality needs are already well served by a shorter, cheaper vector.

Updating a Collection Over Time

A stored embedding collection is rarely static — documents get added, edited, or removed. Because an embedding represents the meaning of a specific piece of text at the time it was generated, editing the underlying text without regenerating its embedding leaves a stale vector that no longer accurately represents what the text now says.

def update_document_embedding(client, stored_documents: list[dict], source_id: str, new_text: str) -> list[dict]:
    new_embedding = client.embeddings.create(model="text-embedding-4", input=new_text).data[0].embedding
    for document in stored_documents:
        if document.get("source_id") == source_id:
            document["text"] = new_text
            document["embedding"] = new_embedding
    return stored_documents

Treating a document's embedding as something that must be regenerated whenever its underlying text changes — never left stale — is a direct consequence of what an embedding actually represents: a snapshot of meaning at a point in time, not a persistent identifier that stays valid regardless of edits. This is the same reasoning behind why source_id, introduced earlier in this lesson, matters in the first place: without a stable identifier to locate the specific record that changed, there would be no reliable way to know which embedding needs updating when its source text changes.

Common Mistakes

Recomputing embeddings for a stored document collection on every search query or application restart, rather than computing each document's embedding once and persisting it, incurring unnecessary repeated cost for text that hasn't changed.

Storing only the embedding vector without the metadata needed to trace it back to its source, making it impossible to update a specific record's embedding later or to attribute a search result to where it came from.

Leaving a document's embedding stale after editing its underlying text, since an embedding represents the meaning of text at the moment it was generated, not a live, automatically-updating representation.

Storing embeddings in a format that doesn't scale to the collection's actual size, such as continuing to rely on an in-memory list and a linear scan well past the point where a proper database or vector store would serve the application better.

Best Practices

Compute an embedding once per piece of text and persist it (to disk, a database, or a vector store), reusing the stored vector for every subsequent comparison rather than regenerating it repeatedly.

Attach a stable identifier and relevant metadata to every stored embedding, not just the vector and raw text, to support updates, filtering, and source attribution later.

Regenerate an embedding whenever its underlying text changes, treating the embedding as derived data that must stay in sync with its source rather than a one-time computation that's valid forever.

Choose a storage approach that matches the collection's actual scale — an in-memory list or a JSON file for a small collection, a database column or dedicated vector store as the collection grows — rather than defaulting to the simplest option regardless of size.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.out Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Mem Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources
Ask about this post
AI Ask about this post

Ask questions about Generating and Storing Embeddings and get answers drawn from it.

Signed-in readers only.