Basic Langchain Q&A use case example throws ValueError when `<html>` tag of website doesn't set `lang` attribute.
Labels: name: area: doc loader, name: auto:bug

### System Info

[Basic Langchain Q&A use case example](https://python.langchain.com/docs/use_cases/question_answering/) throws ValueError when `<html>` tag of website doesn't set `lang` attribute.

In particular, `WebBaseLoader` parses the website soup metadata with:

```
def _build_metadata(soup: Any, url: str) -> dict:
    """Build metadata from BeautifulSoup output."""
    metadata = {"source": url}
    if title := soup.find("title"):
        metadata["title"] = title.get_text()
    if description := soup.find("meta", attrs={"name": "description"}):
        metadata["description"] = description.get("content", None)
    if html := soup.find("html"):
        metadata["language"] = html.get("lang", None)
    return metadata
```

thus setting `metadata["language"] = None` if there's no `lang` attribute for the `html` tag.

`chromadb` then validates the metadata with:

```
def validate_metadata(metadata: Metadata) -> Metadata:
    """Validates metadata to ensure it is a dictionary of strings to strings, ints, or floats"""
    if not isinstance(metadata, dict) and metadata is not None:
        raise ValueError(f"Expected metadata to be a dict or None, got {metadata}")
    if metadata is None:
        return metadata
    if len(metadata) == 0:
        raise ValueError(f"Expected metadata to be a non-empty dict, got {metadata}")
    for key, value in metadata.items():
        if not isinstance(key, str):
            raise ValueError(
                f"Expected metadata key to be a str, got {key} which is a {type(key)}"
            )
        # isinstance(True, int) evaluates to True, so we need to check for bools separately
        if not isinstance(value, (str, int, float)) or isinstance(value, bool):
            raise ValueError(
                f"Expected metadata value to be a str, int, or float, got {value} which is a {type(value)}"
            )
    return metadata
```

and thus raises a ValueError whenever `metadata is None`.

The two obvious solutions:

1) Change the metadata parser to return strings in the event they don't find a metadata tag:

```
def _build_metadata(soup: Any, url: str) -> dict:
    """Build metadata from BeautifulSoup output."""
    metadata = {"source": url}
    if title := soup.find("title"):
        metadata["title"] = title.get_text()
    if description := soup.find("meta", attrs={"name": "description"}):
        metadata["description"] = description.get("content", "No description found.")
    if html := soup.find("html"):
        metadata["language"] = html.get("lang", "No language found.")
    return metadata
```

2) Change the metadata validator in `chromadb` to accept metadata objects with of type `None`.

Option 2 likely involves a lot of knock-on changes to `chromadb` to handle None-type metadata, so I'd prefer option 1, but defer to the maintainers. Will put up a quick PR for option 1 below for reference.



**Full Stack Trace:**

ValueError                                Traceback (most recent call last)
Cell In[6], line 1
----> 1 print(query_site("What is this article about?", "https://www.montyevans.com/sports-betting"))

Cell In[5], line 6, in query_site(question, site_url)
      4 if site_url not in memo_dict:
      5     loader = WebBaseLoader(site_url)
----> 6     index = VectorstoreIndexCreator().from_loaders([loader])
      7     memo_dict[site_url] = index
      9 site_index = memo_dict[site_url]

File [/Volumes/git/aipes/langchain/document_qa/langchain/langchain/indexes/vectorstore.py:73](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/document_qa/langchain/langchain/indexes/vectorstore.py:73), in VectorstoreIndexCreator.from_loaders(self, loaders)
     71 for loader in loaders:
     72     docs.extend(loader.load())
---> 73 return self.from_documents(docs)

File [/Volumes/git/aipes/langchain/document_qa/langchain/langchain/indexes/vectorstore.py:78](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/document_qa/langchain/langchain/indexes/vectorstore.py:78), in VectorstoreIndexCreator.from_documents(self, documents)
     76 """Create a vectorstore index from documents."""
     77 sub_docs = self.text_splitter.split_documents(documents)
---> 78 vectorstore = self.vectorstore_cls.from_documents(
     79     sub_docs, self.embedding, **self.vectorstore_kwargs
     80 )
     81 return VectorStoreIndexWrapper(vectorstore=vectorstore)

File [/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:578](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:578), in Chroma.from_documents(cls, documents, embedding, ids, collection_name, persist_directory, client_settings, client, collection_metadata, **kwargs)
    576 texts = [doc.page_content for doc in documents]
    577 metadatas = [doc.metadata for doc in documents]
--> 578 return cls.from_texts(
    579     texts=texts,
    580     embedding=embedding,
    581     metadatas=metadatas,
    582     ids=ids,
    583     collection_name=collection_name,
    584     persist_directory=persist_directory,
    585     client_settings=client_settings,
    586     client=client,
    587     collection_metadata=collection_metadata,
    588     **kwargs,
    589 )

