From a72fddbf8d0c440d1ebc4bb4d5f924ce5205fc38 Mon Sep 17 00:00:00 2001 From: Isaac Francisco <78627776+isahers1@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:20:27 -0700 Subject: [PATCH] [docs]: vector store integration pages (#24858) Co-authored-by: Erick Friis --- .../docs/integrations/vectorstores/.gitignore | 2 + .../integrations/vectorstores/astradb.ipynb | 748 +++++++------ .../integrations/vectorstores/chroma.ipynb | 804 +++++++------- .../vectorstores/clickhouse.ipynb | 542 +++++----- .../integrations/vectorstores/couchbase.ipynb | 703 ++++++------ .../vectorstores/elasticsearch.ipynb | 997 ++++++------------ .../integrations/vectorstores/faiss.ipynb | 720 +++++++------ .../vectorstores/faiss_index/index.faiss | Bin 258093 -> 0 bytes docs/docs/integrations/vectorstores/index.mdx | 29 + .../integrations/vectorstores/milvus.ipynb | 498 ++++++--- .../vectorstores/mongodb_atlas.ipynb | 680 ++++++------ .../integrations/vectorstores/pgvector.ipynb | 466 ++++---- .../integrations/vectorstores/pinecone.ipynb | 563 +++++----- .../integrations/vectorstores/qdrant.ipynb | 671 +++++++----- .../integrations/vectorstores/redis.ipynb | 840 +++++---------- docs/scripts/vectorstore_feat_table.py | 269 +++++ docs/src/theme/EmbeddingTabs.js | 75 ++ .../docs/vectorstores.ipynb | 41 +- .../integration_template/vectorstores.py | 60 +- .../vectorstores/clickhouse.py | 133 ++- .../langchain_community/vectorstores/faiss.py | 125 ++- .../vectorstores/pgvector.py | 28 +- .../vectorstores/redis/base.py | 166 +-- .../chroma/langchain_chroma/vectorstores.py | 134 ++- .../langchain_couchbase/vectorstores.py | 160 ++- .../langchain_milvus/vectorstores/milvus.py | 212 ++-- .../mongodb/langchain_mongodb/vectorstores.py | 145 ++- .../langchain_pinecone/vectorstores.py | 140 ++- .../qdrant/langchain_qdrant/qdrant.py | 134 ++- 29 files changed, 5649 insertions(+), 4436 deletions(-) create mode 100644 docs/docs/integrations/vectorstores/.gitignore delete mode 100644 docs/docs/integrations/vectorstores/faiss_index/index.faiss create mode 100644 docs/docs/integrations/vectorstores/index.mdx create mode 100644 docs/scripts/vectorstore_feat_table.py create mode 100644 docs/src/theme/EmbeddingTabs.js diff --git a/docs/docs/integrations/vectorstores/.gitignore b/docs/docs/integrations/vectorstores/.gitignore new file mode 100644 index 00000000000..8d45b7a4cab --- /dev/null +++ b/docs/docs/integrations/vectorstores/.gitignore @@ -0,0 +1,2 @@ +# files generated by faiss.ipynb +faiss_index diff --git a/docs/docs/integrations/vectorstores/astradb.ipynb b/docs/docs/integrations/vectorstores/astradb.ipynb index 136279663d4..22db0166a85 100644 --- a/docs/docs/integrations/vectorstores/astradb.ipynb +++ b/docs/docs/integrations/vectorstores/astradb.ipynb @@ -5,33 +5,13 @@ "id": "66d0270a-b74f-4110-901e-7960b00297af", "metadata": {}, "source": [ - "# Astra DB\n", + "# Astra DB Vector Store\n", "\n", - "This page provides a quickstart for using [Astra DB](https://docs.datastax.com/en/astra/home/astra.html) as a Vector Store." - ] - }, - { - "cell_type": "markdown", - "id": "ab8cd64f-3bb2-4f16-a0a9-12d7b1789bf6", - "metadata": {}, - "source": [ - "> DataStax [Astra DB](https://docs.datastax.com/en/astra/home/astra.html) is a serverless vector-capable database built on Apache Cassandra® and made conveniently available through an easy-to-use JSON API." - ] - }, - { - "cell_type": "markdown", - "id": "d2d6ca14-fb7e-4172-9aa0-a3119a064b96", - "metadata": {}, - "source": [ - "_Note: in addition to access to the database, an OpenAI API Key is required to run the full example._" - ] - }, - { - "cell_type": "markdown", - "id": "bb9be7ce-8c70-4d46-9f11-71c42a36e928", - "metadata": {}, - "source": [ - "## Setup and general dependencies" + "This page provides a quickstart for using [Astra DB](https://docs.datastax.com/en/astra/home/astra.html) as a Vector Store.\n", + "\n", + "> DataStax [Astra DB](https://docs.datastax.com/en/astra/home/astra.html) is a serverless vector-capable database built on Apache Cassandra® and made conveniently available through an easy-to-use JSON API.\n", + "\n", + "## Setup" ] }, { @@ -39,7 +19,7 @@ "id": "dbe7c156-0413-47e3-9237-4769c4248869", "metadata": {}, "source": [ - "Use of the integration requires the corresponding Python package:" + "Use of the integration requires the `langchain-astradb` partner package:" ] }, { @@ -49,54 +29,61 @@ "metadata": {}, "outputs": [], "source": [ - "pip install -qU langchain-astradb" + "pip install -qU \"langchain-astradb>=0.3.3\"" ] }, { "cell_type": "markdown", - "id": "2453d83a-bc8f-41e1-a692-befe4dd90156", + "id": "319bf84b", "metadata": {}, "source": [ - "_Make sure you have installed the packages required to run all of this demo:_" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56c1f86e-5921-4976-ac8f-1d62e5a512b0", - "metadata": {}, - "outputs": [], - "source": [ - "pip install -qU langchain langchain-community langchain-openai datasets pypdf" - ] - }, - { - "cell_type": "markdown", - "id": "c2910035-e61f-48d9-a110-d68c401b62aa", - "metadata": {}, - "source": [ - "### Import dependencies" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b06619af-fea2-4863-8149-7f239a8c9c82", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from getpass import getpass\n", + "### Credentials\n", "\n", - "from astrapy.info import CollectionVectorServiceOptions\n", - "from datasets import load_dataset\n", - "from langchain_community.document_loaders import PyPDFLoader\n", - "from langchain_core.documents import Document\n", - "from langchain_core.output_parsers import StrOutputParser\n", - "from langchain_core.prompts import ChatPromptTemplate\n", - "from langchain_core.runnables import RunnablePassthrough\n", - "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n", - "from langchain_text_splitters import RecursiveCharacterTextSplitter" + "In order to use the AstraDB vector store, you must first head to the [AstraDB website](https://astra.datastax.com), create an account, and then create a new database - the initialization might take a few minutes. \n", + "\n", + "Once the database has been initialized, you should [create an application token](https://docs.datastax.com/en/astra-db-serverless/administration/manage-application-tokens.html#generate-application-token) and save it for later use. \n", + "\n", + "You will also want to copy the `API Endpoint` from the `Database Details` and store that in the `ASTRA_DB_API_ENDPOINT` variable.\n", + "\n", + "You may optionally provide a namespace, which you can manage from the `Data Explorer` tab of your database dashboard. If you don't wish to set a namespace, you can leave the `getpass` prompt for `ASTRA_DB_NAMESPACE` empty." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b7843c22", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "\n", + "ASTRA_DB_API_ENDPOINT = getpass.getpass(\"ASTRA_DB_API_ENDPOINT = \")\n", + "ASTRA_DB_APPLICATION_TOKEN = getpass.getpass(\"ASTRA_DB_APPLICATION_TOKEN = \")\n", + "\n", + "desired_namespace = getpass.getpass(\"ASTRA_DB_NAMESPACE = \")\n", + "if desired_namespace:\n", + " ASTRA_DB_NAMESPACE = desired_namespace\n", + "else:\n", + " ASTRA_DB_NAMESPACE = None" + ] + }, + { + "cell_type": "markdown", + "id": "e1c5cd9e", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3cb739c0", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" ] }, { @@ -104,48 +91,59 @@ "id": "22866f09-e10d-4f05-a24b-b9420129462e", "metadata": {}, "source": [ - "## Import the Vector Store" + "## Initialization\n", + "\n", + "There are two ways to create an Astra DB vector store, which differ in how the embeddings are computed.\n", + "\n", + "#### Method 1: Explicit embeddings\n", + "\n", + "You can separately instantiate a `langchain_core.embeddings.Embeddings` class and pass it to the `AstraDBVectorStore` constructor, just like with most other LangChain vector stores.\n", + "\n", + "#### Method 2: Integrated embedding computation\n", + "\n", + "Alternatively, you can use the [Vectorize](https://www.datastax.com/blog/simplifying-vector-embedding-generation-with-astra-vectorize) feature of Astra DB and simply specify the name of a supported embedding model when creating the store. The embedding computations are entirely handled within the database. (To proceed with this method, you must have enabled the desired embedding integration for your database, as described [in the docs](https://docs.datastax.com/en/astra-db-serverless/databases/embedding-generation.html).)\n", + "\n", + "### Explicit Embedding Initialization\n", + "\n", + "Below, we instantiate our vector store using the explicit embedding class:\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, + "id": "d71a1dcb", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, "id": "0b32730d-176e-414c-9d91-fd3644c54211", "metadata": {}, "outputs": [], "source": [ - "from langchain_astradb import AstraDBVectorStore" - ] - }, - { - "cell_type": "markdown", - "id": "68f61b01-3e09-47c1-9d67-5d6915c86626", - "metadata": {}, - "source": [ - "## DB Connection parameters\n", + "from langchain_astradb import AstraDBVectorStore\n", "\n", - "These are found on your Astra DB dashboard:\n", - "\n", - "- the API Endpoint looks like `https://01234567-89ab-cdef-0123-456789abcdef-us-east1.apps.astra.datastax.com`\n", - "- the Token looks like `AstraCS:6gBhNmsk135....`\n", - "- you may optionally provide a _Namespace_ such as `my_namespace`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d78af8ed-cff9-4f14-aa5d-016f99ab547c", - "metadata": {}, - "outputs": [], - "source": [ - "ASTRA_DB_API_ENDPOINT = input(\"ASTRA_DB_API_ENDPOINT = \")\n", - "ASTRA_DB_APPLICATION_TOKEN = getpass(\"ASTRA_DB_APPLICATION_TOKEN = \")\n", - "\n", - "desired_namespace = input(\"(optional) Namespace = \")\n", - "if desired_namespace:\n", - " ASTRA_DB_KEYSPACE = desired_namespace\n", - "else:\n", - " ASTRA_DB_KEYSPACE = None" + "vector_store = AstraDBVectorStore(\n", + " collection_name=\"astra_vector_langchain\",\n", + " embedding=embeddings,\n", + " api_endpoint=ASTRA_DB_API_ENDPOINT,\n", + " token=ASTRA_DB_APPLICATION_TOKEN,\n", + " namespace=ASTRA_DB_NAMESPACE,\n", + ")" ] }, { @@ -153,85 +151,14 @@ "id": "84a1fe85-a42c-4f15-92e1-f79f1dd43ea2", "metadata": {}, "source": [ - "## Create the vector store\n", - "\n", - "There are two ways to create an Astra DB vector store, which differ in how the embeddings are computed.\n", - "\n", - "*Explicit embeddings*. You can separately instantiate a `langchain_core.embeddings.Embeddings` class and pass it to the `AstraDBVectorStore` constructor, just like with most other LangChain vector stores.\n", - "\n", - "*Integrated embedding computation*. Alternatively, you can use the [Vectorize](https://www.datastax.com/blog/simplifying-vector-embedding-generation-with-astra-vectorize) feature of Astra DB and simply specify the name of a supported embedding model when creating the store. The embedding computations are entirely handled within the database. (To proceed with this method, you must have enabled the desired embedding integration for your database, as described [in the docs](https://docs.datastax.com/en/astra-db-serverless/databases/embedding-generation.html).)\n", - "\n", - "**Please choose one method and run the corresponding cells only.**" - ] - }, - { - "cell_type": "markdown", - "id": "8c435386-e8d5-41f4-a9e5-7b609ef781f9", - "metadata": {}, - "source": [ - "### Method 1: provide embeddings explicitly\n", - "\n", - "This demo will use an OpenAI embedding model:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfa5c005-9738-4c53-b8a8-8540fcbb8bad", - "metadata": {}, - "outputs": [], - "source": [ - "os.environ[\"OPENAI_API_KEY\"] = getpass(\"OPENAI_API_KEY = \")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3accae6f-73e2-483a-83f7-76eb33558a1f", - "metadata": {}, - "outputs": [], - "source": [ - "my_embeddings = OpenAIEmbeddings()" - ] - }, - { - "cell_type": "markdown", - "id": "465b1b16-5363-4c4f-9917-a49e02a86c14", - "metadata": {}, - "source": [ - "Now you can create the vector store:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b77553b-8bb5-4949-b87b-8c6abac56a26", - "metadata": {}, - "outputs": [], - "source": [ - "vstore = AstraDBVectorStore(\n", - " embedding=my_embeddings,\n", - " collection_name=\"astra_vector_demo\",\n", - " api_endpoint=ASTRA_DB_API_ENDPOINT,\n", - " token=ASTRA_DB_APPLICATION_TOKEN,\n", - " namespace=ASTRA_DB_KEYSPACE,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "5d5d2bfa-c071-4a5b-8b6e-3daa1b6de164", - "metadata": {}, - "source": [ - "### Method 2: use Astra Vectorize (embeddings integrated in Astra DB)\n", + "### Integrated Embedding Initialization\n", "\n", "Here it is assumed that you have\n", "\n", - "- enabled the OpenAI integration in your Astra DB organization,\n", - "- added an API Key named `\"MY_OPENAI_API_KEY\"` to the integration, and\n", - "- scoped it to the database you are using.\n", + "- Enabled the OpenAI integration in your Astra DB organization,\n", + "- Added an API Key named `\"OPENAI_API_KEY\"` to the integration, and scoped it to the database you are using.\n", "\n", - "For more details please consult the [documentation](https://docs.datastax.com/en/astra-db-serverless/integrations/embedding-providers/openai.html)." + "For more details on how to do this, please consult the [documentation](https://docs.datastax.com/en/astra-db-serverless/integrations/embedding-providers/openai.html)." ] }, { @@ -241,312 +168,355 @@ "metadata": {}, "outputs": [], "source": [ + "from astrapy.info import CollectionVectorServiceOptions\n", + "\n", "openai_vectorize_options = CollectionVectorServiceOptions(\n", " provider=\"openai\",\n", " model_name=\"text-embedding-3-small\",\n", " authentication={\n", - " \"providerKey\": \"MY_OPENAI_API_KEY\",\n", + " \"providerKey\": \"OPENAI_API_KEY\",\n", " },\n", ")\n", "\n", - "vstore = AstraDBVectorStore(\n", - " collection_name=\"astra_vectorize_demo\",\n", + "vector_store_integrated = AstraDBVectorStore(\n", + " collection_name=\"astra_vector_langchain_integrated\",\n", " api_endpoint=ASTRA_DB_API_ENDPOINT,\n", " token=ASTRA_DB_APPLICATION_TOKEN,\n", - " namespace=ASTRA_DB_KEYSPACE,\n", + " namespace=ASTRA_DB_NAMESPACE,\n", " collection_vector_service_options=openai_vectorize_options,\n", ")" ] }, { "cell_type": "markdown", - "id": "9a348678-b2f6-46ca-9a0d-2eb4cc6b66b1", + "id": "d3796b39", "metadata": {}, "source": [ - "## Load a dataset" - ] - }, - { - "cell_type": "markdown", - "id": "552e56b0-301a-4b06-99c7-57ba6faa966f", - "metadata": {}, - "source": [ - "Convert each entry in the source dataset into a `Document`, then write them into the vector store:" + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", - "execution_count": null, - "id": "3a1f532f-ad63-4256-9730-a183841bd8e9", + "execution_count": 23, + "id": "afb3e155", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[UUID('89a5cea1-5f3d-47c1-89dc-7e36e12cf4de'),\n", + " UUID('d4e78c48-f954-4612-8a38-af22923ba23b'),\n", + " UUID('058e4046-ded0-4fc1-b8ac-60e5a5f08ea0'),\n", + " UUID('50ab2a9a-762c-4b78-b102-942a86d77288'),\n", + " UUID('1da5a3c1-ba51-4f2f-aaaf-79a8f5011ce3'),\n", + " UUID('f3055d9e-2eb1-4d25-838e-2c70548f91b5'),\n", + " UUID('4bf0613d-08d0-4fbc-a43c-4955e4c9e616'),\n", + " UUID('18008625-8fd4-45c2-a0d7-92a2cde23dbc'),\n", + " UUID('c712e06f-790b-4fd4-9040-7ab3898965d0'),\n", + " UUID('a9b84820-3445-4810-a46c-e77b76ab85bc')]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "philo_dataset = load_dataset(\"datastax/philosopher-quotes\")[\"train\"]\n", + "from uuid import uuid4\n", "\n", - "docs = []\n", - "for entry in philo_dataset:\n", - " metadata = {\"author\": entry[\"author\"]}\n", - " doc = Document(page_content=entry[\"quote\"], metadata=metadata)\n", - " docs.append(doc)\n", + "from langchain_core.documents import Document\n", "\n", - "inserted_ids = vstore.add_documents(docs)\n", - "print(f\"\\nInserted {len(inserted_ids)} documents.\")" + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" ] }, { "cell_type": "markdown", - "id": "79d4f436-ef04-4288-8f79-97c9abb983ed", + "id": "dfce4edc", "metadata": {}, "source": [ - "In the above, `metadata` dictionaries are created from the source data and are part of the `Document`.\n", + "### Delete items from vector store\n", "\n", - "_Note: check the [Astra DB API Docs](https://docs.datastax.com/en/astra-serverless/docs/develop/dev-with-json.html#_json_api_limits) for the valid metadata field names: some characters are reserved and cannot be used._" - ] - }, - { - "cell_type": "markdown", - "id": "084d8802-ab39-4262-9a87-42eafb746f92", - "metadata": {}, - "source": [ - "Add some more entries, this time with `add_texts`:" + "We can delete items from our vector store by ID by using the `delete` function." ] }, { "cell_type": "code", - "execution_count": null, - "id": "b6b157f5-eb31-4907-a78e-2e2b06893936", + "execution_count": 24, + "id": "d3f69315", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "texts = [\"I think, therefore I am.\", \"To the things themselves!\"]\n", - "metadatas = [{\"author\": \"descartes\"}, {\"author\": \"husserl\"}]\n", - "ids = [\"desc_01\", \"huss_xy\"]\n", + "vector_store.delete(ids=uuids[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "d12e1a07", + "metadata": {}, + "source": [ + "## Query vector store\n", "\n", - "inserted_ids_2 = vstore.add_texts(texts=texts, metadatas=metadatas, ids=ids)\n", - "print(f\"\\nInserted {len(inserted_ids_2)} documents.\")" - ] - }, - { - "cell_type": "markdown", - "id": "63840eb3-8b29-4017-bc2f-301bf5001f28", - "metadata": {}, - "source": [ - "_Note: you may want to speed up the execution of `add_texts` and `add_documents` by increasing the concurrency level for_\n", - "_these bulk operations - check out the `*_concurrency` parameters in the class constructor and the `add_texts` docstrings_\n", - "_for more details. Depending on the network and the client machine specifications, your best-performing choice of parameters may vary._" - ] - }, - { - "cell_type": "markdown", - "id": "c031760a-1fc5-4855-adf2-02ed52fe2181", - "metadata": {}, - "source": [ - "## Run searches" - ] - }, - { - "cell_type": "markdown", - "id": "02a77d8e-1aae-4054-8805-01c77947c49f", - "metadata": {}, - "source": [ - "This section demonstrates metadata filtering and getting the similarity scores back:" + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search with filtering on metadata can be done as follows:" ] }, { "cell_type": "code", - "execution_count": null, - "id": "1761806a-1afd-4491-867c-25a80d92b9fe", + "execution_count": 15, + "id": "770b3467", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet'}]\n" + ] + } + ], "source": [ - "results = vstore.similarity_search(\"Our life is what we make of it\", k=3)\n", + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter={\"source\": \"tweet\"},\n", + ")\n", "for res in results:\n", " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "eebc4f7c-f61a-438e-b3c8-17e6888d8a0b", + "cell_type": "markdown", + "id": "ce112165", "metadata": {}, - "outputs": [], "source": [ - "results_filtered = vstore.similarity_search(\n", - " \"Our life is what we make of it\",\n", - " k=3,\n", - " filter={\"author\": \"plato\"},\n", - ")\n", - "for res in results_filtered:\n", - " print(f\"* {res.page_content} [{res.metadata}]\")" + "#### Similarity search with score\n", + "\n", + "You can also search with score:" ] }, { "cell_type": "code", - "execution_count": null, - "id": "11bbfe64-c0cd-40c6-866a-a5786538450e", + "execution_count": 16, + "id": "5924309a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.776585] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'source': 'news'}]\n" + ] + } + ], "source": [ - "results = vstore.similarity_search_with_score(\"Our life is what we make of it\", k=3)\n", + "results = vector_store.similarity_search_with_score(\n", + " \"Will it be hot tomorrow?\", k=1, filter={\"source\": \"news\"}\n", + ")\n", "for res, score in results:\n", " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "b14ea558-bfbe-41ce-807e-d70670060ada", + "id": "fead7af5", "metadata": {}, "source": [ - "### MMR (Maximal-marginal-relevance) search\n", + "#### Other search methods\n", "\n", - "_Note: the MMR search method is not (yet) supported for vector stores built with Astra Vectorize._" + "There are a variety of other search methods that are not covered in this notebook, such as MMR search or searching by vector. For a full list of the search abilities available for `AstraDBVectorStore` check out the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_astradb.vectorstores.AstraDBVectorStore.html)." + ] + }, + { + "cell_type": "markdown", + "id": "7e40f714", + "metadata": {}, + "source": [ + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. \n", + "\n", + "Here is how to transform your vector store into a retriever and then invoke the retreiever with a simple query and filter." ] }, { "cell_type": "code", - "execution_count": null, - "id": "76381ce8-780a-4e3b-97b1-056d6782d7d5", + "execution_count": 17, + "id": "dcee50e6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "results = vstore.max_marginal_relevance_search(\n", - " \"Our life is what we make of it\",\n", - " k=3,\n", - " filter={\"author\": \"aristotle\"},\n", + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\",\n", + " search_kwargs={\"k\": 1, \"score_threshold\": 0.5},\n", ")\n", - "for res in results:\n", - " print(f\"* {res.page_content} [{res.metadata}]\")" + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" ] }, { "cell_type": "markdown", - "id": "60fda5df-14e4-4fb0-bd17-65a393fab8a9", + "id": "734e683a", "metadata": {}, "source": [ - "### Async\n", + "## Chain usage\n", "\n", - "Note that the Astra DB vector store supports all fully async methods (`asimilarity_search`, `afrom_texts`, `adelete` and so on) natively, i.e. without thread wrapping involved." - ] - }, - { - "cell_type": "markdown", - "id": "1cc86edd-692b-4495-906c-ccfd13b03c23", - "metadata": {}, - "source": [ - "## Deleting stored documents" + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": null, - "id": "38a70ec4-b522-4d32-9ead-c642864fca37", + "execution_count": 25, + "id": "9b3cc97b", "metadata": {}, "outputs": [], "source": [ - "delete_1 = vstore.delete(inserted_ids[:3])\n", - "print(f\"all_succeed={delete_1}\") # True, all documents deleted" + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "d4cf49ed-9d29-4ed9-bdab-51a308c41b8e", + "execution_count": 21, + "id": "08401498", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of such applications. Its capabilities make it a preferred choice for developers in this domain.'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "delete_2 = vstore.delete(inserted_ids[2:5])\n", - "print(f\"some_succeeds={delete_2}\") # True, though some IDs were gone already" - ] - }, - { - "cell_type": "markdown", - "id": "847181ba-77d1-4a17-b7f9-9e2c3d8efd13", - "metadata": {}, - "source": [ - "## A minimal RAG chain" - ] - }, - { - "cell_type": "markdown", - "id": "cd64b844-846f-43c5-a7dd-c26b9ed417d0", - "metadata": {}, - "source": [ - "The next cells will implement a simple RAG pipeline:\n", - "- download a sample PDF file and load it onto the store;\n", - "- create a RAG chain with LCEL (LangChain Expression Language), with the vector store at its heart;\n", - "- run the question-answering chain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5cbc4dba-0d5e-4038-8fc5-de6cadd1c2a9", - "metadata": {}, - "outputs": [], - "source": [ - "!curl -L \\\n", - " \"https://github.com/awesome-astra/datasets/blob/main/demo-resources/what-is-philosophy/what-is-philosophy.pdf?raw=true\" \\\n", - " -o \"what-is-philosophy.pdf\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "459385be-5e9c-47ff-ba53-2b7ae6166b09", - "metadata": {}, - "outputs": [], - "source": [ - "pdf_loader = PyPDFLoader(\"what-is-philosophy.pdf\")\n", - "splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=64)\n", - "docs_from_pdf = pdf_loader.load_and_split(text_splitter=splitter)\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", "\n", - "print(f\"Documents from PDF: {len(docs_from_pdf)}.\")\n", - "inserted_ids_from_pdf = vstore.add_documents(docs_from_pdf)\n", - "print(f\"Inserted {len(inserted_ids_from_pdf)} documents.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5010a66c-4298-4e32-82b5-2da0d36a5c70", - "metadata": {}, - "outputs": [], - "source": [ - "retriever = vstore.as_retriever(search_kwargs={\"k\": 3})\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", "\n", - "philo_template = \"\"\"\n", - "You are a philosopher that draws inspiration from great thinkers of the past\n", - "to craft well-thought answers to user questions. Use the provided context as the basis\n", - "for your answers and do not make up new reasoning paths - just mix-and-match what you are given.\n", - "Your answers must be concise and to the point, and refrain from answering about other topics than philosophy.\n", "\n", - "CONTEXT:\n", - "{context}\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", "\n", - "QUESTION: {question}\n", "\n", - "YOUR ANSWER:\"\"\"\n", - "\n", - "philo_prompt = ChatPromptTemplate.from_template(philo_template)\n", - "\n", - "llm = ChatOpenAI()\n", - "\n", - "chain = (\n", - " {\"context\": retriever, \"question\": RunnablePassthrough()}\n", - " | philo_prompt\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", " | llm\n", " | StrOutputParser()\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcbc1296-6c7c-478b-b55b-533ba4e54ddb", - "metadata": {}, - "outputs": [], - "source": [ - "chain.invoke(\"How does Russel elaborate on Peirce's idea of the security blanket?\")" + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" ] }, { @@ -562,7 +532,7 @@ "id": "177610c7-50d0-4b7b-8634-b03338054c8e", "metadata": {}, "source": [ - "## Cleanup" + "## Cleanup vector store" ] }, { @@ -582,7 +552,17 @@ "metadata": {}, "outputs": [], "source": [ - "vstore.delete_collection()" + "vector_store.delete_collection()" + ] + }, + { + "cell_type": "markdown", + "id": "a14c34be", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `AstraDBVectorStore` features and configurations head to the API reference:https://api.python.langchain.com/en/latest/vectorstores/langchain_astradb.vectorstores.AstraDBVectorStore.html" ] } ], @@ -602,7 +582,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/chroma.ipynb b/docs/docs/integrations/vectorstores/chroma.ipynb index 53a9bfc32b2..d5fafe18825 100644 --- a/docs/docs/integrations/vectorstores/chroma.ipynb +++ b/docs/docs/integrations/vectorstores/chroma.ipynb @@ -7,30 +7,23 @@ "source": [ "# Chroma\n", "\n", - ">[Chroma](https://docs.trychroma.com/getting-started) is a AI-native open-source vector database focused on developer productivity and happiness. Chroma is licensed under Apache 2.0.\n", + "This notebook covers how to get started with the `Chroma` vector store.\n", "\n", + ">[Chroma](https://docs.trychroma.com/getting-started) is a AI-native open-source vector database focused on developer productivity and happiness. Chroma is licensed under Apache 2.0. View the full docs of `Chroma` at [this page](https://docs.trychroma.com/reference/py-collection), and find the API reference for the LangChain integration at [this page](https://api.python.langchain.com/en/latest/vectorstores/langchain_chroma.vectorstores.Chroma.html).\n", "\n", - "Install Chroma with:\n", + "## Setup\n", "\n", - "```sh\n", - "pip install langchain-chroma\n", - "```\n", - "\n", - "Chroma runs in various modes. See below for examples of each integrated with LangChain.\n", - "- `in-memory` - in a python script or jupyter notebook\n", - "- `in-memory with persistance` - in a script or notebook and save/load to disk\n", - "- `in a docker container` - as a server running your local machine or in the cloud\n", - "\n", - "Like any other database, you can: \n", - "- `.add` \n", - "- `.get` \n", - "- `.update`\n", - "- `.upsert`\n", - "- `.delete`\n", - "- `.peek`\n", - "- and `.query` runs the similarity search.\n", - "\n", - "View full docs at [docs](https://docs.trychroma.com/reference/py-collection). To access these methods directly, you can do `._collection.method()`\n" + "To access `Chroma` vector stores you'll need to install the `langchain-chroma` integration package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83a43688", + "metadata": {}, + "outputs": [], + "source": [ + "pip install -qU \"langchain-chroma>=0.1.2\"" ] }, { @@ -38,149 +31,94 @@ "id": "2b5ffbf8", "metadata": {}, "source": [ - "## Basic Example\n", + "### Credentials\n", "\n", - "In this basic example, we take the most recent State of the Union Address, split it into chunks, embed it using an open-source embedding model, load it into Chroma, and then query it." + "You can use the `Chroma` vector store without any credentials, simply installing the package above is enough!" + ] + }, + { + "cell_type": "markdown", + "id": "cd17cfed", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd7e1243", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "f47f73f4", + "metadata": {}, + "source": [ + "## Initialization\n", + "\n", + "### Basic Initialization \n", + "\n", + "Below is a basic initialization, including the use of a directory to save the data locally.\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", "execution_count": 1, - "id": "ae9fcf3e", + "id": "d3ed0a9a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], + "outputs": [], "source": [ - "# import\n", - "from langchain_chroma import Chroma\n", - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.embeddings.sentence_transformer import (\n", - " SentenceTransformerEmbeddings,\n", - ")\n", - "from langchain_text_splitters import CharacterTextSplitter\n", + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", "\n", - "# load the document and split it into chunks\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "\n", - "# split it into chunks\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", - "\n", - "# create the open-source embedding function\n", - "embedding_function = SentenceTransformerEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n", - "\n", - "# load it into Chroma\n", - "db = Chroma.from_documents(docs, embedding_function)\n", - "\n", - "# query it\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = db.similarity_search(query)\n", - "\n", - "# print results\n", - "print(docs[0].page_content)" - ] - }, - { - "cell_type": "markdown", - "id": "5c9a11cc", - "metadata": {}, - "source": [ - "## Basic Example (including saving to disk)\n", - "\n", - "Extending the previous example, if you want to save to disk, simply initialize the Chroma client and pass the directory where you want the data to be saved to. \n", - "\n", - "`Caution`: Chroma makes a best-effort to automatically save data to disk, however multiple in-memory clients can stop each other's work. As a best practice, only have one client per path running at any given time." + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "49f9bd49", + "execution_count": 16, + "id": "3ea11a7b", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], + "outputs": [], "source": [ - "# save to disk\n", - "db2 = Chroma.from_documents(docs, embedding_function, persist_directory=\"./chroma_db\")\n", - "docs = db2.similarity_search(query)\n", + "from langchain_chroma import Chroma\n", "\n", - "# load from disk\n", - "db3 = Chroma(persist_directory=\"./chroma_db\", embedding_function=embedding_function)\n", - "docs = db3.similarity_search(query)\n", - "print(docs[0].page_content)" + "vector_store = Chroma(\n", + " collection_name=\"example_collection\",\n", + " embedding_function=embeddings,\n", + " persist_directory=\"./chroma_langchain_db\", # Where to save data locally, remove if not neccesary\n", + ")" ] }, { "cell_type": "markdown", - "id": "63318cc9", + "id": "ccb62a8c", "metadata": {}, "source": [ - "## Passing a Chroma Client into Langchain\n", + "### Initialization from client\n", "\n", - "You can also create a Chroma Client and pass it to LangChain. This is particularly useful if you want easier access to the underlying database.\n", - "\n", - "You can also specify the collection name that you want LangChain to use." + "You can also initialize from a `Chroma` client, which is particularly useful if you want easier access to the underlying database." ] }, { "cell_type": "code", "execution_count": 3, - "id": "22f4a0ce", + "id": "3fe4457f", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Add of existing embedding ID: 1\n", - "Add of existing embedding ID: 2\n", - "Add of existing embedding ID: 3\n", - "Add of existing embedding ID: 1\n", - "Add of existing embedding ID: 2\n", - "Add of existing embedding ID: 3\n", - "Add of existing embedding ID: 1\n", - "Insert of existing embedding ID: 1\n", - "Add of existing embedding ID: 2\n", - "Insert of existing embedding ID: 2\n", - "Add of existing embedding ID: 3\n", - "Insert of existing embedding ID: 3\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "There are 3 in the collection\n" - ] - } - ], + "outputs": [], "source": [ "import chromadb\n", "\n", @@ -188,320 +126,320 @@ "collection = persistent_client.get_or_create_collection(\"collection_name\")\n", "collection.add(ids=[\"1\", \"2\", \"3\"], documents=[\"a\", \"b\", \"c\"])\n", "\n", - "langchain_chroma = Chroma(\n", + "vector_store_from_client = Chroma(\n", " client=persistent_client,\n", " collection_name=\"collection_name\",\n", - " embedding_function=embedding_function,\n", - ")\n", - "\n", - "print(\"There are\", langchain_chroma._collection.count(), \"in the collection\")" + " embedding_function=embeddings,\n", + ")" ] }, { "cell_type": "markdown", - "id": "e9cf6d70", + "id": "9d037340", "metadata": {}, "source": [ - "## Basic Example (using the Docker Container)\n", + "## Manage vector store\n", "\n", - "You can also run the Chroma Server in a Docker container separately, create a Client to connect to it, and then pass that to LangChain. \n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", "\n", - "Chroma has the ability to handle multiple `Collections` of documents, but the LangChain interface expects one, so we need to specify the collection name. The default collection name used by LangChain is \"langchain\".\n", + "### Add items to vector store\n", "\n", - "Here is how to clone, build, and run the Docker Image:\n", - "```sh\n", - "git clone git@github.com:chroma-core/chroma.git\n", - "```\n", - "\n", - "Edit the `docker-compose.yml` file and add `ALLOW_RESET=TRUE` under `environment`\n", - "```yaml\n", - " ...\n", - " command: uvicorn chromadb.app:app --reload --workers 1 --host 0.0.0.0 --port 8000 --log-config log_config.yml\n", - " environment:\n", - " - IS_PERSISTENT=TRUE\n", - " - ALLOW_RESET=TRUE\n", - " ports:\n", - " - 8000:8000\n", - " ...\n", - "```\n", - "\n", - "Then run `docker-compose up -d --build`" + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "74aee70e", + "execution_count": 17, + "id": "da279339", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], - "source": [ - "# create the chroma client\n", - "import uuid\n", - "\n", - "import chromadb\n", - "from chromadb.config import Settings\n", - "\n", - "client = chromadb.HttpClient(settings=Settings(allow_reset=True))\n", - "client.reset() # resets the database\n", - "collection = client.create_collection(\"my_collection\")\n", - "for doc in docs:\n", - " collection.add(\n", - " ids=[str(uuid.uuid1())], metadatas=doc.metadata, documents=doc.page_content\n", - " )\n", - "\n", - "# tell LangChain to use our client and collection name\n", - "db4 = Chroma(\n", - " client=client,\n", - " collection_name=\"my_collection\",\n", - " embedding_function=embedding_function,\n", - ")\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = db4.similarity_search(query)\n", - "print(docs[0].page_content)" - ] - }, - { - "cell_type": "markdown", - "id": "9ed3ec50", - "metadata": {}, - "source": [ - "## Update and Delete\n", - "\n", - "While building toward a real application, you want to go beyond adding data, and also update and delete data. \n", - "\n", - "Chroma has users provide `ids` to simplify the bookkeeping here. `ids` can be the name of the file, or a combined has like `filename_paragraphNumber`, etc.\n", - "\n", - "Chroma supports all these operations - though some of them are still being integrated all the way through the LangChain interface. Additional workflow improvements will be added soon.\n", - "\n", - "Here is a basic example showing how to do various operations:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "81a02810", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'source': '../../../state_of_the_union.txt'}\n", - "{'ids': ['1'], 'embeddings': None, 'metadatas': [{'new_value': 'hello world', 'source': '../../../state_of_the_union.txt'}], 'documents': ['Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.']}\n", - "count before 46\n", - "count after 45\n" - ] - } - ], - "source": [ - "# create simple ids\n", - "ids = [str(i) for i in range(1, len(docs) + 1)]\n", - "\n", - "# add data\n", - "example_db = Chroma.from_documents(docs, embedding_function, ids=ids)\n", - "docs = example_db.similarity_search(query)\n", - "print(docs[0].metadata)\n", - "\n", - "# update the metadata for a document\n", - "docs[0].metadata = {\n", - " \"source\": \"../../how_to/state_of_the_union.txt\",\n", - " \"new_value\": \"hello world\",\n", - "}\n", - "example_db.update_document(ids[0], docs[0])\n", - "print(example_db._collection.get(ids=[ids[0]]))\n", - "\n", - "# delete the last document\n", - "print(\"count before\", example_db._collection.count())\n", - "example_db._collection.delete(ids=[ids[-1]])\n", - "print(\"count after\", example_db._collection.count())" - ] - }, - { - "cell_type": "markdown", - "id": "ac6bc71a", - "metadata": {}, - "source": [ - "## Use OpenAI Embeddings\n", - "\n", - "Many people like to use OpenAIEmbeddings, here is how to set that up." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "42080f37-8fd1-4cec-acd9-15d2b03b2f4d", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# get a token: https://platform.openai.com/account/api-keys\n", - "\n", - "from getpass import getpass\n", - "\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "OPENAI_API_KEY = getpass()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c7a94d6c-b4d4-4498-9bdd-eb50c92b85c5", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = OPENAI_API_KEY" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "5eabdb75", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], - "source": [ - "embeddings = OpenAIEmbeddings()\n", - "new_client = chromadb.EphemeralClient()\n", - "openai_lc_client = Chroma.from_documents(\n", - " docs, embeddings, client=new_client, collection_name=\"openai_collection\"\n", - ")\n", - "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = openai_lc_client.similarity_search(query)\n", - "print(docs[0].page_content)" - ] - }, - { - "cell_type": "markdown", - "id": "6d9c28ad", - "metadata": {}, - "source": [ - "***\n", - "\n", - "## Other Information" - ] - }, - { - "cell_type": "markdown", - "id": "18152965", - "metadata": {}, - "source": [ - "### Similarity search with score" - ] - }, - { - "cell_type": "markdown", - "id": "346347d7", - "metadata": {}, - "source": [ - "The returned distance score is cosine distance. Therefore, a lower score is better." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "72aaa9c8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "docs = db.similarity_search_with_score(query)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "d88e958e", - "metadata": { - "tags": [] - }, "outputs": [ { "data": { "text/plain": [ - "(Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'source': '../../../state_of_the_union.txt'}),\n", - " 1.1972057819366455)" + "['f22ed484-6db3-4b76-adb1-18a777426cd6',\n", + " 'e0d5bab4-6453-4511-9a37-023d9d288faa',\n", + " '877d76b8-3580-4d9e-a13f-eed0fa3d134a',\n", + " '26eaccab-81ce-4c0a-8e76-bf542647df18',\n", + " 'bcaa8239-7986-4050-bf40-e14fb7dab997',\n", + " 'cdc44b38-a83f-4e49-b249-7765b334e09d',\n", + " 'a7a35354-2687-4bc2-8242-3849a4d18d34',\n", + " '8780caf1-d946-4f27-a707-67d037e9e1d8',\n", + " 'dec6af2a-7326-408f-893d-7d7d717dfda9',\n", + " '3b18e210-bb59-47a0-8e17-c8e51176ea5e']" ] }, - "execution_count": 10, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "docs[0]" + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=1,\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + " id=2,\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=3,\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + " id=4,\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=5,\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + " id=6,\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + " id=7,\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=8,\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + " id=9,\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=10,\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" ] }, { "cell_type": "markdown", - "id": "794a7552", + "id": "7add6366", "metadata": {}, "source": [ - "### Retriever options\n", + "### Update items in vector store\n", "\n", - "This section goes over different options for how to use Chroma as a retriever.\n", - "\n", - "#### MMR\n", - "\n", - "In addition to using similarity search in the retriever object, you can also use `mmr`." + "Now that we have added documents to our vector store, we can update existing documents by using the `update_documents` function. " ] }, { "cell_type": "code", - "execution_count": 11, - "id": "96ff911a", + "execution_count": 5, + "id": "ef5dbd1e", "metadata": {}, "outputs": [], "source": [ - "retriever = db.as_retriever(search_type=\"mmr\")" + "updated_document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and fried eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + " id=1,\n", + ")\n", + "\n", + "updated_document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is sunny and warm, with a high of 82 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + " id=2,\n", + ")\n", + "\n", + "vector_store.update_document(document_id=uuids[0], document=updated_document_1)\n", + "# You can also update multiple documents at once\n", + "vector_store.update_documents(\n", + " ids=uuids[:2], documents=[updated_document_1, updated_document_1]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "74b9a13a", + "metadata": {}, + "source": [ + "### Delete items from vector store\n", + "\n", + "We can also delete items from our vector store as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "56f17791", + "metadata": {}, + "outputs": [], + "source": [ + "vector_store.delete(ids=uuids[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "213acf08", + "metadata": {}, + "source": [ + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e2b96fcf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter={\"source\": \"tweet\"},\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "cdd117ea", + "metadata": {}, + "source": [ + "#### Similarity search with score\n", + "\n", + "If you want to execute a similarity search and receive the corresponding scores you can run:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2768a331", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=1.726390] The stock market is down 500 points today due to fears of a recession. [{'source': 'news'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(\n", + " \"Will it be hot tomorrow?\", k=1, filter={\"source\": \"news\"}\n", + ")\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "92b436c8", + "metadata": {}, + "source": [ + "#### Search by vector\n", + "\n", + "You can also search by vector:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8ea434a5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* I had chocalate chip pancakes and fried eggs for breakfast this morning. [{'source': 'tweet'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_by_vector(\n", + " embedding=embeddings.embed_query(\"I love green eggs and ham!\"), k=1\n", + ")\n", + "for doc in results:\n", + " print(f\"* {doc.page_content} [{doc.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "9c1c1e6f", + "metadata": {}, + "source": [ + "#### Other search methods\n", + "\n", + "There are a variety of other search methods that are not covered in this notebook, such as MMR search or searching by vector. For a full list of the search abilities available for `AstraDBVectorStore` check out the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_astradb.vectorstores.AstraDBVectorStore.html).\n", + "\n", + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. For more information on the different search types and kwargs you can pass, please visit the API reference [here](https://api.python.langchain.com/en/latest/vectorstores/langchain_chroma.vectorstores.Chroma.html#langchain_chroma.vectorstores.Chroma.as_retriever)." ] }, { "cell_type": "code", "execution_count": 12, - "id": "f00be6d0", + "id": "7b6f7867", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'source': '../../../state_of_the_union.txt'})" + "[Document(metadata={'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" ] }, "execution_count": 12, @@ -510,41 +448,89 @@ } ], "source": [ - "retriever.invoke(query)[0]" + "retriever = vector_store.as_retriever(\n", + " search_type=\"mmr\", search_kwargs={\"k\": 1, \"fetch_k\": 5}\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" ] }, { "cell_type": "markdown", - "id": "275dbd0a", + "id": "a2b7b73c", "metadata": {}, "source": [ - "### Filtering on metadata\n", + "## Chain usage\n", "\n", - "It can be helpful to narrow down the collection before working with it.\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", "\n", - "For example, collections can be filtered on metadata using the get method." + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", "execution_count": 13, - "id": "81600dc1", + "id": "9aad065b", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "84a19f48", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'ids': [], 'embeddings': None, 'metadatas': [], 'documents': []}" + "'LangGraph is used for building stateful, agentic applications. It provides a framework that supports the development of such applications efficiently.'" ] }, - "execution_count": 13, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# filter collection for updated source\n", - "example_db.get(where={\"source\": \"some_other_source\"})" + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" + ] + }, + { + "cell_type": "markdown", + "id": "fed28359", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `Chroma` vector store features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_chroma.vectorstores.Chroma.html" ] } ], @@ -564,7 +550,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.10" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/clickhouse.ipynb b/docs/docs/integrations/vectorstores/clickhouse.ipynb index 2b0136dff6b..f3de48038c1 100644 --- a/docs/docs/integrations/vectorstores/clickhouse.ipynb +++ b/docs/docs/integrations/vectorstores/clickhouse.ipynb @@ -9,37 +9,18 @@ "\n", "> [ClickHouse](https://clickhouse.com/) is the fastest and most resource efficient open-source database for real-time apps and analytics with full SQL support and a wide range of functions to assist users in writing analytical queries. Lately added data structures and distance search functions (like `L2Distance`) as well as [approximate nearest neighbor search indexes](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/annindexes) enable ClickHouse to be used as a high performance and scalable vector database to store and search vectors with SQL.\n", "\n", - "You'll need to install `langchain-community` with `pip install -qU langchain-community` to use this integration\n", + "This notebook shows how to use functionality related to the `ClickHouse` vector store.\n", "\n", - "This notebook shows how to use functionality related to the `ClickHouse` vector search." - ] - }, - { - "cell_type": "markdown", - "id": "43ead5d5-2c1f-4dce-a69a-cb00e4f9d6f0", - "metadata": {}, - "source": [ - "## Setting up environments" - ] - }, - { - "cell_type": "markdown", - "id": "b2c434bc", - "metadata": {}, - "source": [ - "Setting up local clickhouse server with docker (optional)" + "## Setup\n", + "\n", + "First set up a local clickhouse server with docker:" ] }, { "cell_type": "code", "execution_count": null, - "id": "249a7751", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:43:43.035606Z", - "start_time": "2023-06-03T08:43:42.618531Z" - } - }, + "id": "8c4d2e16", + "metadata": {}, "outputs": [], "source": [ "! docker run -d -p 8123:8123 -p9000:9000 --name langchain-clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server:23.4.2.11" @@ -47,52 +28,82 @@ }, { "cell_type": "markdown", - "id": "7bd3c1c0", + "id": "0acb2a8d", "metadata": {}, "source": [ - "Setup up clickhouse client driver" + "You'll need to install `langchain-community` and `clickhouse-connect` to use this integration" ] }, { "cell_type": "code", "execution_count": null, - "id": "9d614bf8", + "id": "d454fb7c", "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet clickhouse-connect" + "pip install -qU langchain-community clickhouse-connect" ] }, { "cell_type": "markdown", - "id": "15a1d477-9cdb-4d82-b019-96951ecb2b72", + "id": "3df5501b", "metadata": {}, "source": [ - "We want to use OpenAIEmbeddings so we have to get the OpenAI API Key." + "### Credentials\n", + "\n", + "There are no credentials for this notebook, just make sure you have installed the packages as shown above." + ] + }, + { + "cell_type": "markdown", + "id": "54d5276f", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" ] }, { "cell_type": "code", - "execution_count": 1, - "id": "91003ea5-0c8c-436c-a5de-aaeaeef2f458", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:49:35.383673Z", - "start_time": "2023-06-03T08:49:33.984547Z" - } - }, + "execution_count": null, + "id": "f6fd5b03", + "metadata": {}, "outputs": [], "source": [ - "import getpass\n", - "import os\n", + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "2b87fe34", + "metadata": {}, + "source": [ + "## Instantiation\n", "\n", - "if not os.environ[\"OPENAI_API_KEY\"]:\n", - " os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, + "id": "60276097", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "aac9563e", "metadata": { "ExecuteTime": { @@ -104,176 +115,178 @@ "outputs": [], "source": [ "from langchain_community.vectorstores import Clickhouse, ClickhouseSettings\n", - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_text_splitters import CharacterTextSplitter" + "\n", + "settings = ClickhouseSettings(table=\"clickhouse_example\")\n", + "vector_store = Clickhouse(embeddings, config=settings)" + ] + }, + { + "cell_type": "markdown", + "id": "32dd3f67", + "metadata": {}, + "source": [ + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", - "execution_count": 3, - "id": "a3c3999a", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:33:32.527387Z", - "start_time": "2023-06-03T08:33:32.501312Z" - }, - "tags": [] - }, + "execution_count": null, + "id": "944743ee", + "metadata": {}, "outputs": [], "source": [ - "from langchain_community.document_loaders import TextLoader\n", + "from uuid import uuid4\n", "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", + "from langchain_core.documents import Document\n", "\n", - "embeddings = OpenAIEmbeddings()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "6e104aee", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:33:35.503823Z", - "start_time": "2023-06-03T08:33:33.745832Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Inserting data...: 100%|██████████| 42/42 [00:00<00:00, 2801.49it/s]\n" - ] - } - ], - "source": [ - "for d in docs:\n", - " d.metadata = {\"some\": \"metadata\"}\n", - "settings = ClickhouseSettings(table=\"clickhouse_vector_search_example\")\n", - "docsearch = Clickhouse.from_documents(docs, embeddings, config=settings)\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = docsearch.similarity_search(query)" + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "id": "18af81cc", + "metadata": {}, + "source": [ + "### Delete items from vector store\n", + "\n", + "We can delete items from our vector store by ID by using the `delete` function." ] }, { "cell_type": "code", - "execution_count": 5, - "id": "9c608226", + "execution_count": null, + "id": "12b32762", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], + "outputs": [], "source": [ - "print(docs[0].page_content)" + "vector_store.delete(ids=uuids[-1])" ] }, { "cell_type": "markdown", - "id": "e3a8b105", + "id": "ada27577", "metadata": {}, "source": [ - "## Get connection info and data schema" + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search can be done as follows:" ] }, { "cell_type": "code", - "execution_count": 6, - "id": "69996818", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:28:58.252991Z", - "start_time": "2023-06-03T08:28:58.197560Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[92m\u001b[1mdefault.clickhouse_vector_search_example @ localhost:8123\u001b[0m\n", - "\n", - "\u001b[1musername: None\u001b[0m\n", - "\n", - "Table Schema:\n", - "---------------------------------------------------\n", - "|\u001b[94mid \u001b[0m|\u001b[96mNullable(String) \u001b[0m|\n", - "|\u001b[94mdocument \u001b[0m|\u001b[96mNullable(String) \u001b[0m|\n", - "|\u001b[94membedding \u001b[0m|\u001b[96mArray(Float32) \u001b[0m|\n", - "|\u001b[94mmetadata \u001b[0m|\u001b[96mObject('json') \u001b[0m|\n", - "|\u001b[94muuid \u001b[0m|\u001b[96mUUID \u001b[0m|\n", - "---------------------------------------------------\n", - "\n" - ] - } - ], + "execution_count": null, + "id": "015831a3", + "metadata": {}, + "outputs": [], "source": [ - "print(str(docsearch))" + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\", k=2\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "324ac147", + "id": "623d3b9d", "metadata": {}, "source": [ - "### Clickhouse table schema" - ] - }, - { - "cell_type": "markdown", - "id": "b5bd7c5b", - "metadata": {}, - "source": [ - "> Clickhouse table will be automatically created if not exist by default. Advanced users could pre-create the table with optimized settings. For distributed Clickhouse cluster with sharding, table engine should be configured as `Distributed`." + "#### Similarity search with score\n", + "\n", + "You can also search with score:" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "54f4f561", + "execution_count": null, + "id": "e7d43430", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Clickhouse Table DDL:\n", - "\n", - "CREATE TABLE IF NOT EXISTS default.clickhouse_vector_search_example(\n", - " id Nullable(String),\n", - " document Nullable(String),\n", - " embedding Array(Float32),\n", - " metadata JSON,\n", - " uuid UUID DEFAULT generateUUIDv4(),\n", - " CONSTRAINT cons_vec_len CHECK length(embedding) = 1536,\n", - " INDEX vec_idx embedding TYPE annoy(100,'L2Distance') GRANULARITY 1000\n", - ") ENGINE = MergeTree ORDER BY uuid SETTINGS index_granularity = 8192\n" - ] - } - ], + "outputs": [], "source": [ - "print(f\"Clickhouse Table DDL:\\n\\n{docsearch.schema}\")" + "results = vector_store.similarity_search_with_score(\"Will it be hot tomorrow?\", k=1)\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "f59360c0", + "id": "f5a90c12", "metadata": {}, "source": [ "## Filtering\n", @@ -287,94 +300,131 @@ }, { "cell_type": "code", - "execution_count": 9, - "id": "232055f6", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:29:36.680805Z", - "start_time": "2023-06-03T08:29:34.963676Z" - } - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Inserting data...: 100%|██████████| 42/42 [00:00<00:00, 6939.56it/s]\n" - ] - } - ], + "execution_count": null, + "id": "169d01d1", + "metadata": {}, + "outputs": [], "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.vectorstores import Clickhouse, ClickhouseSettings\n", - "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", - "\n", - "embeddings = OpenAIEmbeddings()\n", - "\n", - "for i, d in enumerate(docs):\n", - " d.metadata = {\"doc_id\": i}\n", - "\n", - "docsearch = Clickhouse.from_documents(docs, embeddings)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "ddbcee77", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:29:43.487436Z", - "start_time": "2023-06-03T08:29:43.040831Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.6779101415357189 {'doc_id': 0} Madam Speaker, Madam...\n", - "0.6997970363474885 {'doc_id': 8} And so many families...\n", - "0.7044504914336727 {'doc_id': 1} Groups of citizens b...\n", - "0.7053558702165094 {'doc_id': 6} And I’m taking robus...\n" - ] - } - ], - "source": [ - "meta = docsearch.metadata_column\n", - "output = docsearch.similarity_search_with_relevance_scores(\n", - " \"What did the president say about Ketanji Brown Jackson?\",\n", + "meta = vector_store.metadata_column\n", + "results = vector_store.similarity_search_with_relevance_scores(\n", + " \"What did I eat for breakfast?\",\n", " k=4,\n", - " where_str=f\"{meta}.doc_id<10\",\n", + " where_str=f\"{meta}.source = 'tweet'\",\n", ")\n", - "for d, dist in output:\n", - " print(dist, d.metadata, d.page_content[:20] + \"...\")" + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "a359ed74", + "id": "d86fa4bf", "metadata": {}, "source": [ - "## Deleting your data" + "#### Other search methods\n", + "\n", + "There are a variety of other search methods that are not covered in this notebook, such as MMR search or searching by vector. For a full list of the search abilities available for `Clickhouse` vector store check out the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.clickhouse.Clickhouse.html)." + ] + }, + { + "cell_type": "markdown", + "id": "afacfd4e", + "metadata": {}, + "source": [ + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. \n", + "\n", + "Here is how to transform your vector store into a retriever and then invoke the retreiever with a simple query and filter." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "fb6a9d36", - "metadata": { - "ExecuteTime": { - "end_time": "2023-06-03T08:30:24.822384Z", - "start_time": "2023-06-03T08:30:24.798571Z" - } - }, + "execution_count": null, + "id": "97187188", + "metadata": {}, "outputs": [], "source": [ - "docsearch.drop()" + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\",\n", + " search_kwargs={\"k\": 1, \"score_threshold\": 0.5},\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" + ] + }, + { + "cell_type": "markdown", + "id": "57fade30", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a7fec6b", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae6871dc", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" + ] + }, + { + "cell_type": "markdown", + "id": "db24787c", + "metadata": {}, + "source": [ + "For more, check out a complete RAG template using Astra DB [here](https://github.com/langchain-ai/langchain/tree/master/templates/rag-astradb)." + ] + }, + { + "cell_type": "markdown", + "id": "02452d34", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `AstraDBVectorStore` features and configurations head to the API reference:https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.clickhouse.Clickhouse.html" ] } ], @@ -394,7 +444,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/couchbase.ipynb b/docs/docs/integrations/vectorstores/couchbase.ipynb index a92629fc62b..e783d4dc4cd 100644 --- a/docs/docs/integrations/vectorstores/couchbase.ipynb +++ b/docs/docs/integrations/vectorstores/couchbase.ipynb @@ -10,7 +10,7 @@ "\n", "Vector Search is a part of the [Full Text Search Service](https://docs.couchbase.com/server/current/learn/services-and-indexes/services/search-service.html) (Search Service) in Couchbase.\n", "\n", - "This tutorial explains how to use Vector Search in Couchbase. You can work with both [Couchbase Capella](https://www.couchbase.com/products/capella/) and your self-managed Couchbase Server." + "This tutorial explains how to use Vector Search in Couchbase. You can work with either [Couchbase Capella](https://www.couchbase.com/products/capella/) and your self-managed Couchbase Server." ] }, { @@ -18,30 +18,64 @@ "id": "43326be4-4433-4de2-ad42-6eb91a722bad", "metadata": {}, "source": [ - "## Installation" + "## Setup\n", + "\n", + "To access the `CouchbaseVectorStore` you first need to install the `langchain-couchbase` partner package:" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "bec8d532-fec7-4dc7-9be3-020aa7bdb01f", "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet langchain langchain-openai langchain-couchbase" + "pip install -qU langchain-couchbase" + ] + }, + { + "cell_type": "markdown", + "id": "30d6861e", + "metadata": {}, + "source": [ + "### Credentials\n", + "\n", + "Head over to the Couchbase [website](https://cloud.couchbase.com) and create a new connection, making sure to save your database username and password:" ] }, { "cell_type": "code", - "execution_count": 2, - "id": "4a972cbc-bf59-46eb-9b50-e5dc3a69dcf0", + "execution_count": null, + "id": "d98e3baa", "metadata": {}, "outputs": [], "source": [ "import getpass\n", - "import os\n", "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + "COUCHBASE_CONNECTION_STRING = getpass.getpass(\n", + " \"Enter the connection string for the Couchbase cluster: \"\n", + ")\n", + "DB_USERNAME = getpass.getpass(\"Enter the username for the Couchbase cluster: \")\n", + "DB_PASSWORD = getpass.getpass(\"Enter the password for the Couchbase cluster: \")" + ] + }, + { + "cell_type": "markdown", + "id": "23ac2c64", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c25ec38", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "# os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass()" ] }, { @@ -49,18 +83,9 @@ "id": "acf1b168-622f-465c-a9a5-d27a6d7e7a8f", "metadata": {}, "source": [ - "## Import the Vector Store and Embeddings" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "23ce45ab-bfd2-42e1-b681-514a550f0232", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_couchbase.vectorstores import CouchbaseVectorStore\n", - "from langchain_openai import OpenAIEmbeddings" + "## Initialization\n", + "\n", + "Before instantiating we need to create a connection." ] }, { @@ -68,31 +93,18 @@ "id": "3144ba02-1eaa-4449-853e-f034ca5706bf", "metadata": {}, "source": [ - "## Create Couchbase Connection Object\n", + "### Create Couchbase Connection Object\n", + "\n", "We create a connection to the Couchbase cluster initially and then pass the cluster object to the Vector Store. \n", "\n", - "Here, we are connecting using the username and password. You can also connect using any other supported way to your cluster. \n", + "Here, we are connecting using the username and password from above. You can also connect using any other supported way to your cluster. \n", "\n", - "For more information on connecting to the Couchbase cluster, please check the [Python SDK documentation](https://docs.couchbase.com/python-sdk/current/hello-world/start-using-sdk.html#connect)." + "For more information on connecting to the Couchbase cluster, please check the [documentation](https://docs.couchbase.com/python-sdk/current/hello-world/start-using-sdk.html#connect)." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "52fe583a-12db-4dc2-9281-1174bf1d4e5c", - "metadata": {}, - "outputs": [], - "source": [ - "COUCHBASE_CONNECTION_STRING = (\n", - " \"couchbase://localhost\" # or \"couchbases://localhost\" if using TLS\n", - ")\n", - "DB_USERNAME = \"Administrator\"\n", - "DB_PASSWORD = \"Password\"" - ] - }, - { - "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "9986c6b9", "metadata": {}, "outputs": [], @@ -123,145 +135,15 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "1b1d0a26-e9d4-4823-9800-9549d24d3d16", "metadata": {}, "outputs": [], "source": [ - "BUCKET_NAME = \"testing\"\n", + "BUCKET_NAME = \"langchain_bucket\"\n", "SCOPE_NAME = \"_default\"\n", - "COLLECTION_NAME = \"_default\"\n", - "SEARCH_INDEX_NAME = \"vector-index\"" - ] - }, - { - "cell_type": "markdown", - "id": "efbac6ff-c2ac-4443-9250-7cc88061346b", - "metadata": {}, - "source": [ - "For this tutorial, we will use OpenAI embeddings" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "87625579-86d7-4de4-8a4d-cee674a6b676", - "metadata": {}, - "outputs": [], - "source": [ - "embeddings = OpenAIEmbeddings()" - ] - }, - { - "cell_type": "markdown", - "id": "3677b4b0-3711-419c-89ff-32ef4d3e3022", - "metadata": {}, - "source": [ - "## Create the Search Index\n", - "Currently, the Search index needs to be created from the Couchbase Capella or Server UI or using the REST interface. \n", - "\n", - "Let us define a Search index with the name `vector-index` on the testing bucket\n", - "\n", - "For this example, let us use the Import Index feature on the Search Service on the UI. \n", - "\n", - "We are defining an index on the `testing` bucket's `_default` scope on the `_default` collection with the vector field set to `embedding` with 1536 dimensions and the text field set to `text`. We are also indexing and storing all the fields under `metadata` in the document as a dynamic mapping to account for varying document structures. The similarity metric is set to `dot_product`." - ] - }, - { - "cell_type": "markdown", - "id": "655117ae-9b1f-4139-b437-ca7685975a54", - "metadata": {}, - "source": [ - "### How to Import an Index to the Full Text Search service?\n", - " - [Couchbase Server](https://docs.couchbase.com/server/current/search/import-search-index.html)\n", - " - Click on Search -> Add Index -> Import\n", - " - Copy the following Index definition in the Import screen\n", - " - Click on Create Index to create the index.\n", - " - [Couchbase Capella](https://docs.couchbase.com/cloud/search/import-search-index.html)\n", - " - Copy the index definition to a new file `index.json`\n", - " - Import the file in Capella using the instructions in the documentation.\n", - " - Click on Create Index to create the index.\n", - " \n" - ] - }, - { - "cell_type": "markdown", - "id": "f85bc468-d9b8-487d-999a-3b5d2fb78e41", - "metadata": {}, - "source": [ - "### Index Definition\n", - "```\n", - "{\n", - " \"name\": \"vector-index\",\n", - " \"type\": \"fulltext-index\",\n", - " \"params\": {\n", - " \"doc_config\": {\n", - " \"docid_prefix_delim\": \"\",\n", - " \"docid_regexp\": \"\",\n", - " \"mode\": \"type_field\",\n", - " \"type_field\": \"type\"\n", - " },\n", - " \"mapping\": {\n", - " \"default_analyzer\": \"standard\",\n", - " \"default_datetime_parser\": \"dateTimeOptional\",\n", - " \"default_field\": \"_all\",\n", - " \"default_mapping\": {\n", - " \"dynamic\": true,\n", - " \"enabled\": true,\n", - " \"properties\": {\n", - " \"metadata\": {\n", - " \"dynamic\": true,\n", - " \"enabled\": true\n", - " },\n", - " \"embedding\": {\n", - " \"enabled\": true,\n", - " \"dynamic\": false,\n", - " \"fields\": [\n", - " {\n", - " \"dims\": 1536,\n", - " \"index\": true,\n", - " \"name\": \"embedding\",\n", - " \"similarity\": \"dot_product\",\n", - " \"type\": \"vector\",\n", - " \"vector_index_optimized_for\": \"recall\"\n", - " }\n", - " ]\n", - " },\n", - " \"text\": {\n", - " \"enabled\": true,\n", - " \"dynamic\": false,\n", - " \"fields\": [\n", - " {\n", - " \"index\": true,\n", - " \"name\": \"text\",\n", - " \"store\": true,\n", - " \"type\": \"text\"\n", - " }\n", - " ]\n", - " }\n", - " }\n", - " },\n", - " \"default_type\": \"_default\",\n", - " \"docvalues_dynamic\": false,\n", - " \"index_dynamic\": true,\n", - " \"store_dynamic\": true,\n", - " \"type_field\": \"_type\"\n", - " },\n", - " \"store\": {\n", - " \"indexType\": \"scorch\",\n", - " \"segmentVersion\": 16\n", - " }\n", - " },\n", - " \"sourceType\": \"gocbcore\",\n", - " \"sourceName\": \"testing\",\n", - " \"sourceParams\": {},\n", - " \"planParams\": {\n", - " \"maxPartitionsPerPIndex\": 103,\n", - " \"indexPartitions\": 10,\n", - " \"numReplicas\": 0\n", - " }\n", - "}\n", - "```" + "COLLECTION_NAME = \"default\"\n", + "SEARCH_INDEX_NAME = \"langchain-test-index\"" ] }, { @@ -269,7 +151,7 @@ "id": "556dc68c-9089-4390-8dc9-b77051e7fc34", "metadata": {}, "source": [ - "For more details on how to create a Search index with support for Vector fields, please refer to the documentation.\n", + "For details on how to create a Search index with support for Vector fields, please refer to the documentation.\n", "\n", "- [Couchbase Capella](https://docs.couchbase.com/cloud/vector-search/create-vector-search-index-ui.html)\n", " \n", @@ -281,17 +163,40 @@ "id": "75f4037d-e509-4de7-a8d1-63a05de24e9d", "metadata": {}, "source": [ - "## Create Vector Store\n", - "We create the vector store object with the cluster information and the search index name." + "### Simple Instantiation\n", + "\n", + "Below, we create the vector store object with the cluster information and the search index name. \n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, + "id": "6706efdd", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "33db4670-76c5-49ba-94d6-a8fa35583058", "metadata": {}, "outputs": [], "source": [ + "from langchain_couchbase.vectorstores import CouchbaseVectorStore\n", + "\n", "vector_store = CouchbaseVectorStore(\n", " cluster=cluster,\n", " bucket_name=BUCKET_NAME,\n", @@ -308,9 +213,18 @@ "metadata": {}, "source": [ "### Specify the Text & Embeddings Field\n", - "You can optionally specify the text & embeddings field for the document using the `text_key` and `embedding_key` fields.\n", - "```\n", - "vector_store = CouchbaseVectorStore(\n", + "\n", + "You can optionally specify the text & embeddings field for the document using the `text_key` and `embedding_key` fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49c38634", + "metadata": {}, + "outputs": [], + "source": [ + "vector_store_specific = CouchbaseVectorStore(\n", " cluster=cluster,\n", " bucket_name=BUCKET_NAME,\n", " scope_name=SCOPE_NAME,\n", @@ -319,73 +233,148 @@ " index_name=SEARCH_INDEX_NAME,\n", " text_key=\"text\",\n", " embedding_key=\"embedding\",\n", - ")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "790dc1ac-0ab8-4cb5-989d-31ca7c241068", - "metadata": {}, - "source": [ - "## Basic Vector Search Example\n", - "For this example, we are going to load the \"state_of_the_union.txt\" file via the TextLoader, chunk the text into 500 character chunks with no overlaps and index all these chunks into Couchbase.\n", - "\n", - "After the data is indexed, we perform a simple query to find the top 4 chunks that are similar to the query \"What did president say about Ketanji Brown Jackson\".\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "440350df-cbc6-48f7-8009-2e783be18306", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_text_splitters import CharacterTextSplitter\n", - "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "9d3b4c7c-abd6-4dfa-ad63-470f16661319", - "metadata": {}, - "outputs": [], - "source": [ - "vector_store = CouchbaseVectorStore.from_documents(\n", - " documents=docs,\n", - " embedding=embeddings,\n", - " cluster=cluster,\n", - " bucket_name=BUCKET_NAME,\n", - " scope_name=SCOPE_NAME,\n", - " collection_name=COLLECTION_NAME,\n", - " index_name=SEARCH_INDEX_NAME,\n", ")" ] }, { - "cell_type": "code", - "execution_count": 11, - "id": "91fdce6c-8f7c-4060-865a-2fd742846664", + "cell_type": "markdown", + "id": "50e95fa6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.' metadata={'source': '../../how_to/state_of_the_union.txt'}\n" - ] - } - ], "source": [ - "query = \"What did president say about Ketanji Brown Jackson\"\n", - "results = vector_store.similarity_search(query)\n", - "print(results[0])" + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65a35f00", + "metadata": {}, + "outputs": [], + "source": [ + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "id": "dd33b030", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a05f294", + "metadata": {}, + "outputs": [], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "id": "d2cc4126", + "metadata": {}, + "source": [ + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent.\n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e00bb23", + "metadata": {}, + "outputs": [], + "source": [ + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { @@ -393,31 +382,21 @@ "id": "d9b46c93-65f6-4e4f-87a2-5cebea3b7a6b", "metadata": {}, "source": [ - "## Similarity Search with Score\n", - "You can fetch the scores for the results by calling the `similarity_search_with_score` method." + "#### Similarity search with Score\n", + "\n", + "You can also fetch the scores for the results by calling the `similarity_search_with_score` method." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "24b146b2-55a2-4fe8-8659-3649032f5dc7", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.' metadata={'source': '../../how_to/state_of_the_union.txt'}\n", - "Score: 0.8211871385574341\n" - ] - } - ], + "outputs": [], "source": [ - "query = \"What did president say about Ketanji Brown Jackson\"\n", - "results = vector_store.similarity_search_with_score(query)\n", - "document, score = results[0]\n", - "print(document)\n", - "print(f\"Score: {score}\")" + "results = vector_store.similarity_search_with_score(\"Will it be hot tomorrow?\", k=1)\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" ] }, { @@ -425,7 +404,8 @@ "id": "9983e83d-efd0-4b75-80db-150e0694e822", "metadata": {}, "source": [ - "## Specifying Fields to Return\n", + "### Specifying Fields to Return\n", + "\n", "You can specify the fields to return from the document using `fields` parameter in the searches. These fields are returned as part of the `metadata` object in the returned Document. You can fetch any field that is stored in the Search index. The `text_key` of the document is returned as part of the document's `page_content`.\n", "\n", "If you do not specify any fields to be fetched, all the fields stored in the index are returned.\n", @@ -437,20 +417,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "ffa743dc-4e89-405b-ad71-7390338889e6", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.' metadata={'source': '../../how_to/state_of_the_union.txt'}\n" - ] - } - ], + "outputs": [], "source": [ - "query = \"What did president say about Ketanji Brown Jackson\"\n", + "query = \"What did I eat for breakfast today?\"\n", "results = vector_store.similarity_search(query, fields=[\"metadata.source\"])\n", "print(results[0])" ] @@ -460,7 +432,8 @@ "id": "a5e45eb2-aa97-45df-bcc5-410e9626e506", "metadata": {}, "source": [ - "## Hybrid Search\n", + "### Hybrid Queries\n", + "\n", "Couchbase allows you to do hybrid searches by combining Vector Search results with searches on non-vector fields of the document like the `metadata` object. \n", "\n", "The results will be based on the combination of the results from both Vector Search and the searches supported by Search Service. The scores of each of the component searches are added up to get the total score of the result.\n", @@ -474,26 +447,26 @@ "id": "a5db3685-1918-4c63-8148-0bb3a71ea677", "metadata": {}, "source": [ - "### Create Diverse Metadata for Hybrid Search\n", + "#### Create Diverse Metadata for Hybrid Search\n", "In order to simulate hybrid search, let us create some random metadata from the existing documents. \n", "We uniformly add three fields to the metadata, `date` between 2010 & 2020, `rating` between 1 & 5 and `author` set to either John Doe or Jane Doe. " ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "7d2e607d-6bbc-4cef-83e3-b6a28bb269ea", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'author': 'John Doe', 'date': '2016-01-01', 'rating': 2, 'source': '../../how_to/state_of_the_union.txt'}\n" - ] - } - ], + "outputs": [], "source": [ + "from langchain_community.document_loaders import TextLoader\n", + "from langchain_text_splitters import CharacterTextSplitter\n", + "\n", + "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", + "documents = loader.load()\n", + "text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)\n", + "docs = text_splitter.split_documents(documents)\n", + "\n", "# Adding metadata to documents\n", "for i, doc in enumerate(docs):\n", " doc.metadata[\"date\"] = f\"{range(2010, 2020)[i % 10]}-01-01\"\n", @@ -512,24 +485,16 @@ "id": "6cad893b-3977-4556-ab1d-d12bce68b306", "metadata": {}, "source": [ - "### Example: Search by Exact Value\n", + "### Query by Exact Value\n", "We can search for exact matches on a textual field like the author in the `metadata` object." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "dc06ba4a-8a6b-4c55-bb69-95cd92db273f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='This is personal to me and Jill, to Kamala, and to so many of you. \\n\\nCancer is the #2 cause of death in America–second only to heart disease. \\n\\nLast month, I announced our plan to supercharge \\nthe Cancer Moonshot that President Obama asked me to lead six years ago. \\n\\nOur goal is to cut the cancer death rate by at least 50% over the next 25 years, turn more cancers from death sentences into treatable diseases. \\n\\nMore support for patients and families.' metadata={'author': 'John Doe'}\n" - ] - } - ], + "outputs": [], "source": [ "query = \"What did the president say about Ketanji Brown Jackson\"\n", "results = vector_store.similarity_search(\n", @@ -545,7 +510,7 @@ "id": "9106b594-b41e-4329-b98c-9b9f8a34d6f7", "metadata": {}, "source": [ - "### Example: Search by Partial Match\n", + "### Query by Partial Match\n", "We can search for partial matches by specifying a fuzziness for the search. This is useful when you want to search for slight variations or misspellings of a search query.\n", "\n", "Here, \"Jae\" is close (fuzziness of 1) to \"Jane\"." @@ -553,18 +518,10 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "fd4749e6-ef4f-4cb5-95ff-37c4fa8283d8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='A former top litigator in private practice. A former federal public defender. And from a family of public school educators and police officers. A consensus builder. Since she’s been nominated, she’s received a broad range of support—from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. \\n\\nAnd if we are to advance liberty and justice, we need to secure the Border and fix the immigration system.' metadata={'author': 'Jane Doe'}\n" - ] - } - ], + "outputs": [], "source": [ "query = \"What did the president say about Ketanji Brown Jackson\"\n", "results = vector_store.similarity_search(\n", @@ -582,24 +539,16 @@ "id": "1bbf9449-6e30-4bd1-9eeb-f3b60952fcab", "metadata": {}, "source": [ - "### Example: Search by Date Range Query\n", + "### Query by Date Range Query\n", "We can search for documents that are within a date range query on a date field like `metadata.date`." ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "b7b47e7d-c32f-4999-bce9-3c3c3cebffd0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='He will never extinguish their love of freedom. He will never weaken the resolve of the free world. \\n\\nWe meet tonight in an America that has lived through two of the hardest years this nation has ever faced. \\n\\nThe pandemic has been punishing. \\n\\nAnd so many families are living paycheck to paycheck, struggling to keep up with the rising cost of food, gas, housing, and so much more. \\n\\nI understand.' metadata={'author': 'Jane Doe', 'date': '2017-01-01', 'rating': 3, 'source': '../../how_to/state_of_the_union.txt'}\n" - ] - } - ], + "outputs": [], "source": [ "query = \"Any mention about independence?\"\n", "results = vector_store.similarity_search(\n", @@ -622,24 +571,16 @@ "id": "a18d4ea2-bfab-4f15-9839-674faf1c6f0d", "metadata": {}, "source": [ - "### Example: Search by Numeric Range Query\n", + "### Query by Numeric Range Query\n", "We can search for documents that are within a range for a numeric field like `metadata.rating`." ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "7e8bf7c5-07d1-4c3f-86d7-1fa3a454dc7f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(Document(page_content='He will never extinguish their love of freedom. He will never weaken the resolve of the free world. \\n\\nWe meet tonight in an America that has lived through two of the hardest years this nation has ever faced. \\n\\nThe pandemic has been punishing. \\n\\nAnd so many families are living paycheck to paycheck, struggling to keep up with the rising cost of food, gas, housing, and so much more. \\n\\nI understand.', metadata={'author': 'Jane Doe', 'date': '2017-01-01', 'rating': 3, 'source': '../../how_to/state_of_the_union.txt'}), 0.9000703597577832)\n" - ] - } - ], + "outputs": [], "source": [ "query = \"Any mention about independence?\"\n", "results = vector_store.similarity_search_with_score(\n", @@ -662,7 +603,7 @@ "id": "0f16bf86-f01c-4a77-8406-275f7313f493", "metadata": {}, "source": [ - "### Example: Combining Multiple Search Queries\n", + "### Combining Multiple Search Queries\n", "Different search queries can be combined using AND (conjuncts) or OR (disjuncts) operators.\n", "\n", "In this example, we are checking for documents with a rating between 3 & 4 and dated between 2015 & 2018." @@ -670,18 +611,10 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "dd0fe7f1-aa40-4c6f-889b-99ad5efcd88b", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(Document(page_content='He will never extinguish their love of freedom. He will never weaken the resolve of the free world. \\n\\nWe meet tonight in an America that has lived through two of the hardest years this nation has ever faced. \\n\\nThe pandemic has been punishing. \\n\\nAnd so many families are living paycheck to paycheck, struggling to keep up with the rising cost of food, gas, housing, and so much more. \\n\\nI understand.', metadata={'author': 'Jane Doe', 'date': '2017-01-01', 'rating': 3, 'source': '../../how_to/state_of_the_union.txt'}), 1.3598770370389914)\n" - ] - } - ], + "outputs": [], "source": [ "query = \"Any mention about independence?\"\n", "results = vector_store.similarity_search_with_score(\n", @@ -710,6 +643,90 @@ "- [Couchbase Server](https://docs.couchbase.com/server/current/search/search-request-params.html#query-object)" ] }, + { + "cell_type": "markdown", + "id": "db0a1d74", + "metadata": {}, + "source": [ + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. \n", + "\n", + "Here is how to transform your vector store into a retriever and then invoke the retreiever with a simple query and filter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3666265a", + "metadata": {}, + "outputs": [], + "source": [ + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\",\n", + " search_kwargs={\"k\": 1, \"score_threshold\": 0.5},\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" + ] + }, + { + "cell_type": "markdown", + "id": "28ab35ec", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a849aa", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e34c9e3a", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" + ] + }, { "cell_type": "markdown", "id": "80958c2b-6a67-45e6-b7f0-fd2461d75e0f", @@ -761,6 +778,16 @@ "* [Couchbase Capella](https://docs.couchbase.com/cloud/search/create-child-mapping.html)\n", "* [Couchbase Server](https://docs.couchbase.com/server/current/search/create-child-mapping.html)" ] + }, + { + "cell_type": "markdown", + "id": "d876b769", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `CouchbaseVectorStore` features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_couchbase.vectorstores.CouchbaseVectorStore.html" + ] } ], "metadata": { @@ -779,7 +806,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/elasticsearch.ipynb b/docs/docs/integrations/vectorstores/elasticsearch.ipynb index 04bc4ddacd4..ff4c11fc187 100644 --- a/docs/docs/integrations/vectorstores/elasticsearch.ipynb +++ b/docs/docs/integrations/vectorstores/elasticsearch.ipynb @@ -11,7 +11,11 @@ "\n", ">[Elasticsearch](https://www.elastic.co/elasticsearch/) is a distributed, RESTful search and analytics engine, capable of performing both vector and lexical search. It is built on top of the Apache Lucene library. \n", "\n", - "This notebook shows how to use functionality related to the `Elasticsearch` database." + "This notebook shows how to use functionality related to the `Elasticsearch` vector store.\n", + "\n", + "## Setup\n", + "\n", + "In order to use the `Elasticsearch` vector search you must install the `langchain-elasticsearch` package." ] }, { @@ -21,7 +25,7 @@ "metadata": {}, "outputs": [], "source": [ - "%pip install --upgrade --quiet langchain-elasticsearch langchain-openai tiktoken langchain" + "%pip install -qU langchain-elasticsearch" ] }, { @@ -32,7 +36,7 @@ "tags": [] }, "source": [ - "## Running and connecting to Elasticsearch" + "### Credentials" ] }, { @@ -54,66 +58,72 @@ "\n", "\n", "### Running Elasticsearch via Docker \n", - "Example: Run a single-node Elasticsearch instance with security disabled. This is not recommended for production use.\n", + "Example: Run a single-node Elasticsearch instance with security disabled. This is not recommended for production use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22942b0c", + "metadata": {}, + "outputs": [], + "source": [ + "%docker run -p 9200:9200 -e \"discovery.type=single-node\" -e \"xpack.security.enabled=false\" -e \"xpack.security.http.ssl.enabled=false\" docker.elastic.co/elasticsearch/elasticsearch:8.12.1" + ] + }, + { + "cell_type": "markdown", + "id": "42e3846d", + "metadata": {}, + "source": [ "\n", - "```bash\n", - "docker run -p 9200:9200 -e \"discovery.type=single-node\" -e \"xpack.security.enabled=false\" -e \"xpack.security.http.ssl.enabled=false\" docker.elastic.co/elasticsearch/elasticsearch:8.12.1\n", - "```\n", - "\n", - "Once the Elasticsearch instance is running, you can connect to it using the Elasticsearch URL and index name along with the embedding object to the constructor.\n", - "\n", - "Example:\n", - "```python\n", - "from langchain_elasticsearch import ElasticsearchStore\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "embedding = OpenAIEmbeddings()\n", - "elastic_vector_search = ElasticsearchStore(\n", - " es_url=\"http://localhost:9200\",\n", - " index_name=\"test_index\",\n", - " embedding=embedding\n", - ")\n", - "```\n", - "### Authentication\n", + "### Running with Authentication\n", "For production, we recommend you run with security enabled. To connect with login credentials, you can use the parameters `es_api_key` or `es_user` and `es_password`.\n", "\n", - "Example:\n", - "```python\n", - "from langchain_elasticsearch import ElasticsearchStore\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "91399482", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", "from langchain_openai import OpenAIEmbeddings\n", "\n", - "embedding = OpenAIEmbeddings()\n", - "elastic_vector_search = ElasticsearchStore(\n", - " es_url=\"http://localhost:9200\",\n", - " index_name=\"test_index\",\n", - " embedding=embedding,\n", - " es_user=\"elastic\",\n", - " es_password=\"changeme\"\n", - ")\n", - "```\n", - "\n", - "You can also use an `Elasticsearch` client object that gives you more flexibility, for example to configure the maximum number of retries.\n", - "\n", - "Example:\n", - "```python\n", - "import elasticsearch\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3fa3cffa", + "metadata": {}, + "outputs": [], + "source": [ "from langchain_elasticsearch import ElasticsearchStore\n", "\n", - "es_client= elasticsearch.Elasticsearch(\n", - " hosts=[\"http://localhost:9200\"],\n", - " es_user=\"elastic\",\n", - " es_password=\"changeme\"\n", - " max_retries=10,\n", - ")\n", - "\n", - "embedding = OpenAIEmbeddings()\n", "elastic_vector_search = ElasticsearchStore(\n", - " index_name=\"test_index\",\n", - " es_connection=es_client,\n", - " embedding=embedding,\n", - ")\n", - "```\n", - "\n", + " es_url=\"http://localhost:9200\",\n", + " index_name=\"langchain_index\",\n", + " embedding=embeddings,\n", + " es_user=\"elastic\",\n", + " es_password=\"changeme\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4d1fa432", + "metadata": {}, + "source": [ "#### How to obtain a password for the default \"elastic\" user?\n", "\n", "To obtain your Elastic Cloud password for the default \"elastic\" user:\n", @@ -133,47 +143,43 @@ "5. Copy the API key and paste it into the `api_key` parameter\n", "\n", "### Elastic Cloud\n", - "To connect to an Elasticsearch instance on Elastic Cloud, you can use either the `es_cloud_id` parameter or `es_url`.\n", "\n", - "Example:\n", - "```python\n", - "from langchain_elasticsearch import ElasticsearchStore\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "embedding = OpenAIEmbeddings()\n", - "elastic_vector_search = ElasticsearchStore(\n", - " es_cloud_id=\"\",\n", - " index_name=\"test_index\",\n", - " embedding=embedding,\n", - " es_user=\"elastic\",\n", - " es_password=\"changeme\"\n", - ")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "ea167a29", - "metadata": {}, - "source": [ - "To use the `OpenAIEmbeddings` we have to configure the OpenAI API Key in the environment." + "To connect to an Elasticsearch instance on Elastic Cloud, you can use either the `es_cloud_id` parameter or `es_url`." ] }, { "cell_type": "code", - "execution_count": 3, - "id": "67ab8afa-f7c6-4fbf-b596-cb512da949da", - "metadata": { - "id": "67ab8afa-f7c6-4fbf-b596-cb512da949da", - "outputId": "fd16b37f-cb76-40a9-b83f-eab58dd0d912", - "tags": [] - }, + "execution_count": null, + "id": "9a0cb623", + "metadata": {}, "outputs": [], "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + "elastic_vector_search = ElasticsearchStore(\n", + " es_cloud_id=\"\",\n", + " index_name=\"test_index\",\n", + " embedding=embeddings,\n", + " es_user=\"elastic\",\n", + " es_password=\"changeme\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "406eefe8", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b109bf1a", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" ] }, { @@ -184,17 +190,14 @@ "tags": [] }, "source": [ - "## Basic Example\n", - "This example we are going to load \"state_of_the_union.txt\" via the TextLoader, chunk the text into 500 word chunks, and then index each chunk into Elasticsearch.\n", + "## Initialization\n", "\n", - "Once the data is indexed, we perform a simple query to find the top 4 chunks that similar to the query \"What did the president say about Ketanji Brown Jackson\".\n", - "\n", - "Elasticsearch is running locally on localhost:9200 with [docker](#running-elasticsearch-via-docker). For more details on how to connect to Elasticsearch from Elastic Cloud, see [connecting with authentication](#authentication) above.\n" + "Elasticsearch is running locally on localhost:9200 with [docker](#running-elasticsearch-via-docker). For more details on how to connect to Elasticsearch from Elastic Cloud, see [connecting with authentication](#running-with-authentication) above.\n" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "id": "aac9563e", "metadata": { "id": "aac9563e", @@ -203,33 +206,25 @@ "outputs": [], "source": [ "from langchain_elasticsearch import ElasticsearchStore\n", - "from langchain_openai import OpenAIEmbeddings" + "\n", + "vector_store = ElasticsearchStore(\n", + " \"langchain-demo\", embedding=embeddings, es_url=\"http://localhost:9201\"\n", + ")" ] }, { - "cell_type": "code", - "execution_count": 5, - "id": "a3c3999a", - "metadata": { - "id": "a3c3999a", - "tags": [] - }, - "outputs": [], + "cell_type": "markdown", + "id": "eede246c", + "metadata": {}, "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_text_splitters import CharacterTextSplitter\n", + "## Manage vector store\n", "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", - "\n", - "embeddings = OpenAIEmbeddings()" + "### Add items to vector store" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "12eb86d8", "metadata": { "id": "12eb86d8", @@ -237,672 +232,305 @@ }, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Document(page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'source': '../../how_to/state_of_the_union.txt'}), Document(page_content='As I said last year, especially to our younger transgender Americans, I will always have your back as your President, so you can be yourself and reach your God-given potential. \\n\\nWhile it often appears that we never agree, that isn’t true. I signed 80 bipartisan bills into law last year. From preventing government shutdowns to protecting Asian-Americans from still-too-common hate crimes to reforming military justice.', metadata={'source': '../../how_to/state_of_the_union.txt'}), Document(page_content='A former top litigator in private practice. A former federal public defender. And from a family of public school educators and police officers. A consensus builder. Since she’s been nominated, she’s received a broad range of support—from the Fraternal Order of Police to former judges appointed by Democrats and Republicans. \\n\\nAnd if we are to advance liberty and justice, we need to secure the Border and fix the immigration system.', metadata={'source': '../../how_to/state_of_the_union.txt'}), Document(page_content='This is personal to me and Jill, to Kamala, and to so many of you. \\n\\nCancer is the #2 cause of death in America–second only to heart disease. \\n\\nLast month, I announced our plan to supercharge \\nthe Cancer Moonshot that President Obama asked me to lead six years ago. \\n\\nOur goal is to cut the cancer death rate by at least 50% over the next 25 years, turn more cancers from death sentences into treatable diseases. \\n\\nMore support for patients and families.', metadata={'source': '../../how_to/state_of_the_union.txt'})]\n" - ] + "data": { + "text/plain": [ + "['21cca03c-9089-42d2-b41c-3d156be2b519',\n", + " 'a6ceb967-b552-4802-bb06-c0e95fce386e',\n", + " '3a35fac4-e5f0-493b-bee0-9143b41aedae',\n", + " '176da099-66b1-4d6a-811b-dfdfe0808d30',\n", + " 'ecfa1a30-3c97-408b-80c0-5c43d68bf5ff',\n", + " 'c0f08baa-e70b-4f83-b387-c6e0a0f36f73',\n", + " '489b2c9c-1925-43e1-bcf0-0fa94cf1cbc4',\n", + " '408c6503-9ba4-49fd-b1cc-95584cd914c5',\n", + " '5248c899-16d5-4377-a9e9-736ca443ad4f',\n", + " 'ca182769-c4fc-4e25-8f0a-8dd0a525955c']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "db = ElasticsearchStore.from_documents(\n", - " docs,\n", - " embeddings,\n", - " es_url=\"http://localhost:9200\",\n", - " index_name=\"test-basic\",\n", + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", ")\n", "\n", - "db.client.indices.refresh(index=\"test-basic\")\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "results = db.similarity_search(query)\n", - "print(results)" + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" ] }, { "cell_type": "markdown", - "id": "f86ec452", + "id": "2a549e3d", "metadata": {}, "source": [ - "# Metadata\n", - "\n", - "`ElasticsearchStore` supports metadata to stored along with the document. This metadata dict object is stored in a metadata object field in the Elasticsearch document. Based on the metadata value, Elasticsearch will automatically setup the mapping by infering the data type of the metadata value. For example, if the metadata value is a string, Elasticsearch will setup the mapping for the metadata object field as a string type." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5d076412", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'source': '../../how_to/state_of_the_union.txt', 'date': '2016-01-01', 'rating': 2, 'author': 'John Doe'}\n" - ] - } - ], - "source": [ - "# Adding metadata to documents\n", - "for i, doc in enumerate(docs):\n", - " doc.metadata[\"date\"] = f\"{range(2010, 2020)[i % 10]}-01-01\"\n", - " doc.metadata[\"rating\"] = range(1, 6)[i % 5]\n", - " doc.metadata[\"author\"] = [\"John Doe\", \"Jane Doe\"][i % 2]\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs, embeddings, es_url=\"http://localhost:9200\", index_name=\"test-metadata\"\n", - ")\n", - "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = db.similarity_search(query)\n", - "print(docs[0].metadata)" - ] - }, - { - "cell_type": "markdown", - "id": "3befa9e0", - "metadata": {}, - "source": [ - "## Filtering Metadata\n", - "With metadata added to the documents, you can add metadata filtering at query time. \n", - "\n", - "### Example: Filter by Exact keyword\n", - "Notice: We are using the keyword subfield thats not analyzed" + "### Delete items from vector store" ] }, { "cell_type": "code", "execution_count": 8, - "id": "b2a4bd1b", + "id": "31c3b785", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'source': '../../how_to/state_of_the_union.txt', 'date': '2016-01-01', 'rating': 2, 'author': 'John Doe'}\n" - ] + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "docs = db.similarity_search(\n", - " query, filter=[{\"term\": {\"metadata.author.keyword\": \"John Doe\"}}]\n", - ")\n", - "print(docs[0].metadata)" + "vector_store.delete(ids=[uuids[-1]])" ] }, { "cell_type": "markdown", - "id": "1898ab77", + "id": "674bcab2", "metadata": {}, "source": [ - "### Example: Filter by Partial Match\n", - "This example shows how to filter by partial match. This is useful when you don't know the exact value of the metadata field. For example, if you want to filter by the metadata field `author` and you don't know the exact value of the author, you can use a partial match to filter by the author's last name. Fuzzy matching is also supported.\n", + "## Query vector store\n", "\n", - "\"Jon\" matches on \"John Doe\" as \"Jon\" is a close match to \"John\" token." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f3d294ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'source': '../../how_to/state_of_the_union.txt', 'date': '2016-01-01', 'rating': 2, 'author': 'John Doe'}\n" - ] - } - ], - "source": [ - "docs = db.similarity_search(\n", - " query,\n", - " filter=[{\"match\": {\"metadata.author\": {\"query\": \"Jon\", \"fuzziness\": \"AUTO\"}}}],\n", - ")\n", - "print(docs[0].metadata)" - ] - }, - { - "cell_type": "markdown", - "id": "647d26ea", - "metadata": {}, - "source": [ - "### Example: Filter by Date Range" + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. These examples also show how to use filtering when searching.\n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search with filtering on metadata can be done as follows:" ] }, { "cell_type": "code", "execution_count": 10, - "id": "55b63a61", + "id": "da079ceb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'source': '../../how_to/state_of_the_union.txt', 'date': '2012-01-01', 'rating': 3, 'author': 'John Doe', 'geo_location': {'lat': 40.12, 'lon': -71.34}}\n" + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet'}]\n" ] } ], "source": [ - "docs = db.similarity_search(\n", - " \"Any mention about Fred?\",\n", - " filter=[{\"range\": {\"metadata.date\": {\"gte\": \"2010-01-01\"}}}],\n", + "results = vector_store.similarity_search(\n", + " query=\"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter=[{\"term\": {\"metadata.source.keyword\": \"tweet\"}}],\n", ")\n", - "print(docs[0].metadata)" + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "08f57936", + "id": "a0fda72e", "metadata": {}, "source": [ - "### Example: Filter by Numeric Range" + "#### Similarity search with score\n", + "\n", + "If you want to execute a similarity search and receive the corresponding scores you can run:" ] }, { "cell_type": "code", "execution_count": 11, - "id": "9b831b3d", + "id": "1013c9e8", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'source': '../../how_to/state_of_the_union.txt', 'date': '2012-01-01', 'rating': 3, 'author': 'John Doe', 'geo_location': {'lat': 40.12, 'lon': -71.34}}\n" + "* [SIM=0.765887] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'source': 'news'}]\n" ] } ], "source": [ - "docs = db.similarity_search(\n", - " \"Any mention about Fred?\", filter=[{\"range\": {\"metadata.rating\": {\"gte\": 2}}}]\n", + "results = vector_store.similarity_search_with_score(\n", + " query=\"Will it be hot tomorrow\",\n", + " k=1,\n", + " filter=[{\"term\": {\"metadata.source.keyword\": \"news\"}}],\n", ")\n", - "print(docs[0].metadata)" + "for doc, score in results:\n", + " print(f\"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "05e7ec40", + "id": "8f2c7b5c", "metadata": {}, "source": [ - "### Example: Filter by Geo Distance\n", - "Requires an index with a geo_point mapping to be declared for `metadata.geo_location`." + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. " ] }, { "cell_type": "code", "execution_count": 12, - "id": "fb1482e7", + "id": "2db8b6a5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.'),\n", + " Document(metadata={'source': 'news'}, page_content='The stock market is down 500 points today due to fears of a recession.'),\n", + " Document(metadata={'source': 'website'}, page_content='Is the new iPhone worth the price? Read this review to find out.'),\n", + " Document(metadata={'source': 'tweet'}, page_content='Building an exciting new project with LangChain - come check it out!')]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "docs = db.similarity_search(\n", - " \"Any mention about Fred?\",\n", - " filter=[\n", - " {\n", - " \"geo_distance\": {\n", - " \"distance\": \"200km\",\n", - " \"metadata.geo_location\": {\"lat\": 40, \"lon\": -70},\n", - " }\n", - " }\n", - " ],\n", + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\", search_kwargs={\"score_threshold\": 0.2}\n", ")\n", - "print(docs[0].metadata)" + "retriever.invoke(\"Stealing from the bank is a crime\")" ] }, { "cell_type": "markdown", - "id": "729eb73b", + "id": "17b509ae", "metadata": {}, "source": [ - "Filter supports many more types of queries than above. \n", + "## Chain usage\n", "\n", - "Read more about them in the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html)." - ] - }, - { - "cell_type": "markdown", - "id": "1d54068c", - "metadata": {}, - "source": [ - "# Distance Similarity Algorithm\n", - "Elasticsearch supports the following vector distance similarity algorithms:\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", "\n", - "- cosine\n", - "- euclidean\n", - "- dot_product\n", - "\n", - "The cosine similarity algorithm is the default.\n", - "\n", - "You can specify the similarity Algorithm needed via the similarity parameter.\n", - "\n", - "**NOTE**\n", - "Depending on the retrieval strategy, the similarity algorithm cannot be changed at query time. It is needed to be set when creating the index mapping for field. If you need to change the similarity algorithm, you need to delete the index and recreate it with the correct distance_strategy.\n", - "\n", - "```python\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs, \n", - " embeddings, \n", - " es_url=\"http://localhost:9200\", \n", - " index_name=\"test\",\n", - " distance_strategy=\"COSINE\"\n", - " # distance_strategy=\"EUCLIDEAN_DISTANCE\"\n", - " # distance_strategy=\"DOT_PRODUCT\"\n", - ")\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", "\n", + "\n", "```" ] }, - { - "cell_type": "markdown", - "id": "b06da4d7", - "metadata": {}, - "source": [ - "# Retrieval Strategies\n", - "Elasticsearch has big advantages over other vector only databases from its ability to support a wide range of retrieval strategies. In this notebook we will configure `ElasticsearchStore` to support some of the most common retrieval strategies. \n", - "\n", - "By default, `ElasticsearchStore` uses the `DenseVectorStrategy` (was called `ApproxRetrievalStrategy` prior to version 0.2.0).\n", - "\n", - "## DenseVectorStrategy\n", - "This will return the top `k` most similar vectors to the query vector. The `k` parameter is set when the `ElasticsearchStore` is initialized. The default value is `10`." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "999b5ef5", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_elasticsearch import DenseVectorStrategy\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs,\n", - " embeddings,\n", - " es_url=\"http://localhost:9200\",\n", - " index_name=\"test\",\n", - " strategy=DenseVectorStrategy(),\n", - ")\n", - "\n", - "docs = db.similarity_search(\n", - " query=\"What did the president say about Ketanji Brown Jackson?\", k=10\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "9b651be5", - "metadata": {}, - "source": [ - "### Example: Hybrid retrieval with dense vector and keyword search\n", - "This example will show how to configure `ElasticsearchStore` to perform a hybrid retrieval, using a combination of approximate semantic search and keyword based search. \n", - "\n", - "We use RRF to balance the two scores from different retrieval methods.\n", - "\n", - "To enable hybrid retrieval, we need to set `hybrid=True` in the `DenseVectorStrategy` constructor.\n", - "\n", - "```python\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs, \n", - " embeddings, \n", - " es_url=\"http://localhost:9200\", \n", - " index_name=\"test\",\n", - " strategy=DenseVectorStrategy(hybrid=True)\n", - ")\n", - "```\n", - "\n", - "When `hybrid` is enabled, the query performed will be a combination of approximate semantic search and keyword based search. \n", - "\n", - "It will use `rrf` (Reciprocal Rank Fusion) to balance the two scores from different retrieval methods.\n", - "\n", - "**Note** RRF requires Elasticsearch 8.9.0 or above.\n", - "\n", - "```json\n", - "{\n", - " \"knn\": {\n", - " \"field\": \"vector\",\n", - " \"filter\": [],\n", - " \"k\": 1,\n", - " \"num_candidates\": 50,\n", - " \"query_vector\": [1.0, ..., 0.0],\n", - " },\n", - " \"query\": {\n", - " \"bool\": {\n", - " \"filter\": [],\n", - " \"must\": [{\"match\": {\"text\": {\"query\": \"foo\"}}}],\n", - " }\n", - " },\n", - " \"rank\": {\"rrf\": {}},\n", - "}\n", - "```\n", - "\n", - "### Example: Dense vector search with Embedding Model in Elasticsearch\n", - "This example will show how to configure `ElasticsearchStore` to use the embedding model deployed in Elasticsearch for dense vector retrieval.\n", - "\n", - "To use this, specify the model_id in `DenseVectorStrategy` constructor via the `query_model_id` argument.\n", - "\n", - "**NOTE** This requires the model to be deployed and running in Elasticsearch ml node. See [notebook example](https://github.com/elastic/elasticsearch-labs/blob/main/notebooks/integrations/hugging-face/loading-model-from-hugging-face.ipynb) on how to deploy the model with eland.\n" - ] - }, { "cell_type": "code", "execution_count": 14, - "id": "0a0c85e7", + "id": "58e17804", "metadata": {}, "outputs": [], "source": [ - "DENSE_SELF_DEPLOYED_INDEX_NAME = \"test-dense-self-deployed\"\n", + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", "\n", - "# Note: This does not have an embedding function specified\n", - "# Instead, we will use the embedding model deployed in Elasticsearch\n", - "db = ElasticsearchStore(\n", - " es_cloud_id=\"\",\n", - " es_user=\"elastic\",\n", - " es_password=\"\",\n", - " index_name=DENSE_SELF_DEPLOYED_INDEX_NAME,\n", - " query_field=\"text_field\",\n", - " vector_query_field=\"vector_query_field.predicted_value\",\n", - " strategy=DenseVectorStrategy(model_id=\"sentence-transformers__all-minilm-l6-v2\"),\n", - ")\n", - "\n", - "# Setup a Ingest Pipeline to perform the embedding\n", - "# of the text field\n", - "db.client.ingest.put_pipeline(\n", - " id=\"test_pipeline\",\n", - " processors=[\n", - " {\n", - " \"inference\": {\n", - " \"model_id\": \"sentence-transformers__all-minilm-l6-v2\",\n", - " \"field_map\": {\"query_field\": \"text_field\"},\n", - " \"target_field\": \"vector_query_field\",\n", - " }\n", - " }\n", - " ],\n", - ")\n", - "\n", - "# creating a new index with the pipeline,\n", - "# not relying on langchain to create the index\n", - "db.client.indices.create(\n", - " index=DENSE_SELF_DEPLOYED_INDEX_NAME,\n", - " mappings={\n", - " \"properties\": {\n", - " \"text_field\": {\"type\": \"text\"},\n", - " \"vector_query_field\": {\n", - " \"properties\": {\n", - " \"predicted_value\": {\n", - " \"type\": \"dense_vector\",\n", - " \"dims\": 384,\n", - " \"index\": True,\n", - " \"similarity\": \"l2_norm\",\n", - " }\n", - " }\n", - " },\n", - " }\n", - " },\n", - " settings={\"index\": {\"default_pipeline\": \"test_pipeline\"}},\n", - ")\n", - "\n", - "db.from_texts(\n", - " [\"hello world\"],\n", - " es_cloud_id=\"\",\n", - " es_user=\"elastic\",\n", - " es_password=\"\",\n", - " index_name=DENSE_SELF_DEPLOYED_INDEX_NAME,\n", - " query_field=\"text_field\",\n", - " vector_query_field=\"vector_query_field.predicted_value\",\n", - " strategy=DenseVectorStrategy(model_id=\"sentence-transformers__all-minilm-l6-v2\"),\n", - ")\n", - "\n", - "# Perform search\n", - "db.similarity_search(\"hello world\", k=10)" - ] - }, - { - "cell_type": "markdown", - "id": "53959de6", - "metadata": {}, - "source": [ - "## SparseVectorStrategy (ELSER)\n", - "This strategy uses Elasticsearch's sparse vector retrieval to retrieve the top-k results. We only support our own \"ELSER\" embedding model for now.\n", - "\n", - "**NOTE** This requires the ELSER model to be deployed and running in Elasticsearch ml node. \n", - "\n", - "To use this, specify `SparseVectorStrategy` (was called `SparseVectorRetrievalStrategy` prior to version 0.2.0) in the `ElasticsearchStore` constructor. You will need to provide a model ID." + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "38a63256", + "execution_count": 15, + "id": "01dac420", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.' metadata={'source': '../../how_to/state_of_the_union.txt'}\n" - ] + "data": { + "text/plain": [ + "'LanGraph is used for building stateful, agentic applications. It serves as a framework to facilitate the development of exciting new projects.'" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "from langchain_elasticsearch import SparseVectorStrategy\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", "\n", - "# Note that this example doesn't have an embedding function. This is because we infer the tokens at index time and at query time within Elasticsearch.\n", - "# This requires the ELSER model to be loaded and running in Elasticsearch.\n", - "db = ElasticsearchStore.from_documents(\n", - " docs,\n", - " es_cloud_id=\"\",\n", - " es_user=\"elastic\",\n", - " es_password=\"\",\n", - " index_name=\"test-elser\",\n", - " strategy=SparseVectorStrategy(model_id=\".elser_model_2\"),\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", ")\n", "\n", - "db.client.indices.refresh(index=\"test-elser\")\n", - "\n", - "results = db.similarity_search(\n", - " \"What did the president say about Ketanji Brown Jackson\", k=4\n", - ")\n", - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "id": "edf3a093", - "metadata": {}, - "source": [ - "## DenseVectorScriptScoreStrategy\n", - "This strategy uses Elasticsearch's script score query to perform exact vector retrieval (also known as brute force) to retrieve the top-k results. (This strategy was called `ExactRetrievalStrategy` prior to version 0.2.0.)\n", - "\n", - "To use this, specify `DenseVectorScriptScoreStrategy` in `ElasticsearchStore` constructor.\n", - "\n", - "```python\n", - "from langchain_elasticsearch import SparseVectorStrategy\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs, \n", - " embeddings, \n", - " es_url=\"http://localhost:9200\", \n", - " index_name=\"test\",\n", - " strategy=DenseVectorScriptScoreStrategy(),\n", - ")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "11b51c47", - "metadata": {}, - "source": [ - "## BM25Strategy\n", - "Finally, you can use full-text keyword search.\n", - "\n", - "To use this, specify `BM25Strategy` in `ElasticsearchStore` constructor.\n", - "\n", - "```python\n", - "from langchain_elasticsearch import BM25Strategy\n", - "\n", - "db = ElasticsearchStore.from_documents(\n", - " docs, \n", - " es_url=\"http://localhost:9200\", \n", - " index_name=\"test\",\n", - " strategy=BM25Strategy(),\n", - ")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "05cdb43d-5e46-46f6-a2dc-91df4aa56ec7", - "metadata": {}, - "source": [ - "## BM25RetrievalStrategy\n", - "This strategy allows the user to perform searches using pure BM25 without vector search.\n", - "\n", - "To use this, specify `BM25RetrievalStrategy` in `ElasticsearchStore` constructor.\n", - "\n", - "Note that in the example below, the embedding option is not specified, indicating that the search is conducted without using embeddings." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "4464a657-08c5-4a1a-b0e8-dba65f5b7ec0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Document(page_content='foo'), Document(page_content='foo bar'), Document(page_content='foo bar baz')]\n" - ] - } - ], - "source": [ - "from langchain_elasticsearch import ElasticsearchStore\n", - "\n", - "db = ElasticsearchStore(\n", - " es_url=\"http://localhost:9200\",\n", - " index_name=\"test_index\",\n", - " strategy=ElasticsearchStore.BM25RetrievalStrategy(),\n", - ")\n", - "\n", - "db.add_texts(\n", - " [\"foo\", \"foo bar\", \"foo bar baz\", \"bar\", \"bar baz\", \"baz\"],\n", - ")\n", - "\n", - "results = db.similarity_search(query=\"foo\", k=10)\n", - "print(results)" - ] - }, - { - "cell_type": "markdown", - "id": "0960fa0a", - "metadata": {}, - "source": [ - "## Customise the Query\n", - "With `custom_query` parameter at search, you are able to adjust the query that is used to retrieve documents from Elasticsearch. This is useful if you want to use a more complex query, to support linear boosting of fields." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "b926a606", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Query Retriever created by the retrieval strategy:\n", - "{'query': {'bool': {'must': [{'text_expansion': {'vector.tokens': {'model_id': '.elser_model_1', 'model_text': 'What did the president say about Ketanji Brown Jackson'}}}], 'filter': []}}}\n", - "\n", - "Query thats actually used in Elasticsearch:\n", - "{'query': {'match': {'text': 'What did the president say about Ketanji Brown Jackson'}}}\n", - "\n", - "Results:\n", - "page_content='One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.' metadata={'source': '../../how_to/state_of_the_union.txt'}\n" - ] - } - ], - "source": [ - "# Example of a custom query thats just doing a BM25 search on the text field.\n", - "def custom_query(query_body: dict, query: str):\n", - " \"\"\"Custom query to be used in Elasticsearch.\n", - " Args:\n", - " query_body (dict): Elasticsearch query body.\n", - " query (str): Query string.\n", - " Returns:\n", - " dict: Elasticsearch query body.\n", - " \"\"\"\n", - " print(\"Query Retriever created by the retrieval strategy:\")\n", - " print(query_body)\n", - " print()\n", - "\n", - " new_query_body = {\"query\": {\"match\": {\"text\": query}}}\n", - "\n", - " print(\"Query thats actually used in Elasticsearch:\")\n", - " print(new_query_body)\n", - " print()\n", - "\n", - " return new_query_body\n", - "\n", - "\n", - "results = db.similarity_search(\n", - " \"What did the president say about Ketanji Brown Jackson\",\n", - " k=4,\n", - " custom_query=custom_query,\n", - ")\n", - "print(\"Results:\")\n", - "print(results[0])" - ] - }, - { - "cell_type": "markdown", - "id": "a125af82-1f45-4337-a085-6f393bca2de8", - "metadata": {}, - "source": [ - "# Customize the Document Builder\n", - "\n", - "With ```doc_builder``` parameter at search, you are able to adjust how a Document is being built using data retrieved from Elasticsearch. This is especially useful if you have indices which were not created using Langchain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5bafd4a0-75d0-471e-885a-243312af182a", - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Dict\n", - "\n", - "from langchain_core.documents import Document\n", - "\n", - "\n", - "def custom_document_builder(hit: Dict) -> Document:\n", - " src = hit.get(\"_source\", {})\n", - " return Document(\n", - " page_content=src.get(\"content\", \"Missing content!\"),\n", - " metadata={\n", - " \"page_number\": src.get(\"page_number\", -1),\n", - " \"original_filename\": src.get(\"original_filename\", \"Missing filename!\"),\n", - " },\n", - " )\n", - "\n", - "\n", - "results = db.similarity_search(\n", - " \"What did the president say about Ketanji Brown Jackson\",\n", - " k=4,\n", - " doc_builder=custom_document_builder,\n", - ")\n", - "print(\"Results:\")\n", - "print(results[0])" + "rag_chain.invoke(\"What is LanGraph used for?\")" ] }, { @@ -1010,32 +638,25 @@ " strategy=DenseVectorScriptScoreStrategy()\n", ")\n", "\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3cff8421", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ObjectApiResponse({'acknowledged': True})" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ + "```\n", + "\n", + "```python\n", "db.client.indices.delete(\n", " index=\"test-metadata, test-elser, test-basic\",\n", " ignore_unavailable=True,\n", " allow_no_indices=True,\n", - ")" + ")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "33388871", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `ElasticSearchStore` features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_elasticsearch.vectorstores.ElasticsearchStore.html" ] } ], @@ -1058,7 +679,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/faiss.ipynb b/docs/docs/integrations/vectorstores/faiss.ipynb index e95d5f76c4b..175ed8060b0 100644 --- a/docs/docs/integrations/vectorstores/faiss.ipynb +++ b/docs/docs/integrations/vectorstores/faiss.ipynb @@ -7,11 +7,9 @@ "source": [ "# Faiss\n", "\n", - ">[Facebook AI Similarity Search (Faiss)](https://engineering.fb.com/2017/03/29/data-infrastructure/faiss-a-library-for-efficient-similarity-search/) is a library for efficient similarity search and clustering of dense vectors. It contains algorithms that search in sets of vectors of any size, up to ones that possibly do not fit in RAM. It also contains supporting code for evaluation and parameter tuning.\n", + ">[Facebook AI Similarity Search (FAISS)](https://engineering.fb.com/2017/03/29/data-infrastructure/faiss-a-library-for-efficient-similarity-search/) is a library for efficient similarity search and clustering of dense vectors. It contains algorithms that search in sets of vectors of any size, up to ones that possibly do not fit in RAM. It also contains supporting code for evaluation and parameter tuning.\n", "\n", - "[Faiss documentation](https://faiss.ai/).\n", - "\n", - "You'll need to install `langchain-community` with `pip install -qU langchain-community` to use this integration\n", + "You can find the FAISS documentation at [this page](https://faiss.ai/).\n", "\n", "This notebook shows how to use functionality related to the `FAISS` vector database. It will show functionality specific to this integration. After going through, it may be useful to explore [relevant use-case pages](/docs/how_to#qa-with-rag) to learn how to use this vectorstore as part of a larger chain." ] @@ -25,28 +23,19 @@ "source": [ "## Setup\n", "\n", - "The integration lives in the `langchain-community` package. We also need to install the `faiss` package itself. We will also be using OpenAI for embeddings, so we need to install those requirements. We can install these with:\n", + "The integration lives in the `langchain-community` package. We also need to install the `faiss` package itself. We can install these with:\n", "\n", - "```bash\n", - "pip install -U langchain-community faiss-cpu langchain-openai tiktoken\n", - "```\n", - "\n", - "Note that you can also install `faiss-gpu` if you want to use the GPU enabled version\n", - "\n", - "Since we are using OpenAI, you will need an OpenAI API Key." + "Note that you can also install `faiss-gpu` if you want to use the GPU enabled version" ] }, { "cell_type": "code", "execution_count": null, - "id": "23984e60-c29a-461a-be2b-219108ac37ee", + "id": "08165d56", "metadata": {}, "outputs": [], "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass()" + "pip install -qU langchain-community faiss-cpu" ] }, { @@ -54,7 +43,7 @@ "id": "408be78f-7b0e-44d4-8d48-56a6cb9b3fb9", "metadata": {}, "source": [ - "It's also helpful (but not needed) to set up [LangSmith](https://smith.langchain.com/) for best-in-class observability" + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" ] }, { @@ -73,200 +62,366 @@ "id": "78dde98a-584f-4f2a-98d5-e776fd9558fa", "metadata": {}, "source": [ - "## Ingestion\n", + "## Initialization\n", "\n", - "Here, we ingest documents into the vectorstore" + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", "execution_count": 1, - "id": "dc37144c-208d-4ab3-9f3a-0407a69fe052", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "42" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Uncomment the following line if you need to initialize FAISS with no AVX2 optimization\n", - "# os.environ['FAISS_NO_AVX2'] = '1'\n", - "\n", - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_community.vectorstores import FAISS\n", - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_text_splitters import CharacterTextSplitter\n", - "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", - "embeddings = OpenAIEmbeddings()\n", - "db = FAISS.from_documents(docs, embeddings)\n", - "print(db.index.ntotal)" - ] - }, - { - "cell_type": "markdown", - "id": "ecdd7a65-f310-4b36-bc1e-2a39dfd58d5f", + "id": "5b394da3", "metadata": {}, + "outputs": [], "source": [ - "## Querying\n", + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", "\n", - "Now, we can query the vectorstore. There a few methods to do this. The most standard is to use `similarity_search`." + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" ] }, { "cell_type": "code", "execution_count": 2, - "id": "5eabdb75", + "id": "dc37144c-208d-4ab3-9f3a-0407a69fe052", "metadata": { "tags": [] }, "outputs": [], "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = db.similarity_search(query)" + "import faiss\n", + "from langchain_community.docstore.in_memory import InMemoryDocstore\n", + "from langchain_community.vectorstores import FAISS\n", + "\n", + "index = faiss.IndexFlatL2(len(embeddings.embed_query(\"hello world\")))\n", + "\n", + "vector_store = FAISS(\n", + " embedding_function=embeddings,\n", + " index=index,\n", + " docstore=InMemoryDocstore(),\n", + " index_to_docstore_id={},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d8761614", + "metadata": {}, + "source": [ + "## Manage vector store\n", + "\n", + "### Add items to vector store" ] }, { "cell_type": "code", "execution_count": 3, - "id": "4b172de8", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], - "source": [ - "print(docs[0].page_content)" - ] - }, - { - "cell_type": "markdown", - "id": "6d9286c2-0802-4f02-8f9a-9f7fae7c79b0", - "metadata": {}, - "source": [ - "## As a Retriever\n", - "\n", - "We can also convert the vectorstore into a [Retriever](/docs/how_to#retrievers) class. This allows us to easily use it in other LangChain methods, which largely work with retrievers" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "6e91b475-3878-44e0-8720-98d903754b46", - "metadata": {}, - "outputs": [], - "source": [ - "retriever = db.as_retriever()\n", - "docs = retriever.invoke(query)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "046739d2-91fe-4101-8b72-c0bcdd9e02b9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" - ] - } - ], - "source": [ - "print(docs[0].page_content)" - ] - }, - { - "cell_type": "markdown", - "id": "f13473b5", - "metadata": {}, - "source": [ - "## Similarity Search with score\n", - "There are some FAISS specific methods. One of them is `similarity_search_with_score`, which allows you to return not only the documents but also the distance score of the query to them. The returned distance score is L2 distance. Therefore, a lower score is better." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "186ee1d8", - "metadata": {}, - "outputs": [], - "source": [ - "docs_and_scores = db.similarity_search_with_score(query)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "284e04b5", + "id": "3867e154", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'source': '../../how_to/state_of_the_union.txt'}),\n", - " 0.36913747)" + "['22f5ce99-cd6f-4e0c-8dab-664128307c72',\n", + " 'dc3f061b-5f88-4fa1-a966-413550c51891',\n", + " 'd33d890b-baad-47f7-b7c1-175f5f7b4e59',\n", + " '6e6c01d2-6020-4a7b-95da-ef43d43f01b5',\n", + " 'e677223d-ad75-4c1a-bef6-b5912bd1de03',\n", + " '47e2a168-6462-4ed2-b1d9-d9edfd7391d6',\n", + " '1e4d66d6-e155-4891-9212-f7be97f36c6a',\n", + " 'c0663096-e1a5-4665-b245-1c2e6c4fb653',\n", + " '8297474a-7f7c-4006-9865-398c1781b1bc',\n", + " '44e4be03-0a8d-4316-b3c4-f35f4bb2b532']" ] }, - "execution_count": 8, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "docs_and_scores[0]" + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" ] }, { "cell_type": "markdown", - "id": "f34420cf", + "id": "a410a2dc", "metadata": {}, "source": [ - "It is also possible to do a search for documents similar to a given embedding vector using `similarity_search_by_vector` which accepts an embedding vector as a parameter instead of a string." + "### Delete items from vector store" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "b558ebb7", + "execution_count": 4, + "id": "c3db04bd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "id": "77de24ff", + "metadata": {}, + "source": [ + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search with filtering on metadata can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "53d95d3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter={\"source\": \"tweet\"},\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "5ae35069", + "metadata": {}, + "source": [ + "#### Similarity search with score\n", + "\n", + "You can also search with score:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a9078ce9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.893688] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'source': 'news'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(\n", + " \"Will it be hot tomorrow?\", k=1, filter={\"source\": \"news\"}\n", + ")\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "e9091b1f", + "metadata": {}, + "source": [ + "#### Other search methods\n", + "\n", + "\n", + "There are a variety of other ways to search a FAISS vector store. For a complete list of those methods, please refer to the [API Reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.faiss.FAISS.html)\n", + "\n", + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "10da64fa", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "retriever = vector_store.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 1})\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" + ] + }, + { + "cell_type": "markdown", + "id": "5edd1909", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6b792eaa", "metadata": {}, "outputs": [], "source": [ - "embedding_vector = embeddings.embed_query(query)\n", - "docs_and_scores = db.similarity_search_by_vector(embedding_vector)" + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "1aca9435", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of these types of applications.'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" ] }, { @@ -280,31 +435,33 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "1b31fe27-e0b3-42c6-b17c-8270b517ee1f", "metadata": {}, "outputs": [], "source": [ - "db.save_local(\"faiss_index\")\n", + "vector_store.save_local(\"faiss_index\")\n", "\n", - "new_db = FAISS.load_local(\"faiss_index\", embeddings)\n", + "new_vector_store = FAISS.load_local(\n", + " \"faiss_index\", embeddings, allow_dangerous_deserialization=True\n", + ")\n", "\n", - "docs = new_db.similarity_search(query)" + "docs = new_vector_store.similarity_search(\"qux\")" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "98378c4e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Document(page_content='Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.', metadata={'source': '../../../state_of_the_union.txt'})" + "Document(metadata={'source': 'tweet'}, page_content='Building an exciting new project with LangChain - come check it out!')" ] }, - "execution_count": 9, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -313,33 +470,6 @@ "docs[0]" ] }, - { - "cell_type": "markdown", - "id": "30c8f57b", - "metadata": {}, - "source": [ - "# Serializing and De-Serializing to bytes\n", - "\n", - "you can pickle the FAISS Index by these functions. If you use embeddings model which is of 90 mb (sentence-transformers/all-MiniLM-L6-v2 or any other model), the resultant pickle size would be more than 90 mb. the size of the model is also included in the overall size. To overcome this, use the below functions. These functions only serializes FAISS index and size would be much lesser. this can be helpful if you wish to store the index in database like sql." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8faead5", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_huggingface import HuggingFaceEmbeddings\n", - "\n", - "pkl = db.serialize_to_bytes() # serializes the faiss\n", - "embeddings = HuggingFaceEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n", - "\n", - "db = FAISS.deserialize_from_bytes(\n", - " embeddings=embeddings, serialized=pkl\n", - ") # Load the index" - ] - }, { "cell_type": "markdown", "id": "57da60d4", @@ -351,10 +481,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "9b8f5e31-3f40-4e94-8d97-5883125efba7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'b752e805-350e-4cf5-ba54-0883d46a3a44': Document(page_content='foo')}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "db1 = FAISS.from_texts([\"foo\"], embeddings)\n", "db2 = FAISS.from_texts([\"bar\"], embeddings)\n", @@ -364,17 +505,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 14, "id": "83392605", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'807e0c63-13f6-4070-9774-5c6f0fbb9866': Document(page_content='bar', metadata={})}" + "{'08192d92-746d-4cd1-b681-bdfba411f459': Document(page_content='bar')}" ] }, - "execution_count": 10, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -385,7 +526,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 15, "id": "a3fcc1c7", "metadata": {}, "outputs": [], @@ -395,18 +536,18 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 16, "id": "41c51f89", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'068c473b-d420-487a-806b-fb0ccea7f711': Document(page_content='foo', metadata={}),\n", - " '807e0c63-13f6-4070-9774-5c6f0fbb9866': Document(page_content='bar', metadata={})}" + "{'b752e805-350e-4cf5-ba54-0883d46a3a44': Document(page_content='foo'),\n", + " '08192d92-746d-4cd1-b681-bdfba411f459': Document(page_content='bar')}" ] }, - "execution_count": 13, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -417,169 +558,12 @@ }, { "cell_type": "markdown", - "id": "f4294b96", + "id": "65654d80", "metadata": {}, "source": [ - "## Similarity Search with filtering\n", - "FAISS vectorstore can also support filtering, since the FAISS does not natively support filtering we have to do it manually. This is done by first fetching more results than `k` and then filtering them. This filter is either a callble that takes as input a metadata dict and returns a bool, or a metadata dict where each missing key is ignored and each present k must be in a list of values. You can also set the `fetch_k` parameter when calling any search method to set how many documents you want to fetch before filtering. Here is a small example:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d5bf812c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo, Metadata: {'page': 1}, Score: 5.159960813797904e-15\n", - "Content: foo, Metadata: {'page': 2}, Score: 5.159960813797904e-15\n", - "Content: foo, Metadata: {'page': 3}, Score: 5.159960813797904e-15\n", - "Content: foo, Metadata: {'page': 4}, Score: 5.159960813797904e-15\n" - ] - } - ], - "source": [ - "from langchain_core.documents import Document\n", + "## API reference\n", "\n", - "list_of_documents = [\n", - " Document(page_content=\"foo\", metadata=dict(page=1)),\n", - " Document(page_content=\"bar\", metadata=dict(page=1)),\n", - " Document(page_content=\"foo\", metadata=dict(page=2)),\n", - " Document(page_content=\"barbar\", metadata=dict(page=2)),\n", - " Document(page_content=\"foo\", metadata=dict(page=3)),\n", - " Document(page_content=\"bar burr\", metadata=dict(page=3)),\n", - " Document(page_content=\"foo\", metadata=dict(page=4)),\n", - " Document(page_content=\"bar bruh\", metadata=dict(page=4)),\n", - "]\n", - "db = FAISS.from_documents(list_of_documents, embeddings)\n", - "results_with_scores = db.similarity_search_with_score(\"foo\")\n", - "for doc, score in results_with_scores:\n", - " print(f\"Content: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}\")" - ] - }, - { - "cell_type": "markdown", - "id": "3d33c126", - "metadata": {}, - "source": [ - "Now we make the same query call but we filter for only `page = 1` " - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "83159330", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo, Metadata: {'page': 1}, Score: 5.159960813797904e-15\n", - "Content: bar, Metadata: {'page': 1}, Score: 0.3131446838378906\n" - ] - } - ], - "source": [ - "results_with_scores = db.similarity_search_with_score(\"foo\", filter=dict(page=1))\n", - "# Or with a callable:\n", - "# results_with_scores = db.similarity_search_with_score(\"foo\", filter=lambda d: d[\"page\"] == 1)\n", - "for doc, score in results_with_scores:\n", - " print(f\"Content: {doc.page_content}, Metadata: {doc.metadata}, Score: {score}\")" - ] - }, - { - "cell_type": "markdown", - "id": "0be136e0", - "metadata": {}, - "source": [ - "Same thing can be done with the `max_marginal_relevance_search` as well." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "432c6980", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo, Metadata: {'page': 1}\n", - "Content: bar, Metadata: {'page': 1}\n" - ] - } - ], - "source": [ - "results = db.max_marginal_relevance_search(\"foo\", filter=dict(page=1))\n", - "for doc in results:\n", - " print(f\"Content: {doc.page_content}, Metadata: {doc.metadata}\")" - ] - }, - { - "cell_type": "markdown", - "id": "1b4ecd86", - "metadata": {}, - "source": [ - "Here is an example of how to set `fetch_k` parameter when calling `similarity_search`. Usually you would want the `fetch_k` parameter >> `k` parameter. This is because the `fetch_k` parameter is the number of documents that will be fetched before filtering. If you set `fetch_k` to a low number, you might not get enough documents to filter from." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "1fd60fd1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo, Metadata: {'page': 1}\n" - ] - } - ], - "source": [ - "results = db.similarity_search(\"foo\", filter=dict(page=1), k=1, fetch_k=4)\n", - "for doc in results:\n", - " print(f\"Content: {doc.page_content}, Metadata: {doc.metadata}\")" - ] - }, - { - "cell_type": "markdown", - "id": "1becca53", - "metadata": {}, - "source": [ - "## Delete\n", - "\n", - "You can also delete records from vectorstore. In the example below `db.index_to_docstore_id` represents a dictionary with elements of the FAISS index." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "1408b870", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "count before: 8\n", - "count after: 7" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "print(\"count before:\", db.index.ntotal)\n", - "db.delete([db.index_to_docstore_id[0]])\n", - "print(\"count after:\", db.index.ntotal)" + "For detailed documentation of all `FAISS` vector store features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.faiss.FAISS.html" ] } ], @@ -599,7 +583,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/faiss_index/index.faiss b/docs/docs/integrations/vectorstores/faiss_index/index.faiss deleted file mode 100644 index 92aab3fe39c91c5a57ef23ccd8067dab938d7d2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258093 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 Q5pF diff --git a/docs/docs/integrations/vectorstores/index.mdx b/docs/docs/integrations/vectorstores/index.mdx new file mode 100644 index 00000000000..fb35683b644 --- /dev/null +++ b/docs/docs/integrations/vectorstores/index.mdx @@ -0,0 +1,29 @@ +--- +sidebar_position: 0 +sidebar_class_name: hidden +keywords: [compatibility] +custom_edit_url: +--- + +# Vectorstores + +## Features + +The table below lists the features for some of our most popular vector stores. + +Vectorstore|Delete by ID|Filtering|Search by Vector|Search with score|Async|Passes Standard Tests|Multi Tenancy|Local/Cloud|IDs in add Documents +:-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-: +AstraDBVectorStore|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ +Chroma|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ +Clickhouse|✅|✅|❌|✅|❌|❌|❌|❌|Local|✅ +CouchbaseVectorStore|✅|✅|❌|✅|✅|❌|❌|❌|Local|✅ +ElasticsearchStore|✅|✅|✅|❌|✅|❌|❌|❌|Local|✅ +FAISS|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ +InMemoryVectorStore|✅|✅|❌|✅|✅|❌|❌|❌|Local|✅ +Milvus|✅|✅|❌|✅|✅|❌|❌|❌|Local|✅ +MongoDBAtlasVectorSearch|✅|✅|❌|❌|✅|❌|❌|❌|Local|✅ +PGVector|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ +PineconeVectorStore|✅|✅|✅|❌|✅|❌|❌|❌|Local|✅ +QdrantVectorStore|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ +Redis|✅|✅|✅|✅|✅|❌|❌|❌|Local|✅ + diff --git a/docs/docs/integrations/vectorstores/milvus.ipynb b/docs/docs/integrations/vectorstores/milvus.ipynb index 4c314dfa15f..b6c806e637b 100644 --- a/docs/docs/integrations/vectorstores/milvus.ipynb +++ b/docs/docs/integrations/vectorstores/milvus.ipynb @@ -11,7 +11,9 @@ "\n", "This notebook shows how to use functionality related to the Milvus vector database.\n", "\n", - "You'll need to install `langchain-milvus` with `pip install -qU langchain-milvus` to use this integration\n" + "## Setup\n", + "\n", + "You'll need to install `langchain-milvus` with `pip install -qU langchain-milvus` to use this integration.\n" ] }, { @@ -23,7 +25,7 @@ }, "outputs": [], "source": [ - "%pip install --upgrade --quiet langchain_milvus" + "%pip install -qU langchain_milvus" ] }, { @@ -31,119 +33,59 @@ "id": "633addc3", "metadata": {}, "source": [ - "The latest version of pymilvus comes with a local vector database Milvus Lite, good for prototyping. If you have large scale of data such as more than a million docs, we recommend setting up a more performant Milvus server on [docker or kubernetes](https://milvus.io/docs/install_standalone-docker.md#Start-Milvus)." + "The latest version of pymilvus comes with a local vector database Milvus Lite, good for prototyping. If you have large scale of data such as more than a million docs, we recommend setting up a more performant Milvus server on [docker or kubernetes](https://milvus.io/docs/install_standalone-docker.md#Start-Milvus).\n", + "\n", + "### Credentials\n", + "\n", + "No credentials are needed to use the `Milvus` vector store.\n", + "\n", + "## Initialization\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { - "cell_type": "markdown", - "id": "7a0f9e02-8eb0-4aef-b11f-8861360472ee", + "cell_type": "code", + "execution_count": 25, + "id": "a7dd253f", "metadata": {}, - "source": [ - "We want to use OpenAIEmbeddings so we have to get the OpenAI API Key." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "8b6ed9cd-81b9-46e5-9c20-5aafca2844d0", - "metadata": { - "tags": [] - }, "outputs": [], "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "aac9563e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_milvus.vectorstores import Milvus\n", + "# | output: false\n", + "# | echo: false\n", "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_text_splitters import CharacterTextSplitter" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a3c3999a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", "\n", - "embeddings = OpenAIEmbeddings()" + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 28, "id": "dcf88bdf", "metadata": { "tags": [] }, "outputs": [], "source": [ + "from langchain_milvus import Milvus\n", + "\n", "# The easiest way is to use Milvus Lite where everything is stored in a local file.\n", "# If you have a Milvus server you can use the server URI such as \"http://localhost:19530\".\n", - "URI = \"./milvus_demo.db\"\n", + "URI = \"./milvus_example.db\"\n", "\n", - "vector_db = Milvus.from_documents(\n", - " docs,\n", - " embeddings,\n", + "vector_store = Milvus(\n", + " embedding_function=embeddings,\n", " connection_args={\"uri\": URI},\n", ")" ] }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a8c513ab", - "metadata": {}, - "outputs": [], - "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = vector_db.similarity_search(query)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "fc516993", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \\n\\nTonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \\n\\nOne of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \\n\\nAnd I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "docs[0].page_content" - ] - }, { "cell_type": "markdown", - "id": "e40d558b", + "id": "cae1a7d5", "metadata": {}, "source": [ "### Compartmentalize the data with Milvus Collections\n", @@ -153,7 +95,7 @@ }, { "cell_type": "markdown", - "id": "82c00f6e", + "id": "c07cd24b", "metadata": {}, "source": [ "Here's how you can create a new collection" @@ -161,22 +103,24 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "f7ff38ab", + "execution_count": 29, + "id": "c6f4973d", "metadata": {}, "outputs": [], "source": [ - "vector_db = Milvus.from_documents(\n", - " docs,\n", + "from langchain_core.documents import Document\n", + "\n", + "vector_store_saved = Milvus.from_documents(\n", + " [Document(page_content=\"foo!\")],\n", " embeddings,\n", - " collection_name=\"collection_1\",\n", + " collection_name=\"langchain_example\",\n", " connection_args={\"uri\": URI},\n", ")" ] }, { "cell_type": "markdown", - "id": "891cec1f", + "id": "3b12df8c", "metadata": {}, "source": [ "And here is how you retrieve that stored collection" @@ -184,24 +128,333 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "e9e873e9", + "execution_count": 30, + "id": "12817d16", "metadata": {}, "outputs": [], "source": [ - "vector_db = Milvus(\n", + "vector_store_loaded = Milvus(\n", " embeddings,\n", " connection_args={\"uri\": URI},\n", - " collection_name=\"collection_1\",\n", + " collection_name=\"langchain_example\",\n", ")" ] }, { "cell_type": "markdown", - "id": "9cc65535", + "id": "f1fc3818", "metadata": {}, "source": [ - "After retrieval you can go on querying it as usual." + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "3ced24f6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['b0248595-2a41-4f6b-9c25-3a24c1278bb3',\n", + " 'fa642726-5329-4495-a072-187e948dd71f',\n", + " '9905001c-a4a3-455e-ab94-72d0ed11b476',\n", + " 'eacc7256-d7fa-4036-b1f7-83d7a4bee0c5',\n", + " '7508f7ff-c0c9-49ea-8189-634f8a0244d8',\n", + " '2e179609-3ff7-4c6a-9e05-08978903fe26',\n", + " 'fab1f2ac-43e1-45f9-b81b-fc5d334c6508',\n", + " '1206d237-ee3a-484f-baf2-b5ac38eeb314',\n", + " 'd43cbf9a-a772-4c40-993b-9439065fec01',\n", + " '25e667bb-6f09-4574-a368-661069301906']" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "id": "e23c22d8", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "1f387fa8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(insert count: 0, delete count: 1, upsert count: 0, timestamp: 0, success count: 0, err count: 0, cost: 0)" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "id": "fb12fa75", + "metadata": {}, + "source": [ + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search with filtering on metadata can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "35801a55", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'pk': '9905001c-a4a3-455e-ab94-72d0ed11b476', 'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'pk': '1206d237-ee3a-484f-baf2-b5ac38eeb314', 'source': 'tweet'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter={\"source\": \"tweet\"},\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "35574409", + "metadata": {}, + "source": [ + "#### Similarity search with score\n", + "\n", + "You can also search with score:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "c360af3d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=21192.628906] bar [{'pk': '2', 'source': 'https://example.com'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(\n", + " \"Will it be hot tomorrow?\", k=1, filter={\"source\": \"news\"}\n", + ")\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "14db337f", + "metadata": {}, + "source": [ + "For a full list of all the search options available when using the `Milvus` vector store, you can visit the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_milvus.vectorstores.milvus.Milvus.html).\n", + "\n", + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. " + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "f6d9357c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'pk': 'eacc7256-d7fa-4036-b1f7-83d7a4bee0c5', 'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "retriever = vector_store.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 1})\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" + ] + }, + { + "cell_type": "markdown", + "id": "8ac953f1", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "d17118c2", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "7bbe3b95", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of such applications effectively.'" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" ] }, { @@ -325,47 +578,12 @@ }, { "cell_type": "markdown", - "id": "89756e9e", + "id": "f1a873c5", "metadata": {}, "source": [ - "### To delete or upsert (update/insert) one or more entities" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "21c4edcf", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.documents import Document\n", + "## API reference\n", "\n", - "# Insert data sample\n", - "docs = [\n", - " Document(page_content=\"foo\", metadata={\"id\": 1}),\n", - " Document(page_content=\"bar\", metadata={\"id\": 2}),\n", - " Document(page_content=\"baz\", metadata={\"id\": 3}),\n", - "]\n", - "vector_db = Milvus.from_documents(\n", - " docs,\n", - " embeddings,\n", - " connection_args={\"uri\": URI},\n", - ")\n", - "\n", - "# Search pks (primary keys) using expression\n", - "expr = \"id in [1,2]\"\n", - "pks = vector_db.get_pks(expr)\n", - "\n", - "# Delete entities by pks\n", - "result = vector_db.delete(pks)\n", - "\n", - "# Upsert (Update/Insert)\n", - "new_docs = [\n", - " Document(page_content=\"new_foo\", metadata={\"id\": 1}),\n", - " Document(page_content=\"new_bar\", metadata={\"id\": 2}),\n", - " Document(page_content=\"upserted_bak\", metadata={\"id\": 3}),\n", - "]\n", - "upserted_pks = vector_db.upsert(pks, new_docs)" + "For detailed documentation of all __ModuleName__VectorStore features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_milvus.vectorstores.milvus.Milvus.html" ] } ], @@ -385,7 +603,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/mongodb_atlas.ipynb b/docs/docs/integrations/vectorstores/mongodb_atlas.ipynb index c0415fed23c..c71fbd6526c 100644 --- a/docs/docs/integrations/vectorstores/mongodb_atlas.ipynb +++ b/docs/docs/integrations/vectorstores/mongodb_atlas.ipynb @@ -19,74 +19,40 @@ "id": "359b8e9b", "metadata": {}, "source": [ - "## Prerequisites\n", + "## Setup\n", + "\n", ">*An Atlas cluster running MongoDB version 6.0.11, 7.0.2, or later (including RCs).\n", "\n", - ">*An OpenAI API Key. You must have a paid OpenAI account with credits available for API requests.\n", + "To use MongoDB Atlas, you must first deploy a cluster. We have a Forever-Free tier of clusters available. To get started head over to Atlas here: [quick start](https://www.mongodb.com/docs/atlas/getting-started/).\n", "\n", - "You'll need to install `langchain-mongodb` to use this integration" - ] - }, - { - "cell_type": "markdown", - "id": "d899e588", - "metadata": {}, - "source": [ - "## Setting up MongoDB Atlas Cluster\n", - "To use MongoDB Atlas, you must first deploy a cluster. We have a Forever-Free tier of clusters available. To get started head over to Atlas here: [quick start](https://www.mongodb.com/docs/atlas/getting-started/)." - ] - }, - { - "cell_type": "markdown", - "id": "1b5ce18d", - "metadata": {}, - "source": [ - "## Usage\n", - "In the notebook we will demonstrate how to perform `Retrieval Augmented Generation` (RAG) using MongoDB Atlas, OpenAI and Langchain. We will be performing Similarity Search, Similarity Search with Metadata Pre-Filtering, and Question Answering over the PDF document for [GPT 4 technical report](https://arxiv.org/pdf/2303.08774.pdf) that came out in March 2023 and hence is not part of the OpenAI's Large Language Model(LLM)'s parametric memory, which had a knowledge cutoff of September 2021." - ] - }, - { - "cell_type": "markdown", - "id": "457ace44-1d95-4001-9dd5-78811ab208ad", - "metadata": {}, - "source": [ - "We want to use `OpenAIEmbeddings` so we need to set up our OpenAI API Key. " + "You'll need to install `langchain-mongodb` and `pymongo` to use this integration." ] }, { "cell_type": "code", "execution_count": null, - "id": "2d8f240d", + "id": "73cf7c9f", "metadata": {}, "outputs": [], "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" + "pip install -qU langchain-mongodb pymongo" ] }, { "cell_type": "markdown", - "id": "70482cd8", + "id": "a61832ea", "metadata": {}, "source": [ - "Now we will setup the environment variables for the MongoDB Atlas cluster" + "### Credentials\n", + "\n", + "For this notebook you will need to find your MongoDB cluster URI.\n", + "\n", + "For information on finding your cluster URI read through [this guide](https://www.mongodb.com/docs/manual/reference/connection-string/)." ] }, { "cell_type": "code", - "execution_count": null, - "id": "4d7788cf", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install --upgrade --quiet langchain langchain-mongodb pypdf pymongo langchain-openai tiktoken" - ] - }, - { - "cell_type": "code", - "execution_count": null, + "execution_count": 33, "id": "7ef41b37", "metadata": {}, "outputs": [], @@ -96,76 +62,78 @@ "MONGODB_ATLAS_CLUSTER_URI = getpass.getpass(\"MongoDB Atlas Cluster URI:\")" ] }, + { + "cell_type": "markdown", + "id": "1f23de23", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, { "cell_type": "code", "execution_count": null, + "id": "908e7772", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "a53673ae", + "metadata": {}, + "source": [ + "## Initialization\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "f5fed614", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-small\")" + ] + }, + { + "cell_type": "code", + "execution_count": 56, "id": "00d78318", "metadata": {}, "outputs": [], "source": [ + "from langchain_mongodb.vectorstores import MongoDBAtlasVectorSearch\n", "from pymongo import MongoClient\n", "\n", "# initialize MongoDB python client\n", "client = MongoClient(MONGODB_ATLAS_CLUSTER_URI)\n", "\n", - "DB_NAME = \"langchain_db\"\n", - "COLLECTION_NAME = \"test\"\n", - "ATLAS_VECTOR_SEARCH_INDEX_NAME = \"index_name\"\n", + "DB_NAME = \"langchain_test_db\"\n", + "COLLECTION_NAME = \"langchain_test_vectorstores\"\n", + "ATLAS_VECTOR_SEARCH_INDEX_NAME = \"langchain-test-index-vectorstores\"\n", "\n", - "MONGODB_COLLECTION = client[DB_NAME][COLLECTION_NAME]" - ] - }, - { - "cell_type": "markdown", - "id": "eb0cc10f-b84e-4e5e-b445-eb61f10bf085", - "metadata": {}, - "source": [ - "## Create Vector Search Index" - ] - }, - { - "cell_type": "markdown", - "id": "1f3ecc42", - "metadata": {}, - "source": [ - "Now, let's create a vector search index on your cluster. More detailed steps can be found at [Create Vector Search Index for LangChain](https://www.mongodb.com/docs/atlas/atlas-vector-search/ai-integrations/langchain/#create-the-atlas-vector-search-index) section.\n", - "In the below example, `embedding` is the name of the field that contains the embedding vector. Please refer to the [documentation](https://www.mongodb.com/docs/atlas/atlas-vector-search/create-index/) to get more details on how to define an Atlas Vector Search index.\n", - "You can name the index `{ATLAS_VECTOR_SEARCH_INDEX_NAME}` and create the index on the namespace `{DB_NAME}.{COLLECTION_NAME}`. Finally, write the following definition in the JSON editor on MongoDB Atlas:\n", + "MONGODB_COLLECTION = client[DB_NAME][COLLECTION_NAME]\n", "\n", - "```json\n", - "{\n", - " \"fields\":[\n", - " {\n", - " \"type\": \"vector\",\n", - " \"path\": \"embedding\",\n", - " \"numDimensions\": 1536,\n", - " \"similarity\": \"cosine\"\n", - " }\n", - " ]\n", - "}\n", - "```\n", - "\n", - "Additionally, if you are running a MongoDB M10 cluster with server version 6.0+, you can leverage the `MongoDBAtlasVectorSearch.create_index`. To add the above index its usage would look like this.\n", - "\n", - "```python\n", - "from langchain_community.embeddings.openai import OpenAIEmbeddings\n", - "from langchain_mongodb.vectorstores import MongoDBAtlasVectorSearch\n", - "from pymongo import MongoClient\n", - "\n", - "mongo_client = MongoClient(\"\")\n", - "collection = mongo_client[\"\"][\"\"]\n", - "embeddings = OpenAIEmbeddings()\n", - "\n", - "vectorstore = MongoDBAtlasVectorSearch(\n", - " collection=collection,\n", - " embedding=embeddings,\n", - " index_name=\"\",\n", - " relevance_score_fn=\"cosine\",\n", - ")\n", - "\n", - "# Creates an index using the index_name provided and relevance_score_fn type\n", - "vectorstore.create_index(dimensions=1536)\n", - "```" + "vector_store = MongoDBAtlasVectorSearch(\n", + " collection=MONGODB_COLLECTION,\n", + " embedding=embeddings,\n", + " index_name=ATLAS_VECTOR_SEARCH_INDEX_NAME,\n", + " relevance_score_fn=\"cosine\",\n", + ")" ] }, { @@ -173,126 +141,224 @@ "id": "42873e5a", "metadata": {}, "source": [ - "# Insert Data" + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 57, "id": "aac9563e", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "['03ad81e8-32a0-46f0-b7d8-f5b977a6b52a',\n", + " '8396a68d-f4a3-4176-a581-a1a8c303eea4',\n", + " 'e7d95150-67f6-499f-b611-84367c50fa60',\n", + " '8c31b84e-2636-48b6-8b99-9fccb47f7051',\n", + " 'aa02e8a2-a811-446a-9785-8cea0faba7a9',\n", + " '19bd72ff-9766-4c3b-b1fd-195c732c562b',\n", + " '642d6f2f-3e34-4efa-a1ed-c4ba4ef0da8d',\n", + " '7614bb54-4eb5-4b3b-990c-00e35cb31f99',\n", + " '69e18c67-bf1b-43e5-8a6e-64fb3f240e52',\n", + " '30d599a7-4a1a-47a9-bbf8-6ed393e2e33c']" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "from langchain_community.document_loaders import PyPDFLoader\n", + "from uuid import uuid4\n", "\n", - "# Load the PDF\n", - "loader = PyPDFLoader(\"https://arxiv.org/pdf/2303.08774.pdf\")\n", - "data = loader.load()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5578113", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_text_splitters import RecursiveCharacterTextSplitter\n", + "from langchain_core.documents import Document\n", "\n", - "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)\n", - "docs = text_splitter.split_documents(data)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d378168f", - "metadata": {}, - "outputs": [], - "source": [ - "print(docs[0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6e104aee", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_community.vectorstores import MongoDBAtlasVectorSearch\n", - "from langchain_openai import OpenAIEmbeddings\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", "\n", - "# insert the documents in MongoDB Atlas with their embedding\n", - "vector_search = MongoDBAtlasVectorSearch.from_documents(\n", - " documents=docs,\n", - " embedding=OpenAIEmbeddings(disallowed_special=()),\n", - " collection=MONGODB_COLLECTION,\n", - " index_name=ATLAS_VECTOR_SEARCH_INDEX_NAME,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7bf6841e", - "metadata": {}, - "outputs": [], - "source": [ - "# Perform a similarity search between the embedding of the query and the embeddings of the documents\n", - "query = \"What were the compute requirements for training GPT 4\"\n", - "results = vector_search.similarity_search(query)\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", "\n", - "print(results[0].page_content)" + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" ] }, { "cell_type": "markdown", - "id": "9e58c2d8", + "id": "639f29da", "metadata": {}, "source": [ - "# Querying data" - ] - }, - { - "cell_type": "markdown", - "id": "851a2ec9-9390-49a4-8412-3e132c9f789d", - "metadata": {}, - "source": [ - "We can also instantiate the vector store directly and execute a query as follows:" + "### Delete items from vector store\n" ] }, { "cell_type": "code", - "execution_count": null, - "id": "985d28fe", + "execution_count": 58, + "id": "bbb5fd5c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "from langchain_community.vectorstores import MongoDBAtlasVectorSearch\n", - "from langchain_openai import OpenAIEmbeddings\n", + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "id": "d6111eb6", + "metadata": {}, + "source": [ + "## Query vector store\n", "\n", - "vector_search = MongoDBAtlasVectorSearch.from_connection_string(\n", - " MONGODB_ATLAS_CLUSTER_URI,\n", - " DB_NAME + \".\" + COLLECTION_NAME,\n", - " OpenAIEmbeddings(disallowed_special=()),\n", - " index_name=ATLAS_VECTOR_SEARCH_INDEX_NAME,\n", - ")" + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "19b60ac0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'_id': 'e7d95150-67f6-499f-b611-84367c50fa60', 'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'_id': '7614bb54-4eb5-4b3b-990c-00e35cb31f99', 'source': 'tweet'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\", k=2\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "02aef29c-5da0-41b8-b4fc-98fd71b94abf", + "id": "6c624606", "metadata": {}, "source": [ - "## Pre-filtering with Similarity Search" + "#### Similarity search with score\n", + "\n", + "You can also search with score:" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "e919fa51", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.784560] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'_id': '8396a68d-f4a3-4176-a581-a1a8c303eea4', 'source': 'news'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(\"Will it be hot tomorrow?\", k=1)\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", - "id": "f3b2d36d-d47a-482f-999d-85c23eb67eed", + "id": "513a1416", + "metadata": {}, + "source": [ + "### Pre-filtering with Similarity Search" + ] + }, + { + "cell_type": "markdown", + "id": "ac58c6c7", "metadata": {}, "source": [ "Atlas Vector Search supports pre-filtering using MQL Operators for filtering. Below is an example index and query on the same data loaded above that allows you do metadata filtering on the \"page\" field. You can update your existing index with the filter defined and do pre-filtering with vector search." @@ -300,7 +366,7 @@ }, { "cell_type": "markdown", - "id": "2b385a46-1e54-471f-95b2-202813d90bb2", + "id": "dacac7b8", "metadata": {}, "source": [ "```json\n", @@ -314,7 +380,7 @@ " },\n", " {\n", " \"type\": \"filter\",\n", - " \"path\": \"page\"\n", + " \"path\": \"source\"\n", " }\n", " ]\n", "}\n", @@ -325,128 +391,134 @@ "```python\n", "vectorstore.create_index(\n", " dimensions=1536,\n", - " filters=[{\"type\":\"filter\", \"path\":\"page\"}],\n", + " filters=[{\"type\":\"filter\", \"path\":\"source\"}],\n", " update=True\n", ")\n", + "```\n", + "\n", + "And then you can run a query with filter as follows:\n", + "\n", + "```python\n", + "results = vector_store.similarity_search(query=\"foo\",k=1,pre_filter={\"source\": {\"$eq\": \"https://example.com\"}})\n", + "for doc in results:\n", + " print(f\"* {doc.page_content} [{doc.metadata}]\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "32b13a9b", + "metadata": {}, + "source": [ + "#### Other search methods\n", + "\n", + "There are a variety of other search methods that are not covered in this notebook, such as MMR search or searching by vector. For a full list of the search abilities available for `AstraDBVectorStore` check out the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_astradb.vectorstores.AstraDBVectorStore.html)." + ] + }, + { + "cell_type": "markdown", + "id": "01316a42", + "metadata": {}, + "source": [ + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. \n", + "\n", + "Here is how to transform your vector store into a retriever and then invoke the retreiever with a simple query and filter." + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "8f246301", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'_id': '8c31b84e-2636-48b6-8b99-9fccb47f7051', 'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\",\n", + " search_kwargs={\"k\": 1, \"score_threshold\": 0.2},\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\")" + ] + }, + { + "cell_type": "markdown", + "id": "72312657", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", "```" ] }, { "cell_type": "code", - "execution_count": null, - "id": "dfc8487d-14ec-42c9-9670-80fe02816196", + "execution_count": 66, + "id": "a42da723", "metadata": {}, "outputs": [], "source": [ - "query = \"What were the compute requirements for training GPT 4\"\n", + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", "\n", - "results = vector_search.similarity_search_with_score(\n", - " query=query, k=5, pre_filter={\"page\": {\"$eq\": 1}}\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "80c1130f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of such applications.'" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", ")\n", "\n", - "# Display results\n", - "for result in results:\n", - " print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "6d9a2dbe", - "metadata": {}, - "source": [ - "## Similarity Search with Score" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "497baffa", - "metadata": {}, - "outputs": [], - "source": [ - "query = \"What were the compute requirements for training GPT 4\"\n", - "\n", - "results = vector_search.similarity_search_with_score(\n", - " query=query,\n", - " k=5,\n", - ")\n", - "\n", - "# Display results\n", - "for result in results:\n", - " print(result)" - ] - }, - { - "cell_type": "markdown", - "id": "cbade5f0", - "metadata": {}, - "source": [ - "## Question Answering " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc6475f9", - "metadata": {}, - "outputs": [], - "source": [ - "qa_retriever = vector_search.as_retriever(\n", - " search_type=\"similarity\",\n", - " search_kwargs={\"k\": 25},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e13e96c", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_core.prompts import PromptTemplate\n", - "\n", - "prompt_template = \"\"\"Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.\n", - "\n", - "{context}\n", - "\n", - "Question: {question}\n", - "\"\"\"\n", - "PROMPT = PromptTemplate(\n", - " template=prompt_template, input_variables=[\"context\", \"question\"]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ff0edb02", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain.chains import RetrievalQA\n", - "from langchain_openai import OpenAI\n", - "\n", - "qa = RetrievalQA.from_chain_type(\n", - " llm=OpenAI(),\n", - " chain_type=\"stuff\",\n", - " retriever=qa_retriever,\n", - " return_source_documents=True,\n", - " chain_type_kwargs={\"prompt\": PROMPT},\n", - ")\n", - "\n", - "docs = qa({\"query\": \"gpt-4 compute requirements\"})\n", - "\n", - "print(docs[\"result\"])\n", - "print(docs[\"source_documents\"])" - ] - }, - { - "cell_type": "markdown", - "id": "61636bb2", - "metadata": {}, - "source": [ - "GPT-4 requires significantly more compute than earlier GPT models. On a dataset derived from OpenAI's internal codebase, GPT-4 requires 100p (petaflops) of compute to reach the lowest loss, while the smaller models require 1-10n (nanoflops)." + "rag_chain.invoke(\"What is LangGraph used for?\")" ] }, { @@ -460,6 +532,16 @@ ">* The langchain version 0.0.305 ([release notes](https://github.com/langchain-ai/langchain/releases/tag/v0.0.305)) introduces the support for $vectorSearch MQL stage, which is available with MongoDB Atlas 6.0.11 and 7.0.2. Users utilizing earlier versions of MongoDB Atlas need to pin their LangChain version to <=0.0.304\n", "> " ] + }, + { + "cell_type": "markdown", + "id": "186ef502", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `MongoDBAtlasVectorSearch` features and configurations head to the API reference: https://api.python.langchain.com/en/latest/mongodb_api_reference.html" + ] } ], "metadata": { @@ -478,7 +560,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/pgvector.ipynb b/docs/docs/integrations/vectorstores/pgvector.ipynb index 0a83ccb219b..d88add2147c 100644 --- a/docs/docs/integrations/vectorstores/pgvector.ipynb +++ b/docs/docs/integrations/vectorstores/pgvector.ipynb @@ -11,12 +11,6 @@ "\n", "The code lives in an integration package called: [langchain_postgres](https://github.com/langchain-ai/langchain-postgres/).\n", "\n", - "You can run the following command to spin up a a postgres container with the `pgvector` extension:\n", - "\n", - "```shell\n", - "docker run --name pgvector-container -e POSTGRES_USER=langchain -e POSTGRES_PASSWORD=langchain -e POSTGRES_DB=langchain -p 6024:5432 -d pgvector/pgvector:pg16\n", - "```\n", - "\n", "## Status\n", "\n", "This code has been ported over from `langchain_community` into a dedicated package called `langchain-postgres`. The following changes have been made:\n", @@ -27,30 +21,39 @@ "\n", "\n", "Currently, there is **no mechanism** that supports easy data migration on schema changes. So any schema changes in the vectorstore will require the user to recreate the tables and re-add the documents.\n", - "If this is a concern, please use a different vectorstore. If not, this implementation should be fine for your use case." - ] - }, - { - "cell_type": "markdown", - "id": "342cd5e9-f349-42b4-9713-12e63779835b", - "metadata": {}, - "source": [ - "## Install dependencies\n", + "If this is a concern, please use a different vectorstore. If not, this implementation should be fine for your use case.\n", "\n", - "Here, we're using `langchain_cohere` for embeddings, but you can use other embeddings providers." + "## Setup\n", + "\n", + "First donwload the partner package:" ] }, { "cell_type": "code", - "execution_count": 1, - "id": "42d42297-11b8-44e3-bf21-7c3d1bce8277", - "metadata": { - "tags": [] - }, + "execution_count": null, + "id": "92df32f0", + "metadata": {}, "outputs": [], "source": [ - "!pip install --quiet -U langchain_cohere\n", - "!pip install --quiet -U langchain_postgres" + "pip install -qU langchain_postgres" + ] + }, + { + "cell_type": "markdown", + "id": "0dd87fcc", + "metadata": {}, + "source": [ + "You can run the following command to spin up a a postgres container with the `pgvector` extension:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2acbaf9b", + "metadata": {}, + "outputs": [], + "source": [ + "%docker run --name pgvector-container -e POSTGRES_USER=langchain -e POSTGRES_PASSWORD=langchain -e POSTGRES_DB=langchain -p 6024:5432 -d pgvector/pgvector:pg16" ] }, { @@ -58,7 +61,56 @@ "id": "eee31ce1-2c28-484d-82be-d22d9f9a31fd", "metadata": {}, "source": [ - "## Initialize the vectorstore" + "### Credentials\n", + "\n", + "There are no credentials needed to run this notebook, just make sure you downloaded the `langchain_postgres` package and correctly started the postgres container." + ] + }, + { + "cell_type": "markdown", + "id": "fa4026f7", + "metadata": {}, + "source": [ + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f8e2f23", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" + ] + }, + { + "cell_type": "markdown", + "id": "ec44dfcc", + "metadata": {}, + "source": [ + "## Instantiation\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "94f5c129", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" ] }, { @@ -70,7 +122,6 @@ }, "outputs": [], "source": [ - "from langchain_cohere import CohereEmbeddings\n", "from langchain_core.documents import Document\n", "from langchain_postgres import PGVector\n", "from langchain_postgres.vectorstores import PGVector\n", @@ -78,9 +129,9 @@ "# See docker command above to launch a postgres instance with pgvector enabled.\n", "connection = \"postgresql+psycopg://langchain:langchain@localhost:6024/langchain\" # Uses psycopg3!\n", "collection_name = \"my_docs\"\n", - "embeddings = CohereEmbeddings(model=\"embed-english-v3.0\")\n", "\n", - "vectorstore = PGVector(\n", + "\n", + "vector_store = PGVector(\n", " embeddings=embeddings,\n", " collection_name=collection_name,\n", " connection=connection,\n", @@ -88,95 +139,22 @@ ")" ] }, - { - "cell_type": "markdown", - "id": "0fc32168-5a82-4629-a78d-158fe2615086", - "metadata": {}, - "source": [ - "## Drop tables\n", - "\n", - "If you need to drop tables (e.g., updating the embedding to a different dimension or just updating the embedding provider): " - ] - }, - { - "cell_type": "markdown", - "id": "5de5ef98-7dbb-4892-853f-47c7dc87b70e", - "metadata": { - "tags": [] - }, - "source": [ - "```python\n", - "vectorstore.drop_tables()\n", - "````" - ] - }, { "cell_type": "markdown", "id": "61a224a1-d70b-4daf-86ba-ab6e43c08b50", "metadata": {}, "source": [ - "## Add documents\n", + "## Manage vector store\n", "\n", - "Add documents to the vectorstore" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "88a288cc-ffd4-4800-b011-750c72b9fd10", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "docs = [\n", - " Document(\n", - " page_content=\"there are cats in the pond\",\n", - " metadata={\"id\": 1, \"location\": \"pond\", \"topic\": \"animals\"},\n", - " ),\n", - " Document(\n", - " page_content=\"ducks are also found in the pond\",\n", - " metadata={\"id\": 2, \"location\": \"pond\", \"topic\": \"animals\"},\n", - " ),\n", - " Document(\n", - " page_content=\"fresh apples are available at the market\",\n", - " metadata={\"id\": 3, \"location\": \"market\", \"topic\": \"food\"},\n", - " ),\n", - " Document(\n", - " page_content=\"the market also sells fresh oranges\",\n", - " metadata={\"id\": 4, \"location\": \"market\", \"topic\": \"food\"},\n", - " ),\n", - " Document(\n", - " page_content=\"the new art exhibit is fascinating\",\n", - " metadata={\"id\": 5, \"location\": \"museum\", \"topic\": \"art\"},\n", - " ),\n", - " Document(\n", - " page_content=\"a sculpture exhibit is also at the museum\",\n", - " metadata={\"id\": 6, \"location\": \"museum\", \"topic\": \"art\"},\n", - " ),\n", - " Document(\n", - " page_content=\"a new coffee shop opened on Main Street\",\n", - " metadata={\"id\": 7, \"location\": \"Main Street\", \"topic\": \"food\"},\n", - " ),\n", - " Document(\n", - " page_content=\"the book club meets at the library\",\n", - " metadata={\"id\": 8, \"location\": \"library\", \"topic\": \"reading\"},\n", - " ),\n", - " Document(\n", - " page_content=\"the library hosts a weekly story time for kids\",\n", - " metadata={\"id\": 9, \"location\": \"library\", \"topic\": \"reading\"},\n", - " ),\n", - " Document(\n", - " page_content=\"a cooking class for beginners is offered at the community center\",\n", - " metadata={\"id\": 10, \"location\": \"community center\", \"topic\": \"classes\"},\n", - " ),\n", - "]" + "### Add items to vector store\n", + "\n", + "Note that adding documents by ID will over-write any existing documents that match that ID." ] }, { "cell_type": "code", "execution_count": 6, - "id": "73aa9124-9d49-4e10-8ed3-82255e7a4106", + "id": "88a288cc-ffd4-4800-b011-750c72b9fd10", "metadata": { "tags": [] }, @@ -192,58 +170,6 @@ "output_type": "execute_result" } ], - "source": [ - "vectorstore.add_documents(docs, ids=[doc.metadata[\"id\"] for doc in docs])" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a5b2b71f-49eb-407d-b03a-dea4c0a517d6", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the book club meets at the library', metadata={'id': 8, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the market also sells fresh oranges', metadata={'id': 4, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a cooking class for beginners is offered at the community center', metadata={'id': 10, 'topic': 'classes', 'location': 'community center'}),\n", - " Document(page_content='fresh apples are available at the market', metadata={'id': 3, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a sculpture exhibit is also at the museum', metadata={'id': 6, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='a new coffee shop opened on Main Street', metadata={'id': 7, 'topic': 'food', 'location': 'Main Street'})]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "vectorstore.similarity_search(\"kitty\", k=10)" - ] - }, - { - "cell_type": "markdown", - "id": "1d87a413-015a-4b46-a64e-332f30806524", - "metadata": {}, - "source": [ - "Adding documents by ID will over-write any existing documents that match that ID." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "13c69357-aaee-4de0-bcc2-7ab4419c920e", - "metadata": { - "tags": [] - }, - "outputs": [], "source": [ "docs = [\n", " Document(\n", @@ -286,7 +212,29 @@ " page_content=\"a cooking class for beginners is offered at the community center\",\n", " metadata={\"id\": 10, \"location\": \"community center\", \"topic\": \"classes\"},\n", " ),\n", - "]" + "]\n", + "\n", + "vector_store.add_documents(docs, ids=[doc.metadata[\"id\"] for doc in docs])" + ] + }, + { + "cell_type": "markdown", + "id": "0c712fa3", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a5b2b71f-49eb-407d-b03a-dea4c0a517d6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "vector_store.delete(ids=[\"3\"])" ] }, { @@ -294,7 +242,11 @@ "id": "59f82250-7903-4279-8300-062542c83416", "metadata": {}, "source": [ - "## Filtering Support\n", + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Filtering Support\n", "\n", "The vectorstore supports a set of filters that can be applied against the metadata fields of the documents.\n", "\n", @@ -312,33 +264,38 @@ "| \\$like | Text (like) |\n", "| \\$ilike | Text (case-insensitive like) |\n", "| \\$and | Logical (and) |\n", - "| \\$or | Logical (or) |" + "| \\$or | Logical (or) |\n", + "\n", + "### Query directly\n", + "\n", + "Performing a simple similarity search can be done as follows:" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "id": "f15a2359-6dc3-4099-8214-785f167a9ca4", "metadata": { "tags": [] }, "outputs": [ { - "data": { - "text/plain": [ - "[Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'})]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "* there are cats in the pond [{'id': 1, 'topic': 'animals', 'location': 'pond'}]\n", + "* the library hosts a weekly story time for kids [{'id': 9, 'topic': 'reading', 'location': 'library'}]\n", + "* ducks are also found in the pond [{'id': 2, 'topic': 'animals', 'location': 'pond'}]\n", + "* the new art exhibit is fascinating [{'id': 5, 'topic': 'art', 'location': 'museum'}]\n" + ] } ], "source": [ - "vectorstore.similarity_search(\"kitty\", k=10, filter={\"id\": {\"$in\": [1, 5, 2, 9]}})" + "results = vector_store.similarity_search(\n", + " \"kitty\", k=10, filter={\"id\": {\"$in\": [1, 5, 2, 9]}}\n", + ")\n", + "for doc in results:\n", + " print(f\"* {doc.page_content} [{doc.metadata}]\")" ] }, { @@ -351,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 16, "id": "88f919e4-e4b0-4b5f-99b3-24c675c26d33", "metadata": { "tags": [] @@ -360,17 +317,17 @@ { "data": { "text/plain": [ - "[Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'})]" + "[Document(metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}, page_content='there are cats in the pond'),\n", + " Document(metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}, page_content='ducks are also found in the pond')]" ] }, - "execution_count": 10, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vectorstore.similarity_search(\n", + "vector_store.similarity_search(\n", " \"ducks\",\n", " k=10,\n", " filter={\"id\": {\"$in\": [1, 5, 2, 9]}, \"location\": {\"$in\": [\"pond\", \"market\"]}},\n", @@ -379,7 +336,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 17, "id": "88f423a4-6575-4fb8-9be2-a3da01106591", "metadata": { "tags": [] @@ -388,17 +345,17 @@ { "data": { "text/plain": [ - "[Document(page_content='ducks are also found in the pond', metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}),\n", - " Document(page_content='there are cats in the pond', metadata={'id': 1, 'topic': 'animals', 'location': 'pond'})]" + "[Document(metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}, page_content='there are cats in the pond'),\n", + " Document(metadata={'id': 2, 'topic': 'animals', 'location': 'pond'}, page_content='ducks are also found in the pond')]" ] }, - "execution_count": 11, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vectorstore.similarity_search(\n", + "vector_store.similarity_search(\n", " \"ducks\",\n", " k=10,\n", " filter={\n", @@ -410,34 +367,145 @@ ")" ] }, + { + "cell_type": "markdown", + "id": "2e65adc1", + "metadata": {}, + "source": [ + "If you want to execute a similarity search and receive the corresponding scores you can run:" + ] + }, { "cell_type": "code", - "execution_count": 12, - "id": "65133340-2acd-4957-849e-029b6b5d60f0", - "metadata": { - "tags": [] - }, + "execution_count": 18, + "id": "7d92e7b3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.763449] there are cats in the pond [{'id': 1, 'topic': 'animals', 'location': 'pond'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(query=\"cats\", k=1)\n", + "for doc, score in results:\n", + " print(f\"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "8d40db8c", + "metadata": {}, + "source": [ + "For a full list of the different searches you can execute on a `PGVector` vector store, please refer to the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_postgres.vectorstores.PGVector.html).\n", + "\n", + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "7cd1fb75", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Document(page_content='the book club meets at the library', metadata={'id': 8, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='the new art exhibit is fascinating', metadata={'id': 5, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='the library hosts a weekly story time for kids', metadata={'id': 9, 'topic': 'reading', 'location': 'library'}),\n", - " Document(page_content='a sculpture exhibit is also at the museum', metadata={'id': 6, 'topic': 'art', 'location': 'museum'}),\n", - " Document(page_content='the market also sells fresh oranges', metadata={'id': 4, 'topic': 'food', 'location': 'market'}),\n", - " Document(page_content='a cooking class for beginners is offered at the community center', metadata={'id': 10, 'topic': 'classes', 'location': 'community center'}),\n", - " Document(page_content='a new coffee shop opened on Main Street', metadata={'id': 7, 'topic': 'food', 'location': 'Main Street'}),\n", - " Document(page_content='fresh apples are available at the market', metadata={'id': 3, 'topic': 'food', 'location': 'market'})]" + "[Document(metadata={'id': 1, 'topic': 'animals', 'location': 'pond'}, page_content='there are cats in the pond')]" ] }, - "execution_count": 12, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vectorstore.similarity_search(\"bird\", k=10, filter={\"location\": {\"$ne\": \"pond\"}})" + "retriever = vector_store.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 1})\n", + "retriever.invoke(\"kitty\")" + ] + }, + { + "cell_type": "markdown", + "id": "7ecd77a0", + "metadata": {}, + "source": [ + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f0b14168", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a4eba12c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'There are cats in the pond right now.'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"Who is at the pond right now?\")" + ] + }, + { + "cell_type": "markdown", + "id": "f451f361", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all __ModuleName__VectorStore features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_postgres.vectorstores.PGVector.html" ] } ], @@ -457,7 +525,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/pinecone.ipynb b/docs/docs/integrations/vectorstores/pinecone.ipynb index 8efd1a4d0d5..66416950652 100644 --- a/docs/docs/integrations/vectorstores/pinecone.ipynb +++ b/docs/docs/integrations/vectorstores/pinecone.ipynb @@ -12,8 +12,9 @@ "\n", "This notebook shows how to use functionality related to the `Pinecone` vector database.\n", "\n", - "Set the following environment variables to follow along in this doc:\n", - "- `OPENAI_API_KEY`: Your OpenAI API key, for using `OpenAIEmbeddings`" + "## Setup\n", + "\n", + "To use the `PineconeVectorStore` you first need to install the partner package, as well as the other packages used throughout this notebook." ] }, { @@ -25,12 +26,7 @@ }, "outputs": [], "source": [ - "%pip install --upgrade --quiet \\\n", - " langchain-pinecone \\\n", - " langchain-openai \\\n", - " langchain \\\n", - " langchain-community \\\n", - " pinecone-notebooks" + "%pip install -qU langchain-pinecone pinecone-notebooks" ] }, { @@ -43,76 +39,52 @@ }, { "cell_type": "markdown", - "id": "42f2ea67", + "id": "ef6dc4de", "metadata": {}, "source": [ - "First, let's split our state of the union document into chunked `docs`." + "### Credentials\n", + "\n", + "Create a new Pinecone account, or sign into your existing one, and create an API key to use in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "eb554814", + "metadata": {}, + "outputs": [], + "source": [ + "import getpass\n", + "import os\n", + "import time\n", + "\n", + "from pinecone import Pinecone, ServerlessSpec\n", + "\n", + "if not os.getenv(\"PINECONE_API_KEY\"):\n", + " os.environ[\"PINECONE_API_KEY\"] = getpass.getpass(\"Enter your Pinecone API key: \")\n", + "\n", + "pinecone_api_key = os.environ.get(\"PINECONE_API_KEY\")\n", + "\n", + "pc = Pinecone(api_key=pinecone_api_key)" + ] + }, + { + "cell_type": "markdown", + "id": "6ef1d828", + "metadata": {}, + "source": [ + "If you want to get automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" ] }, { "cell_type": "code", "execution_count": 5, - "id": "a3c3999a", + "id": "23b5ac5e", "metadata": {}, "outputs": [], "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_text_splitters import CharacterTextSplitter\n", - "\n", - "loader = TextLoader(\"../../how_to/state_of_the_union.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", - "\n", - "embeddings = OpenAIEmbeddings()" - ] - }, - { - "cell_type": "markdown", - "id": "ef6dc4de", - "metadata": {}, - "source": [ - "Now let's create a new Pinecone account, or sign into your existing one, and create an API key to use in this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1fdc3c36", - "metadata": {}, - "outputs": [], - "source": [ - "from pinecone_notebooks.colab import Authenticate\n", - "\n", - "Authenticate()" - ] - }, - { - "cell_type": "markdown", - "id": "54da1a39", - "metadata": {}, - "source": [ - "The newly created API key has been stored in the `PINECONE_API_KEY` environment variable. We will use it to setup the Pinecone client." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eb554814", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "\n", - "pinecone_api_key = os.environ.get(\"PINECONE_API_KEY\")\n", - "pinecone_api_key\n", - "\n", - "import time\n", - "\n", - "from pinecone import Pinecone, ServerlessSpec\n", - "\n", - "pc = Pinecone(api_key=pinecone_api_key)" + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" ] }, { @@ -120,26 +92,28 @@ "id": "658706a3", "metadata": {}, "source": [ - "Next, let's connect to your Pinecone index. If one named `index_name` doesn't exist, it will be created." + "## Initialization\n", + "\n", + "Before initializing our vector store, let's connect to a Pinecone index. If one named `index_name` doesn't exist, it will be created." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "276a06dd", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", - "index_name = \"langchain-index\" # change if desired\n", + "index_name = \"langchain-test-index\" # change if desired\n", "\n", "existing_indexes = [index_info[\"name\"] for index_info in pc.list_indexes()]\n", "\n", "if index_name not in existing_indexes:\n", " pc.create_index(\n", " name=index_name,\n", - " dimension=1536,\n", + " dimension=3072,\n", " metric=\"cosine\",\n", " spec=ServerlessSpec(cloud=\"aws\", region=\"us-east-1\"),\n", " )\n", @@ -154,24 +128,188 @@ "id": "3a4d377f", "metadata": {}, "source": [ - "Now that our Pinecone index is setup, we can upsert those chunked docs as contents with `PineconeVectorStore.from_documents`." + "Now that our Pinecone index is setup, we can initialize our vector store. \n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 13, + "id": "1485db56", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "id": "6e104aee", "metadata": {}, "outputs": [], "source": [ "from langchain_pinecone import PineconeVectorStore\n", "\n", - "docsearch = PineconeVectorStore.from_documents(docs, embeddings, index_name=index_name)" + "vector_store = PineconeVectorStore(index=index, embedding=embeddings)" + ] + }, + { + "cell_type": "markdown", + "id": "48721e29", + "metadata": {}, + "source": [ + "## Manage vector store\n", + "\n", + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 15, + "id": "70e688f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['167b8681-5974-467f-adcb-6e987a18df01',\n", + " 'd16010fd-41f8-4d49-9c22-c66d5555a3fe',\n", + " 'ffcacfb3-2bc2-44c3-a039-c2256a905c0e',\n", + " 'cf3bfc9f-5dc7-4f5e-bb41-edb957394126',\n", + " 'e99b07eb-fdff-4cb9-baa8-619fd8efeed3',\n", + " '68c93033-a24f-40bd-8492-92fa26b631a4',\n", + " 'b27a4ecb-b505-4c5d-89ff-526e3d103558',\n", + " '4868a9e6-e6fb-4079-b400-4a1dfbf0d4c4',\n", + " '921c0e9c-0550-4eb5-9a6c-ed44410788b2',\n", + " 'c446fc23-64e8-47e7-8c19-ecf985e9411e']" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "id": "120922b3", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "5b8437cd", + "metadata": {}, + "outputs": [], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "id": "5ee21c89", + "metadata": {}, + "source": [ + "## Query vector store\n", + "\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", + "\n", + "### Query directly\n", + "\n", + "Performing a simple similarity search can be done as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, "id": "ffbcb3fb", "metadata": {}, "outputs": [ @@ -179,214 +317,169 @@ "name": "stdout", "output_type": "stream", "text": [ - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n" + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet'}]\n" ] } ], "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "docs = docsearch.similarity_search(query)\n", - "print(docs[0].page_content)" + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\",\n", + " k=2,\n", + " filter={\"source\": \"tweet\"},\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "86a4b96b", + "id": "79f3494d", "metadata": {}, "source": [ - "### Adding More Text to an Existing Index\n", + "#### Similarity search with score\n", "\n", - "More text can embedded and upserted to an existing Pinecone index using the `add_texts` function\n" + "You can also search with score:" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "38a7a60e", + "execution_count": 18, + "id": "5fb24583", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.553187] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'source': 'news'}]\n" + ] + } + ], + "source": [ + "results = vector_store.similarity_search_with_score(\n", + " \"Will it be hot tomorrow?\", k=1, filter={\"source\": \"news\"}\n", + ")\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "id": "1855941b", + "metadata": {}, + "source": [ + "#### Other search methods\n", + "\n", + "There are more search methods (such as MMR) not listed in this notebook, to find all of them be sure to read the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_pinecone.vectorstores.PineconeVectorStore.html).\n", + "\n", + "### Query by turning into retriever\n", + "\n", + "You can also transform the vector store into a retriever for easier usage in your chains." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "78140e87", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['24631802-4bad-44a7-a4ba-fd71f00cc160']" + "[Document(metadata={'source': 'news'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" ] }, - "execution_count": 8, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "vectorstore = PineconeVectorStore(index_name=index_name, embedding=embeddings)\n", - "\n", - "vectorstore.add_texts([\"More text!\"])" + "retriever = vector_store.as_retriever(\n", + " search_type=\"similarity_score_threshold\",\n", + " search_kwargs={\"k\": 1, \"score_threshold\": 0.5},\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\", filter={\"source\": \"news\"})" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "d46d1452", + "id": "72990cb5", "metadata": {}, "source": [ - "### Maximal Marginal Relevance Searches\n", + "## Chain usage\n", "\n", - "In addition to using similarity search in the retriever object, you can also use `mmr` as retriever.\n" + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "a359ed74", + "execution_count": 20, + "id": "f12560cb", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "## Document 0\n", - "\n", - "Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence.\n", - "\n", - "## Document 1\n", - "\n", - "And I’m taking robust action to make sure the pain of our sanctions is targeted at Russia’s economy. And I will use every tool at our disposal to protect American businesses and consumers. \n", - "\n", - "Tonight, I can announce that the United States has worked with 30 other countries to release 60 Million barrels of oil from reserves around the world. \n", - "\n", - "America will lead that effort, releasing 30 Million barrels from our own Strategic Petroleum Reserve. And we stand ready to do more if necessary, unified with our allies. \n", - "\n", - "These steps will help blunt gas prices here at home. And I know the news about what’s happening can seem alarming. \n", - "\n", - "But I want you to know that we are going to be okay. \n", - "\n", - "When the history of this era is written Putin’s war on Ukraine will have left Russia weaker and the rest of the world stronger. \n", - "\n", - "While it shouldn’t have taken something so terrible for people around the world to see what’s at stake now everyone sees it clearly.\n", - "\n", - "## Document 2\n", - "\n", - "We can’t change how divided we’ve been. But we can change how we move forward—on COVID-19 and other issues we must face together. \n", - "\n", - "I recently visited the New York City Police Department days after the funerals of Officer Wilbert Mora and his partner, Officer Jason Rivera. \n", - "\n", - "They were responding to a 9-1-1 call when a man shot and killed them with a stolen gun. \n", - "\n", - "Officer Mora was 27 years old. \n", - "\n", - "Officer Rivera was 22. \n", - "\n", - "Both Dominican Americans who’d grown up on the same streets they later chose to patrol as police officers. \n", - "\n", - "I spoke with their families and told them that we are forever in debt for their sacrifice, and we will carry on their mission to restore the trust and safety every community deserves. \n", - "\n", - "I’ve worked on these issues a long time. \n", - "\n", - "I know what works: Investing in crime prevention and community police officers who’ll walk the beat, who’ll know the neighborhood, and who can restore trust and safety.\n", - "\n", - "## Document 3\n", - "\n", - "One was stationed at bases and breathing in toxic smoke from “burn pits” that incinerated wastes of war—medical and hazard material, jet fuel, and more. \n", - "\n", - "When they came home, many of the world’s fittest and best trained warriors were never the same. \n", - "\n", - "Headaches. Numbness. Dizziness. \n", - "\n", - "A cancer that would put them in a flag-draped coffin. \n", - "\n", - "I know. \n", - "\n", - "One of those soldiers was my son Major Beau Biden. \n", - "\n", - "We don’t know for sure if a burn pit was the cause of his brain cancer, or the diseases of so many of our troops. \n", - "\n", - "But I’m committed to finding out everything we can. \n", - "\n", - "Committed to military families like Danielle Robinson from Ohio. \n", - "\n", - "The widow of Sergeant First Class Heath Robinson. \n", - "\n", - "He was born a soldier. Army National Guard. Combat medic in Kosovo and Iraq. \n", - "\n", - "Stationed near Baghdad, just yards from burn pits the size of football fields. \n", - "\n", - "Heath’s widow Danielle is here with us tonight. They loved going to Ohio State football games. He loved building Legos with their daughter.\n" - ] - } - ], + "outputs": [], "source": [ - "retriever = docsearch.as_retriever(search_type=\"mmr\")\n", - "matched_docs = retriever.invoke(query)\n", - "for i, d in enumerate(matched_docs):\n", - " print(f\"\\n## Document {i}\\n\")\n", - " print(d.page_content)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "7c477287", - "metadata": {}, - "source": [ - "Or use `max_marginal_relevance_search` directly:" + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "9ca82740", + "execution_count": 21, + "id": "262651fc", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "1. Tonight. I call on the Senate to: Pass the Freedom to Vote Act. Pass the John Lewis Voting Rights Act. And while you’re at it, pass the Disclose Act so Americans can know who is funding our elections. \n", - "\n", - "Tonight, I’d like to honor someone who has dedicated his life to serve this country: Justice Stephen Breyer—an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court. Justice Breyer, thank you for your service. \n", - "\n", - "One of the most serious constitutional responsibilities a President has is nominating someone to serve on the United States Supreme Court. \n", - "\n", - "And I did that 4 days ago, when I nominated Circuit Court of Appeals Judge Ketanji Brown Jackson. One of our nation’s top legal minds, who will continue Justice Breyer’s legacy of excellence. \n", - "\n", - "2. We can’t change how divided we’ve been. But we can change how we move forward—on COVID-19 and other issues we must face together. \n", - "\n", - "I recently visited the New York City Police Department days after the funerals of Officer Wilbert Mora and his partner, Officer Jason Rivera. \n", - "\n", - "They were responding to a 9-1-1 call when a man shot and killed them with a stolen gun. \n", - "\n", - "Officer Mora was 27 years old. \n", - "\n", - "Officer Rivera was 22. \n", - "\n", - "Both Dominican Americans who’d grown up on the same streets they later chose to patrol as police officers. \n", - "\n", - "I spoke with their families and told them that we are forever in debt for their sacrifice, and we will carry on their mission to restore the trust and safety every community deserves. \n", - "\n", - "I’ve worked on these issues a long time. \n", - "\n", - "I know what works: Investing in crime prevention and community police officers who’ll walk the beat, who’ll know the neighborhood, and who can restore trust and safety. \n", - "\n" - ] + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of these types of applications.'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "found_docs = docsearch.max_marginal_relevance_search(query, k=2, fetch_k=10)\n", - "for i, doc in enumerate(found_docs):\n", - " print(f\"{i + 1}.\", doc.page_content, \"\\n\")" + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" + ] + }, + { + "cell_type": "markdown", + "id": "0d5722bc", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all __ModuleName__VectorStore features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_pinecone.vectorstores.PineconeVectorStore.html" ] } ], @@ -406,7 +499,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/qdrant.ipynb b/docs/docs/integrations/vectorstores/qdrant.ipynb index e6d7ba00ba7..74edf0d9dc4 100644 --- a/docs/docs/integrations/vectorstores/qdrant.ipynb +++ b/docs/docs/integrations/vectorstores/qdrant.ipynb @@ -14,6 +14,9 @@ "\n", "> This page documents the `QdrantVectorStore` class that supports multiple retrieval modes via Qdrant's new [Query API](https://qdrant.tech/blog/qdrant-1.10.x/). It requires you to run Qdrant v1.10.0 or above.\n", "\n", + "\n", + "## Setup\n", + "\n", "There are various modes of how to run `Qdrant`, and depending on the chosen one, there will be some subtle differences. The options include:\n", "- Local mode, no server required\n", "- Docker deployments\n", @@ -31,56 +34,30 @@ }, "outputs": [], "source": [ - "%pip install langchain-qdrant langchain-openai langchain" + "%pip install -qU langchain-qdrant 'qdrant-client[fastembed]'" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "7b2f111b-357a-4f42-9730-ef0603bdc1b5", + "id": "7d387fea", "metadata": {}, "source": [ - "We will use `OpenAIEmbeddings` for demonstration." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "aac9563e", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:22.282884Z", - "start_time": "2023-04-04T10:51:21.408077Z" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "from langchain_community.document_loaders import TextLoader\n", - "from langchain_openai import OpenAIEmbeddings\n", - "from langchain_qdrant import QdrantVectorStore\n", - "from langchain_text_splitters import CharacterTextSplitter" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "a3c3999a", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:22.520144Z", - "start_time": "2023-04-04T10:51:22.285826Z" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "loader = TextLoader(\"some-file.txt\")\n", - "documents = loader.load()\n", - "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n", - "docs = text_splitter.split_documents(documents)\n", + "### Credentials\n", "\n", - "embeddings = OpenAIEmbeddings()" + "There are no credentials needed to run the code in this notebook.\n", + "\n", + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4912937d", + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" ] }, { @@ -89,7 +66,7 @@ "id": "eeead681", "metadata": {}, "source": [ - "## Connecting to Qdrant from LangChain\n", + "## Initialization\n", "\n", "### Local mode\n", "\n", @@ -97,12 +74,33 @@ "\n", "#### In-memory\n", "\n", - "For some testing scenarios and quick experiments, you may prefer to keep all the data in memory only, so it gets lost when the client is destroyed - usually at the end of your script/notebook." + "For some testing scenarios and quick experiments, you may prefer to keep all the data in memory only, so it gets lost when the client is destroyed - usually at the end of your script/notebook.\n", + "\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, + "id": "1df86797", + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, "id": "8429667e", "metadata": { "ExecuteTime": { @@ -113,11 +111,21 @@ }, "outputs": [], "source": [ - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " embeddings,\n", - " location=\":memory:\", # Local mode with in-memory storage only\n", - " collection_name=\"my_documents\",\n", + "from langchain_qdrant import QdrantVectorStore\n", + "from qdrant_client import QdrantClient\n", + "from qdrant_client.http.models import Distance, VectorParams\n", + "\n", + "client = QdrantClient(\":memory:\")\n", + "\n", + "client.create_collection(\n", + " collection_name=\"demo_collection\",\n", + " vectors_config=VectorParams(size=3072, distance=Distance.COSINE),\n", + ")\n", + "\n", + "vector_store = QdrantVectorStore(\n", + " client=client,\n", + " collection_name=\"demo_collection\",\n", + " embedding=embeddings,\n", ")" ] }, @@ -134,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "24b370e2", "metadata": { "ExecuteTime": { @@ -145,11 +153,17 @@ }, "outputs": [], "source": [ - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " embeddings,\n", - " path=\"/tmp/local_qdrant\",\n", - " collection_name=\"my_documents\",\n", + "client = QdrantClient(path=\"/tmp/langchain_qdrant\")\n", + "\n", + "client.create_collection(\n", + " collection_name=\"demo_collection\",\n", + " vectors_config=VectorParams(size=3072, distance=Distance.COSINE),\n", + ")\n", + "\n", + "vector_store = QdrantVectorStore(\n", + " client=client,\n", + " collection_name=\"demo_collection\",\n", + " embedding=embeddings,\n", ")" ] }, @@ -177,6 +191,7 @@ "outputs": [], "source": [ "url = \"<---qdrant url here --->\"\n", + "docs = [] # put docs here\n", "qdrant = QdrantVectorStore.from_documents(\n", " docs,\n", " embeddings,\n", @@ -252,37 +267,144 @@ ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "93540013", + "id": "3cddef6e", "metadata": {}, "source": [ - "## Recreating the collection\n", + "## Manage vector store\n", "\n", - "The collection is reused if it already exists. Setting `force_recreate` to `True` allows to remove the old collection and start from scratch." + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", "execution_count": 8, - "id": "30a87570", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:24.854117Z", - "start_time": "2023-04-04T10:51:24.845385Z" + "id": "7697a362", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['c04134c3-273d-4766-949a-eee46052ad32',\n", + " '9e6ba50c-794f-4b88-94e5-411f15052a02',\n", + " 'd3202666-6f2b-4186-ac43-e35389de8166',\n", + " '50d8d6ee-69bf-4173-a6a2-b254e9928965',\n", + " 'bd2eae02-74b5-43ec-9fcf-09e9d9db6fd3',\n", + " '6dae6b37-826d-4f14-8376-da4603b35de3',\n", + " 'b0964ab5-5a14-47b4-a983-37fa5c5bd154',\n", + " '91ed6c56-fe53-49e2-8199-c3bb3c33c3eb',\n", + " '42a580cb-7469-4324-9927-0febab57ce92',\n", + " 'ff774e5c-f158-4d12-94e2-0a0162b22f27']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" } - }, - "outputs": [], + ], "source": [ - "url = \"<---qdrant url here --->\"\n", - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " embeddings,\n", - " url=url,\n", - " prefer_grpc=True,\n", - " collection_name=\"my_documents\",\n", - " force_recreate=True,\n", - ")" + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "id": "5fd23102", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "999cafcc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" ] }, { @@ -296,33 +418,18 @@ } }, "source": [ - "## Similarity search\n", + "## Query vector store\n", "\n", - "The simplest scenario for using Qdrant vector store is to perform a similarity search. Under the hood, our query will be encoded into vector embeddings and used to find similar documents in Qdrant collection.\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", "\n", - "`QdrantVectorStore` supports 3 modes for similarity searches. They can be configured using the `retrieval_mode` parameter when setting up the class.\n", + "### Query directly\n", "\n", - "- Dense Vector Search(Default)\n", - "- Sparse Vector Search\n", - "- Hybrid Search" - ] - }, - { - "cell_type": "markdown", - "id": "b3a78d46", - "metadata": {}, - "source": [ - "### Dense Vector Search\n", - "\n", - "To search with only dense vectors,\n", - "\n", - "- The `retrieval_mode` parameter should be set to `RetrievalMode.DENSE`(default).\n", - "- A [dense embeddings](https://python.langchain.com/v0.2/docs/integrations/text_embedding/) value should be provided to the `embedding` parameter." + "The simplest scenario for using Qdrant vector store is to perform a similarity search. Under the hood, our query will be encoded into vector embeddings and used to find similar documents in Qdrant collection." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "a8c513ab", "metadata": { "ExecuteTime": { @@ -331,20 +438,22 @@ }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Building an exciting new project with LangChain - come check it out! [{'source': 'tweet', '_id': 'd3202666-6f2b-4186-ac43-e35389de8166', '_collection_name': 'demo_collection'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'source': 'tweet', '_id': '91ed6c56-fe53-49e2-8199-c3bb3c33c3eb', '_collection_name': 'demo_collection'}]\n" + ] + } + ], "source": [ - "from langchain_qdrant import RetrievalMode\n", - "\n", - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " embedding=embeddings,\n", - " location=\":memory:\",\n", - " collection_name=\"my_documents\",\n", - " retrieval_mode=RetrievalMode.DENSE,\n", + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\", k=2\n", ")\n", - "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.similarity_search(query)" + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" ] }, { @@ -352,6 +461,19 @@ "id": "dbd93d85", "metadata": {}, "source": [ + "`QdrantVectorStore` supports 3 modes for similarity searches. They can be configured using the `retrieval_mode` parameter when setting up the class.\n", + "\n", + "- Dense Vector Search(Default)\n", + "- Sparse Vector Search\n", + "- Hybrid Search\n", + "\n", + "### Dense Vector Search\n", + "\n", + "To search with only dense vectors,\n", + "\n", + "- The `retrieval_mode` parameter should be set to `RetrievalMode.DENSE`(default).\n", + "- A [dense embeddings](https://python.langchain.com/v0.2/docs/integrations/text_embedding/) value should be provided to the `embedding` parameter.\n", + "\n", "### Sparse Vector Search\n", "\n", "To search with only sparse vectors,\n", @@ -361,47 +483,6 @@ "\n", "The `langchain-qdrant` package provides a [FastEmbed](https://github.com/qdrant/fastembed) based implementation out of the box.\n", "\n", - "To use it, install the FastEmbed package." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ceb493a3", - "metadata": {}, - "outputs": [], - "source": [ - "%pip install fastembed" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "052e3412", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_qdrant import FastEmbedSparse, RetrievalMode\n", - "\n", - "sparse_embeddings = FastEmbedSparse(model_name=\"Qdrant/BM25\")\n", - "\n", - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " sparse_embedding=sparse_embeddings,\n", - " location=\":memory:\",\n", - " collection_name=\"my_documents\",\n", - " retrieval_mode=RetrievalMode.SPARSE,\n", - ")\n", - "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.similarity_search(query)" - ] - }, - { - "cell_type": "markdown", - "id": "f4b6c456", - "metadata": {}, - "source": [ "### Hybrid Vector Search\n", "\n", "To perform a hybrid search using dense and sparse vectors with score fusion,\n", @@ -413,45 +494,49 @@ "Note that if you've added documents with the `HYBRID` mode, you can switch to any retrieval mode when searching. Since both the dense and sparse vectors are available in the collection." ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce56f6e9", - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_qdrant import FastEmbedSparse, RetrievalMode\n", - "\n", - "sparse_embeddings = FastEmbedSparse(model_name=\"Qdrant/BM25\")\n", - "\n", - "qdrant = QdrantVectorStore.from_documents(\n", - " docs,\n", - " embedding=embeddings,\n", - " sparse_embedding=sparse_embeddings,\n", - " location=\":memory:\",\n", - " collection_name=\"my_documents\",\n", - " retrieval_mode=RetrievalMode.HYBRID,\n", - ")\n", - "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.similarity_search(query)" - ] - }, { "attachments": {}, "cell_type": "markdown", "id": "1bda9bf5", "metadata": {}, "source": [ - "## Similarity search with score\n", - "\n", - "Sometimes we might want to perform the search, but also obtain a relevancy score to know how good is a particular result. \n", - "The returned distance score is cosine distance. Therefore, a lower score is better." + "If you want to execute a similarity search and receive the corresponding scores you can run:" ] }, { "cell_type": "code", "execution_count": 11, + "id": "cf772328", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "([Record(id='42a580cb-7469-4324-9927-0febab57ce92', payload={'page_content': 'The stock market is down 500 points today due to fears of a recession.', 'metadata': {'source': 'news'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='50d8d6ee-69bf-4173-a6a2-b254e9928965', payload={'page_content': 'Robbers broke into the city bank and stole $1 million in cash.', 'metadata': {'source': 'news'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='6dae6b37-826d-4f14-8376-da4603b35de3', payload={'page_content': 'Is the new iPhone worth the price? Read this review to find out.', 'metadata': {'source': 'website'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='91ed6c56-fe53-49e2-8199-c3bb3c33c3eb', payload={'page_content': 'LangGraph is the best framework for building stateful, agentic applications!', 'metadata': {'source': 'tweet'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='9e6ba50c-794f-4b88-94e5-411f15052a02', payload={'page_content': 'The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.', 'metadata': {'source': 'news'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='b0964ab5-5a14-47b4-a983-37fa5c5bd154', payload={'page_content': 'The top 10 soccer players in the world right now.', 'metadata': {'source': 'website'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='bd2eae02-74b5-43ec-9fcf-09e9d9db6fd3', payload={'page_content': \"Wow! That was an amazing movie. I can't wait to see it again.\", 'metadata': {'source': 'tweet'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='c04134c3-273d-4766-949a-eee46052ad32', payload={'page_content': 'I had chocalate chip pancakes and scrambled eggs for breakfast this morning.', 'metadata': {'source': 'tweet'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='d3202666-6f2b-4186-ac43-e35389de8166', payload={'page_content': 'Building an exciting new project with LangChain - come check it out!', 'metadata': {'source': 'tweet'}}, vector=None, shard_key=None, order_value=None),\n", + " Record(id='ff774e5c-f158-4d12-94e2-0a0162b22f27', payload={'page_content': 'I have a bad feeling I am going to get deleted :(', 'metadata': {'source': 'tweet'}}, vector=None, shard_key=None, order_value=None)],\n", + " None)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.scroll(collection_name=\"demo_collection\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, "id": "8804a21d", "metadata": { "ExecuteTime": { @@ -459,27 +544,21 @@ "start_time": "2023-04-04T10:51:25.227384Z" } }, - "outputs": [], - "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.similarity_search_with_score(query)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "756a6887", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:25.642282Z", - "start_time": "2023-04-04T10:51:25.635947Z" + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* [SIM=0.531834] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'source': 'news', '_id': '9e6ba50c-794f-4b88-94e5-411f15052a02', '_collection_name': 'demo_collection'}]\n" + ] } - }, - "outputs": [], + ], "source": [ - "document, score = found_docs[0]\n", - "print(document.page_content)\n", - "print(f\"\\nScore: {score}\")" + "results = vector_store.similarity_search_with_score(\n", + " query=\"Will it be hot tomorrow\", k=1\n", + ")\n", + "for doc, score in results:\n", + " print(f\"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]\")" ] }, { @@ -488,73 +567,46 @@ "id": "525e3582", "metadata": {}, "source": [ + "For a full list of all the search functions available for a `QdrantVectorStore`, read the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_qdrant.vectorstores.Qdrant.html)\n", + "\n", "### Metadata filtering\n", "\n", "Qdrant has an [extensive filtering system](https://qdrant.tech/documentation/concepts/filtering/) with rich type support. It is also possible to use the filters in Langchain, by passing an additional param to both the `similarity_search_with_score` and `similarity_search` methods." ] }, { - "attachments": {}, - "cell_type": "markdown", - "id": "1c2c58dc", + "cell_type": "code", + "execution_count": 14, + "id": "dc7cffc8", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* The top 10 soccer players in the world right now. [{'source': 'website', '_id': 'b0964ab5-5a14-47b4-a983-37fa5c5bd154', '_collection_name': 'demo_collection'}]\n" + ] + } + ], "source": [ - "```python\n", "from qdrant_client.http import models\n", "\n", - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.similarity_search_with_score(query, filter=models.Filter(...))\n", - "```" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "c58c30bf", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:39:53.032744Z", - "start_time": "2023-04-04T10:39:53.028673Z" - } - }, - "source": [ - "## Maximum marginal relevance search (MMR)\n", - "\n", - "If you'd like to look up some similar documents, but you'd also like to receive diverse results, MMR is the method you should consider. Maximal marginal relevance optimizes for similarity to query AND diversity among selected documents.\n", - "\n", - "Note that MMR search is only available if you've added documents with `DENSE` or `HYBRID` modes. Since it requires dense vectors." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "76810fb6", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:26.010947Z", - "start_time": "2023-04-04T10:51:25.647687Z" - } - }, - "outputs": [], - "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "found_docs = qdrant.max_marginal_relevance_search(query, k=2, fetch_k=10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "80c6db11", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:26.016979Z", - "start_time": "2023-04-04T10:51:26.013329Z" - } - }, - "outputs": [], - "source": [ - "for i, doc in enumerate(found_docs):\n", - " print(f\"{i + 1}.\", doc.page_content, \"\\n\")" + "results = vector_store.similarity_search(\n", + " query=\"Who are the best soccer players in the world?\",\n", + " k=1,\n", + " filter=models.Filter(\n", + " should=[\n", + " models.FieldCondition(\n", + " key=\"page_content\",\n", + " match=models.MatchValue(\n", + " value=\"The top 10 soccer players in the world right now.\"\n", + " ),\n", + " ),\n", + " ]\n", + " ),\n", + ")\n", + "for doc in results:\n", + " print(f\"* {doc.page_content} [{doc.metadata}]\")" ] }, { @@ -563,14 +615,14 @@ "id": "691a82d6", "metadata": {}, "source": [ - "## Qdrant as a Retriever\n", + "### Query by turning into retriever\n", "\n", - "Qdrant, as all the other vector stores, is a LangChain Retriever. " + "You can also transform the vector store into a retriever for easier usage in your chains. " ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "9427195f", "metadata": { "ExecuteTime": { @@ -578,49 +630,90 @@ "start_time": "2023-04-04T10:51:26.018763Z" } }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[Document(metadata={'source': 'news', '_id': '50d8d6ee-69bf-4173-a6a2-b254e9928965', '_collection_name': 'demo_collection'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "retriever = qdrant.as_retriever()" + "retriever = vector_store.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 1})\n", + "retriever.invoke(\"Stealing from the bank is a crime\")" ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "0c851b4f", + "id": "6ac07288", "metadata": {}, "source": [ - "It might be also specified to use MMR as a search strategy, instead of similarity." + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": null, - "id": "64348f1b", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:26.043909Z", - "start_time": "2023-04-04T10:51:26.034284Z" - } - }, + "execution_count": 16, + "id": "07bd9785", + "metadata": {}, "outputs": [], "source": [ - "retriever = qdrant.as_retriever(search_type=\"mmr\")" + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "f3c70c31", - "metadata": { - "ExecuteTime": { - "end_time": "2023-04-04T10:51:26.495652Z", - "start_time": "2023-04-04T10:51:26.046407Z" + "execution_count": 17, + "id": "d97f0c91", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LangGraph is used for building stateful, agentic applications. It provides a framework that facilitates the development of such applications.'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" } - }, - "outputs": [], + ], "source": [ - "query = \"What did the president say about Ketanji Brown Jackson\"\n", - "retriever.invoke(query)[0]" + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", + ")\n", + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" ] }, { @@ -647,10 +740,12 @@ }, "outputs": [], "source": [ + "from langchain_qdrant import RetrievalMode, SparseEmbeddings\n", + "\n", "QdrantVectorStore.from_documents(\n", " docs,\n", " embedding=embeddings,\n", - " sparse_embedding=sparse_embeddings,\n", + " sparse_embedding=SparseEmbeddings(),\n", " location=\":memory:\",\n", " collection_name=\"my_documents_2\",\n", " retrieval_mode=RetrievalMode.HYBRID,\n", @@ -707,12 +802,14 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "2300e785", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `QdrantVectorStore` features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_qdrant.vectorstores.Qdrant.html" + ] } ], "metadata": { @@ -731,7 +828,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/docs/integrations/vectorstores/redis.ipynb b/docs/docs/integrations/vectorstores/redis.ipynb index 4e013528261..d2565beab59 100644 --- a/docs/docs/integrations/vectorstores/redis.ipynb +++ b/docs/docs/integrations/vectorstores/redis.ipynb @@ -122,10 +122,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Setting up\n", - "\n", - "\n", - "### Install Redis Python client\n", + "## Setup\n", "\n", "`Redis-py` is the officially supported client by Redis. Recently released is the `RedisVL` client which is purpose-built for the Vector Database use cases. Both can be installed with pip." ] @@ -138,37 +135,7 @@ }, "outputs": [], "source": [ - "%pip install --upgrade --quiet redis redisvl langchain-openai tiktoken" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We want to use `OpenAIEmbeddings` so we have to get the OpenAI API Key." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import os\n", - "\n", - "os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "embeddings = OpenAIEmbeddings()" + "%pip install -qU redis redisvl langchain-community" ] }, { @@ -182,7 +149,7 @@ "```console\n", "docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest\n", "```\n", - "If things are running correctly you should see a nice Redis UI at `http://localhost:8001`. See the [Deployment options](#deployment-options) section above for other ways to deploy.\n" + "If things are running correctly you should see a nice Redis UI at `http://localhost:8001`. See the [Deployment options](#deployment-options) section above for other ways to deploy." ] }, { @@ -231,57 +198,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Sample data\n", - "\n", - "First we will describe some sample data so that the various attributes of the Redis vector store can be demonstrated." + "If you want to get best in-class automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "metadata = [\n", - " {\n", - " \"user\": \"john\",\n", - " \"age\": 18,\n", - " \"job\": \"engineer\",\n", - " \"credit_score\": \"high\",\n", - " },\n", - " {\n", - " \"user\": \"derrick\",\n", - " \"age\": 45,\n", - " \"job\": \"doctor\",\n", - " \"credit_score\": \"low\",\n", - " },\n", - " {\n", - " \"user\": \"nancy\",\n", - " \"age\": 94,\n", - " \"job\": \"doctor\",\n", - " \"credit_score\": \"high\",\n", - " },\n", - " {\n", - " \"user\": \"tyler\",\n", - " \"age\": 100,\n", - " \"job\": \"engineer\",\n", - " \"credit_score\": \"high\",\n", - " },\n", - " {\n", - " \"user\": \"joe\",\n", - " \"age\": 35,\n", - " \"job\": \"dentist\",\n", - " \"credit_score\": \"medium\",\n", - " },\n", - "]\n", - "texts = [\"foo\", \"foo\", \"foo\", \"bar\", \"bar\"]" + "# os.environ[\"LANGSMITH_API_KEY\"] = getpass.getpass(\"Enter your LangSmith API key: \")\n", + "# os.environ[\"LANGSMITH_TRACING\"] = \"true\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Create Redis vector store\n", + "## Initialization\n", "\n", "The Redis VectorStore instance can be initialized in a number of ways. There are multiple class methods that can be used to initialize a Redis VectorStore instance.\n", "\n", @@ -291,12 +225,31 @@ "- ``Redis.from_texts_return_keys`` - Initialize from a list of texts (optionally with metadata) and return the keys\n", "- ``Redis.from_existing_index`` - Initialize from an existing Redis index\n", "\n", - "Below we will use the ``Redis.from_texts`` method." + "Below we will use the ``Redis.__init__`` method. \n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import OpenAIEmbeddings\n", + "\n", + "embeddings = OpenAIEmbeddings(model=\"text-embedding-3-large\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, "metadata": { "tags": [] }, @@ -304,61 +257,176 @@ "source": [ "from langchain_community.vectorstores.redis import Redis\n", "\n", - "rds = Redis.from_texts(\n", - " texts,\n", - " embeddings,\n", - " metadatas=metadata,\n", + "vector_store = Redis(\n", " redis_url=\"redis://localhost:6379\",\n", + " embedding=embeddings,\n", " index_name=\"users\",\n", ")" ] }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'users'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "rds.index_name" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Inspecting the created Index\n", + "## Manage vector store\n", "\n", - "Once the ``Redis`` VectorStore object has been constructed, an index will have been created in Redis if it did not already exist. The index can be inspected with both the ``rvl``and the ``redis-cli`` command line tool. If you installed ``redisvl`` above, you can use the ``rvl`` command line tool to inspect the index." + "Once you have created your vector store, we can interact with it by adding and deleting different items.\n", + "\n", + "### Add items to vector store\n", + "\n", + "We can add items to our vector store by using the `add_documents` function." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['doc:users:622f5f19-9b4b-4896-9a16-e1e95f19db4b',\n", + " 'doc:users:032b489f-d37e-4bf1-85ec-4c2275be48ef',\n", + " 'doc:users:5daf0855-b352-45bd-9d29-e21ff66e38c8',\n", + " 'doc:users:b9204897-190b-4dd9-af2b-081ed4e9cbb0',\n", + " 'doc:users:9395caff-1a6a-46c1-bc5c-7c5558eadf46',\n", + " 'doc:users:28243c3d-463d-4662-936e-003a2dc0dc30',\n", + " 'doc:users:1e1cdb91-c226-4836-b38e-ee4b61444913',\n", + " 'doc:users:4005bba2-2a08-4160-a16f-5cc3cf9d4aea',\n", + " 'doc:users:8c88440a-06d2-4a68-95f1-c58d0cf99d29',\n", + " 'doc:users:cc20438f-741a-40fd-bed8-4f1cee113680']" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from uuid import uuid4\n", + "\n", + "from langchain_core.documents import Document\n", + "\n", + "document_1 = Document(\n", + " page_content=\"I had chocalate chip pancakes and scrambled eggs for breakfast this morning.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_2 = Document(\n", + " page_content=\"The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_3 = Document(\n", + " page_content=\"Building an exciting new project with LangChain - come check it out!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_4 = Document(\n", + " page_content=\"Robbers broke into the city bank and stole $1 million in cash.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_5 = Document(\n", + " page_content=\"Wow! That was an amazing movie. I can't wait to see it again.\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_6 = Document(\n", + " page_content=\"Is the new iPhone worth the price? Read this review to find out.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_7 = Document(\n", + " page_content=\"The top 10 soccer players in the world right now.\",\n", + " metadata={\"source\": \"website\"},\n", + ")\n", + "\n", + "document_8 = Document(\n", + " page_content=\"LangGraph is the best framework for building stateful, agentic applications!\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "document_9 = Document(\n", + " page_content=\"The stock market is down 500 points today due to fears of a recession.\",\n", + " metadata={\"source\": \"news\"},\n", + ")\n", + "\n", + "document_10 = Document(\n", + " page_content=\"I have a bad feeling I am going to get deleted :(\",\n", + " metadata={\"source\": \"tweet\"},\n", + ")\n", + "\n", + "documents = [\n", + " document_1,\n", + " document_2,\n", + " document_3,\n", + " document_4,\n", + " document_5,\n", + " document_6,\n", + " document_7,\n", + " document_8,\n", + " document_9,\n", + " document_10,\n", + "]\n", + "uuids = [str(uuid4()) for _ in range(len(documents))]\n", + "\n", + "vector_store.add_documents(documents=documents, ids=uuids)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Delete items from vector store" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vector_store.delete(ids=[uuids[-1]])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Inspecting the created Index\n", + "\n", + "Once the ``Redis`` VectorStore object has been constructed, an index will have been created in Redis if it did not already exist. The index can be inspected with both the ``rvl``and the ``redis-cli`` command line tool. If you installed ``redisvl`` above, you can use the ``rvl`` command line tool to inspect the index." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[32m16:58:26\u001b[0m \u001b[34m[RedisVL]\u001b[0m \u001b[1;30mINFO\u001b[0m Indices:\n", - "\u001b[32m16:58:26\u001b[0m \u001b[34m[RedisVL]\u001b[0m \u001b[1;30mINFO\u001b[0m 1. users\n" + "\u001b[32m17:24:03\u001b[0m \u001b[34m[RedisVL]\u001b[0m \u001b[1;30mINFO\u001b[0m Indices:\n", + "\u001b[32m17:24:03\u001b[0m \u001b[34m[RedisVL]\u001b[0m \u001b[1;30mINFO\u001b[0m 1. users\n" ] } ], "source": [ "# assumes you're running Redis locally (use --host, --port, --password, --username, to change this)\n", - "!rvl index listall" + "!rvl index listall --port 6379" ] }, { @@ -373,7 +441,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -389,21 +457,17 @@ "│ users │ HASH │ ['doc:users'] │ [] │ 0 │\n", "╰──────────────┴────────────────┴───────────────┴─────────────────┴────────────╯\n", "Index Fields:\n", - "╭────────────────┬────────────────┬─────────┬────────────────┬────────────────╮\n", - "│ Name │ Attribute │ Type │ Field Option │ Option Value │\n", - "├────────────────┼────────────────┼─────────┼────────────────┼────────────────┤\n", - "│ user │ user │ TEXT │ WEIGHT │ 1 │\n", - "│ job │ job │ TEXT │ WEIGHT │ 1 │\n", - "│ credit_score │ credit_score │ TEXT │ WEIGHT │ 1 │\n", - "│ content │ content │ TEXT │ WEIGHT │ 1 │\n", - "│ age │ age │ NUMERIC │ │ │\n", - "│ content_vector │ content_vector │ VECTOR │ │ │\n", - "╰────────────────┴────────────────┴─────────┴────────────────┴────────────────╯\n" + "╭────────────────┬────────────────┬────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬────────────────┬─────────────────┬────────────────╮\n", + "│ Name │ Attribute │ Type │ Field Option │ Option Value │ Field Option │ Option Value │ Field Option │ Option Value │ Field Option │ Option Value │\n", + "├────────────────┼────────────────┼────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┼────────────────┼─────────────────┼────────────────┤\n", + "│ content │ content │ TEXT │ WEIGHT │ 1 │ │ │ │ │ │ │\n", + "│ content_vector │ content_vector │ VECTOR │ algorithm │ FLAT │ data_type │ FLOAT32 │ dim │ 3072 │ distance_metric │ COSINE │\n", + "╰────────────────┴────────────────┴────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴────────────────┴─────────────────┴────────────────╯\n" ] } ], "source": [ - "!rvl index info -i users" + "!rvl index info -i users --port 6379" ] }, { @@ -420,31 +484,31 @@ "╭─────────────────────────────┬─────────────╮\n", "│ Stat Key │ Value │\n", "├─────────────────────────────┼─────────────┤\n", - "│ num_docs │ 5 │\n", - "│ num_terms │ 15 │\n", - "│ max_doc_id │ 5 │\n", - "│ num_records │ 33 │\n", + "│ num_docs │ 10 │\n", + "│ num_terms │ 100 │\n", + "│ max_doc_id │ 10 │\n", + "│ num_records │ 116 │\n", "│ percent_indexed │ 1 │\n", "│ hash_indexing_failures │ 0 │\n", - "│ number_of_uses │ 4 │\n", - "│ bytes_per_record_avg │ 4.60606 │\n", - "│ doc_table_size_mb │ 0.000524521 │\n", - "│ inverted_sz_mb │ 0.000144958 │\n", - "│ key_table_size_mb │ 0.000193596 │\n", + "│ number_of_uses │ 1 │\n", + "│ bytes_per_record_avg │ 88.2931 │\n", + "│ doc_table_size_mb │ 0.00108719 │\n", + "│ inverted_sz_mb │ 0.00976753 │\n", + "│ key_table_size_mb │ 0.000304222 │\n", "│ offset_bits_per_record_avg │ 8 │\n", - "│ offset_vectors_sz_mb │ 2.19345e-05 │\n", - "│ offsets_per_term_avg │ 0.69697 │\n", - "│ records_per_doc_avg │ 6.6 │\n", + "│ offset_vectors_sz_mb │ 0.000102043 │\n", + "│ offsets_per_term_avg │ 0.922414 │\n", + "│ records_per_doc_avg │ 11.6 │\n", "│ sortable_values_size_mb │ 0 │\n", - "│ total_indexing_time │ 0.32 │\n", - "│ total_inverted_index_blocks │ 16 │\n", - "│ vector_index_sz_mb │ 6.0126 │\n", + "│ total_indexing_time │ 1.373 │\n", + "│ total_inverted_index_blocks │ 100 │\n", + "│ vector_index_sz_mb │ 12.0086 │\n", "╰─────────────────────────────┴─────────────╯\n" ] } ], "source": [ - "!rvl stats -i users" + "!rvl stats -i users --port 6379" ] }, { @@ -458,79 +522,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Querying\n", + "## Query vector store\n", "\n", - "There are multiple ways to query the ``Redis`` VectorStore implementation based on what use case you have:\n", + "Once your vector store has been created and the relevant documents have been added you will most likely wish to query it during the running of your chain or agent. \n", "\n", - "- ``similarity_search``: Find the most similar vectors to a given vector.\n", - "- ``similarity_search_with_score``: Find the most similar vectors to a given vector and return the vector distance\n", - "- ``similarity_search_limit_score``: Find the most similar vectors to a given vector and limit the number of results to the ``score_threshold``\n", - "- ``similarity_search_with_relevance_scores``: Find the most similar vectors to a given vector and return the vector similarities\n", - "- ``max_marginal_relevance_search``: Find the most similar vectors to a given vector while also optimizing for diversity" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "foo\n" - ] - } - ], - "source": [ - "results = rds.similarity_search(\"foo\")\n", - "print(results[0].page_content)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Key of the document in Redis: doc:users:a70ca43b3a4e4168bae57c78753a200f\n", - "Metadata of the document: {'user': 'derrick', 'job': 'doctor', 'credit_score': 'low', 'age': '45'}\n" - ] - } - ], - "source": [ - "# return metadata\n", - "results = rds.similarity_search(\"foo\", k=3)\n", - "meta = results[1].metadata\n", - "print(\"Key of the document in Redis: \", meta.pop(\"id\"))\n", - "print(\"Metadata of the document: \", meta)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n", - "Content: bar --- Score: 0.1566\n", - "Content: bar --- Score: 0.1566\n" - ] - } - ], - "source": [ - "# with scores (distances)\n", - "results = rds.similarity_search_with_score(\"foo\", k=5)\n", - "for result in results:\n", - " print(f\"Content: {result[0].page_content} --- Score: {result[1]}\")" + "### Query directly\n", + "\n", + "#### Similarity search\n", + "\n", + "Performing a simple similarity search can be done as follows:" ] }, { @@ -542,17 +542,26 @@ "name": "stdout", "output_type": "stream", "text": [ - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n" + "* Building an exciting new project with LangChain - come check it out! [{'id': 'doc:users:5daf0855-b352-45bd-9d29-e21ff66e38c8'}]\n", + "* LangGraph is the best framework for building stateful, agentic applications! [{'id': 'doc:users:4005bba2-2a08-4160-a16f-5cc3cf9d4aea'}]\n" ] } ], "source": [ - "# limit the vector distance that can be returned\n", - "results = rds.similarity_search_with_score(\"foo\", k=5, distance_threshold=0.1)\n", - "for result in results:\n", - " print(f\"Content: {result[0].page_content} --- Score: {result[1]}\")" + "results = vector_store.similarity_search(\n", + " \"LangChain provides abstractions to make working with LLMs easy\", k=2\n", + ")\n", + "for res in results:\n", + " print(f\"* {res.page_content} [{res.metadata}]\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Similarity search with score\n", + "\n", + "You can also search with score:" ] }, { @@ -564,110 +573,24 @@ "name": "stdout", "output_type": "stream", "text": [ - "Content: foo --- Similiarity: 1.0\n", - "Content: foo --- Similiarity: 1.0\n", - "Content: foo --- Similiarity: 1.0\n", - "Content: bar --- Similiarity: 0.8434\n", - "Content: bar --- Similiarity: 0.8434\n" + "* [SIM=0.446900] The weather forecast for tomorrow is cloudy and overcast, with a high of 62 degrees. [{'id': 'doc:users:032b489f-d37e-4bf1-85ec-4c2275be48ef'}]\n" ] } ], "source": [ - "# with scores\n", - "results = rds.similarity_search_with_relevance_scores(\"foo\", k=5)\n", - "for result in results:\n", - " print(f\"Content: {result[0].page_content} --- Similiarity: {result[1]}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo --- Similarity: 1.0\n", - "Content: foo --- Similarity: 1.0\n", - "Content: foo --- Similarity: 1.0\n" - ] - } - ], - "source": [ - "# limit scores (similarities have to be over .9)\n", - "results = rds.similarity_search_with_relevance_scores(\"foo\", k=5, score_threshold=0.9)\n", - "for result in results:\n", - " print(f\"Content: {result[0].page_content} --- Similarity: {result[1]}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['doc:users:b9c71d62a0a34241a37950b448dafd38']" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# you can also add new documents as follows\n", - "new_document = [\"baz\"]\n", - "new_metadata = [{\"user\": \"sam\", \"age\": 50, \"job\": \"janitor\", \"credit_score\": \"high\"}]\n", - "# both the document and metadata must be lists\n", - "rds.add_texts(new_document, new_metadata)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'id': 'doc:users:b9c71d62a0a34241a37950b448dafd38', 'user': 'sam', 'job': 'janitor', 'credit_score': 'high', 'age': '50'}\n" - ] - } - ], - "source": [ - "# now query the new document\n", - "results = rds.similarity_search(\"baz\", k=3)\n", - "print(results[0].metadata)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "# use maximal marginal relevance search to diversify results\n", - "results = rds.max_marginal_relevance_search(\"foo\")" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "# the lambda_mult parameter controls the diversity of the results, the lower the more diverse\n", - "results = rds.max_marginal_relevance_search(\"foo\", lambda_mult=0.1)" + "results = vector_store.similarity_search_with_score(\"Will it be hot tomorrow?\", k=1)\n", + "for res, score in results:\n", + " print(f\"* [SIM={score:3f}] {res.page_content} [{res.metadata}]\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "#### Other search methods\n", + "\n", + "For a list of all the search functions available to the `Redis` vector store, please refer to the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.redis.base.Redis.html)\n", + "\n", "## Connect to an existing Index\n", "\n", "In order to have the same metadata indexed when using the ``Redis`` VectorStore. You will need to have the same ``index_schema`` passed in either as a path to a yaml file or as a dictionary. The following shows how to obtain the schema from an index and connect to an existing index." @@ -680,56 +603,7 @@ "outputs": [], "source": [ "# write the schema to a yaml file\n", - "rds.write_schema(\"redis_schema.yaml\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The schema file for this example should look something like:\n", - "\n", - "```yaml\n", - "numeric:\n", - "- name: age\n", - " no_index: false\n", - " sortable: false\n", - "text:\n", - "- name: user\n", - " no_index: false\n", - " no_stem: false\n", - " sortable: false\n", - " weight: 1\n", - " withsuffixtrie: false\n", - "- name: job\n", - " no_index: false\n", - " no_stem: false\n", - " sortable: false\n", - " weight: 1\n", - " withsuffixtrie: false\n", - "- name: credit_score\n", - " no_index: false\n", - " no_stem: false\n", - " sortable: false\n", - " weight: 1\n", - " withsuffixtrie: false\n", - "- name: content\n", - " no_index: false\n", - " no_stem: false\n", - " sortable: false\n", - " weight: 1\n", - " withsuffixtrie: false\n", - "vector:\n", - "- algorithm: FLAT\n", - " block_size: 1000\n", - " datatype: FLOAT32\n", - " dims: 1536\n", - " distance_metric: COSINE\n", - " initial_cap: 20000\n", - " name: content_vector\n", - "```\n", - "\n", - "**Notice**, this include **all** possible fields for the schema. You can remove any fields that you don't need." + "vector_store.write_schema(\"redis_schema.yaml\")" ] }, { @@ -776,7 +650,7 @@ ], "source": [ "# see the schemas are the same\n", - "new_rds.schema == rds.schema" + "new_rds.schema == vector_store.schema" ] }, { @@ -841,6 +715,8 @@ " \"text\": [{\"name\": \"user\"}, {\"name\": \"job\"}],\n", " \"numeric\": [{\"name\": \"age\"}],\n", "}\n", + "texts = [] # list of texts\n", + "metadata = {} # dictionary of metadata\n", "\n", "rds, keys = Redis.from_texts_return_keys(\n", " texts,\n", @@ -1019,248 +895,110 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Redis as Retriever\n", + "### Query by turning into retriever\n", "\n", - "Here we go over different options for using the vector store as a retriever.\n", + "You can also transform the vector store into a retriever for easier usage in your chains. Here we go over different options for using the vector store as a retriever.\n", "\n", - "There are three different search methods we can use to do retrieval. By default, it will use semantic similarity." + "There are three different search methods we can use to do retrieval. By default, it will use semantic similarity. To see all the options, please refer to the [API reference](https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.redis.base.Redis.html#langchain_community.vectorstores.redis.base.Redis.as_retriever)" ] }, { "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n", - "Content: foo --- Score: 0.0\n" - ] - } - ], - "source": [ - "query = \"foo\"\n", - "results = rds.similarity_search_with_score(query, k=3, return_metadata=True)\n", - "\n", - "for result in results:\n", - " print(\"Content:\", result[0].page_content, \" --- Score: \", result[1])" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "retriever = rds.as_retriever(search_type=\"similarity\", search_kwargs={\"k\": 4})" - ] - }, - { - "cell_type": "code", - "execution_count": 28, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Document(page_content='foo', metadata={'id': 'doc:users_modified:988ecca7574048e396756efc0e79aeca', 'user': 'john', 'job': 'engineer', 'credit_score': 'high', 'age': '18'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:009b1afeb4084cc6bdef858c7a99b48e', 'user': 'derrick', 'job': 'doctor', 'credit_score': 'low', 'age': '45'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:7087cee9be5b4eca93c30fbdd09a2731', 'user': 'nancy', 'job': 'doctor', 'credit_score': 'high', 'age': '94'}),\n", - " Document(page_content='bar', metadata={'id': 'doc:users_modified:01ef6caac12b42c28ad870aefe574253', 'user': 'tyler', 'job': 'engineer', 'credit_score': 'high', 'age': '100'})]" + "[Document(metadata={'id': 'doc:users:b9204897-190b-4dd9-af2b-081ed4e9cbb0'}, page_content='Robbers broke into the city bank and stole $1 million in cash.')]" ] }, - "execution_count": 28, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "docs = retriever.invoke(query)\n", - "docs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There is also the `similarity_distance_threshold` retriever which allows the user to specify the vector distance" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [], - "source": [ - "retriever = rds.as_retriever(\n", - " search_type=\"similarity_distance_threshold\",\n", - " search_kwargs={\"k\": 4, \"distance_threshold\": 0.1},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='foo', metadata={'id': 'doc:users_modified:988ecca7574048e396756efc0e79aeca', 'user': 'john', 'job': 'engineer', 'credit_score': 'high', 'age': '18'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:009b1afeb4084cc6bdef858c7a99b48e', 'user': 'derrick', 'job': 'doctor', 'credit_score': 'low', 'age': '45'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:7087cee9be5b4eca93c30fbdd09a2731', 'user': 'nancy', 'job': 'doctor', 'credit_score': 'high', 'age': '94'})]" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "docs = retriever.invoke(query)\n", - "docs" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Lastly, the ``similarity_score_threshold`` allows the user to define the minimum score for similar documents" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [], - "source": [ - "retriever = rds.as_retriever(\n", + "retriever = vector_store.as_retriever(\n", " search_type=\"similarity_score_threshold\",\n", - " search_kwargs={\"score_threshold\": 0.9, \"k\": 10},\n", - ")" + " search_kwargs={\"k\": 1, \"score_threshold\": 0.2},\n", + ")\n", + "retriever.invoke(\"Stealing from the bank is a crime\")" ] }, { - "cell_type": "code", - "execution_count": 32, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[Document(page_content='foo', metadata={'id': 'doc:users_modified:988ecca7574048e396756efc0e79aeca', 'user': 'john', 'job': 'engineer', 'credit_score': 'high', 'age': '18'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:009b1afeb4084cc6bdef858c7a99b48e', 'user': 'derrick', 'job': 'doctor', 'credit_score': 'low', 'age': '45'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users_modified:7087cee9be5b4eca93c30fbdd09a2731', 'user': 'nancy', 'job': 'doctor', 'credit_score': 'high', 'age': '94'})]" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "retriever.invoke(\"foo\")" + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ - "retriever = rds.as_retriever(\n", - " search_type=\"mmr\", search_kwargs={\"fetch_k\": 20, \"k\": 4, \"lambda_mult\": 0.1}\n", - ")" + "# | output: false\n", + "# | echo: false\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "llm = ChatOpenAI(model=\"gpt-4o-mini\")" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[Document(page_content='foo', metadata={'id': 'doc:users:8f6b673b390647809d510112cde01a27', 'user': 'john', 'job': 'engineer', 'credit_score': 'high', 'age': '18'}),\n", - " Document(page_content='bar', metadata={'id': 'doc:users:93521560735d42328b48c9c6f6418d6a', 'user': 'tyler', 'job': 'engineer', 'credit_score': 'high', 'age': '100'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users:125ecd39d07845eabf1a699d44134a5b', 'user': 'nancy', 'job': 'doctor', 'credit_score': 'high', 'age': '94'}),\n", - " Document(page_content='foo', metadata={'id': 'doc:users:d6200ab3764c466082fde3eaab972a2a', 'user': 'derrick', 'job': 'doctor', 'credit_score': 'low', 'age': '45'})]" + "'LangGraph is used for building stateful, agentic applications. It provides a framework to facilitate the development of such applications.'" ] }, - "execution_count": 13, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "retriever.invoke(\"foo\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Delete keys and index" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To delete your entries you have to address them by their keys." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Redis.delete(keys, redis_url=\"redis://localhost:6379\")" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# delete the indices too\n", - "Redis.drop_index(\n", - " index_name=\"users\", delete_documents=True, redis_url=\"redis://localhost:6379\"\n", + "from langchain import hub\n", + "from langchain_core.output_parsers import StrOutputParser\n", + "from langchain_core.runnables import RunnablePassthrough\n", + "\n", + "prompt = hub.pull(\"rlm/rag-prompt\")\n", + "\n", + "\n", + "def format_docs(docs):\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs)\n", + "\n", + "\n", + "rag_chain = (\n", + " {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n", + " | prompt\n", + " | llm\n", + " | StrOutputParser()\n", ")\n", - "Redis.drop_index(\n", - " index_name=\"users_modified\",\n", - " delete_documents=True,\n", - " redis_url=\"redis://localhost:6379\",\n", - ")" + "\n", + "rag_chain.invoke(\"What is LangGraph used for?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## API reference\n", + "\n", + "For detailed documentation of all `Redis` vector store features and configurations head to the API reference: https://api.python.langchain.com/en/latest/vectorstores/langchain_community.vectorstores.redis.base.Redis.html" ] } ], @@ -1280,7 +1018,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/scripts/vectorstore_feat_table.py b/docs/scripts/vectorstore_feat_table.py new file mode 100644 index 00000000000..4e2af5e1534 --- /dev/null +++ b/docs/scripts/vectorstore_feat_table.py @@ -0,0 +1,269 @@ +import inspect +import sys +from pathlib import Path + +from langchain_astradb import AstraDBVectorStore +from langchain_chroma import Chroma +from langchain_community import vectorstores +from langchain_core.vectorstores import VectorStore +from langchain_couchbase import CouchbaseVectorStore +from langchain_milvus import Milvus +from langchain_mongodb import MongoDBAtlasVectorSearch +from langchain_pinecone import PineconeVectorStore +from langchain_qdrant import QdrantVectorStore + +vectorstore_list = [ + "FAISS", + "ElasticsearchStore", + "PGVector", + "Redis", + "Clickhouse", + "InMemoryVectorStore", +] + +from_partners = [ + ("Chroma", Chroma), + ("AstraDBVectorStore", AstraDBVectorStore), + ("QdrantVectorStore", QdrantVectorStore), + ("PineconeVectorStore", PineconeVectorStore), + ("Milvus", Milvus), + ("MongoDBAtlasVectorSearch", MongoDBAtlasVectorSearch), + ("CouchbaseVectorStore", CouchbaseVectorStore), +] + +VECTORSTORE_TEMPLATE = """\ +--- +sidebar_position: 1 +sidebar_class_name: hidden +keywords: [compatibility] +custom_edit_url: +--- + +# Vectorstores + +## Features + +The table below lists the features for some of our most popular vector stores. + +{table} + +""" + + +def get_vectorstore_table(): + vectorstore_feat_table = { + "FAISS": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "ElasticsearchStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "PGVector": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "Redis": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "Clickhouse": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "InMemoryVectorStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "Chroma": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "AstraDBVectorStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "QdrantVectorStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "PineconeVectorStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "Milvus": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "MongoDBAtlasVectorSearch": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + "CouchbaseVectorStore": { + "Delete by ID": True, + "Filtering": True, + "similarity_search_by_vector": True, + "similarity_search_with_score": True, + "asearch": True, + "Passes Standard Tests": False, + "Multi Tenancy": False, + "Local/Cloud": "Local", + "IDs in add Documents": True, + }, + } + for vs in vectorstore_list + from_partners: + if isinstance(vs, tuple): + cls = vs[1] + vs_name = vs[0] + else: + cls = getattr(vectorstores, vs) + vs_name = vs + for feat in ( + "similarity_search_with_score", + "similarity_search_by_vector", + "asearch", + ): + feat, name = feat, feat + if getattr(cls, feat) != getattr(VectorStore, feat): + vectorstore_feat_table[vs_name][name] = True + else: + vectorstore_feat_table[vs_name][name] = False + + if "filter" not in [ + key + for key, _ in inspect.signature( + getattr(cls, "similarity_search") + ).parameters.items() + ]: + vectorstore_feat_table[vs_name]["Filtering"] = False + + header = [ + "Vectorstore", + "Delete by ID", + "Filtering", + "similarity_search_by_vector", + "similarity_search_with_score", + "asearch", + "Passes Standard Tests", + "Multi Tenancy", + "Local/Cloud", + "IDs in add Documents", + ] + title = [ + "Vectorstore", + "Delete by ID", + "Filtering", + "Search by Vector", + "Search with score", + "Async", + "Passes Standard Tests", + "Multi Tenancy", + "Local/Cloud", + "IDs in add Documents", + ] + rows = [title, [":-"] + [":-:"] * (len(title) - 1)] + for vs, feats in sorted(vectorstore_feat_table.items()): + rows += [ + [vs, "✅"] + + [ + ("✅" if feats.get(h) else "❌") if h != "Local/Cloud" else feats.get(h) + for h in header[1:] + ] + ] + return "\n".join(["|".join(row) for row in rows]) + + +if __name__ == "__main__": + output_dir = Path(sys.argv[1]) + output_integrations_dir = output_dir / "integrations" + output_integrations_dir_vectorstore = output_integrations_dir / "vectorstores" + output_integrations_dir_vectorstore.mkdir(parents=True, exist_ok=True) + + vectorstore_page = VECTORSTORE_TEMPLATE.format(table=get_vectorstore_table()) + with open(output_integrations_dir / "vectorstores" / "index.mdx", "w") as f: + f.write(vectorstore_page) diff --git a/docs/src/theme/EmbeddingTabs.js b/docs/src/theme/EmbeddingTabs.js new file mode 100644 index 00000000000..7ad62a515ad --- /dev/null +++ b/docs/src/theme/EmbeddingTabs.js @@ -0,0 +1,75 @@ +import React from "react"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import CodeBlock from "@theme-original/CodeBlock"; + +export default function EmbeddingTabs(props) { + const { + openaiParams, + hideOpenai, + huggingFaceParams, + hideHuggingFace, + fakeEmbeddingParams, + hideFakeEmbedding, + customVarName, + } = props; + + const openAIParamsOrDefault = openaiParams ?? `model="text-embedding-3-large"`; + const huggingFaceParamsOrDefault = huggingFaceParams ?? `model="sentence-transformers/all-mpnet-base-v2"`; + const fakeEmbeddingParamsOrDefault = fakeEmbeddingParams ?? `size=4096`; + + const embeddingVarName = customVarName ?? "embeddings"; + + const tabItems = [ + { + value: "OpenAI", + label: "OpenAI", + text: `from langchain_openai import OpenAIEmbeddings\n\n${embeddingVarName} = OpenAIEmbeddings(${openAIParamsOrDefault})`, + apiKeyName: "OPENAI_API_KEY", + packageName: "langchain-openai", + default: true, + shouldHide: hideOpenai, + }, + { + value: "HuggingFace", + label: "HuggingFace", + text: `from langchain_huggingface import HuggingFaceEmbeddings\n\n${embeddingVarName} = HuggingFaceEmbeddings(${huggingFaceParamsOrDefault})`, + apiKeyName: undefined, + packageName: "langchain-huggingface", + default: false, + shouldHide: hideHuggingFace, + }, + { + value: "Fake Embedding", + label: "Fake Embedding", + text: `from langchain_core.embeddings import FakeEmbeddings\n\n${embeddingVarName} = FakeEmbeddings(${fakeEmbeddingParamsOrDefault})`, + apiKeyName: undefined, + packageName: "langchain-core", + default: false, + shouldHide: hideFakeEmbedding, + }, + ]; + + return ( + + {tabItems + .filter((tabItem) => !tabItem.shouldHide) + .map((tabItem) => { + const apiKeyText = tabItem.apiKeyName ? `import getpass + + os.environ["${tabItem.apiKeyName}"] = getpass.getpass()` : ''; + return ( + + {`pip install -qU ${tabItem.packageName}`} + {apiKeyText + (apiKeyText ? "\n\n" : '') + tabItem.text} + + ); + }) + } + + ); + } \ No newline at end of file diff --git a/libs/cli/langchain_cli/integration_template/docs/vectorstores.ipynb b/libs/cli/langchain_cli/integration_template/docs/vectorstores.ipynb index d281fe4c3ed..c78091133fb 100644 --- a/libs/cli/langchain_cli/integration_template/docs/vectorstores.ipynb +++ b/libs/cli/langchain_cli/integration_template/docs/vectorstores.ipynb @@ -67,15 +67,13 @@ "import getpass\n", "import os\n", "\n", - "import os\n", - "\n", "if not os.getenv(\"__MODULE_NAME___API_KEY\"):\n", - " import getpass\n", " os.environ[\"__MODULE_NAME___API_KEY\"] = getpass.getpass(\"Enter your __ModuleName__ API key: \")" ] }, { "cell_type": "markdown", + "id": "7f98392b", "metadata": {}, "source": [ "If you want to get automated tracing of your model calls you can also set your [LangSmith](https://docs.smith.langchain.com/) API key by uncommenting below:" @@ -84,6 +82,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e7b6a6e0", "metadata": {}, "outputs": [], "source": [ @@ -96,9 +95,16 @@ "id": "93df377e", "metadata": {}, "source": [ - "## Instantiation\n", + "## Initialization\n", "\n", - "- TODO: Fill out with relevant init params" + "- TODO: Fill out with relevant init params\n", + "\n", + "\n", + "```{=mdx}\n", + "import EmbeddingTabs from \"@theme/EmbeddingTabs\";\n", + "\n", + "\n", + "```" ] }, { @@ -112,7 +118,7 @@ "source": [ "from __module_name__.vectorstores import __ModuleName__VectorStore\n", "\n", - "vector_store = __ModuleName__VectorStore()" + "vector_store = __ModuleName__VectorStore(embeddings=embeddings)" ] }, { @@ -146,14 +152,14 @@ " metadata={\"source\": \"https://example.com\"}\n", ")\n", "\n", - "document_2 = Document(\n", + "document_3 = Document(\n", " page_content=\"baz\",\n", " metadata={\"source\": \"https://example.com\"}\n", ")\n", "\n", - "documents = [document_1, document_2]\n", + "documents = [document_1, document_2, document_3]\n", "\n", - "vector_store.add_documents(documents=documents,ids=[\"1\",\"2\"])" + "vector_store.add_documents(documents=documents,ids=[\"1\",\"2\",\"3\"])" ] }, { @@ -224,7 +230,7 @@ "metadata": {}, "outputs": [], "source": [ - "results = vector_store.similarity_search(query=\"thud\",k=1,filter={\"source\":\"https://example.com\"})\n", + "results = vector_store.similarity_search(query=\"thud\",k=1,filter={\"source\":\"https://another-example.com\"})\n", "for doc in results:\n", " print(f\"* {doc.page_content} [{doc.metadata}]\")" ] @@ -270,7 +276,10 @@ "metadata": {}, "outputs": [], "source": [ - "retriever = vector_store.as_retriever()\n", + "retriever = vector_store.as_retriever(\n", + " search_type=\"mmr\",\n", + " search_kwargs={\"k\": 1}\n", + ")\n", "retriever.invoke(\"thud\")" ] }, @@ -279,7 +288,15 @@ "id": "901c75dc", "metadata": {}, "source": [ - "Using retriever in a simple RAG chain:" + "## Chain usage\n", + "\n", + "The code below shows how to use the vector store as a retriever in a simple RAG chain:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" ] }, { diff --git a/libs/cli/langchain_cli/integration_template/integration_template/vectorstores.py b/libs/cli/langchain_cli/integration_template/integration_template/vectorstores.py index db74b79e7d3..4236e8c037e 100644 --- a/libs/cli/langchain_cli/integration_template/integration_template/vectorstores.py +++ b/libs/cli/langchain_cli/integration_template/integration_template/vectorstores.py @@ -57,6 +57,7 @@ class __ModuleName__VectorStore(VectorStore): .. code-block:: python from __module_name__.vectorstores import __ModuleName__VectorStore + from langchain_openai import OpenAIEmbeddings vector_store = __ModuleName__VectorStore( collection_name="foo", @@ -71,24 +72,19 @@ class __ModuleName__VectorStore(VectorStore): from langchain_core.documents import Document - document = Document(page_content="foo", metadata={"baz": "bar"}) - vector_store.add_documents([document],ids=["1"]) + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) # TODO: Populate with relevant variables. - Update Documents: - .. code-block:: python - - updated_document = Document( - page_content="qux", - metadata={"bar": "baz"} - ) - - vector_store.update_documents(document_id="1",document=updated_document) - Delete Documents: .. code-block:: python - vector_store.delete(ids=["1"]) + vector_store.delete(ids=["3"]) # TODO: Fill out with relevant variables and example output. Search: @@ -102,11 +98,23 @@ class __ModuleName__VectorStore(VectorStore): # TODO: Example output + # TODO: Fill out with relevant variables and example output. + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + # TODO: Fill out with relevant variables and example output. Search with score: .. code-block:: python - results = vector_store.similarity_search_with_score(query="thud",k=1) + results = vector_store.similarity_search_with_score(query="qux",k=1) for doc, score in results: print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") @@ -114,13 +122,35 @@ class __ModuleName__VectorStore(VectorStore): # TODO: Example output + # TODO: Fill out with relevant variables and example output. + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + # TODO: Fill out with relevant variables and example output. Use as Retriever: .. code-block:: python retriever = vector_store.as_retriever( search_type="mmr", - search_kwargs={"k": 1, "fetch_k": 10, "lambda_mult": 0.5}, + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, ) retriever.invoke("thud") diff --git a/libs/community/langchain_community/vectorstores/clickhouse.py b/libs/community/langchain_community/vectorstores/clickhouse.py index d950a541be8..9f4e8cfb579 100644 --- a/libs/community/langchain_community/vectorstores/clickhouse.py +++ b/libs/community/langchain_community/vectorstores/clickhouse.py @@ -102,18 +102,124 @@ class ClickhouseSettings(BaseSettings): class Clickhouse(VectorStore): - """`ClickHouse VectorSearch` vector store. + """ClickHouse vector store integration. - You need a `clickhouse-connect` python package, and a valid account - to connect to ClickHouse. + Setup: + Install ``langchain_community`` and ``clickhouse-connect``: - ClickHouse can not only search with simple vector indexes, - it also supports complex query with multiple conditions, - constraints and even sub-queries. + .. code-block:: bash - For more information, please visit - [ClickHouse official site](https://clickhouse.com/clickhouse) - """ + pip install -qU langchain_community clickhouse-connect + + Key init args — indexing params: + embedding: Embeddings + Embedding function to use. + + Key init args — client params: + config: Optional[ClickhouseSettings] + ClickHouse client configuration. + + Instantiate: + .. code-block:: python + + from langchain_community.vectorstores import Clickhouse, ClickhouseSettings + from langchain_openai import OpenAIEmbeddings + + settings = ClickhouseSettings(table="clickhouse_example") + vector_store = Clickhouse(embedding=OpenAIEmbeddings(), config=settings) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + # TODO: Fill out example output. + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with relevant variables and example output. + Search with filter: + .. code-block:: python + + # TODO: Edit filter if needed + results = vector_store.similarity_search(query="thud",k=1,filter="metadata.baz='bar'") + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + # TODO: Example output + + """ # noqa: E501 def __init__( self, @@ -123,10 +229,11 @@ class Clickhouse(VectorStore): ) -> None: """ClickHouse Wrapper to LangChain - embedding_function (Embeddings): - config (ClickHouseSettings): Configuration to ClickHouse Client - Other keyword arguments will pass into - [clickhouse-connect](https://docs.clickhouse.com/) + Args: + embedding_function (Embeddings): embedding function to use + config (ClickHouseSettings): Configuration to ClickHouse Client + kwargs (any): Other keyword arguments will pass into + [clickhouse-connect](https://docs.clickhouse.com/) """ try: from clickhouse_connect import get_client diff --git a/libs/community/langchain_community/vectorstores/faiss.py b/libs/community/langchain_community/vectorstores/faiss.py index 5e907a795f5..860708b9851 100644 --- a/libs/community/langchain_community/vectorstores/faiss.py +++ b/libs/community/langchain_community/vectorstores/faiss.py @@ -72,21 +72,130 @@ def _len_check_if_sized(x: Any, y: Any, x_name: str, y_name: str) -> None: class FAISS(VectorStore): - """`Meta Faiss` vector store. + """FAISS vector store integration. - To use, you must have the ``faiss`` python package installed. + Setup: + Install ``langchain_community`` and ``faiss-cpu`` python packages. - Example: + .. code-block:: bash + + pip install -qU langchain_community faiss-cpu + + Key init args — indexing params: + embedding_function: Embeddings + Embedding function to use. + + Key init args — client params: + index: Any + FAISS index to use. + docstore: Docstore + Docstore to use. + index_to_docstore_id: Dict[int, str] + Mapping of index to docstore id. + + Instantiate: .. code-block:: python - from langchain_community.embeddings.openai import OpenAIEmbeddings + import faiss from langchain_community.vectorstores import FAISS + from langchain_community.docstore.in_memory import InMemoryDocstore + from langchain_openai import OpenAIEmbeddings - embeddings = OpenAIEmbeddings() - texts = ["FAISS is an important library", "LangChain supports FAISS"] - faiss = FAISS.from_texts(texts, embeddings) + index = faiss.IndexFlatL2(len(OpenAIEmbeddings().embed_query("hello world"))) - """ + vector_store = FAISS( + embedding_function=OpenAIEmbeddings(), + index=index, + docstore= InMemoryDocstore(), + index_to_docstore_id={} + ) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz'}] + + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz'}] + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.335304] foo [{'baz': 'bar'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.335304] foo [{'baz': 'bar'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'bar': 'baz'}, page_content='thud')] + + """ # noqa: E501 def __init__( self, diff --git a/libs/community/langchain_community/vectorstores/pgvector.py b/libs/community/langchain_community/vectorstores/pgvector.py index 83661b534c8..afeca95b568 100644 --- a/libs/community/langchain_community/vectorstores/pgvector.py +++ b/libs/community/langchain_community/vectorstores/pgvector.py @@ -274,22 +274,22 @@ class PGVector(VectorStore): disabling creation is useful when using ReadOnly Databases. Example: - .. code-block:: python - from langchain_community.vectorstores import PGVector - from langchain_community.embeddings.openai import OpenAIEmbeddings + .. code-block:: python - CONNECTION_STRING = "postgresql+psycopg2://hwc@localhost:5432/test3" - COLLECTION_NAME = "state_of_the_union_test" - embeddings = OpenAIEmbeddings() - vectorestore = PGVector.from_documents( - embedding=embeddings, - documents=docs, - collection_name=COLLECTION_NAME, - connection_string=CONNECTION_STRING, - use_jsonb=True, - ) - """ + from langchain_community.vectorstores import PGVector + from langchain_community.embeddings.openai import OpenAIEmbeddings + CONNECTION_STRING = "postgresql+psycopg2://hwc@localhost:5432/test3" + COLLECTION_NAME = "state_of_the_union_test" + embeddings = OpenAIEmbeddings() + vectorestore = PGVector.from_documents( + embedding=embeddings, + documents=docs, + collection_name=COLLECTION_NAME, + connection_string=CONNECTION_STRING, + use_jsonb=True, + + """ # noqa: E501 def __init__( self, diff --git a/libs/community/langchain_community/vectorstores/redis/base.py b/libs/community/langchain_community/vectorstores/redis/base.py index 6490e54c22f..78f400413b1 100644 --- a/libs/community/langchain_community/vectorstores/redis/base.py +++ b/libs/community/langchain_community/vectorstores/redis/base.py @@ -73,103 +73,135 @@ def check_index_exists(client: RedisType, index_name: str) -> bool: class Redis(VectorStore): """Redis vector database. - To use, you should have the ``redis`` python package installed - and have a running Redis Enterprise or Redis-Stack server + Deployment Options: + Below, we will use a local deployment as an example. However, Redis can be deployed in all of the following ways: - For production use cases, it is recommended to use Redis Enterprise - as the scaling, performance, stability and availability is much - better than Redis-Stack. + - [Redis Cloud](https://redis.com/redis-enterprise-cloud/overview/) + - [Docker (Redis Stack)](https://hub.docker.com/r/redis/redis-stack) + - Cloud marketplaces: [AWS Marketplace](https://aws.amazon.com/marketplace/pp/prodview-e6y7ork67pjwg?sr=0-2&ref_=beagle&applicationId=AWSMPContessa), [Google Marketplace](https://console.cloud.google.com/marketplace/details/redislabs-public/redis-enterprise?pli=1), or [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/garantiadata.redis_enterprise_1sp_public_preview?tab=Overview) + - On-premise: [Redis Enterprise Software](https://redis.com/redis-enterprise-software/overview/) + - Kubernetes: [Redis Enterprise Software on Kubernetes](https://docs.redis.com/latest/kubernetes/) - For testing and prototyping, however, this is not required. - Redis-Stack is available as a docker container the full vector - search API available. + Setup: + Install ``redis``, ``redisvl``, and ``langchain-community`` and run Redis locally. - .. code-block:: bash + .. code-block:: bash - # to run redis stack in docker locally - docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest + pip install -qU redis redisvl langchain-community + docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latest - Once running, you can connect to the redis server with the following url schemas: - - redis://: # simple connection - - redis://:@: # connection with authentication - - rediss://: # connection with SSL - - rediss://:@: # connection with SSL and auth + Key init args — indexing params: + index_name: str + Name of the index. + index_schema: Optional[Union[Dict[str, ListOfDict], str, os.PathLike]] + Schema of the index and the vector schema. Can be a dict, or path to yaml file. + embedding: Embeddings + Embedding function to use. + Key init args — client params: + redis_url: str + Redis connection url. - Examples: - - The following examples show various ways to use the Redis VectorStore with - LangChain. - - For all the following examples assume we have the following imports: - - .. code-block:: python - - from langchain_community.vectorstores import Redis - from langchain_community.embeddings import OpenAIEmbeddings - - Initialize, create index, and load Documents + Instantiate: .. code-block:: python - from langchain_community.vectorstores import Redis - from langchain_community.embeddings import OpenAIEmbeddings + from langchain_community.vectorstores.redis import Redis + from langchain_openai import OpenAIEmbeddings - rds = Redis.from_documents( - documents, # a list of Document objects from loaders or created - embeddings, # an Embeddings object + vector_store = Redis( redis_url="redis://localhost:6379", + embedding=OpenAIEmbeddings(), + index_name="users", ) - Initialize, create index, and load Documents with metadata + Add Documents: .. code-block:: python + from langchain_core.documents import Document - rds = Redis.from_texts( - texts, # a list of strings - metadata, # a list of metadata dicts - embeddings, # an Embeddings object - redis_url="redis://localhost:6379", - ) + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") - Initialize, create index, and load Documents with metadata and return keys + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") .. code-block:: python - rds, keys = Redis.from_texts_return_keys( - texts, # a list of strings - metadata, # a list of metadata dicts - embeddings, # an Embeddings object - redis_url="redis://localhost:6379", - ) + * thud [{'id': 'doc:users:2'}] - For use cases where the index needs to stay alive, you can initialize - with an index name such that it's easier to reference later + Search with filter: + .. code-block:: python + + from langchain_community.vectorstores.redis import RedisTag + + results = vector_store.similarity_search(query="thud",k=1,filter=(RedisTag("baz") != "bar")) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") .. code-block:: python - rds = Redis.from_texts( - texts, # a list of strings - metadata, # a list of metadata dicts - embeddings, # an Embeddings object - index_name="my-index", - redis_url="redis://localhost:6379", - ) + * thud [{'id': 'doc:users:2'}] - Initialize and connect to an existing index (from above) + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") .. code-block:: python - # must pass in schema and key_prefix from another index - existing_rds = Redis.from_existing_index( - embeddings, # an Embeddings object - index_name="my-index", - schema=rds.schema, # schema dumped from another index - key_prefix=rds.key_prefix, # key prefix from another index - redis_url="redis://localhost:6379", + * [SIM=0.167700] foo [{'id': 'doc:users:1'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.167700] foo [{'id': 'doc:users:1'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, ) + retriever.invoke("thud") + .. code-block:: python - Advanced examples: + [Document(metadata={'id': 'doc:users:2'}, page_content='thud')] + + **Advanced examples:** Custom vector schema can be supplied to change the way that Redis creates the underlying vector schema. This is useful @@ -235,7 +267,7 @@ class Redis(VectorStore): Otherwise, the schema for newly added samples will be incorrect and metadata will not be returned. - """ + """ # noqa: E501 DEFAULT_VECTOR_SCHEMA = { "name": "content_vector", diff --git a/libs/partners/chroma/langchain_chroma/vectorstores.py b/libs/partners/chroma/langchain_chroma/vectorstores.py index 59debf689cd..76ad86d67a7 100644 --- a/libs/partners/chroma/langchain_chroma/vectorstores.py +++ b/libs/partners/chroma/langchain_chroma/vectorstores.py @@ -131,19 +131,137 @@ def maximal_marginal_relevance( class Chroma(VectorStore): - """`ChromaDB` vector store. + """Chroma vector store integration. - To use, you should have the ``chromadb`` python package installed. + Setup: + Install ``chromadb``, ``langchain-chroma`` packages: - Example: + .. code-block:: bash + + pip install -qU chromadb langchain-chroma + + Key init args — indexing params: + collection_name: str + Name of the collection. + embedding_function: Embeddings + Embedding function to use. + + Key init args — client params: + client: Optional[Client] + Chroma client to use. + client_settings: Optional[chromadb.config.Settings] + Chroma client settings. + persist_directory: Optional[str] + Directory to persist the collection. + + Instantiate: .. code-block:: python - from langchain_chroma import Chroma - from langchain_openai import OpenAIEmbeddings + from langchain_chroma import Chroma + from langchain_openai import OpenAIEmbeddings - embeddings = OpenAIEmbeddings() - vectorstore = Chroma("langchain_store", embeddings) - """ + vector_store = Chroma( + collection_name="foo", + embedding_function=OpenAIEmbeddings(), + # other params... + ) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Update Documents: + .. code-block:: python + + updated_document = Document( + page_content="qux", + metadata={"bar": "baz"} + ) + + vector_store.update_documents(ids=["1"],documents=[updated_document]) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'baz': 'bar'}] + + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"baz": "bar"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * foo [{'baz': 'bar'}] + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.000000] qux [{'bar': 'baz', 'baz': 'bar'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.335463] foo [{'baz': 'bar'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'baz': 'bar'}, page_content='thud')] + + """ # noqa: E501 _LANGCHAIN_DEFAULT_COLLECTION_NAME = "langchain" diff --git a/libs/partners/couchbase/langchain_couchbase/vectorstores.py b/libs/partners/couchbase/langchain_couchbase/vectorstores.py index 6553abb5e71..3748a698cc3 100644 --- a/libs/partners/couchbase/langchain_couchbase/vectorstores.py +++ b/libs/partners/couchbase/langchain_couchbase/vectorstores.py @@ -24,45 +24,161 @@ from langchain_core.vectorstores import VectorStore class CouchbaseVectorStore(VectorStore): - """Couchbase vector store. + """__ModuleName__ vector store integration. - To use it, you need - - a Couchbase database with a pre-defined Search index with support for - vector fields + Setup: + Install ``langchain-couchbase`` and head over to the Couchbase [website](https://cloud.couchbase.com) and create a new connection, with a bucket, collection, and search index. + + .. code-block:: bash + + pip install -U langchain-couchbase - Example: .. code-block:: python - from langchain_couchbase import CouchbaseVectorStore - from langchain_openai import OpenAIEmbeddings + import getpass + + COUCHBASE_CONNECTION_STRING = getpass.getpass("Enter the connection string for the Couchbase cluster: ") + DB_USERNAME = getpass.getpass("Enter the username for the Couchbase cluster: ") + DB_PASSWORD = getpass.getpass("Enter the password for the Couchbase cluster: ") + + Key init args — indexing params: + embedding: Embeddings + Embedding function to use. + + Key init args — client params: + cluster: Cluster + Couchbase cluster object with active connection. + bucket_name: str + Name of the bucket to store documents in. + scope_name: str + Name of the scope in the bucket to store documents in. + collection_name: str + Name of the collection in the scope to store documents in. + index_name: str + Name of the Search index to use. + + Instantiate: + .. code-block:: python - from couchbase.cluster import Cluster - from couchbase.auth import PasswordAuthenticator - from couchbase.options import ClusterOptions from datetime import timedelta + from langchain_openai import OpenAIEmbeddings + from couchbase.auth import PasswordAuthenticator + from couchbase.cluster import Cluster + from couchbase.options import ClusterOptions - auth = PasswordAuthenticator(username, password) + auth = PasswordAuthenticator(DB_USERNAME, DB_PASSWORD) options = ClusterOptions(auth) - connect_string = "couchbases://localhost" - cluster = Cluster(connect_string, options) + cluster = Cluster(COUCHBASE_CONNECTION_STRING, options) # Wait until the cluster is ready for use. cluster.wait_until_ready(timedelta(seconds=5)) - embeddings = OpenAIEmbeddings() + BUCKET_NAME = "langchain_bucket" + SCOPE_NAME = "_default" + COLLECTION_NAME = "default" + SEARCH_INDEX_NAME = "langchain-test-index" - vectorstore = CouchbaseVectorStore( + vector_store = CouchbaseVectorStore( cluster=cluster, - bucket_name="", - scope_name="", - collection_name="", + bucket_name=BUCKET_NAME, + scope_name=SCOPE_NAME, + collection_name=COLLECTION_NAME, embedding=embeddings, - index_name="vector-index", + index_name=SEARCH_INDEX_NAME, ) - vectorstore.add_texts(["hello", "world"]) - results = vectorstore.similarity_search("ola", k=1) - """ + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + # TODO: Fill out with example output. + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with relevant variables and example output. + Search with filter: + .. code-block:: python + + # TODO: Update filter to correct format + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + # TODO: Example output + + # TODO: Fill out with example output. + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + # TODO: Example output + + """ # noqa: E501 # Default batch size DEFAULT_BATCH_SIZE = 100 diff --git a/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py b/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py index d048e8528b1..f498f9a5b3a 100644 --- a/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py +++ b/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py @@ -96,118 +96,126 @@ def maximal_marginal_relevance( class Milvus(VectorStore): - """`Milvus` vector store. + """Milvus vector store integration. - You need to install `pymilvus` and run Milvus. + Setup: + Install ``langchain_milvus`` package: - See the following documentation for how to run a Milvus instance: - https://milvus.io/docs/install_standalone-docker.md + .. code-block:: bash - If looking for a hosted Milvus, take a look at this documentation: - https://zilliz.com/cloud and make use of the Zilliz vectorstore found in - this project. + pip install -qU langchain_milvus - IF USING L2/IP metric, IT IS HIGHLY SUGGESTED TO NORMALIZE YOUR DATA. + Key init args — indexing params: + collection_name: str + Name of the collection. + collection_description: str + Description of the collection. + embedding_function: Embeddings + Embedding function to use. - Parameters: - embedding_function (Embeddings): Function used to embed the text. - collection_name (str): Which Milvus collection to use. Defaults to - "LangChainCollection". - collection_description (str): The description of the collection. Defaults to - "". - collection_properties (Optional[dict[str, any]]): The collection properties. - Defaults to None. - If set, will override collection existing properties. - For example: {"collection.ttl.seconds": 60}. - connection_args (Optional[dict[str, any]]): The connection args used for - this class comes in the form of a dict. - consistency_level (str): The consistency level to use for a collection. - Defaults to "Session". - index_params (Optional[dict]): Which index params to use. Defaults to - HNSW/AUTOINDEX depending on service. - search_params (Optional[dict]): Which search params to use. Defaults to - default of index. - drop_old (Optional[bool]): Whether to drop the current collection. Defaults - to False. - auto_id (bool): Whether to enable auto id for primary key. Defaults to False. - If False, you need to provide text ids (string less than 65535 bytes). - If True, Milvus will generate unique integers as primary keys. - primary_field (str): Name of the primary key field. Defaults to "pk". - text_field (str): Name of the text field. Defaults to "text". - vector_field (str): Name of the vector field. Defaults to "vector". - enable_dynamic_field (Optional[bool]): Whether to enable - dynamic schema or not in Milvus. Defaults to False. - For more information about dynamic schema, please refer to - https://milvus.io/docs/enable-dynamic-field.md - metadata_field (str): Name of the metadata field. Defaults to None. - When metadata_field is specified, - the document's metadata will store as json. - This argument is about to be deprecated, - because it can be replaced by setting `enable_dynamic_field`=True. - partition_key_field (Optional[str]): Name of the partition key field. - Defaults to None. For more information about partition key, please refer to - https://milvus.io/docs/use-partition-key.md#Use-Partition-Key - partition_names (Optional[list]): List of specific partition names. - Defaults to None. For more information about partition, please refer to - https://milvus.io/docs/manage-partitions.md#Manage-Partitions - replica_number (int): The number of replicas for the collection. Defaults to 1. - For more information about replica, please refer to - https://milvus.io/docs/replica.md#In-Memory-Replica - timeout (Optional[float]): The timeout for Milvus operations. Defaults to None. - An optional duration of time in seconds to allow for the RPCs. - If timeout is not set, the client keeps waiting until the server responds - or an error occurs. - num_shards (Optional[int]): The number of shards for the collection. - Defaults to None. For more information about shards, please refer to - https://milvus.io/docs/glossary.md#Shard + Key init args — client params: + connection_args: Optional[dict] + Connection arguments. - The connection args used for this class comes in the form of a dict, - here are a few of the options: - address (str): The actual address of Milvus - instance. Example address: "localhost:19530" - uri (str): The uri of Milvus instance. Example uri: - "path/to/local/directory/milvus_demo.db" for Milvus Lite. - "http://randomwebsite:19530", - "tcp:foobarsite:19530", - "https://ok.s3.south.com:19530". - host (str): The host of Milvus instance. Default at "localhost", - PyMilvus will fill in the default host if only port is provided. - port (str/int): The port of Milvus instance. Default at 19530, PyMilvus - will fill in the default port if only host is provided. - user (str): Use which user to connect to Milvus instance. If user and - password are provided, we will add related header in every RPC call. - password (str): Required when user is provided. The password - corresponding to the user. - secure (bool): Default is false. If set to true, tls will be enabled. - client_key_path (str): If use tls two-way authentication, need to - write the client.key path. - client_pem_path (str): If use tls two-way authentication, need to - write the client.pem path. - ca_pem_path (str): If use tls two-way authentication, need to write - the ca.pem path. - server_pem_path (str): If use tls one-way authentication, need to - write the server.pem path. - server_name (str): If use tls, need to write the common name. - - Example: + Instantiate: .. code-block:: python - from langchain_milvus.vectorstores import Milvus - from langchain_openai.embeddings import OpenAIEmbeddings + from langchain_milvus import Milvus + from langchain_openai import OpenAIEmbeddings - embedding = OpenAIEmbeddings() - # Connect to a milvus instance on localhost - milvus_store = Milvus( - embedding_function = Embeddings, - collection_name = "LangChainCollection", - connection_args = {"uri": "./milvus_demo.db"}, - drop_old = True, - auto_id = True - ) + URI = "./milvus_example.db" - Raises: - ValueError: If the pymilvus python package is not installed. - """ + vector_store = Milvus( + embedding_function=OpenAIEmbeddings(), + connection_args={"uri": URI}, + ) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"baz": "baz"}) + document_3 = Document(page_content="i will be deleted :(", metadata={"baz": "qux"}) + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'baz': 'baz', 'pk': '2'}] + + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'baz': 'baz', 'pk': '2'}] + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.335463] foo [{'baz': 'bar', 'pk': '1'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.335463] foo [{'baz': 'bar', 'pk': '1'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'baz': 'baz', 'pk': '2'}, page_content='thud')] + + """ # noqa: E501 def __init__( self, diff --git a/libs/partners/mongodb/langchain_mongodb/vectorstores.py b/libs/partners/mongodb/langchain_mongodb/vectorstores.py index d177f1653f5..beaf8b6cc36 100644 --- a/libs/partners/mongodb/langchain_mongodb/vectorstores.py +++ b/libs/partners/mongodb/langchain_mongodb/vectorstores.py @@ -45,25 +45,142 @@ DEFAULT_INSERT_BATCH_SIZE = 100_000 class MongoDBAtlasVectorSearch(VectorStore): - """`MongoDB Atlas Vector Search` vector store. + """MongoDBAtlas vector store integration. - To use, you should have both: - - the ``pymongo`` python package installed - - a connection string associated with a MongoDB Atlas Cluster having deployed an - Atlas Search index + Setup: + Install ``langchain-mongodb`` and ``pymongo`` and setup a MongoDB Atlas cluster (read through [this guide](https://www.mongodb.com/docs/manual/reference/connection-string/) to do so). + + .. code-block:: bash + + pip install -qU langchain-mongodb pymongo - Example: .. code-block:: python - from langchain_mongodb import MongoDBAtlasVectorSearch - from langchain_openai import OpenAIEmbeddings - from pymongo import MongoClient + import getpass - mongo_client = MongoClient("") - collection = mongo_client[""][""] - embeddings = OpenAIEmbeddings() - vectorstore = MongoDBAtlasVectorSearch(collection, embeddings) - """ + MONGODB_ATLAS_CLUSTER_URI = getpass.getpass("MongoDB Atlas Cluster URI:") + + Key init args — indexing params: + embedding: Embeddings + Embedding function to use. + + Key init args — client params: + collection: Collection + MongoDB collection to use. + index_name: str + Name of the Atlas Search index. + + Instantiate: + .. code-block:: python + + from pymongo import MongoClient + from langchain_mongodb.vectorstores import MongoDBAtlasVectorSearch + from pymongo import MongoClient + from langchain_openai import OpenAIEmbeddings + + # initialize MongoDB python client + client = MongoClient(MONGODB_ATLAS_CLUSTER_URI) + + DB_NAME = "langchain_test_db" + COLLECTION_NAME = "langchain_test_vectorstores" + ATLAS_VECTOR_SEARCH_INDEX_NAME = "langchain-test-index-vectorstores" + + MONGODB_COLLECTION = client[DB_NAME][COLLECTION_NAME] + + vector_store = MongoDBAtlasVectorSearch( + collection=MONGODB_COLLECTION, + embedding=OpenAIEmbeddings(), + index_name=ATLAS_VECTOR_SEARCH_INDEX_NAME, + relevance_score_fn="cosine", + ) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'_id': '2', 'baz': 'baz'}] + + + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'_id': '2', 'baz': 'baz'}] + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.916096] foo [{'_id': '1', 'baz': 'bar'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.916096] foo [{'_id': '1', 'baz': 'bar'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'_id': '2', 'embedding': [-0.01850726455450058, -0.0014740974875167012, -0.009762819856405258, ...], 'baz': 'baz'}, page_content='thud')] + + """ # noqa: E501 def __init__( self, diff --git a/libs/partners/pinecone/langchain_pinecone/vectorstores.py b/libs/partners/pinecone/langchain_pinecone/vectorstores.py index 5bcc2a96f67..d1cadfcf217 100644 --- a/libs/partners/pinecone/langchain_pinecone/vectorstores.py +++ b/libs/partners/pinecone/langchain_pinecone/vectorstores.py @@ -33,24 +33,140 @@ VST = TypeVar("VST", bound=VectorStore) class PineconeVectorStore(VectorStore): - """`Pinecone` vector store. + """Pinecone vector store integration. - Setup: set the `PINECONE_API_KEY` environment variable to your Pinecone API key. + Setup: + Install ``langchain-pinecone`` and set the environment variable ``PINECONE_API_KEY``. - Example: + .. code-block:: bash + + pip install -qU langchain-pinecone + export PINECONE_API_KEY = "your-pinecone-api-key" + + Key init args — indexing params: + embedding: Embeddings + Embedding function to use. + + Key init args — client params: + index: Optional[Index] + Index to use. + + + # TODO: Replace with relevant init params. + Instantiate: .. code-block:: python - from langchain_pinecone import PineconeVectorStore, PineconeEmbeddings + import time + import os + from pinecone import Pinecone, ServerlessSpec + from langchain_pinecone import PineconeVectorStore + from langchain_openai import OpenAIEmbeddings - embeddings = PineconeEmbeddings(model="multilingual-e5-large") - index_name = "my-index" - namespace = "my-namespace" - vectorstore = PineconeVectorStore( - index_name=index_name, - embedding=embedding, - namespace=namespace, + pc = Pinecone(api_key=os.environ.get("PINECONE_API_KEY")) + + index_name = "langchain-test-index" # change if desired + + existing_indexes = [index_info["name"] for index_info in pc.list_indexes()] + + if index_name not in existing_indexes: + pc.create_index( + name=index_name, + dimension=1536, + metric="cosine", + spec=ServerlessSpec(cloud="aws", region="us-east-1"), + ) + while not pc.describe_index(index_name).status["ready"]: + time.sleep(1) + + index = pc.Index(index_name) + vector_store = PineconeVectorStore(index=index, embedding=OpenAIEmbeddings()) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = ["1", "2", "3"] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=["3"]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz'}] + + Search with filter: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1,filter={"bar": "baz"}) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz'}] + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.832268] foo [{'baz': 'bar'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.832268] foo [{'baz': 'bar'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, ) - """ + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'bar': 'baz'}, page_content='thud')] + + """ # noqa: E501 def __init__( self, diff --git a/libs/partners/qdrant/langchain_qdrant/qdrant.py b/libs/partners/qdrant/langchain_qdrant/qdrant.py index 331de264398..7490baaa97f 100644 --- a/libs/partners/qdrant/langchain_qdrant/qdrant.py +++ b/libs/partners/qdrant/langchain_qdrant/qdrant.py @@ -39,14 +39,138 @@ class RetrievalMode(str, Enum): class QdrantVectorStore(VectorStore): - """`QdrantVectorStore` - Vector store implementation using https://qdrant.tech/ + """Qdrant vector store integration. - Example: + Setup: + Install ``langchain-qdrant`` and ``qdrant-client[fastembed]`` packages. + + .. code-block:: bash + + pip install -qU langchain-qdrant 'qdrant-client[fastembed]' + + Key init args — indexing params: + collection_name: str + Name of the collection. + embedding: Embeddings + Embedding function to use. + + Key init args — client params: + client: QdrantClient + Qdrant client to use. + retrieval_mode: RetrievalMode + Retrieval mode to use. + + Instantiate: .. code-block:: python - from langchain_qdrant import QdrantVectorStore - store = QdrantVectorStore.from_existing_collection("my-collection", embedding, url="http://localhost:6333") - """ + from langchain_qdrant import QdrantVectorStore + from qdrant_client import QdrantClient + from qdrant_client.http.models import Distance, VectorParams + from langchain_openai import OpenAIEmbeddings + + client = QdrantClient(":memory:") + + client.create_collection( + collection_name="demo_collection", + vectors_config=VectorParams(size=1536, distance=Distance.COSINE), + ) + + vector_store = QdrantVectorStore( + client=client, + collection_name="demo_collection", + embedding=OpenAIEmbeddings(), + ) + + Add Documents: + .. code-block:: python + + from langchain_core.documents import Document + from uuid import uuid4 + + document_1 = Document(page_content="foo", metadata={"baz": "bar"}) + document_2 = Document(page_content="thud", metadata={"bar": "baz"}) + document_3 = Document(page_content="i will be deleted :(") + + documents = [document_1, document_2, document_3] + ids = [str(uuid4()) for _ in range(len(documents))] + vector_store.add_documents(documents=documents, ids=ids) + + Delete Documents: + .. code-block:: python + + vector_store.delete(ids=[ids[-1]]) + + Search: + .. code-block:: python + + results = vector_store.similarity_search(query="thud",k=1) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz', '_id': '0d706099-6dd9-412a-9df6-a71043e020de', '_collection_name': 'demo_collection'}] + + Search with filter: + .. code-block:: python + + from qdrant_client.http import models + + results = vector_store.similarity_search(query="thud",k=1,filter=models.Filter(must=[models.FieldCondition(key="metadata.bar", match=models.MatchValue(value="baz"),)])) + for doc in results: + print(f"* {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * thud [{'bar': 'baz', '_id': '0d706099-6dd9-412a-9df6-a71043e020de', '_collection_name': 'demo_collection'}] + + + Search with score: + .. code-block:: python + + results = vector_store.similarity_search_with_score(query="qux",k=1) + for doc, score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.832268] foo [{'baz': 'bar', '_id': '44ec7094-b061-45ac-8fbf-014b0f18e8aa', '_collection_name': 'demo_collection'}] + + Async: + .. code-block:: python + + # add documents + # await vector_store.aadd_documents(documents=documents, ids=ids) + + # delete documents + # await vector_store.adelete(ids=["3"]) + + # search + # results = vector_store.asimilarity_search(query="thud",k=1) + + # search with score + results = await vector_store.asimilarity_search_with_score(query="qux",k=1) + for doc,score in results: + print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]") + + .. code-block:: python + + * [SIM=0.832268] foo [{'baz': 'bar', '_id': '44ec7094-b061-45ac-8fbf-014b0f18e8aa', '_collection_name': 'demo_collection'}] + + Use as Retriever: + .. code-block:: python + + retriever = vector_store.as_retriever( + search_type="mmr", + search_kwargs={"k": 1, "fetch_k": 2, "lambda_mult": 0.5}, + ) + retriever.invoke("thud") + + .. code-block:: python + + [Document(metadata={'bar': 'baz', '_id': '0d706099-6dd9-412a-9df6-a71043e020de', '_collection_name': 'demo_collection'}, page_content='thud')] + + """ # noqa: E501 CONTENT_KEY: str = "page_content" METADATA_KEY: str = "metadata"