AW Dev Rethought

βœ‚οΈ Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away - Antoine de Saint-ExupΓ©ry

🧠 AI with Python – πŸ” Building a FAISS Vector Store


Description:

Large Language Models (LLMs) are powerful at generating responses, but they don't automatically have access to private documents, company knowledge bases, or recently updated information. Retrieval-Augmented Generation (RAG) addresses this limitation by retrieving relevant information from external sources and providing it to an LLM as additional context.

But how do we efficiently search through large collections of documents and identify the information most relevant to a user's question?

This is where Vector Stores become important. In this project, we'll explore how to build a vector store using FAISS (Facebook AI Similarity Search) and Sentence Transformers, enabling efficient semantic search with Python.


What is a Vector Store?

A Vector Store organises numerical representations of information, known as embeddings, so that similar content can be retrieved efficiently.

Traditional keyword search relies heavily on matching words or phrases. Semantic search takes a different approach by comparing the meaning of text, allowing it to identify relevant information even when the query and document use different wording.

For example, consider the questions "How can I learn Python programming?" and "What's the best way to get started with Python?" Although their wording differs, both express a similar intent.

Embedding models convert these sentences into numerical vectors that capture aspects of their semantic meaning. A vector store then makes it possible to search those embeddings and retrieve the closest matches.


Understanding FAISS

FAISS (Facebook AI Similarity Search) is an open-source library developed by Meta for efficient similarity search over dense numerical vectors.

Instead of comparing a search query against every document's text, FAISS works with embeddings generated by an embedding model. It provides different indexing strategies for finding vectors that are close to a query vector.

For this project, we'll use IndexFlatIP, which performs exact nearest-neighbor search using inner-product similarity. By normalising our embeddings, we can use inner products to calculate cosine similarity.

FAISS is particularly useful for building semantic search applications, recommendation systems, and the retrieval components of RAG pipelines.


Generating Document Embeddings

Before building a vector store, we need to convert our documents into numerical embeddings. We'll use Sentence Transformers with the lightweight all-MiniLM-L6-v2 model, which generates 384-dimensional embeddings.

from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer(
    "sentence-transformers/all-MiniLM-L6-v2"
)

document_embeddings = embedding_model.encode(
    documents,
    convert_to_numpy=True
)

Each document is transformed into a vector representing its semantic characteristics. Documents discussing similar concepts will generally have embeddings that are closer together in the vector space.

The same embedding model should be used for both documents and search queries to ensure their vectors are compatible.


Building and Searching the FAISS Index

Once the embeddings are generated, we can organise them inside a FAISS index. We'll normalise the vectors and use IndexFlatIP to perform cosine similarity searches.

import faiss
import numpy as np

document_embeddings = np.asarray(document_embeddings, dtype="float32")
faiss.normalize_L2(document_embeddings)

dimension = document_embeddings.shape[1]
index = faiss.IndexFlatIP(dimension)
index.add(document_embeddings)

Our documents are now indexed and ready for semantic search.

When a user submits a question, we convert it into an embedding using the same model, normalize it, and search the FAISS index for the closest matches.

query = "How does RAG retrieve relevant information?"

query_embedding = embedding_model.encode(
    [query],
    convert_to_numpy=True
)

query_embedding = np.asarray(query_embedding, dtype="float32")
faiss.normalize_L2(query_embedding)

scores, indices = index.search(query_embedding, 3)

FAISS returns the similarity scores and positions of the three most relevant documents. We can then use those positions to retrieve the original text from our document collection.

This retrieval process forms the foundation of semantic search in a RAG system.


How Does FAISS Fit into RAG?

A typical RAG pipeline begins by collecting documents, dividing larger documents into manageable chunks, and converting those chunks into embeddings. These embeddings are then stored in a vector index.

When a user asks a question, the system generates an embedding for the query and searches the index for relevant information. The retrieved content is combined with the original question and provided to an LLM, which uses that context to generate a response.

The workflow can be summarised as:

Documents β†’ Embeddings β†’ FAISS Index β†’ Similarity Search β†’ Retrieved Context β†’ LLM Response

FAISS handles the retrieval stage rather than generating answers itself. This separation allows developers to combine vector search with different language models depending on their application requirements.

FAISS indexes can also be saved and reloaded, avoiding the need to rebuild them every time an application starts. However, original document content and metadata must be stored separately because FAISS primarily manages numerical vectors.


Real-World Applications

FAISS-based vector stores are useful in applications where information needs to be retrieved based on meaning rather than exact keyword matches.

In document question-answering systems, FAISS can retrieve relevant sections from PDFs, manuals, and technical documentation. Enterprise knowledge assistants can use similar techniques to search internal knowledge bases, while customer support applications can retrieve troubleshooting instructions and relevant support articles.

