Crafting Custom Embeddings for LangChain’s Vector Stores: A Deep Dive

Introduction

In the realm of artificial intelligence, the ability to retrieve semantically relevant information from vast datasets is pivotal for applications like semantic search, question-answering systems, and recommendation engines. LangChain, a versatile framework for building AI-driven solutions, provides robust vector stores that rely on embeddings—numerical representations of text or data—to enable similarity search. While LangChain supports popular embedding providers like OpenAI and HuggingFace, creating custom embeddings allows developers to tailor representations to specific use cases, domains, or performance requirements. This comprehensive guide explores how to craft and integrate custom embeddings with LangChain’s vector stores, diving into their setup, core features, performance optimization, practical applications, and advanced configurations.

To understand LangChain’s broader ecosystem, start with LangChain Fundamentals.

What are Custom Embeddings in LangChain?

Custom embeddings in LangChain refer to user-defined functions or classes that convert text or data into fixed-length vector representations, which are then used by vector stores for similarity search. Unlike pre-built embedding providers, custom embeddings allow developers to leverage domain-specific models, fine-tuned transformers, or entirely bespoke algorithms. LangChain’s flexible Embeddings interface makes it seamless to integrate these custom embeddings with any vector store, such as FAISS, Pinecone, or Chroma, enabling tailored semantic search capabilities.

For a primer on vector stores, see Vector Stores Introduction.

Why Custom Embeddings?

Custom embeddings offer several advantages:

  • Domain Specificity: Tailor embeddings to specific industries (e.g., medical, legal) for better semantic accuracy.
  • Cost Efficiency: Use open-source or local models to reduce dependency on paid APIs.
  • Performance Optimization: Optimize for speed, memory, or accuracy based on application needs.
  • Flexibility: Combine multiple models or techniques (e.g., transformers, TF-IDF) for hybrid embeddings.

Explore embedding techniques at the HuggingFace Transformers Documentation.

Setting Up Custom Embeddings

To use custom embeddings with LangChain’s vector stores, you need to implement the Embeddings interface, which requires two methods: embed_documents for batch text embedding and embed_query for single query embedding. Below is a basic setup using a HuggingFace transformer model as a custom embedding function with a Chroma vector store:

from langchain_core.embeddings import Embeddings
from langchain_chroma import Chroma
from transformers import AutoTokenizer, AutoModel
import torch

class HuggingFaceCustomEmbeddings(Embeddings):
    def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2"):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModel.from_pretrained(model_name)

    def embed_documents(self, texts):
        # Tokenize and encode texts
        encoded_input = self.tokenizer(texts, padding=True, truncation=True, return_tensors="pt", max_length=512)
        with torch.no_grad():
            model_output = self.model(**encoded_input)
        # Mean pooling to get sentence embeddings
        embeddings = model_output.last_hidden_state.mean(dim=1).numpy()
        return embeddings.tolist()

    def embed_query(self, text):
        # Tokenize and encode single query
        encoded_input = self.tokenizer(text, padding=True, truncation=True, return_tensors="pt", max_length=512)
        with torch.no_grad():
            model_output = self.model(**encoded_input)
        embedding = model_output.last_hidden_state.mean(dim=1).numpy()
        return embedding.tolist()[0]

# Initialize custom embeddings
embedding_function = HuggingFaceCustomEmbeddings()

# Initialize vector store
vector_store = Chroma(
    collection_name="langchain_example",
    embedding_function=embedding_function,
    persist_directory="./chroma_db"
)

This creates a custom embedding class using the sentence-transformers/all-MiniLM-L6-v2 model, generating 384-dimensional vectors, and integrates it with a Chroma vector store.

For other vector store options, see Vector Store Use Cases.

Installation

Install the required packages:

pip install langchain-chroma langchain-openai transformers torch chromadb

The transformers and torch packages are needed for the HuggingFace model. For other embedding approaches (e.g., TF-IDF), install relevant libraries like scikit-learn. Ensure a MongoDB Atlas cluster is set up for persistent storage if using MongoDB as the vector store backend.

For detailed installation guidance, see Chroma Integration.

Configuration Options

Customize the custom embeddings and vector store during initialization:

  • Embedding Class:
    • model_name: Specify the transformer model (e.g., bert-base-uncased).
    • max_length: Maximum token length for tokenization (default: 512).
    • device: Compute device (cpu, cuda; default: cpu).
  • Vector Store:
    • collection_name: Name of the collection (e.g., langchain_example).
    • persist_directory: Directory for persistent storage (default: None for in-memory).
    • collection_metadata: Metadata for indexing (e.g., {"hnsw:space": "cosine"}).

Example with a fine-tuned model and MongoDB Atlas:

