Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Building a FAISS Retriever from Scratch

Recommended resources

Welcome to the next step in building our RAG pipeline. In the previous lesson, we established a solid data ingestion process, converting raw text into a clean, standardized set of document chunks and initializing an embedding model. We now have all the necessary ingredients to build the searchable knowledge base for our application.

This lesson focuses on creating the core retrieval component. You will learn how to set up a local FAISS vector store, a highly efficient library for similarity search. We'll take the document chunks we prepared, embed them, and ingest them into this store. Finally, we'll wrap the store in a LangChain Retriever object, creating the standardized component that our LCEL chain will use to fetch relevant context.

The Role of the Vector Store

At its core, a RAG system's "retrieval" step is a nearest-neighbor search problem in a high-dimensional vector space. After converting both our document chunks and the user's query into embedding vectors, the goal is to find the document vectors that are "closest" to the query vector.

A vector store (or vector database) is a data structure optimized for this exact task. It indexes the embedding vectors in a way that makes similarity searches extremely fast, even across millions of vectors. For our local development, we will use FAISS (Facebook AI Similarity Search), a powerful and popular library that can run entirely in memory.

The overall retrieval process, which we are about to implement, is illustrated below.

This diagram shows the retrieval workflow. A user's query is converted into an embedding vector. FAISS uses this vector to perform a similarity search against its index of document chunk embeddings, returning the 'Top K' most similar chunks to be used as context.

Step 1: Installing FAISS

FAISS is not a core LangChain dependency, so we need to add it to our project. LangChain integrates with the CPU version of FAISS via the faiss-cpu package.

Run the following command in your terminal to install it:

uv pip install faiss-cpu

Step 2: Creating and Populating the Vector Store

With the document chunks and embedding model from our previous lesson, populating a FAISS vector store is remarkably straightforward. LangChain's FAISS vector store class provides a convenient from_documents method that handles the entire process:

  1. It takes a list of Document objects and an embedding model.
  2. It iterates through each document, passing its page_content to the embedding model to generate a vector.
  3. It builds a FAISS index from all the generated vectors.
  4. It returns an initialized FAISS vector store object.

The following video from the Ryan & Matt Data Science channel provides a concise walkthrough of this process.

FAISS Vector Library with LangChain and OpenAI (Semantic Search)

This segment shows how to use the FAISS.from_documents method to quickly create a vector store from document chunks and an embedding model.

Watch the short section from creating the library. Notice how it combines the docs (our chunks) and the embeddings model in a single, efficient call.

Let's integrate this into our data_pipeline.py script. We'll add a new function to create the vector store.




# src/lcel_quickstart/data_pipeline.py (updates)




# ... other imports ...
from langchain_community.vectorstores import FAISS




# ... load_documents, split_documents, get_embedding_model functions are the same ...

def create_vector_store(chunks: list[Document], embedding_model: OpenAIEmbeddings) -> FAISS:
    """
    Creates a FAISS vector store from document chunks.
    """
    print("Creating FAISS vector store...")
    vector_store = FAISS.from_documents(chunks, embedding_model)
    print("Vector store created.")
    return vector_store

if __name__ == '__main__':
    docs = load_documents()
    chunks = split_documents(docs)
    embedding_model = get_embedding_model()
    vector_store = create_vector_store(chunks, embedding_model)




    # Let's test the vector store with a similarity search
    query = "What is LCEL?"
    results = vector_store.similarity_search(query, k=2)
    
    print("\n--- Similarity Search Demonstration ---")
    print(f"Query: '{query}'")
    for i, doc in enumerate(results):
        print(f"\nResult {i+1}:")
        print(f"Source: {doc.metadata.get('source', 'Unknown')}")
        print(f"Content: {doc.page_content}")

Run the updated script. It will now load and split the documents, then create the FAISS index. The output will show the top 2 most similar chunks from our document for the query "What is LCEL?", demonstrating that the search functionality is working correctly.


Step 3: Persisting the Vector Store

Creating the vector store involves making API calls to OpenAI for each chunk, which can be time-consuming and incur costs. For any real application, you'll want to do this once and then save the resulting index to disk.

The FAISS class includes save_local and load_local methods for this purpose. This is a crucial pattern for production readiness, as it allows your application to start up quickly without re-indexing the source documents every time.

The official LangChain documentation on FAISS provides clear examples of this.

Faiss - Docs by LangChain

This part of the documentation demonstrates how to save a created FAISS index to disk and how to load it back into memory.

Focus on the section titled Saving and loading. The code shows the simple vector_store.save_local("faiss_index") command and the corresponding FAISS.load_local(...) class method. Note the allow_dangerous_deserialization=True argument, which is required when loading an index with a custom embedding function.

Let's refactor our script to implement this load-or-create logic. This is a common pattern in data engineering pipelines.




# src/lcel_quickstart/data_pipeline.py (updated with persistence)
import os



# ... other imports ...

VECTOR_STORE_PATH = "faiss_index"




# ... load_documents, split_documents, get_embedding_model functions are the same ...

def create_vector_store(chunks: list[Document], embedding_model: OpenAIEmbeddings) -> FAISS:
    """
    Creates and persists a FAISS vector store.
    """
    print("Creating FAISS vector store...")
    vector_store = FAISS.from_documents(chunks, embedding_model)
    print("Saving vector store locally...")
    vector_store.save_local(VECTOR_STORE_PATH)
    print(f"Vector store saved to {VECTOR_STORE_PATH}")
    return vector_store

