Converting text into vectors: OpenAIEmbeddings and HuggingFaceEmbeddings, dimensions and normalization, then storing the index in Chroma, FAISS, pgvector, and Qdrant with add_documents and index persistence.

In episode 9 you successfully turned PDFs, web pages, CSV, and JSON into neat chunks. But those chunks are still text — a search engine has to find them by meaning, not just keywords. That's where embeddings and vector stores come in.
This episode builds the storage layer of the RAG pipeline. First, embeddings: how OpenAIEmbeddings and HuggingFaceEmbeddings convert text into numeric vectors, plus the concepts of dimensions and normalization. Second, vector stores: storing and searching those vectors in Chroma, FAISS, pgvector, and Qdrant, complete with add_documents and index persistence.
An embedding is a representation of text as a vector of real numbers. Sentences with similar meaning produce vectors that sit close together in high-dimensional space, so meaning similarity can be measured by vector similarity — usually via cosine similarity.
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vektor = embeddings.embed_query("Apa itu retrieval augmented generation?")
print(len(vektor)) # vector dimensions
print(vektor[:5]) # part of the vector contentsembeddings.embed_query(...) produces one vector for one text. There's also embed_documents for batches. The length of the list is what's called the dimension — the larger it is, the more nuance it can capture, but the more expensive storage and compute become.
Two of the most common embedding families: hosted and locally running.
from langchain_huggingface import HuggingFaceEmbeddings
# Local, free, no API key
huggingface = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
vektor_lokal = huggingface.embed_query("Halo dunia")
print(len(vektor_lokal))The choice of embedder affects everything: vector dimensions, retrieval quality, cost, and latency. Rule of thumb: prototype fast with a small local embedder, then measure retrieval quality before deciding to move up to a larger paid embedder.
Vector dimensions are determined by the embedding model. text-embedding-3-small produces 1536 dimensions, all-MiniLM-L6-v2 produces 384 dimensions. Larger dimensions capture more context but consume more storage — an important consideration when data reaches millions of chunks.
Normalization means converting a vector so its length is one unit. A normalized vector makes dot product comparison equivalent to cosine similarity, and some vector stores leverage that for faster search.
import numpy as np
vektor = np.array([0.2, 0.5, -0.3])
normalized = vektor / np.linalg.norm(vektor)
print(normalized)Many providers return already-normalized vectors by default. The key thing to understand: if you normalize manually and the vector store also normalizes, the result can be changed twice — make sure the embedding side and the index side are consistent.
A vector store stores vectors and provides similarity-based search. Chroma is the easiest choice to get started: runs locally, persists to a directory, and requires no separate server.
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db",
)
print(vectorstore._collection.count())from_documents takes the list of Documents from episode 9, embeds their text, then stores them in ./chroma_db. One step replaces two manual operations: embedding and insert.
Each vector store has different strengths. FAISS is Meta's vector search library — fast, lightweight, and storable as a single local index file.
from langchain_community.vectorstores import FAISS
faiss_store = FAISS.from_documents(chunks, embeddings)
faiss_store.save_local("faiss_index")
faiss_store = FAISS.load_local(
"faiss_index",
embeddings,
allow_dangerous_deserialization=True,
)save_local writes the index to a folder, load_local loads it back — practical for applications without a separate vector server. pgvector stores vectors as a column in PostgreSQL, integrated with your relational data; Qdrant offers a dedicated server with strong filtering and scaling for large-scale production.
Warning
FAISS uses pickle when saving the index — always set allow_dangerous_deserialization=True only if the index comes from a trusted source. Don't load an index downloaded from the internet.
In production, data grows continuously. add_documents allows incremental insertion without rebuilding the entire index.
dokumen_baru = [{"page_content": "Bab 2: RAG di produksi", "metadata": {"sumber": "buku.md"}}]
vectorstore.add_documents(dokumen_baru)
vectorstore.persist() # for Chroma, save changes to diskThe common pattern: build the initial index via from_documents the first time, then use add_documents for daily updates or new batches. For Chroma, persist() writes changes to the directory; FAISS simply re-saves via save_local when needed.
The storage layer of the RAG pipeline now stands. You can convert the chunks from episode 9 into vectors with OpenAIEmbeddings or HuggingFaceEmbeddings, understand the impact of dimensions and normalization, then store the index in Chroma, FAISS, pgvector, or Qdrant with add_documents and persistence. All that remains is searching — and that's the theme of the next episode.
Key takeaways:
OpenAIEmbeddings for hosted production, HuggingFaceEmbeddings for local and free.add_documents for incremental additions; persist the index periodically so data isn't lost.In episode 11 you'll use everything that's been built: Retrieval & Basic RAG — VectorStoreRetriever, similarity_search with top-k and metadata filtering, assembling a retrieve-prompt-generate pipeline with LCEL, and evaluating answer quality. See you there!