File [/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:542](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:542), in Chroma.from_texts(cls, texts, embedding, metadatas, ids, collection_name, persist_directory, client_settings, client, collection_metadata, **kwargs)
    514 """Create a Chroma vectorstore from a raw documents.
    515 
    516 If a persist_directory is specified, the collection will be persisted there.
   (...)
    531     Chroma: Chroma vectorstore.
    532 """
    533 chroma_collection = cls(
    534     collection_name=collection_name,
    535     embedding_function=embedding,
   (...)
    540     **kwargs,
    541 )
--> 542 chroma_collection.add_texts(texts=texts, metadatas=metadatas, ids=ids)
    543 return chroma_collection

File [/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:193](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/document_qa/langchain/langchain/vectorstores/chroma.py:193), in Chroma.add_texts(self, texts, metadatas, ids, **kwargs)
    189     embeddings_with_metadatas = (
    190         [embeddings[i] for i in non_empty] if embeddings else None
    191     )
    192     ids_with_metadata = [ids[i] for i in non_empty]
--> 193     self._collection.upsert(
    194         metadatas=metadatas,
    195         embeddings=embeddings_with_metadatas,
    196         documents=texts_with_metadatas,
    197         ids=ids_with_metadata,
    198     )
    200 texts = [texts[j] for j in empty]
    201 embeddings = [embeddings[j] for j in empty] if embeddings else None

File [~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/models/Collection.py:298](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/models/Collection.py:298), in Collection.upsert(self, ids, embeddings, metadatas, documents, increment_index)
    278 def upsert(
    279     self,
    280     ids: OneOrMany[ID],
   (...)
    284     increment_index: bool = True,
    285 ) -> None:
    286     """Update the embeddings, metadatas or documents for provided ids, or create them if they don't exist.
    287 
    288     Args:
   (...)
    295         None
    296     """
--> 298     ids, embeddings, metadatas, documents = self._validate_embedding_set(
    299         ids, embeddings, metadatas, documents
    300     )
    302     self._client._upsert(
    303         collection_id=self.id,
    304         ids=ids,
   (...)
    308         increment_index=increment_index,
    309     )

File [~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/models/Collection.py:357](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/models/Collection.py:357), in Collection._validate_embedding_set(self, ids, embeddings, metadatas, documents, require_embeddings_or_documents)
    350 ids = validate_ids(maybe_cast_one_to_many(ids))
    351 embeddings = (
    352     validate_embeddings(maybe_cast_one_to_many(embeddings))
    353     if embeddings is not None
    354     else None
    355 )
    356 metadatas = (
--> 357     validate_metadatas(maybe_cast_one_to_many(metadatas))
    358     if metadatas is not None
    359     else None
    360 )
    361 documents = maybe_cast_one_to_many(documents) if documents is not None else None
    363 # Check that one of embeddings or documents is provided

File [~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/types.py:169](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/types.py:169), in validate_metadatas(metadatas)
    167     raise ValueError(f"Expected metadatas to be a list, got {metadatas}")
    168 for metadata in metadatas:
--> 169     validate_metadata(metadata)
    170 return metadatas

File [~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/types.py:140](https://file+.vscode-resource.vscode-cdn.net/Volumes/git/aipes/langchain/~/mambaforge/envs/langchain/lib/python3.10/site-packages/chromadb/api/types.py:140), in validate_metadata(metadata)
    138     # isinstance(True, int) evaluates to True, so we need to check for bools separately
    139     if not isinstance(value, (str, int, float)) or isinstance(value, bool):
--> 140         raise ValueError(
    141             f"Expected metadata value to be a str, int, or float, got {value} which is a {type(value)}"
    142         )
    143 return metadata

### Who can help?

@hwchase17 @eyurtsev 

### Information

- [X] The official example notebooks/scripts
- [ ] My own modified scripts

### Related Components

- [X] LLMs/Chat Models
- [ ] Embedding Models
- [ ] Prompts / Prompt Templates / Prompt Selectors
- [ ] Output Parsers
- [X] Document Loaders
- [X] Vector Stores / Retrievers
- [ ] Memory
- [ ] Agents / Agent Executors
- [ ] Tools / Toolkits
- [ ] Chains
- [ ] Callbacks/Tracing
- [ ] Async

### Reproduction

```
from langchain.document_loaders import WebBaseLoader
from langchain.indexes import VectorstoreIndexCreator

memo_dict = {}

def query_site(question, site_url):
    if site_url not in memo_dict:
        loader = WebBaseLoader(site_url)
        index = VectorstoreIndexCreator().from_loaders([loader])
        memo_dict[site_url] = index

    site_index = memo_dict[site_url]
    return site_index.query(question)

print(query_site("What is this article about?", "https://www.montyevans.com/sports-betting"))
```

### Expected behavior

I expect to see an answer to the question, based on the site content, printed to console; instead ChromaDB raises a ValueError.