def load_vector_store(embedding_model: OpenAIEmbeddings) -> FAISS:
    """
    Loads an existing FAISS vector store.
    """
    if not os.path.exists(VECTOR_STORE_PATH):
        raise FileNotFoundError(f"Vector store not found at {VECTOR_STORE_PATH}")
    
    print(f"Loading vector store from {VECTOR_STORE_PATH}...")
    vector_store = FAISS.load_local(
        VECTOR_STORE_PATH, 
        embedding_model,
        allow_dangerous_deserialization=True # This is needed for custom embeddings
    )
    print("Vector store loaded.")
    return vector_store

if __name__ == '__main__':
    embedding_model = get_embedding_model()

    if os.path.exists(VECTOR_STORE_PATH):
        vector_store = load_vector_store(embedding_model)
    else:
        docs = load_documents()
        chunks = split_documents(docs)
        vector_store = create_vector_store(chunks, embedding_model)
    



    # Test the loaded/created vector store
    query = "How does LCEL help with production use cases?"
    results = vector_store.similarity_search(query, k=1)
    
    print("\n--- Similarity Search Demonstration ---")
    print(f"Query: '{query}'")
    if results:
        print(f"Top result: {results[0].page_content}")

Run this script twice. The first time, it will create and save the index. The second time, it will skip the creation step and load the index directly from disk, which will be much faster.


Step 4: Creating a Retriever

While you can use the vector store directly with similarity_search, the standard LangChain pattern is to wrap it in a Retriever object. A retriever is a generic interface with a get_relevant_documents method (and its async/streaming variants), which makes it a plug-and-play component in LCEL chains.

You can easily get a retriever from any vector store using the .as_retriever() method. This method also allows you to configure the search behavior.

How to Build RAG Applications with LangChain

This article section shows how to convert a vector store into a retriever, a key step for integrating it into a chain.

Read the code block under the heading "Basic RAG Chain". Focus on this line: retriever = vectorstore.as_retriever(...). This is the exact method we will use.

The .as_retriever() method accepts search_kwargs to control how many documents to fetch (k). It also allows you to specify a search_type. The two most common types are:

  • similarity (default): Performs a standard nearest-neighbor search.
  • mmr (Maximal Marginal Relevance): This is a more advanced technique that aims to improve the diversity of the results. It first fetches a larger number of documents (controlled by fetch_k) and then re-ranks them to select a diverse subset (k) that still maintains high relevance to the query. This helps avoid returning multiple chunks that contain redundant information.

Let's create a final function in a new file, src/lcel_quickstart/chain.py, that will be responsible for building and returning our retriever.

Create src/lcel_quickstart/chain.py and add the following:




# src/lcel_quickstart/chain.py
import os
from langchain_community.vectorstores import FAISS
from langchain_core.vectorstores import VectorStoreRetriever
from .data_pipeline import load_vector_store, get_embedding_model, VECTOR_STORE_PATH

def get_retriever() -> VectorStoreRetriever:
    """
    Creates and returns a retriever from the local FAISS vector store.
    """
    embedding_model = get_embedding_model()
    
    if not os.path.exists(VECTOR_STORE_PATH):



        # A simple placeholder for a real-world scenario where you'd
        # ensure the index is built before the app starts.
        raise FileNotFoundError(
            "FAISS index not found. Please run data_pipeline.py to create it."
        )

    vector_store = load_vector_store(embedding_model)




    # as_retriever is the key method that turns a vector store into a retriever
    return vector_store.as_retriever(
        search_type="similarity",
        search_kwargs={"k": 3} # Return top 3 most similar chunks
    )

if __name__ == '__main__':



    # This allows us to test the retriever independently
    retriever = get_retriever()
    query = "What is the Runnable interface?"
    
    print("--- Retriever Demonstration ---")
    print(f"Query: '{query}'\n")
    



    # The retriever's invoke method runs the search
    retrieved_docs = retriever.invoke(query)
    
    print(f"Retrieved {len(retrieved_docs)} documents:\n")
    for i, doc in enumerate(retrieved_docs):
        print(f"--- Document {i+1} ---\n")
        print(doc.page_content)
        print("-" * 20)

Now run this new file: uv run src/lcel_quickstart/chain.py. This will load your persisted FAISS index, create a retriever, and use it to fetch and display the documents most relevant to the query.


Conclusion

In this lesson, you have completed the indexing and retrieval portion of our RAG pipeline. You now have a persistent, searchable knowledge base ready to be queried.

Here are the key takeaways:

  • FAISS Vector Store: You learned how to create a FAISS vector store from document chunks using the convenient FAISS.from_documents method.
  • Persistence: You implemented a crucial production pattern by saving the vector store index to disk with save_local and loading it with load_local, avoiding costly re-computation.
  • Retriever Object: You created a Retriever using .as_retriever(). This object provides the standardized interface that LangChain uses to fetch context, making it a composable component for our chain.

With a fully functional retriever in hand, we are now ready for the final assembly. In the next lesson, you will plug this retriever into the LCEL chain we started building in Module 1, replacing the fake_retriever and creating a complete, end-to-end RAG pipeline that can answer questions based on your documents.