Generating and Storing Embeddings

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

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.output_text 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 Memorizing Names 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 Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
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.