Vector similarity search is also widely applicable to recommendation systems, research assistants, and AI-powered search engines.

These applications demonstrate why efficient information retrieval is an important foundation for building reliable RAG systems.


Final Thoughts

Building a FAISS Vector Store introduces one of the most important components of Retrieval-Augmented Generation: semantic information retrieval.

In this project, we explored how Sentence Transformers converts documents into embeddings, how FAISS organizes those embeddings for efficient similarity search, and how relevant documents can be retrieved using natural-language queries.

By combining embedding models with vector similarity search, developers can build the retrieval foundation for document search engines, enterprise knowledge assistants, and more advanced RAG applications.


Code Snippet:

# =========================================================
# AI with Python – RAG Systems
# Building a FAISS Vector Store
# Programmer: python_scripts (Abhijith Warrier)
# =========================================================

import faiss
import numpy as np
import json
from pathlib import Path
from sentence_transformers import SentenceTransformer

# =========================================================
# Step 1 - Load the Embedding Model
# =========================================================

print("=" * 70)
print("Loading Sentence Transformer Model...")
print("=" * 70)

embedding_model = SentenceTransformer(
    "sentence-transformers/all-MiniLM-L6-v2"
)

print("Embedding model loaded successfully!")

# =========================================================
# Step 2 - Create Document Collection
# =========================================================

documents = [
    "Python is a versatile programming language used for web development, automation, and data science.",
    "Machine Learning allows computers to identify patterns and learn from data.",
    "Deep Learning uses artificial neural networks with multiple layers.",
    "Natural Language Processing enables computers to understand and generate human language.",
    "Retrieval-Augmented Generation combines information retrieval with language model generation.",
    "Vector databases store numerical embeddings for efficient similarity searches.",
    "FAISS is a library developed by Meta for efficient similarity search over dense vectors.",
    "Sentence Transformers generates embeddings that represent the semantic meaning of text.",
    "Large Language Models can generate text, answer questions, and summarize information.",
    "Semantic search retrieves information based on meaning rather than exact keyword matches."
]

print("\n" + "=" * 70)
print("Document Collection")
print("=" * 70)

for i, document in enumerate(documents, start=1):
    print(f"{i}. {document}")

print(f"\nTotal Documents: {len(documents)}")

# =========================================================
# Step 3 - Generate Document Embeddings
# =========================================================

print("\n" + "=" * 70)
print("Generating Document Embeddings...")
print("=" * 70)

document_embeddings = embedding_model.encode(
    documents,
    convert_to_numpy=True
)

document_embeddings = np.asarray(
    document_embeddings,
    dtype="float32"
)

print(f"Embedding Shape: {document_embeddings.shape}")
print(f"Embedding Dimensions: {document_embeddings.shape[1]}")
print(f"Embedding Data Type: {document_embeddings.dtype}")

# =========================================================
# Step 4 - Normalize Document Embeddings
# =========================================================

faiss.normalize_L2(document_embeddings)

print("\nDocument embeddings normalized successfully!")

# =========================================================
# Step 5 - Create FAISS Vector Index
# =========================================================

dimension = document_embeddings.shape[1]

index = faiss.IndexFlatIP(dimension)
index.add(document_embeddings)

print("\n" + "=" * 70)
print("FAISS Vector Index")
print("=" * 70)
print(f"Index Type: IndexFlatIP")
print(f"Vector Dimensions: {index.d}")
print(f"Total Indexed Documents: {index.ntotal}")

# =========================================================
# Step 6 - Generate Query Embedding
# =========================================================

query = "How does RAG help language models access external information?"

query_embedding = embedding_model.encode(
    [query],
    convert_to_numpy=True
)

query_embedding = np.asarray(
    query_embedding,
    dtype="float32"
)

faiss.normalize_L2(query_embedding)

print("\n" + "=" * 70)
print("Query Embedding")
print("=" * 70)
print(f"Query: {query}")
print(f"Query Embedding Shape: {query_embedding.shape}")

# =========================================================
# Step 7 - Perform Semantic Similarity Search
# =========================================================

top_k = 3

scores, indices = index.search(
    query_embedding,
    top_k
)

print("\n" + "=" * 70)
print("Semantic Search Results")
print("=" * 70)

for rank, (doc_index, score) in enumerate(zip(indices[0], scores[0]), start=1):
    print(f"\nRank {rank}")
    print(f"Document: {documents[doc_index]}")
    print(f"Similarity Score: {score:.4f}")

# =========================================================
# Step 8 - Create Reusable Semantic Search Function
# =========================================================