from langchain_mongodb import MongoDBAtlasVectorSearch
from pymongo import MongoClient

client = MongoClient("mongodb+srv://:@.mongodb.net/")
collection = client["langchain_db"]["example_collection"]
embedding_function = HuggingFaceCustomEmbeddings(model_name="distilbert-base-uncased")
vector_store = MongoDBAtlasVectorSearch(
    collection=collection,
    embedding=embedding_function,
    index_name="vector_index"
)

Core Features

1. Indexing Documents

Indexing is the foundation of similarity search, enabling vector stores to organize embeddings for rapid retrieval. Custom embeddings allow developers to define how texts are converted into vectors, which are then indexed by the vector store.

  • Key Methods:
    • from_documents(documents, embedding, **kwargs): Creates a vector store from a list of Document objects.
      • Parameters:
        • documents: List of Document objects with page_content and optional metadata.
        • embedding: Custom embedding function (e.g., HuggingFaceCustomEmbeddings).
      • Returns: A vector store instance (e.g., Chroma, MongoDBAtlasVectorSearch).
    • from_texts(texts, embedding, metadatas=None, ids=None, **kwargs): Creates a vector store from a list of texts.
    • add_documents(documents, ids=None, **kwargs): Adds documents to an existing index.
      • Parameters:
        • documents: List of Document objects.
        • ids: Optional list of unique IDs.
      • Returns: List of assigned IDs.
    • add_texts(texts, metadatas=None, ids=None, **kwargs): Adds texts to an existing index.
  • Embedding Process:
    • The custom embedding function processes texts into fixed-length vectors.
    • For transformers, mean pooling or CLS token pooling is common:
    • def embed_documents(self, texts):
              encoded_input = self.tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
              with torch.no_grad():
                  model_output = self.model(**encoded_input)
              # CLS token pooling
              embeddings = model_output.last_hidden_state[:, 0].numpy()
              return embeddings.tolist()
  • Example (Indexing with Chroma):
  • from langchain_core.documents import Document
      documents = [
          Document(page_content="The sky is blue.", metadata={"source": "sky", "id": 1}),
          Document(page_content="The grass is green.", metadata={"source": "grass", "id": 2}),
          Document(page_content="The sun is bright.", metadata={"source": "sun", "id": 3})
      ]
      vector_store = Chroma.from_documents(
          documents,
          embedding=embedding_function,
          collection_name="langchain_example",
          persist_directory="./chroma_db"
      )
  • Example (Indexing with MongoDB Atlas):
  • vector_store = MongoDBAtlasVectorSearch.from_documents(
          documents,
          embedding=embedding_function,
          collection=collection,
          index_name="vector_index"
      )

For advanced indexing, see Document Indexing.

Similarity search retrieves documents closest to a query based on vector similarity, leveraging the custom embeddings’ semantic representations.

  • Key Methods:
    • similarity_search(query, k=4, filter=None, **kwargs): Searches for the top k documents.
      • Parameters:
        • query: Input text.
        • k: Number of results (default: 4).
        • filter: Optional metadata filter (format depends on vector store).
      • Returns: List of Document objects.
    • similarity_search_with_score(query, k=4, filter=None, **kwargs): Returns tuples of (Document, score), where scores are distances or similarities.
    • similarity_search_by_vector(embedding, k=4, filter=None, **kwargs): Searches using a pre-computed embedding.
    • max_marginal_relevance_search(query, k=4, fetch_k=20, lambda_mult=0.5, filter=None, **kwargs): Uses Maximal Marginal Relevance (MMR) for relevance and diversity.
      • Parameters:
        • fetch_k: Number of candidates to fetch (default: 20).
        • lambda_mult: Diversity weight (0 for max diversity, 1 for min; default: 0.5).
  • Distance Metrics:
    • Custom embeddings work with vector store-specific metrics (e.g., cosine, l2, dot_product).
    • For Chroma:
    • vector_store = Chroma(
              collection_name="langchain_example",
              embedding_function=embedding_function,
              collection_metadata={"hnsw:space": "cosine"}
          )
    • For MongoDB Atlas:
    • vector_store = MongoDBAtlasVectorSearch(
              collection=collection,
              embedding=embedding_function,
              index_name="vector_index",
              relevance_score_fn="cosine"
          )
  • Example (Similarity Search with Chroma):
  • query = "What is blue?"
      results = vector_store.similarity_search_with_score(
          query,
          k=2,
          filter={"source": {"$eq": "sky"}}
      )
      for doc, score in results:
          print(f"Text: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}")
  • Example (MMR Search):
  • results = vector_store.max_marginal_relevance_search(
          query,
          k=2,
          fetch_k=10,
          lambda_mult=0.5
      )
      for doc in results:
          print(f"MMR Text: {doc.page_content}, Metadata: {doc.metadata}")

