How to build a FAISS index with Sentence Transformers

Local semantic search depends on two compatible pieces: dense vectors that capture text meaning and an index that can rank those vectors for each query. Sentence Transformers produces the embeddings, while FAISS provides an in-process search index that can be saved and loaded without a separate vector database service.

The corpus uses sentence-transformers/all-MiniLM-L6-v2 with an exact IndexFlatIP index. Normalizing document and query embeddings makes inner-product ranking equivalent to cosine-similarity ranking; IndexFlatIP still scans every stored vector, so larger collections may need an approximate FAISS index instead.

Flat indexes return sequential row numbers rather than application records. A JSON sidecar kept in the same order as the indexed vectors maps each result back to its document ID and text, and a row-count check stops the search when the saved index and metadata no longer agree.

Steps to build a FAISS index with Sentence Transformers:

  1. Install Sentence Transformers and the CPU build of FAISS in the active Python environment.
    $ python -m pip install --upgrade sentence-transformers faiss-cpu
  2. Create build_faiss_index.py with the imports and ordered corpus records.
    build_faiss_index.py
    import json
    from pathlib import Path
     
    import faiss
    import numpy as np
    from sentence_transformers import SentenceTransformer
     
     
    corpus = [
        {
            "id": "doc-001",
            "text": "Sentence Transformers converts text into dense embeddings.",
        },
        {
            "id": "doc-002",
            "text": "FAISS stores vectors and searches nearest neighbors locally.",
        },
        {
            "id": "doc-003",
            "text": "Cross-encoders rerank a small set of retrieved passages.",
        },
        {
            "id": "doc-004",
            "text": "Qdrant stores vectors behind a database service API.",
        },
    ]
  3. Append the normalized document-embedding and exact-index section to build_faiss_index.py.
    model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
    texts = [item["text"] for item in corpus]
    document_embeddings = model.encode_document(
        texts,
        normalize_embeddings=True,
        convert_to_numpy=True,
    )
    document_embeddings = np.asarray(document_embeddings, dtype="float32")
     
    dimension = document_embeddings.shape[1]
    index = faiss.IndexFlatIP(dimension)
    index.add(document_embeddings)

    encode_document() preserves the document route or prompt defined by retrieval models that distinguish corpus text from queries.

  4. Append the index and metadata persistence section to build_faiss_index.py.
    faiss.write_index(index, "support-faq.faiss")
    Path("support-faq.json").write_text(
        json.dumps(corpus, indent=2),
        encoding="utf-8",
    )
  5. Append the persisted-index search section to build_faiss_index.py.
    loaded_index = faiss.read_index("support-faq.faiss")
    metadata = json.loads(Path("support-faq.json").read_text(encoding="utf-8"))
    if loaded_index.ntotal != len(metadata):
        raise RuntimeError("FAISS rows and metadata rows do not match")
     
    query_embedding = model.encode_query(
        ["Which library searches vectors nearest neighbors locally?"],
        normalize_embeddings=True,
        convert_to_numpy=True,
    )
    query_embedding = np.asarray(query_embedding, dtype="float32")
    scores, row_ids = loaded_index.search(query_embedding, k=2)
     
    print(f"embedding dimension: {dimension}")
    print(f"indexed vectors: {loaded_index.ntotal}")
    print("top matches:")
    for rank, (score, row_id) in enumerate(zip(scores[0], row_ids[0]), start=1):
        record = metadata[int(row_id)]
        print(f"{rank}. {record['id']} score={score:.4f} text={record['text']}")

    encode_query() applies the matching query route or prompt before FAISS searches the reloaded index.

  6. Run build_faiss_index.py to build the searchable index.
    $ python build_faiss_index.py
    embedding dimension: 384
    indexed vectors: 4
    top matches:
    1. doc-002 score=0.6675 text=FAISS stores vectors and searches nearest neighbors locally.
    2. doc-004 score=0.3530 text=Qdrant stores vectors behind a database service API.

    The first run may download the embedding model before the search output appears.