{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "KV1BD1ptEchv"
      },
      "source": [
        "# Multivector Search: Efficient Document Retrieval with ColPali and LanceDB  \n",
        "\n",
        "Modern documents—PDFs, scans, forms, invoices, or scientific diagrams—rely heavily on visual elements like tables, figures, and spatial layouts to convey meaning. Retrieving context from these documents poses unique challenges:  \n",
        "- 🖼️ **Loss of Context**: Plain-text extraction destroys critical visual relationships (e.g., a table's structure or a diagram's annotations).  \n",
        "- 🧩 **Multi-Modal Complexity**: Layouts combine text, images, and structured elements that require joint understanding.  \n",
        "- 📏 **Scale vs. Precision**: Balancing pixel-perfect accuracy with efficient search across millions of documents.   \n",
        "\n",
        "### **Why Traditional Methods Fail**  \n",
        "The traditional method is a brittle, multi-stage pipeline where visual context is eroded at every step. Retrieval becomes a \"best guess\" based on partial text. Usually, it will involve the following steps:\n",
        "1. OCR Text Extraction - extract raw text from scanned PDFs/images.\n",
        "2. Layout Detection - use models like LayoutLM or rule-based tools to segment pages into regions (titles, tables, figures).\n",
        "3. Structure Reconstruction - use heuristic rules or ML models try to infer reading order and hierarchy.\n",
        "4. Optional: Image/Table Captioning - apply vision-language models (e.g., GPT-4V) to describe figures/tables in natural language.\n",
        "5. Text Chunking - split text into fixed-size chunks or \"semantic\" passages (e.g., by paragraphs).\n",
        "6. Embedding & Indexing- use text-based embeddings (e.g., BERT) and store in a vector DB (e.g., LanceDB).\n",
        "\n",
        "\n",
        "## **Our Approach: ColPali with XTR for performant retrieval**  \n",
        "ColPali (Contextualized Late Interaction Over PaliGemma) enhances document retrieval by combining a vision-language model (VLM) with a multi-vector late interaction framework inspired by ColBERT. In this framework, documents and queries are encoded as collections of contextualized vectors—precomputed for documents and indexed for queries. Unlike traditional methods, late interaction defers complex similarity computations between query and document vectors until the final retrieval stage, enabling nuanced semantic matching while maintaining efficiency.\n",
        "\n",
        "To further accelerate retrieval, we integrate XTR (ConteXtualized Token Retriever), which prioritizes critical document tokens during initial retrieval stage and removes the gathering stage to significantly improve the performance. By focusing on the most semantically salient tokens early in the process, XTR reduces computational complexity with improved recall, ensuring rapid identification of candidate documents.\n",
        "\n",
        "We used the [UFO dataset](https://huggingface.co/datasets/davanstrien/ufo-ColPali), a dataset with rich tables, images and text, to demonstrate how to efficiently retrieve documents with ColPali and LanceDB.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9N9tRap0YYp4"
      },
      "source": [
        "## Step 1: Install Required Libraries\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "collapsed": true,
        "id": "gpdJn4hACfKH",
        "outputId": "cbd70419-10be-4e4a-ba71-ac0d69a8ddbc"
      },
      "outputs": [],
      "source": [
        "!pip install lancedb colpali-engine datasets tqdm"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ksA8HdigzAUX"
      },
      "source": [
        "## Step 2: Load the UFO dataset\n",
        "\n",
        "The UFO dataset has 2243 rows in total with an embedding of 128 dimension each. We show an example of the document to show how complicated it is with text and images blended in the document."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 966,
          "referenced_widgets": [
            "d775b534a39344cba9e195a01c51be75",
            "90e52ab802524ded8984c7b78a418a03",
            "ae669a2953964f39b25dbd2e7751411d",
            "98b6780cc7844590826e5cd88c137ea1",
            "c8535071d9844f799906c05600c20054",
            "e2278f45421341bb8b1734cba0af03cb",
            "005c8d29152442a49c19a0a83af59e7a",
            "a0bf2b988497416f84be6a5dc885f737",
            "6d96275900c6476aaab29a63b6d60633",
            "756d72d9a4ba4ee5b769f1195b102baa",
            "11a92b5b3ceb4bd6a1999cbf782b1e64",
            "b9a22198fef8401ebf0bc2e8c82375a1",
            "ec250244f0b646319d681b05a04d82fe",
            "e09850af523c4686a7a0dce2dc5bb5ff",
            "6154a5ea5cd54dd19f0d068c776cb557",
            "01d711bc69e84f41a72e0ce9a2b478ed",
            "05b4d493f761440b91bc0854f04e40d7",
            "e6b3bee55adb42779ddf515be4046558",
            "93f5eb6c41634347832f80dd87c7a170",
            "e4516c525f794c2b9b5a262cf7dae8a6",
            "5e7ab3f1efae4dd491d503ac66b4d8ae",
            "ac080da8dc854206a2569d976b1e678c",
            "85af8ed925b64edda995c303bbd056dc",
            "9e29e04a66d04dad8c20d27e6b728c08",
            "672f5f6abe484d189fb3935ed8dfedbe",
            "6d1be941a7d747159a14b9ed0d17c870",
            "05e78c7519de4f8d89b26f48dc939653",
            "f0e80b24db734aa58da30af62d1b4a45",
            "615c1d314b3e4af893d6ac7a1bba947d",
            "0796ad794d8246f7a6b68a10dab2507a",
            "08ba8c88373440f8902641a9ca2af659",
            "95d85d6556e047a4b923cc2464f85038",
            "0ded705820f14618a77cb022c5f7e60d"
          ]
        },
        "id": "LlclgxJPD5RX",
        "outputId": "ada253b5-2a73-4a66-f20e-7ff0725db89a"
      },
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n",
            "The secret `HF_TOKEN` does not exist in your Colab secrets.\n",
            "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n",
            "You will be able to reuse this secret in all of your notebooks.\n",
            "Please note that authentication is recommended but still optional to access public models or datasets.\n",
            "  warnings.warn(\n"
          ]
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "d775b534a39344cba9e195a01c51be75",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "README.md:   0%|          | 0.00/1.20k [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "b9a22198fef8401ebf0bc2e8c82375a1",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "train-00000-of-00001.parquet:   0%|          | 0.00/293M [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "85af8ed925b64edda995c303bbd056dc",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "Generating train split:   0%|          | 0/2243 [00:00<?, ? examples/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/jpeg": "[encoded data, 84244 characters]",
            "image/png": "[encoded data, 264444 characters]",
            "text/plain": [
              "<PIL.JpegImagePlugin.JpegImageFile image mode=RGB size=516x729>"
            ]
          },
          "execution_count": 2,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from math import sqrt\n",
        "\n",
        "import pyarrow as pa\n",
        "from tqdm import tqdm\n",
        "import lancedb\n",
        "from datasets import load_dataset\n",
        "from colpali_engine.models import ColPali, ColPaliProcessor\n",
        "import torch\n",
        "\n",
        "dataset = load_dataset(\"davanstrien/ufo-ColPali\", split=\"train\")\n",
        "dataset\n",
        "dataset[333][\"image\"]"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "JNOEkERhHPj2"
      },
      "source": [
        "## Step 3: Load the ColPali model\n",
        "Note: select \"cuda\" if you are using a Nvidia GPU or \"cpu\" if there is no GPU available. Mac users, please use \"mps\". This step can take a few minutes."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 209,
          "referenced_widgets": [
            "cde94d99d80b4cd2840d059915fa013c",
            "68ad5106210e41ab8e734654efb72a15",
            "c97d2a0f056a489796e2a3d94d215e7a",
            "357592d483cf455bb8c1827206c4095e",
            "2e4c2c91a55245948ab081f28c1b82f8",
            "0ba339486c664527bdc054b9b263788c",
            "f3ffd4ad251444b5b3853892283647eb",
            "36c8a4e9678441ddaab31058055bd1f6",
            "e1c7d6755a0b441789f11e8036f64887",
            "aca80906222b44f3bc07f853d1bd8bec",
            "5553411ece384eab80f386ff96baf699",
            "6566b65210dd43999016b0df0849fc73",
            "a553e15b721c48ddb97a4598d86666a9",
            "e1a8b2ca2f4646f8845418b16fc59d5a",
            "807f767e339a46b294722598a81c3e59",
            "70fa11934f7248ebb747ec3c596fc701",
            "d19ede522db8485a90856d53faa8c9d0",
            "bb52b41e3c8c41318818e67b56fc65c5",
            "f62b475cd41c4851a81b975d83b52749",
            "d5de815e1a8248a4b9575fa5598c5c62",
            "bfa1711e640c4201a11df8d9afae050e",
            "09d915c9edf549f6b232cc8ce7cfc996",
            "cdd4ecaaecf245f7af640c257b9a2766",
            "da332a65a37749339228b607235d9c7a",
            "7fc947f4987c4fd98d40bdbccfb5258e",
            "13a43832485f48baa1ba6ba4fce79a6d",
            "a9b76d6b46304e6e9ad0c6453b2ed586",
            "6f469e077ca84d23982f613370e39cea",
            "2026e624f025456c883bff0c8049018f",
            "ef20ca0fb9de4cab8565c9d97caa8b95",
            "5158feb04bf64f83acccae97a0865dc3",
            "d876b1afc539414e89faa4c006bf1817",
            "4189dce33b624109981b95ac07772da7",
            "f9d3b4ce592f4ab9b02d8b9edff02009",
            "ede10e47e8e44e3e8e01e1ece68d5153",
            "350b217283524a57bd99046cedf5ba40",
            "3b71fe200a0649a193c756a64795caa1",
            "2df22e0c5104439bb7016684af2410ed",
            "3f6a812bdcf24e268a503c7ae301c9cd",
            "5404823a8c46495a9270c01f73b4ef7c",
            "dbf17f8bcc6c4b38957e68182f8ca8ed",
            "60940513638a4cf19b11c1ac6c4ebb92",
            "6797a69ac9994287b925f85bebe75d94",
            "73190fa8b6ff419790fea7ad08f39fe6",
            "ae63c7933e4f4d4e96444bc62278c8fa",
            "fd374e9970944060a5ebb8945f40c011",
            "ac58985e76c847409cca99656873af12",
            "c6329d230cf840fc9ec6f218dc138f36",
            "6ec05172da164cdebf0f3b7f7ad8f261",
            "832e39123e7748b8a0f38f146748a257",
            "489bddd5e8144339b564790884cac871",
            "55110dbbd8e84f5cb4ffebe941773fcd",
            "61dbfaf2b1074ec9852a3f0fa0f4852b",
            "9df60a8d17ab4d68a985ff9fc4646a19",
            "0b024509a62441b7ae9820c8ae3c1ffc",
            "d86ef06f81b54514b5f39355adc03a25",
            "b47747512c2e4da3be712bb365d83639",
            "30cc200421da4851b748dbf99a5ba170",
            "c3d9356deb674b15ad984f23f4503262",
            "61027258a71d4dfdb3c8529c84eadcdd",
            "0bbe0ea286a140e38271cb2cf941788c",
            "c835a0d14e7a41d8bec9fbffe5825cc2",
            "ea756961fae74bad8f72c962c052167b",
            "adf0fd690527498faa8467c966ea516f",
            "cc1e5b6f7d114c48a5ac586ed5726990",
            "4e326b3e73c64666976e24301297927c"
          ]
        },
        "id": "ilFhcjuoEOs-",
        "outputId": "1db488c2-b574-45f2-a38a-c913b597829e"
      },
      "outputs": [
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "cde94d99d80b4cd2840d059915fa013c",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "Loading checkpoint shards:   0%|          | 0/2 [00:00<?, ?it/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "6566b65210dd43999016b0df0849fc73",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "adapter_model.safetensors:   0%|          | 0.00/157M [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "cdd4ecaaecf245f7af640c257b9a2766",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "preprocessor_config.json:   0%|          | 0.00/425 [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "f9d3b4ce592f4ab9b02d8b9edff02009",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "tokenizer_config.json:   0%|          | 0.00/243k [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "ae63c7933e4f4d4e96444bc62278c8fa",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "tokenizer.json:   0%|          | 0.00/17.8M [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "application/vnd.jupyter.widget-view+json": {
              "model_id": "d86ef06f81b54514b5f39355adc03a25",
              "version_major": 2,
              "version_minor": 0
            },
            "text/plain": [
              "special_tokens_map.json:   0%|          | 0.00/733 [00:00<?, ?B/s]"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# load the model\n",
        "colpali_model = ColPali.from_pretrained(\n",
        "    \"davanstrien/finetune_colpali_v1_2-ufo-4bit\",\n",
        "    torch_dtype=torch.bfloat16,\n",
        "    device_map=\"cuda\",  # change to cuda if you have a Nvidia GPU, or cpu if you don't have any GPU\n",
        ")\n",
        "colpali_processor = ColPaliProcessor.from_pretrained(\n",
        "    \"vidore/colpaligemma-3b-pt-448-base\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "hyuqjOGJaMIL"
      },
      "source": [
        "## Step 4: Connect to LanceDB\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "G_nDw0HWHFM3"
      },
      "outputs": [],
      "source": [
        "# create the lancedb table\n",
        "multivector_type = pa.list_(pa.list_(pa.float32(), 128))\n",
        "schema = pa.schema(\n",
        "    [\n",
        "        pa.field(\"id\", pa.int64()),\n",
        "        pa.field(\"vector\", multivector_type),\n",
        "    ]\n",
        ")\n",
        "db = lancedb.connect(\"my_database\")\n",
        "table = db.create_table(\"ufo\", schema=schema, exist_ok=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "d5oAQri0fJOH"
      },
      "source": [
        "**⚠️ WARNING: LONG EMBEDDING & INGESTION STEP**  \n",
        "**❗ Skip this cell unless you want to re-run the full embedding process.**  \n",
        "\n",
        "---\n",
        "\n",
        "**Why?**  \n",
        "Embedding the UFO dataset and ingesting it into LanceDB takes **~2 hours on a T4 GPU**. To save time:  \n",
        "- **Use the pre-prepared table with index created** (provided below) to proceed directly to **Step 7**: search.  \n",
        "- **Step 5a** contains the full ingestion code for reference (run it only if necessary).  \n",
        "- **Step 6** contains the details on creating the index on the multivector column"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "Z3aOlPI5eP2Y"
      },
      "source": [
        "## Step 5: Open the LanceDB table"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "collapsed": true,
        "id": "pVv1IuWeX84a",
        "outputId": "e0e8abcf-2823-4f4a-9b5c-acb684e0c9f6"
      },
      "outputs": [],
      "source": [
        "!wget http://vectordb-recipes.s3.us-west-2.amazonaws.com/multivector_example.zip\n",
        "!unzip multivector_example.zip\n",
        "!rm multivector_example.zip"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "wW3Tmy4qb52_"
      },
      "outputs": [],
      "source": [
        "db = lancedb.connect(\"multivector_example\")\n",
        "table_name = \"ufo\"\n",
        "table = db.open_table(\"ufo\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "N18gaP-sjufJ"
      },
      "source": [
        "## Step 7: Retrieve documents from a query\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "jjbXmUH4kAfi",
        "outputId": "d680c038-bf65-4081-9ef1-b121e319e83b"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "query shape: (15, 128)\n",
            "query shape: (16, 128)\n",
            "query shape: (15, 128)\n"
          ]
        }
      ],
      "source": [
        "# search the table\n",
        "queries = [\"alien\", \"crop circles\", \"unidentified\"]\n",
        "image_results = []\n",
        "for query_text in queries:\n",
        "    # encode the query\n",
        "    query = colpali_processor.process_queries([query_text]).to(colpali_model.device)\n",
        "    query = colpali_model(**query)[0].cpu().float().numpy()\n",
        "    print(f\"query shape: {query.shape}\")\n",
        "\n",
        "    # search the table\n",
        "    results = table.search(query).select([\"id\"]).limit(5).to_arrow()\n",
        "    id = results[\"id\"][0].as_py()\n",
        "    image = dataset[id][\"image\"]\n",
        "    image_results.append(image)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ZcHRPwxFt-mz"
      },
      "source": [
        "# Let's see the retrieved documents!"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 1000
        },
        "id": "Jt1OGYVItlDI",
        "outputId": "03c706d0-7b79-4b69-929c-50198ea07c31"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 144456 characters]",
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "[encoded data, 156080 characters]",
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "[encoded data, 180492 characters]",
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "for image in image_results:\n",
        "    plt.figure()\n",
        "    plt.imshow(image)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Step 5a: Embed the UFO dataset and ingest data into LanceDB\n",
        "\n",
        "Note: This step will take up to 2h when running with T4 GPU with a `batch_size=4`. You can increase the `batch_size` to accelerate the process if there is more memory available, e.g. `batch_size=32` requires 60GB of memory.\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "# this would take 40 mins for first run on Apple M3 Max, may be longer if you are using CPU\n",
        "batch_size = 4 # low it if you have a low memory GPU\n",
        "with tqdm(total=len(dataset), desc=\"ingesting\") as pbar:\n",
        "    for i in range(0, len(dataset), batch_size):\n",
        "        batch = dataset[i : i + batch_size]\n",
        "        images = batch[\"image\"]\n",
        "\n",
        "        # encode the images\n",
        "        with torch.no_grad():\n",
        "            batch_images = colpali_processor.process_images(images).to(\n",
        "                colpali_model.device\n",
        "            )\n",
        "            image_embeddings = colpali_model(**batch_images)\n",
        "\n",
        "        real_size = len(images)\n",
        "        multivector = image_embeddings.cpu().float().numpy()\n",
        "        multivector = pa.array(multivector.tolist(), type=multivector_type)\n",
        "        data = pa.Table.from_pydict(\n",
        "            {\n",
        "                \"id\": list(range(i, i + real_size)),\n",
        "                \"vector\": multivector,\n",
        "            }\n",
        "        )\n",
        "        table.add(data)\n",
        "        pbar.update(real_size)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Step 6: Create an index on the multivector column\n",
        "Note: LanceDB Cloud automatically infers the multivector column directly from the schema. If your dataset contains only one column with a list of vectors, no manual specification is required when building the vector index—the system handles this implicitly.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [],
      "source": [
        "num_rows = table.count_rows()\n",
        "table.create_index(\n",
        "    metric=\"cosine\",  # for now only cosine is supported for multivector\n",
        "    num_partitions=int(\n",
        "        sqrt(num_rows * 1030) # 1030 is number of embeddings per document\n",
        "    ),  # it's recommended to set sqrt of the number of embeddings as the number of partitions\n",
        "    num_sub_vectors=32,  # higher for accuracy, lower for speed\n",
        "    index_type=\"IVF_PQ\",\n",
        ")"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