For querying strategies, see Querying Vector Stores.

3. Metadata Filtering

Metadata filtering refines search results based on metadata fields, with implementation varying by vector store.

  • Filter Syntax:
    • For Chroma, filters use key-value pairs with operators like $eq, $and:
    • filter = {
              "$and": [
                  {"source": {"$eq": "sky"}},
                  {"id": {"$gt": 0}}
              ]
          }
          results = vector_store.similarity_search(query, k=2, filter=filter)
    • For MongoDB Atlas, filters use MongoDB query syntax:
    • filter = {
              "$and": [
                  {"metadata.source": {"$eq": "sky"}},
                  {"metadata.id": {"$gt": 0}}
              ]
          }
          results = vector_store.similarity_search(query, k=2, filter=filter)
  • Advanced Filtering:
    • MongoDB Atlas supports complex queries like $in, $regex, and nested fields.
    • Example:
    • filter = {
              "metadata.tags": {"$in": ["nature", "sky"]}
          }
          results = vector_store.similarity_search(query, k=2, filter=filter)

For advanced filtering, see Metadata Filtering.

4. Persistence and Serialization

Persistence depends on the vector store backend, with custom embeddings ensuring compatibility.

  • Chroma:
    • Saves indexes to disk using persist_directory:
    • vector_store = Chroma.from_texts(
              texts=["The sky is blue."],
              embedding=embedding_function,
              persist_directory="./chroma_db"
          )
          vector_store.persist()
    • Load with:
    • vector_store = Chroma(
              collection_name="langchain_example",
              embedding_function=embedding_function,
              persist_directory="./chroma_db"
          )
  • MongoDB Atlas:
    • Persistent by default in the cloud:
    • vector_store = MongoDBAtlasVectorSearch.from_texts(
              texts=["The sky is blue."],
              embedding=embedding_function,
              collection=collection
          )
  • Delete Operations:
    • Chroma:
    • vector_store.delete(where={"source": "sky"})
    • MongoDB Atlas:
    • vector_store.delete(filter={"metadata.source": "sky"})

5. Document Store Management

The document store manages texts, embeddings, and metadata, with custom embeddings defining the vector representation.

  • Document Structure:
    • Chroma:
      • Stores records with id, embedding, metadata, and document.
      • Example:
      • {
                "id": "doc1",
                "embedding": [0.1, 0.2, ...],
                "metadata": {"source": "sky", "id": 1},
                "document": "The sky is blue."
              }
    • MongoDB Atlas:
      • Stores BSON documents with _id, text_key, embedding_key, and metadata.
      • Example:
      • {
                "_id": "507f1f77bcf86cd799439011",
                "text": "The sky is blue.",
                "embedding": [0.1, 0.2, ...],
                "metadata": {"source": "sky", "id": 1}
              }
  • Custom Embeddings:
    • Ensure consistent vector dimensions across embed_documents and embed_query.
    • Example:
    • def embed_documents(self, texts):
              embeddings = self.model(**self.tokenizer(texts, return_tensors="pt")).last_hidden_state.mean(dim=1).numpy()
              return embeddings.tolist()
  • Example:
  • documents = [
          Document(page_content="The sky is blue.", metadata={"source": "sky", "id": 1})
      ]
      vector_store.add_documents(documents)

Performance Optimization

Custom embeddings and vector stores can be optimized for speed and accuracy.

Embedding Optimization

  • Model Selection:
    • Use lightweight models (e.g., all-MiniLM-L6-v2) for faster inference.
    • Example:
    • embedding_function = HuggingFaceCustomEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
  • Batching:
    • Process texts in batches to leverage GPU parallelism:
    • def embed_documents(self, texts, batch_size=32):
              embeddings = []
              for i in range(0, len(texts), batch_size):
                  batch = texts[i:i + batch_size]
                  encoded = self.tokenizer(batch, padding=True, truncation=True, return_tensors="pt")
                  with torch.no_grad():
                      output = self.model(**encoded)
                  embeddings.extend(output.last_hidden_state.mean(dim=1).numpy().tolist())
              return embeddings
  • Device:
    • Use GPU for faster inference:
    • class HuggingFaceCustomEmbeddings(Embeddings):
              def __init__(self, model_name, device="cuda"):
                  self.tokenizer = AutoTokenizer.from_pretrained(model_name)
                  self.model = AutoModel.from_pretrained(model_name).to(device)
                  self.device = device