def semantic_search(query, top_k=3, search_index=None, document_store=None):
    search_index = index if search_index is None else search_index
    document_store = documents if document_store is None else document_store

    if top_k < 1:
        raise ValueError("top_k must be greater than zero.")

    if search_index.ntotal != len(document_store):
        raise ValueError("Document count does not match indexed vectors.")

    query_vector = embedding_model.encode(
        [query],
        convert_to_numpy=True
    )

    query_vector = np.asarray(query_vector, dtype="float32")
    faiss.normalize_L2(query_vector)

    scores, indices = search_index.search(
        query_vector,
        min(top_k, search_index.ntotal)
    )

    results = []

    for doc_index, score in zip(indices[0], scores[0]):
        if doc_index == -1:
            continue

        results.append({
            "document_id": int(doc_index),
            "document": document_store[doc_index],
            "similarity": float(score)
        })

    return results

# =========================================================
# Step 9 - Test Multiple Search Queries
# =========================================================

queries = [
    "What is semantic search?",
    "How do neural networks work?",
    "What is FAISS used for?",
    "How can Python be used in data science?"
]

print("\n" + "=" * 70)
print("Testing Multiple Semantic Search Queries")
print("=" * 70)

for search_query in queries:
    print(f"\nQuery: {search_query}")

    results = semantic_search(search_query, top_k=3)

    for rank, result in enumerate(results, start=1):
        print(f"{rank}. {result['document']}")
        print(f"   Similarity: {result['similarity']:.4f}")

# =========================================================
# Step 10 - Save FAISS Index and Document Metadata
# =========================================================

storage_path = Path("faiss_vector_store")
storage_path.mkdir(parents=True, exist_ok=True)

index_path = storage_path / "knowledge_base.index"
documents_path = storage_path / "documents.json"

faiss.write_index(index, str(index_path))

with open(documents_path, "w", encoding="utf-8") as file:
    json.dump(documents, file, indent=4, ensure_ascii=False)

print("\n" + "=" * 70)
print("Saving FAISS Vector Store")
print("=" * 70)
print(f"FAISS Index Saved: {index_path}")
print(f"Documents Saved: {documents_path}")

# =========================================================
# Step 11 - Reload FAISS Index and Documents
# =========================================================

# Only load FAISS index files from trusted sources.
loaded_index = faiss.read_index(str(index_path))

with open(documents_path, "r", encoding="utf-8") as file:
    loaded_documents = json.load(file)

if loaded_index.ntotal != len(loaded_documents):
    raise ValueError("Loaded index and document collection do not match.")

print("\n" + "=" * 70)
print("Reloading FAISS Vector Store")
print("=" * 70)
print(f"Loaded Index Dimensions: {loaded_index.d}")
print(f"Loaded Indexed Documents: {loaded_index.ntotal}")
print(f"Loaded Document Count: {len(loaded_documents)}")

# =========================================================
# Step 12 - Search Using Reloaded Vector Store
# =========================================================

test_query = "How can computers understand human language?"

loaded_results = semantic_search(
    test_query,
    top_k=3,
    search_index=loaded_index,
    document_store=loaded_documents
)

print("\n" + "=" * 70)
print("Search Results from Reloaded FAISS Index")
print("=" * 70)
print(f"Query: {test_query}")

for rank, result in enumerate(loaded_results, start=1):
    print(f"\nRank {rank}")
    print(f"Document: {result['document']}")
    print(f"Similarity Score: {result['similarity']:.4f}")

# =========================================================
# Step 13 - Prepare Retrieved Context for RAG
# =========================================================

rag_query = "What is Retrieval-Augmented Generation?"

retrieved_documents = semantic_search(
    rag_query,
    top_k=3
)

retrieved_context = "\n".join(
    result["document"]
    for result in retrieved_documents
)

rag_prompt = f"""Answer the following question using the provided context.
If the context does not contain enough information, say so.

Context:
{retrieved_context}

Question:
{rag_query}

Answer:"""

print("\n" + "=" * 70)
print("RAG Context Preparation")
print("=" * 70)
print(rag_prompt)

# =========================================================
# Step 14 - Display Final Summary
# =========================================================

print("\n" + "=" * 70)
print("FAISS Vector Store Summary")
print("=" * 70)
print(f"Embedding Model: all-MiniLM-L6-v2")
print(f"Total Documents: {len(documents)}")
print(f"Embedding Dimensions: {dimension}")
print(f"FAISS Index Type: IndexFlatIP")
print(f"Similarity Metric: Cosine Similarity")
print(f"Indexed Vectors: {index.ntotal}")
print(f"Saved Index: {index_path}")
print(f"Saved Documents: {documents_path}")

# =========================================================
# Program Completed
# =========================================================

print("\n" + "=" * 70)
print("Building a FAISS Vector Store Demo Completed Successfully!")
print("=" * 70)

Link copied!

Comments

Add Your Comment

Comment Added!