Vector Store Optimization

  • Chroma:
    • Configure HNSW parameters:
    • vector_store = Chroma(
              collection_name="langchain_example",
              embedding_function=embedding_function,
              collection_metadata={"hnsw:M": 32, "hnsw:ef_construction": 100}
          )
  • MongoDB Atlas:
    • Optimize index parameters:
    • {
            "mappings": {
              "fields": {
                "embedding": {
                  "type": "knnVector",
                  "dimensions": 384,
                  "similarity": "cosine",
                  "indexOptions": {"maxConnections": 32}
                }
              }
            }
          }

For optimization tips, see Vector Store Performance.

Practical Applications

Custom embeddings with LangChain’s vector stores power diverse AI applications:

  1. Semantic Search:
    • Index domain-specific documents for natural language queries.
    • Example: A medical knowledge base for clinical queries.
  1. Question Answering:
  1. Recommendation Systems:
    • Index product descriptions with custom embeddings for personalized recommendations.
  1. Chatbot Context:

Try the Document Search Engine Tutorial.

Comprehensive Example

Here’s a complete semantic search system with custom embeddings, metadata filtering, and MMR using Chroma:

from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from transformers import AutoTokenizer, AutoModel
import torch

# Custom embedding class
class HuggingFaceCustomEmbeddings(Embeddings):
    def __init__(self, model_name="sentence-transformers/all-MiniLM-L6-v2"):
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.model = AutoModel.from_pretrained(model_name)

    def embed_documents(self, texts):
        encoded_input = self.tokenizer(texts, padding=True, truncation=True, return_tensors="pt", max_length=512)
        with torch.no_grad():
            model_output = self.model(**encoded_input)
        embeddings = model_output.last_hidden_state.mean(dim=1).numpy()
        return embeddings.tolist()

    def embed_query(self, text):
        encoded_input = self.tokenizer(text, padding=True, truncation=True, return_tensors="pt", max_length=512)
        with torch.no_grad():
            model_output = self.model(**encoded_input)
        embedding = model_output.last_hidden_state.mean(dim=1).numpy()
        return embedding.tolist()[0]

# Initialize embeddings
embedding_function = HuggingFaceCustomEmbeddings()

# Create documents
documents = [
    Document(page_content="The sky is blue and vast.", metadata={"source": "sky", "id": 1}),
    Document(page_content="The grass is green and lush.", metadata={"source": "grass", "id": 2}),
    Document(page_content="The sun is bright and warm.", metadata={"source": "sun", "id": 3})
]

# Initialize vector store
vector_store = Chroma.from_documents(
    documents,
    embedding=embedding_function,
    collection_name="langchain_example",
    persist_directory="./chroma_db"
)

# Similarity search
query = "What is blue?"
results = vector_store.similarity_search_with_score(
    query,
    k=2,
    filter={"source": {"$eq": "sky"}}
)
for doc, score in results:
    print(f"Text: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}")

# MMR search
mmr_results = vector_store.max_marginal_relevance_search(
    query,
    k=2,
    fetch_k=10
)
for doc in mmr_results:
    print(f"MMR Text: {doc.page_content}, Metadata: {doc.metadata}")

# Persist and delete
vector_store.persist()
vector_store.delete(where={"source": "sky"})

Output:

Text: The sky is blue and vast., Metadata: {'source': 'sky', 'id': 1}, Score: 0.1234
MMR Text: The sky is blue and vast., Metadata: {'source': 'sky', 'id': 1}
MMR Text: The sun is bright and warm., Metadata: {'source': 'sun', 'id': 3}

Error Handling

Common issues include:

  • Dimension Mismatch: Ensure embed_documents and embed_query produce consistent vector sizes.
  • Embedding Errors: Verify model compatibility and input text length.
  • Persistence Issues: Check persist_directory permissions for Chroma or MongoDB connection for Atlas.
  • Filter Syntax: Ensure filter format matches the vector store’s requirements.

See Troubleshooting.

Limitations

  • Custom Implementation Complexity: Requires expertise to optimize embeddings for specific domains.
  • Compute Requirements: Transformer models may need GPU for large-scale indexing.
  • Vector Store Dependency: Custom embeddings must align with the vector store’s distance metrics.
  • No Native Hybrid Search: Combining dense and sparse embeddings requires additional logic.

Conclusion

Crafting custom embeddings for LangChain’s vector stores unlocks tailored semantic search capabilities, enabling domain-specific, cost-efficient, and optimized AI applications. By leveraging models like HuggingFace transformers and integrating with vector stores like Chroma or MongoDB Atlas, developers can build powerful systems for semantic search, question answering, and recommendations. Start experimenting with custom embeddings to enhance your LangChain projects.

For official documentation, visit LangChain Custom Embeddings.