diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c59f99..8988b03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.5.6 + rev: v0.15.12 hooks: # Run the linter. - id: ruff diff --git a/src/vdf_io/import_vdf/astradb_import.py b/src/vdf_io/import_vdf/astradb_import.py index 5d20dc0..0fa05b7 100644 --- a/src/vdf_io/import_vdf/astradb_import.py +++ b/src/vdf_io/import_vdf/astradb_import.py @@ -124,7 +124,7 @@ def upsert_data(self, via_cql=False): data_path = namespace_meta["data_path"] final_data_path = self.get_final_data_path(data_path) new_index_name = index_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) @@ -162,7 +162,7 @@ def upsert_data(self, via_cql=False): self.session.execute( f"CREATE TABLE IF NOT EXISTS {self.args['keyspace']}.{new_index_name}" - f" (id text PRIMARY KEY, \"$vector\" vector)" + f' (id text PRIMARY KEY, "$vector" vector)' ) parquet_files = self.get_parquet_files(final_data_path) vectors = {} @@ -208,7 +208,7 @@ def flush_to_db(self, vectors, metadata, collection, via_cql, parallel=True): keys = list(set(vectors.keys()).union(set(metadata.keys()))) for id in keys: self.session.execute( - f"INSERT INTO {self.args['keyspace']}.{collection.name} (id, \"$vector\", {', '.join(metadata[id].keys())}) " + f'INSERT INTO {self.args["keyspace"]}.{collection.name} (id, "$vector", {", ".join(metadata[id].keys())}) ' f"VALUES ('{id}', {vectors[id]}, {', '.join([str(v) for v in metadata[id].values()])})" ) return len(vectors) @@ -248,12 +248,15 @@ def flush_batch_to_db(collection, keys, vectors, metadata): for i in range(0, total_points, BATCH_SIZE) ] - with concurrent.futures.ThreadPoolExecutor( - max_workers=num_parallel_threads - ) as executor, tqdm( - total=total_points, - desc=f"Flushing to DB in batches of {BATCH_SIZE} in {num_parallel_threads} threads", - ) as pbar: + with ( + concurrent.futures.ThreadPoolExecutor( + max_workers=num_parallel_threads + ) as executor, + tqdm( + total=total_points, + desc=f"Flushing to DB in batches of {BATCH_SIZE} in {num_parallel_threads} threads", + ) as pbar, + ): future_to_batch = { executor.submit(flush_batch_to_db, collection, *batch): batch for batch in batches diff --git a/src/vdf_io/import_vdf/chroma_import.py b/src/vdf_io/import_vdf/chroma_import.py index 079898c..0ad247f 100644 --- a/src/vdf_io/import_vdf/chroma_import.py +++ b/src/vdf_io/import_vdf/chroma_import.py @@ -123,7 +123,7 @@ def upsert_data(self): parquet_files = self.get_parquet_files(final_data_path) new_index_name = index_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) diff --git a/src/vdf_io/import_vdf/kdbai_import.py b/src/vdf_io/import_vdf/kdbai_import.py index 9e9a523..4aaa1c0 100644 --- a/src/vdf_io/import_vdf/kdbai_import.py +++ b/src/vdf_io/import_vdf/kdbai_import.py @@ -99,7 +99,7 @@ def upsert_data(self): data_path = namespace_meta["data_path"] final_data_path = self.get_final_data_path(data_path) index_name = index_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) diff --git a/src/vdf_io/import_vdf/lancedb_import.py b/src/vdf_io/import_vdf/lancedb_import.py index 458cab1..50b82bc 100644 --- a/src/vdf_io/import_vdf/lancedb_import.py +++ b/src/vdf_io/import_vdf/lancedb_import.py @@ -89,7 +89,7 @@ def upsert_data(self): parquet_files = self.get_parquet_files(final_data_path) new_index_name = index_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) diff --git a/src/vdf_io/import_vdf/milvus_import.py b/src/vdf_io/import_vdf/milvus_import.py index 87250b7..5605934 100644 --- a/src/vdf_io/import_vdf/milvus_import.py +++ b/src/vdf_io/import_vdf/milvus_import.py @@ -82,7 +82,7 @@ def upsert_data(self): self.set_dims(namespace_meta, collection_name) data_path = namespace_meta["data_path"] index_name = collection_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) diff --git a/src/vdf_io/import_vdf/qdrant_import.py b/src/vdf_io/import_vdf/qdrant_import.py index 65041d9..fc0725f 100644 --- a/src/vdf_io/import_vdf/qdrant_import.py +++ b/src/vdf_io/import_vdf/qdrant_import.py @@ -313,12 +313,15 @@ def get_nested_config(config, keys, default=None): total_points = len(points) num_parallel_threads = self.args.get("parallel", 5) or 5 - with concurrent.futures.ThreadPoolExecutor( - max_workers=num_parallel_threads - ) as executor, tqdm( - total=total_points, - desc=f"Uploading points in batches of {BATCH_SIZE} in {num_parallel_threads} threads", - ) as pbar: + with ( + concurrent.futures.ThreadPoolExecutor( + max_workers=num_parallel_threads + ) as executor, + tqdm( + total=total_points, + desc=f"Uploading points in batches of {BATCH_SIZE} in {num_parallel_threads} threads", + ) as pbar, + ): # Create a future to batch mapping to update progress bar correctly after each batch completion future_to_batch = { executor.submit( diff --git a/src/vdf_io/import_vdf/turbopuffer_import.py b/src/vdf_io/import_vdf/turbopuffer_import.py index a31ef0e..f4ff170 100644 --- a/src/vdf_io/import_vdf/turbopuffer_import.py +++ b/src/vdf_io/import_vdf/turbopuffer_import.py @@ -73,7 +73,7 @@ def upsert_data(self): parquet_files = self.get_parquet_files(final_data_path) new_index_name = index_name + ( - f'_{namespace_meta["namespace"]}' + f"_{namespace_meta['namespace']}" if namespace_meta["namespace"] else "" ) diff --git a/src/vdf_io/marqo_vespa_util.py b/src/vdf_io/marqo_vespa_util.py index e3326c8..d8351bc 100644 --- a/src/vdf_io/marqo_vespa_util.py +++ b/src/vdf_io/marqo_vespa_util.py @@ -85,7 +85,7 @@ def get_all_documents( [f"{key}={value}" for key, value in query_params.items() if value] ) url = f"{self.document_url}/document/v1/{schema}/{schema}/docid" - url = f'{url.strip("?")}?{query_string}' + url = f"{url.strip('?')}?{query_string}" print(f"{url=}") resp = self.http_client.get(url) except httpx.HTTPError as e: diff --git a/src/vdf_io/notebooks/01_Introducing_txtai.ipynb b/src/vdf_io/notebooks/01_Introducing_txtai.ipynb index 40b7e95..d6e85e7 100644 --- a/src/vdf_io/notebooks/01_Introducing_txtai.ipynb +++ b/src/vdf_io/notebooks/01_Introducing_txtai.ipynb @@ -1,1112 +1,1169 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "POWZoSJR6XzK" - }, - "source": [ - "# Introducing txtai\n", - "\n", - "[txtai](https://github.com/neuml/txtai) is an all-in-one embeddings database for semantic search, LLM orchestration and language model workflows.\n", - "\n", - "Embeddings databases are a union of vector indexes (sparse and dense), graph networks and relational databases. This enables vector search with SQL, topic modeling, retrieval augmented generation and more.\n", - "\n", - "Embeddings databases can stand on their own and/or serve as a powerful knowledge source for large language model (LLM) prompts.\n", - "\n", - "The following is a summary of key features:\n", - "\n", - "- πŸ”Ž Vector search with SQL, object storage, topic modeling, graph analysis and multimodal indexing\n", - "- πŸ“„ Create embeddings for text, documents, audio, images and video\n", - "- πŸ’‘ Pipelines powered by language models that run LLM prompts, question-answering, labeling, transcription, translation, summarization and more\n", - "- β†ͺ️️ Workflows to join pipelines together and aggregate business logic. txtai processes can be simple microservices or multi-model workflows.\n", - "- βš™οΈ Build with Python or YAML. API bindings available for [JavaScript](https://github.com/neuml/txtai.js), [Java](https://github.com/neuml/txtai.java), [Rust](https://github.com/neuml/txtai.rs) and [Go](https://github.com/neuml/txtai.go).\n", - "- ☁️ Run local or scale out with container orchestration\n", - "\n", - "txtai is built with Python 3.8+, [Hugging Face Transformers](https://github.com/huggingface/transformers), [Sentence Transformers](https://github.com/UKPLab/sentence-transformers) and [FastAPI](https://github.com/tiangolo/fastapi). txtai is open-source under an Apache 2.0 license." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "qa_PPKVX6XzN" - }, - "source": [ - "# Install dependencies\n", - "\n", - "Install `txtai` and all dependencies." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19", - "_kg_hide-output": true, - "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5", - "id": "24q-1n5i6XzQ", - "trusted": true - }, - "outputs": [], - "source": [ - "%%capture\n", - "# !pip install git+https://github.com/neuml/txtai#egg=txtai[graph]\n", - "\n", - "# Install translation pipeline dependencies for later examples\n", - "!pip install txtai sentencepiece sacremoses fasttext" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DLIjSzbq6Xzx" - }, - "source": [ - "# Semantic search\n", - "\n", - "Embeddings databases are the engine that delivers semantic search. Data is transformed into embeddings vectors where similar concepts will produce similar vectors. Indexes both large and small are built with these vectors. The indexes are used to find results that have the same meaning, not necessarily the same keywords.\n", - "\n", - "The basic use case for an embeddings database is building an approximate nearest neighbor (ANN) index for semantic search. The following example indexes a small number of text entries to demonstrate the value of semantic search.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "QxX9EtIc6Xzg", - "trusted": true - }, - "outputs": [ - { - "ename": "", - "evalue": "", - "output_type": "error", - "traceback": [ - "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", - "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", - "\u001b[1;31mClick here for more info. \n", - "\u001b[1;31mView Jupyter log for further details." - ] - } - ], - "source": [ - "from txtai import Embeddings\n", - "\n", - "# Works with a list, dataset or generator\n", - "data = [\n", - " \"US tops 5 million confirmed virus cases\",\n", - " \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\",\n", - " \"Beijing mobilises invasion craft along coast as Taiwan tensions escalate\",\n", - " \"The National Park Service warns against sacrificing slower friends in a bear attack\",\n", - " \"Maine man wins $1M from $25 lottery ticket\",\n", - " \"Make huge profits without work, earn up to $100,000 a day\"\n", - "]\n", - "\n", - "# Create an embeddings\n", - "embeddings = Embeddings(path=\"sentence-transformers/nli-mpnet-base-v2\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "cXfZtdHD6Xzy", - "outputId": "369b637e-1e1c-4229-f68e-92917be5fbd0", - "trusted": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Query Best Match\n", - "--------------------------------------------------\n", - "feel good story Maine man wins $1M from $25 lottery ticket\n", - "climate change Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n", - "public health story US tops 5 million confirmed virus cases\n", - "war Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", - "wildlife The National Park Service warns against sacrificing slower friends in a bear attack\n", - "asia Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", - "lucky Maine man wins $1M from $25 lottery ticket\n", - "dishonest junk Make huge profits without work, earn up to $100,000 a day\n" - ] - } - ], - "source": [ - "# Create an index for the list of text\n", - "embeddings.index(data)\n", - "\n", - "print(\"%-20s %s\" % (\"Query\", \"Best Match\"))\n", - "print(\"-\" * 50)\n", - "\n", - "# Run an embeddings search for each query\n", - "for query in (\"feel good story\", \"climate change\", \"public health story\", \"war\", \"wildlife\", \"asia\", \"lucky\", \"dishonest junk\"):\n", - " # Extract uid of first result\n", - " # search result format: (uid, score)\n", - " uid = embeddings.search(query, 1)[0][0]\n", - "\n", - " # Print text\n", - " print(\"%-20s %s\" % (query, data[uid]))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "kIMbLW0t6Xzw" - }, - "source": [ - "The example above shows that for all of the queries, the query text isn’t in the data. This is the true power of transformers models over token based search. What you get out of the box is πŸ”₯πŸ”₯πŸ”₯!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6m7sYUj_AdOL" - }, - "source": [ - "# Updates and deletes\n", - "\n", - "Updates and deletes are supported for embeddings. The upsert operation will insert new data and update existing data\n", - "\n", - "The following section runs a query, then updates a value changing the top result and finally deletes the updated value to revert back to the original query results." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "2CERR0U2Ac8C", - "outputId": "0c1f4dd2-1319-410b-91a4-7753adba2c26" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Initial: Maine man wins $1M from $25 lottery ticket\n", - "After update: See it: baby panda born\n", - "After delete: Maine man wins $1M from $25 lottery ticket\n" - ] - } - ], - "source": [ - "# Run initial query\n", - "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", - "print(\"Initial: \", data[uid])\n", - "\n", - "# Create a copy of data to modify\n", - "udata = data.copy()\n", - "\n", - "# Update data\n", - "udata[0] = \"See it: baby panda born\"\n", - "embeddings.upsert([(0, udata[0], None)])\n", - "\n", - "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", - "print(\"After update: \", udata[uid])\n", - "\n", - "# Remove record just added from index\n", - "embeddings.delete([0])\n", - "\n", - "# Ensure value matches previous value\n", - "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", - "print(\"After delete: \", udata[uid])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "6TCVl6QA6Xz5" - }, - "source": [ - "# Persistence\n", - "\n", - "Embeddings can be saved to storage and reloaded." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "5gyO90Hc6Xz7", - "outputId": "5460fcd8-5b9f-4064-9ac3-f72e5db9ecf4", - "trusted": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n" - ] - } - ], - "source": [ - "embeddings.save(\"index\")\n", - "\n", - "embeddings = Embeddings()\n", - "embeddings.load(\"index\")\n", - "\n", - "uid = embeddings.search(\"climate change\", 1)[0][0]\n", - "print(data[uid])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "giNZ_fHmqT8u" - }, - "source": [ - "# Hybrid search\n", - "\n", - "While dense vector indexes are by far the best option for semantic search systems, sparse keyword indexes can still add value. There may be cases where finding an exact match is important.\n", - "\n", - "Hybrid search combines the results from sparse and dense vector indexes for the best of both worlds." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "lclxiRFRqsFv", - "outputId": "3bd15b63-3bf4-4132-a819-0f560fce3f92" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Query Best Match\n", - "--------------------------------------------------\n", - "feel good story Maine man wins $1M from $25 lottery ticket\n", - "climate change Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n", - "public health story US tops 5 million confirmed virus cases\n", - "war Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", - "wildlife The National Park Service warns against sacrificing slower friends in a bear attack\n", - "asia Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", - "lucky Maine man wins $1M from $25 lottery ticket\n", - "dishonest junk Make huge profits without work, earn up to $100,000 a day\n" - ] - } - ], - "source": [ - "# Create an embeddings\n", - "embeddings = Embeddings(hybrid=True, path=\"sentence-transformers/nli-mpnet-base-v2\")\n", - "\n", - "# Create an index for the list of text\n", - "embeddings.index(data)\n", - "\n", - "print(\"%-20s %s\" % (\"Query\", \"Best Match\"))\n", - "print(\"-\" * 50)\n", - "\n", - "# Run an embeddings search for each query\n", - "for query in (\"feel good story\", \"climate change\", \"public health story\", \"war\", \"wildlife\", \"asia\", \"lucky\", \"dishonest junk\"):\n", - " # Extract uid of first result\n", - " # search result format: (uid, score)\n", - " uid = embeddings.search(query, 1)[0][0]\n", - "\n", - " # Print text\n", - " print(\"%-20s %s\" % (query, data[uid]))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "d9beQSw-vhz8" - }, - "source": [ - "Same results as with semantic search. Let's run the same example with just a keyword index to view those results." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "WykNb8y3vohL", - "outputId": "5617e912-1014-495c-9dc9-e5729988d77f" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[]\n", - "[(4, 0.5234998733628726)]\n" - ] - } - ], - "source": [ - "# Create an embeddings\n", - "embeddings = Embeddings(keyword=True)\n", - "\n", - "# Create an index for the list of text\n", - "embeddings.index(data)\n", - "\n", - "print(embeddings.search(\"feel good story\"))\n", - "print(embeddings.search(\"lottery\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "P0FLRsrmv2hB" - }, - "source": [ - "See that when the embeddings instance only uses a keyword index, it can't find semantic matches, only keyword matches." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "0p3WCDniUths" - }, - "source": [ - "# Content storage\n", - "\n", - "Up to this point, all the examples are referencing the original data array to retrieve the input text. This works fine for a demo but what if you have millions of documents? In this case, the text needs to be retrieved from an external datastore using the id.\n", - "\n", - "Content storage adds an associated database (i.e. SQLite, DuckDB) that stores associated metadata with the vector index. The document text, additional metadata and additional objects can be stored and retrieved right alongside the indexed vectors." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "MOntBQIdVv-J", - "outputId": "c9d0d3e7-d7b4-4421-f63d-402db6918cca" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Maine man wins $1M from $25 lottery ticket\n" - ] - } - ], - "source": [ - "# Create embeddings with content enabled. The default behavior is to only store indexed vectors.\n", - "embeddings = Embeddings(path=\"sentence-transformers/nli-mpnet-base-v2\", content=True, objects=True)\n", - "\n", - "# Create an index for the list of text\n", - "embeddings.index(data)\n", - "\n", - "print(embeddings.search(\"feel good story\", 1)[0][\"text\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "hHGvhZm-ZTzL" - }, - "source": [ - "The only change above is setting the *content* flag to True. This enables storing text and metadata content (if provided) alongside the index. Note how the text is pulled right from the query result!\n", - "\n", - "Let's add some metadata." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "BYWUFBUGyKyY" - }, - "source": [ - "# Query with SQL\n", - "\n", - "When content is enabled, the entire dictionary is stored and can be queried. In addition to vector queries, txtai accepts SQL queries. This enables combined queries using both a vector index and content stored in a database backend." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "aPH-dnV2ZuL1", - "outputId": "c563060c-d292-4b19-aa64-f4c629008cdb" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[{'text': 'The National Park Service warns against sacrificing slower friends in a bear attack', 'score': 0.3151373863220215}]\n", - "[{'text': 'Maine man wins $1M from $25 lottery ticket', 'length': 42, 'score': 0.08329027891159058}]\n", - "[{'count(*)': 6, 'min(length)': 39, 'max(length)': 94, 'sum(length)': 387}]\n" - ] - } - ], - "source": [ - "# Create an index for the list of text\n", - "embeddings.index([{\"text\": text, \"length\": len(text)} for text in data])\n", - "\n", - "# Filter by score\n", - "print(embeddings.search(\"select text, score from txtai where similar('hiking danger') and score >= 0.15\"))\n", - "\n", - "# Filter by metadata field 'length'\n", - "print(embeddings.search(\"select text, length, score from txtai where similar('feel good story') and score >= 0.05 and length >= 40\"))\n", - "\n", - "# Run aggregate queries\n", - "print(embeddings.search(\"select count(*), min(length), max(length), sum(length) from txtai\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "oH4Yd9BOlo5u" - }, - "source": [ - "This example above adds a simple additional field, text length.\n", - "\n", - "Note the second query is filtering on the metadata field length along with a `similar` query clause. This gives a great blend of vector search with traditional filtering to help identify the best results." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "lGmiYXyqyjtQ" - }, - "source": [ - "# Object storage\n", - "\n", - "In addition to metadata, binary content can also be associated with documents. The example below downloads an image, upserts it along with associated text into the embeddings index." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 307 - }, - "id": "Ef4-Gd8ZtzUF", - "outputId": "aaa811e8-ee3a-43ed-dab3-994ca6014a64" - }, - "outputs": [ - { - "data": { - "image/png": "R0lGODlhlwQ4AvUAABITFMzMzMTExKurrGtsbCYnKJOTlIeIiHN0dLOzs0pLTJeYmHt7fFdXWLy8vJubm6SkpBobHBUWGBcZGBAqHyUdKPRDNmglIcw7MOxBNYcsJqczKts+Mrg2LeqA/Mxx3HREfaVdsth36Y1RmLFjvuN89bxpyw80IxIbGgOp9AaQzgtdgwSg5wlwnwd+tADmdgl8RQS2YAHccgeVUAPEZgWsWwLLaf/rO2pjItzLNe3bOIV8J7CjLsi4MryuMAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQEFAD/ACwAAAAAlwQ4AgAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyFwBy8zNzcnQ0dLT1KTO18zV2tvc3d6A2Njf4+Tl5ufK4c7o7O3u7/A46uvx9fb3+Obzz/n9/v8Age3LFrCgwYMI3wwI0KDKwGUJI0qceK6AAwEF2ixsyMJiggg2GuwbAOBhAIooU/+qhGaAoZuNLggEOBDyok1mNh+UfLiyp8+fuRQEIPnSpYsERnGIhGDCJNCnUKOmepCUDcyYM3UsbcpTqtevYC9ZxJhCwQGkAgYcUHCiAdW0CDKWKHBWwLIDckccaGYAQIOFafMCKIAAcIK1JGDWPSyYxNjGNLaWcBq2suXLh2T2RSFy2QC0mxN7hmBT8Ni0pJexHWF2cWcDpEl09nxXtEzUnimgoMrxhmQSlDELH07cTksCKZAiH1Gg8V4IeREMnUCCgODbJ07TNDEWL2vBgB/IFRpguQnp231Pn9y1uPv38M1chDzCLn3mEEuQ7o0CKeTTEpywV2j95adXADq1tZ7/ekyxN1B8EEYoIRbL3AcAVQtYiN55WamwQFUijMUfcCCasFB6Ivw2l4E1NUjigxPGKOOMRjyWQgHh0deSOgQSBlgzqzlWYXZDqnAVawuueJJSSYoQHI1QRiklDTaq8JdnQYpAmloLdOnlAgggOdQBCJSJVJYhFqkkdUaCKNRHRAZg4QsqOtnelHjmqaeQS7KgQGrmXVgeCxcFKiiag6nJ55wAHJlik/j12SJXMO5p6aVQKroCdiNsqIJHJpZYJVeI6ucmpGkKkEOdO1WK6auwwmffC6PWasKbSiJq64EonuCoX6gCSxSDlO4T67HICkeVocxZVx2LgiaQJWFyiSim/6iaihkXc/z9iusJnhLroLHJlmtuVOEqmQAEqZW4owPsXiTngUOldkBLQRJwb2pqMTAiALfRhqBoI34b6r8zsPrkuQw3PNGoJRDwAFqMcVZXWojJxm9D0gWpDpxzXXxYlt6iumvCkC7s8MosA7RfIJoNoXLLNNcMD6t7+CfznTb37PM5+P4R886u/mz00dtAnAeoRMyM9NNQRw2D01JXbbXVVF+t9dY+Z83112CHLfbYZJdt9tlop6322my37fbbcMct99x012333XjnrffefPft99+ABy744IQXbvjhiCeu+OKMN+7445BHLvnklFdu+eWYZ6755px37vnnoIcu+v/opJdu+umop6766qy37vrrsMcu++y012777bjnrvvuvPfu++/ABy/88MQXb/zxyCev/PLMN+/889BHL/301Fdv/fXYZ6/99tx37/334Icv/vjkl2/++einr/767Lfv/vvwxy///PTXb//9+Oev//789+///wAMoAAHSMACGvCACEygAhfIwAY68IEQjKAEJ0jBClrwghjMoAY3yMEOevCDIAyhCEdIwhKa8IQoTKEKV8jCFrrwhTCMoQxnSMMa2vCGOMyhDnfIwx768IdADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMb/MprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWuMylLnfJy1768pfADKYwh0nMYhrzmMhMpjKXycxmOvOZ0IymNKdJzWpa85rYzKY2t8nNbnrzm+AMpzjHSc5ymvOc6EynOtfJzna6853wjKc850nPetrznvjMpz73yc9++vOfAA2oQAdK0IIa9KAITahCF8rQhjr0oRCNqEQnStGKWvSiGM2oRjfK0Y56//SjIA2pSEdK0pKa9KQoTalKV8rS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", - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": { - "image/png": { - "width": 600 - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "import urllib\n", - "\n", - "from IPython.display import Image\n", - "\n", - "# Get an image\n", - "request = urllib.request.urlopen(\"https://raw.githubusercontent.com/neuml/txtai/master/demo.gif\")\n", - "\n", - "# Upsert new record having both text and an object\n", - "embeddings.upsert([(\"txtai\", {\"text\": \"txtai executes machine-learning workflows to transform data and build AI-powered semantic search applications.\", \"object\": request.read()}, None)])\n", - "\n", - "# Query txtai for the most similar result to \"machine learning\" and get associated object\n", - "result = embeddings.search(\"select object from txtai where similar('machine learning') limit 1\")[0][\"object\"]\n", - "\n", - "# Display image\n", - "Image(result.getvalue(), width=600)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "boEY-GSUsi_L" - }, - "source": [ - "# Topic modeling\n", - "\n", - "Topic modeling is enabled via semantic graphs. Semantic graphs, also known as knowledge graphs or semantic networks, build a graph network with semantic relationships connecting the nodes. In txtai, they can take advantage of the relationships inherently learned within an embeddings index." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "k7eRzturtCwr", - "outputId": "794d10d6-8463-4e8c-c1d1-0af59e97e59f" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'topic': 'confirmed_cases_us_5',\n", - " 'category': 'health',\n", - " 'text': 'US tops 5 million confirmed virus cases'},\n", - " {'topic': 'collapsed_iceberg_ice_intact',\n", - " 'category': 'climate',\n", - " 'text': \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\"},\n", - " {'topic': 'beijing_along_craft_tensions',\n", - " 'category': 'world politics',\n", - " 'text': 'Beijing mobilises invasion craft along coast as Taiwan tensions escalate'}]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create embeddings with a graph index\n", - "embeddings = Embeddings(\n", - " path=\"sentence-transformers/nli-mpnet-base-v2\",\n", - " content=True,\n", - " functions=[\n", - " {\"name\": \"graph\", \"function\": \"graph.attribute\"},\n", - " ],\n", - " expressions=[\n", - " {\"name\": \"category\", \"expression\": \"graph(indexid, 'category')\"},\n", - " {\"name\": \"topic\", \"expression\": \"graph(indexid, 'topic')\"},\n", - " ],\n", - " graph={\n", - " \"topics\": {\n", - " \"categories\": [\"health\", \"climate\", \"finance\", \"world politics\"]\n", - " }\n", - " }\n", - ")\n", - "\n", - "embeddings.index(data)\n", - "embeddings.search(\"select topic, category, text from txtai\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "0VTB-LjExpfv" - }, - "source": [ - "When a graph index is enabled, topics are assigned to each of the entries in the embeddings instance. Topics are dynamically created using a sparse index over graph nodes grouped by [community detection algorithms](https://en.wikipedia.org/wiki/Community_structure).\n", - "\n", - "Topic categories are also be derived as shown above." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "0aOJOxE3y4vD" - }, - "source": [ - "# Subindexes\n", - "\n", - "Subindexes can be configured for an embeddings. A single embeddings instance can have multiple subindexes each with different configurations.\n", - "\n", - "We'll build an embeddings index having both a keyword and dense index to demonstrate." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "id": "TOwwKw3w_eJG" - }, - "outputs": [], - "source": [ - "# Create embeddings with subindexes\n", - "embeddings = Embeddings(\n", - " content=True,\n", - " defaults=False,\n", - " indexes={\n", - " \"keyword\": {\n", - " \"keyword\": True\n", - " },\n", - " \"dense\": {\n", - " \"path\": \"sentence-transformers/nli-mpnet-base-v2\"\n", - " }\n", - " }\n", - ")\n", - "embeddings.index(data)" - ] - }, + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "POWZoSJR6XzK" + }, + "source": [ + "# Introducing txtai\n", + "\n", + "[txtai](https://github.com/neuml/txtai) is an all-in-one embeddings database for semantic search, LLM orchestration and language model workflows.\n", + "\n", + "Embeddings databases are a union of vector indexes (sparse and dense), graph networks and relational databases. This enables vector search with SQL, topic modeling, retrieval augmented generation and more.\n", + "\n", + "Embeddings databases can stand on their own and/or serve as a powerful knowledge source for large language model (LLM) prompts.\n", + "\n", + "The following is a summary of key features:\n", + "\n", + "- πŸ”Ž Vector search with SQL, object storage, topic modeling, graph analysis and multimodal indexing\n", + "- πŸ“„ Create embeddings for text, documents, audio, images and video\n", + "- πŸ’‘ Pipelines powered by language models that run LLM prompts, question-answering, labeling, transcription, translation, summarization and more\n", + "- β†ͺ️️ Workflows to join pipelines together and aggregate business logic. txtai processes can be simple microservices or multi-model workflows.\n", + "- βš™οΈ Build with Python or YAML. API bindings available for [JavaScript](https://github.com/neuml/txtai.js), [Java](https://github.com/neuml/txtai.java), [Rust](https://github.com/neuml/txtai.rs) and [Go](https://github.com/neuml/txtai.go).\n", + "- ☁️ Run local or scale out with container orchestration\n", + "\n", + "txtai is built with Python 3.8+, [Hugging Face Transformers](https://github.com/huggingface/transformers), [Sentence Transformers](https://github.com/UKPLab/sentence-transformers) and [FastAPI](https://github.com/tiangolo/fastapi). txtai is open-source under an Apache 2.0 license." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qa_PPKVX6XzN" + }, + "source": [ + "# Install dependencies\n", + "\n", + "Install `txtai` and all dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19", + "_kg_hide-output": true, + "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5", + "id": "24q-1n5i6XzQ", + "trusted": true + }, + "outputs": [], + "source": [ + "%%capture\n", + "# !pip install git+https://github.com/neuml/txtai#egg=txtai[graph]\n", + "\n", + "# Install translation pipeline dependencies for later examples\n", + "!pip install txtai sentencepiece sacremoses fasttext" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "DLIjSzbq6Xzx" + }, + "source": [ + "# Semantic search\n", + "\n", + "Embeddings databases are the engine that delivers semantic search. Data is transformed into embeddings vectors where similar concepts will produce similar vectors. Indexes both large and small are built with these vectors. The indexes are used to find results that have the same meaning, not necessarily the same keywords.\n", + "\n", + "The basic use case for an embeddings database is building an approximate nearest neighbor (ANN) index for semantic search. The following example indexes a small number of text entries to demonstrate the value of semantic search.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QxX9EtIc6Xzg", + "trusted": true + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "M0HKb9mzxkL-", - "outputId": "16200bfc-715a-4dfe-89c4-cd6476c0425a" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "embeddings.search(\"feel good story\", limit=1, index=\"keyword\")" - ] + "ename": "", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[1;31mThe Kernel crashed while executing code in the current cell or a previous cell. \n", + "\u001b[1;31mPlease review the code in the cell(s) to identify a possible cause of the failure. \n", + "\u001b[1;31mClick here for more info. \n", + "\u001b[1;31mView Jupyter log for further details." + ] + } + ], + "source": [ + "from txtai import Embeddings\n", + "\n", + "# Works with a list, dataset or generator\n", + "data = [\n", + " \"US tops 5 million confirmed virus cases\",\n", + " \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\",\n", + " \"Beijing mobilises invasion craft along coast as Taiwan tensions escalate\",\n", + " \"The National Park Service warns against sacrificing slower friends in a bear attack\",\n", + " \"Maine man wins $1M from $25 lottery ticket\",\n", + " \"Make huge profits without work, earn up to $100,000 a day\",\n", + "]\n", + "\n", + "# Create an embeddings\n", + "embeddings = Embeddings(path=\"sentence-transformers/nli-mpnet-base-v2\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "cXfZtdHD6Xzy", + "outputId": "369b637e-1e1c-4229-f68e-92917be5fbd0", + "trusted": true + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "-SnA1s0kxw9x", - "outputId": "9f6d7cc6-7325-4ac4-ded0-aa0502d088e0" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'id': '4',\n", - " 'text': 'Maine man wins $1M from $25 lottery ticket',\n", - " 'score': 0.08329027891159058}]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "embeddings.search(\"feel good story\", limit=1, index=\"dense\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Query Best Match\n", + "--------------------------------------------------\n", + "feel good story Maine man wins $1M from $25 lottery ticket\n", + "climate change Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n", + "public health story US tops 5 million confirmed virus cases\n", + "war Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", + "wildlife The National Park Service warns against sacrificing slower friends in a bear attack\n", + "asia Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", + "lucky Maine man wins $1M from $25 lottery ticket\n", + "dishonest junk Make huge profits without work, earn up to $100,000 a day\n" + ] + } + ], + "source": [ + "# Create an index for the list of text\n", + "embeddings.index(data)\n", + "\n", + "print(\"%-20s %s\" % (\"Query\", \"Best Match\"))\n", + "print(\"-\" * 50)\n", + "\n", + "# Run an embeddings search for each query\n", + "for query in (\n", + " \"feel good story\",\n", + " \"climate change\",\n", + " \"public health story\",\n", + " \"war\",\n", + " \"wildlife\",\n", + " \"asia\",\n", + " \"lucky\",\n", + " \"dishonest junk\",\n", + "):\n", + " # Extract uid of first result\n", + " # search result format: (uid, score)\n", + " uid = embeddings.search(query, 1)[0][0]\n", + "\n", + " # Print text\n", + " print(\"%-20s %s\" % (query, data[uid]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "kIMbLW0t6Xzw" + }, + "source": [ + "The example above shows that for all of the queries, the query text isn’t in the data. This is the true power of transformers models over token based search. What you get out of the box is πŸ”₯πŸ”₯πŸ”₯!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6m7sYUj_AdOL" + }, + "source": [ + "# Updates and deletes\n", + "\n", + "Updates and deletes are supported for embeddings. The upsert operation will insert new data and update existing data\n", + "\n", + "The following section runs a query, then updates a value changing the top result and finally deletes the updated value to revert back to the original query results." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "2CERR0U2Ac8C", + "outputId": "0c1f4dd2-1319-410b-91a4-7753adba2c26" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "7vFe31Gax-0r" - }, - "source": [ - "Once again, this example demonstrates the difference between keyword and semantic search. The first search call uses the defined keyword index, the second uses the dense vector index." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial: Maine man wins $1M from $25 lottery ticket\n", + "After update: See it: baby panda born\n", + "After delete: Maine man wins $1M from $25 lottery ticket\n" + ] + } + ], + "source": [ + "# Run initial query\n", + "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", + "print(\"Initial: \", data[uid])\n", + "\n", + "# Create a copy of data to modify\n", + "udata = data.copy()\n", + "\n", + "# Update data\n", + "udata[0] = \"See it: baby panda born\"\n", + "embeddings.upsert([(0, udata[0], None)])\n", + "\n", + "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", + "print(\"After update: \", udata[uid])\n", + "\n", + "# Remove record just added from index\n", + "embeddings.delete([0])\n", + "\n", + "# Ensure value matches previous value\n", + "uid = embeddings.search(\"feel good story\", 1)[0][0]\n", + "print(\"After delete: \", udata[uid])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6TCVl6QA6Xz5" + }, + "source": [ + "# Persistence\n", + "\n", + "Embeddings can be saved to storage and reloaded." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "5gyO90Hc6Xz7", + "outputId": "5460fcd8-5b9f-4064-9ac3-f72e5db9ecf4", + "trusted": true + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "1M_OMEndzgnG" - }, - "source": [ - "# LLM orchestration\n", - "\n", - "txtai is an all-in-one embeddings database. It is the only vector database that also supports sparse indexes, graph networks and relational databases with inline SQL support. In addition to this, txtai has support for LLM orchestration.\n", - "\n", - "The [extractor pipeline](https://neuml.github.io/txtai/pipeline/text/extractor/) is txtai's spin on retrieval augmented generation (RAG). This pipeline extracts knowledge from content by joining a prompt, context data store and generative model together.\n", - "\n", - "The following example shows how a large language model (LLM) can use an embeddings database for context." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n" + ] + } + ], + "source": [ + "embeddings.save(\"index\")\n", + "\n", + "embeddings = Embeddings()\n", + "embeddings.load(\"index\")\n", + "\n", + "uid = embeddings.search(\"climate change\", 1)[0][0]\n", + "print(data[uid])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "giNZ_fHmqT8u" + }, + "source": [ + "# Hybrid search\n", + "\n", + "While dense vector indexes are by far the best option for semantic search systems, sparse keyword indexes can still add value. There may be cases where finding an exact match is important.\n", + "\n", + "Hybrid search combines the results from sparse and dense vector indexes for the best of both worlds." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "lclxiRFRqsFv", + "outputId": "3bd15b63-3bf4-4132-a819-0f560fce3f92" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "vWX9Q6Iy0X3Z", - "outputId": "82f9f9cc-b7fb-4ee9-cd35-9b00c022f83a" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{'answer': 'Canada', 'reference': 'da633124-33ff-58d6-8ecb-14f7a44c042a'}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import torch\n", - "from txtai.pipeline import Extractor\n", - "\n", - "def prompt(question):\n", - " return [{\n", - " \"query\": question,\n", - " \"question\": f\"\"\"\n", - "Answer the following question using the context below.\n", - "Question: {question}\n", - "Context:\n", - "\"\"\"\n", - "}]\n", - "\n", - "# Create embeddings\n", - "embeddings = Embeddings(path=\"sentence-transformers/nli-mpnet-base-v2\", content=True, autoid=\"uuid5\")\n", - "\n", - "# Create an index for the list of text\n", - "embeddings.index(data)\n", - "\n", - "# Create and run extractor instance\n", - "extractor = Extractor(embeddings, \"google/flan-t5-large\", torch_dtype=torch.bfloat16, output=\"reference\")\n", - "extractor(prompt(\"What country is having issues with climate change?\"))[0]" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Query Best Match\n", + "--------------------------------------------------\n", + "feel good story Maine man wins $1M from $25 lottery ticket\n", + "climate change Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\n", + "public health story US tops 5 million confirmed virus cases\n", + "war Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", + "wildlife The National Park Service warns against sacrificing slower friends in a bear attack\n", + "asia Beijing mobilises invasion craft along coast as Taiwan tensions escalate\n", + "lucky Maine man wins $1M from $25 lottery ticket\n", + "dishonest junk Make huge profits without work, earn up to $100,000 a day\n" + ] + } + ], + "source": [ + "# Create an embeddings\n", + "embeddings = Embeddings(hybrid=True, path=\"sentence-transformers/nli-mpnet-base-v2\")\n", + "\n", + "# Create an index for the list of text\n", + "embeddings.index(data)\n", + "\n", + "print(\"%-20s %s\" % (\"Query\", \"Best Match\"))\n", + "print(\"-\" * 50)\n", + "\n", + "# Run an embeddings search for each query\n", + "for query in (\n", + " \"feel good story\",\n", + " \"climate change\",\n", + " \"public health story\",\n", + " \"war\",\n", + " \"wildlife\",\n", + " \"asia\",\n", + " \"lucky\",\n", + " \"dishonest junk\",\n", + "):\n", + " # Extract uid of first result\n", + " # search result format: (uid, score)\n", + " uid = embeddings.search(query, 1)[0][0]\n", + "\n", + " # Print text\n", + " print(\"%-20s %s\" % (query, data[uid]))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d9beQSw-vhz8" + }, + "source": [ + "Same results as with semantic search. Let's run the same example with just a keyword index to view those results." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "WykNb8y3vohL", + "outputId": "5617e912-1014-495c-9dc9-e5729988d77f" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "lqsZreJQuSfO" - }, - "source": [ - "The logic above first builds an embeddings index. It then loads a LLM and uses the embeddings index to drive a LLM prompt.\n", - "\n", - "The extractor pipeline can optionally return a reference to the id of the best matching record with the answer. That id can be used to resolve the full answer reference. Note that the embeddings above used an [uuid autosequence](https://neuml.github.io/txtai/embeddings/configuration/general/#autoid)." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n", + "[(4, 0.5234998733628726)]\n" + ] + } + ], + "source": [ + "# Create an embeddings\n", + "embeddings = Embeddings(keyword=True)\n", + "\n", + "# Create an index for the list of text\n", + "embeddings.index(data)\n", + "\n", + "print(embeddings.search(\"feel good story\"))\n", + "print(embeddings.search(\"lottery\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P0FLRsrmv2hB" + }, + "source": [ + "See that when the embeddings instance only uses a keyword index, it can't find semantic matches, only keyword matches." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0p3WCDniUths" + }, + "source": [ + "# Content storage\n", + "\n", + "Up to this point, all the examples are referencing the original data array to retrieve the input text. This works fine for a demo but what if you have millions of documents? In this case, the text needs to be retrieved from an external datastore using the id.\n", + "\n", + "Content storage adds an associated database (i.e. SQLite, DuckDB) that stores associated metadata with the vector index. The document text, additional metadata and additional objects can be stored and retrieved right alongside the indexed vectors." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "MOntBQIdVv-J", + "outputId": "c9d0d3e7-d7b4-4421-f63d-402db6918cca" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 16, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ioC-gY4wwWVQ", - "outputId": "d6eab14a-83cd-434c-faa8-2afe285e842b" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'id': 'da633124-33ff-58d6-8ecb-14f7a44c042a',\n", - " 'text': \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\"}]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "uid = extractor(prompt(\"What country is having issues with climate change?\"))[0][\"reference\"]\n", - "embeddings.search(f\"select id, text from txtai where id = '{uid}'\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Maine man wins $1M from $25 lottery ticket\n" + ] + } + ], + "source": [ + "# Create embeddings with content enabled. The default behavior is to only store indexed vectors.\n", + "embeddings = Embeddings(\n", + " path=\"sentence-transformers/nli-mpnet-base-v2\", content=True, objects=True\n", + ")\n", + "\n", + "# Create an index for the list of text\n", + "embeddings.index(data)\n", + "\n", + "print(embeddings.search(\"feel good story\", 1)[0][\"text\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hHGvhZm-ZTzL" + }, + "source": [ + "The only change above is setting the *content* flag to True. This enables storing text and metadata content (if provided) alongside the index. Note how the text is pulled right from the query result!\n", + "\n", + "Let's add some metadata." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "BYWUFBUGyKyY" + }, + "source": [ + "# Query with SQL\n", + "\n", + "When content is enabled, the entire dictionary is stored and can be queried. In addition to vector queries, txtai accepts SQL queries. This enables combined queries using both a vector index and content stored in a database backend." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "aPH-dnV2ZuL1", + "outputId": "c563060c-d292-4b19-aa64-f4c629008cdb" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "fwVMGqV2nHcP" - }, - "source": [ - "LLM inference can also be run standalone." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'text': 'The National Park Service warns against sacrificing slower friends in a bear attack', 'score': 0.3151373863220215}]\n", + "[{'text': 'Maine man wins $1M from $25 lottery ticket', 'length': 42, 'score': 0.08329027891159058}]\n", + "[{'count(*)': 6, 'min(length)': 39, 'max(length)': 94, 'sum(length)': 387}]\n" + ] + } + ], + "source": [ + "# Create an index for the list of text\n", + "embeddings.index([{\"text\": text, \"length\": len(text)} for text in data])\n", + "\n", + "# Filter by score\n", + "print(\n", + " embeddings.search(\n", + " \"select text, score from txtai where similar('hiking danger') and score >= 0.15\"\n", + " )\n", + ")\n", + "\n", + "# Filter by metadata field 'length'\n", + "print(\n", + " embeddings.search(\n", + " \"select text, length, score from txtai where similar('feel good story') and score >= 0.05 and length >= 40\"\n", + " )\n", + ")\n", + "\n", + "# Run aggregate queries\n", + "print(\n", + " embeddings.search(\n", + " \"select count(*), min(length), max(length), sum(length) from txtai\"\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oH4Yd9BOlo5u" + }, + "source": [ + "This example above adds a simple additional field, text length.\n", + "\n", + "Note the second query is filtering on the metadata field length along with a `similar` query clause. This gives a great blend of vector search with traditional filtering to help identify the best results." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lGmiYXyqyjtQ" + }, + "source": [ + "# Object storage\n", + "\n", + "In addition to metadata, binary content can also be associated with documents. The example below downloads an image, upserts it along with associated text into the embeddings index." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 307 }, + "id": "Ef4-Gd8ZtzUF", + "outputId": "aaa811e8-ee3a-43ed-dab3-994ca6014a64" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 36 - }, - "id": "NAFMSJO-k8qW", - "outputId": "c2b07b49-f50d-4f74-a2fe-17bc699a91f1" - }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'national museum of american history'" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from txtai.pipeline import LLM\n", - "\n", - "llm = LLM(\"google/flan-t5-large\", torch_dtype=torch.bfloat16)\n", - "llm(\"Where is one place you'd go in Washington, DC?\")" + "data": { + "image/png": "R0lGODlhlwQ4AvUAABITFMzMzMTExKurrGtsbCYnKJOTlIeIiHN0dLOzs0pLTJeYmHt7fFdXWLy8vJubm6SkpBobHBUWGBcZGBAqHyUdKPRDNmglIcw7MOxBNYcsJqczKts+Mrg2LeqA/Mxx3HREfaVdsth36Y1RmLFjvuN89bxpyw80IxIbGgOp9AaQzgtdgwSg5wlwnwd+tADmdgl8RQS2YAHccgeVUAPEZgWsWwLLaf/rO2pjItzLNe3bOIV8J7CjLsi4MryuMAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQEFAD/ACwAAAAAlwQ4AgAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5ydnp+goaKjpKWmp6ipqqusra6vsLGys7S1tre4ubq7vL2+v8DBwsPExcbHyFwBy8zNzcnQ0dLT1KTO18zV2tvc3d6A2Njf4+Tl5ufK4c7o7O3u7/A46uvx9fb3+Obzz/n9/v8Age3LFrCgwYMI3wwI0KDKwGUJI0qceK6AAwEF2ixsyMJiggg2GuwbAOBhAIooU/+qhGaAoZuNLggEOBDyok1mNh+UfLiyp8+fuRQEIPnSpYsERnGIhGDCJNCnUKOmepCUDcyYM3UsbcpTqtevYC9ZxJhCwQGkAgYcUHCiAdW0CDKWKHBWwLIDckccaGYAQIOFafMCKIAAcIK1JGDWPSyYxNjGNLaWcBq2suXLh2T2RSFy2QC0mxN7hmBT8Ni0pJexHWF2cWcDpEl09nxXtEzUnimgoMrxhmQSlDELH07cTksCKZAiH1Gg8V4IeREMnUCCgODbJ07TNDEWL2vBgB/IFRpguQnp231Pn9y1uPv38M1chDzCLn3mEEuQ7o0CKeTTEpywV2j95adXADq1tZ7/ekyxN1B8EEYoIRbL3AcAVQtYiN55WamwQFUijMUfcCCasFB6Ivw2l4E1NUjigxPGKOOMRjyWQgHh0deSOgQSBlgzqzlWYXZDqnAVawuueJJSSYoQHI1QRiklDTaq8JdnQYpAmloLdOnlAgggOdQBCJSJVJYhFqkkdUaCKNRHRAZg4QsqOtnelHjmqaeQS7KgQGrmXVgeCxcFKiiag6nJ55wAHJlik/j12SJXMO5p6aVQKroCdiNsqIJHJpZYJVeI6ucmpGkKkEOdO1WK6auwwmffC6PWasKbSiJq64EonuCoX6gCSxSDlO4T67HICkeVocxZVx2LgiaQJWFyiSim/6iaihkXc/z9iusJnhLroLHJlmtuVOEqmQAEqZW4owPsXiTngUOldkBLQRJwb2pqMTAiALfRhqBoI34b6r8zsPrkuQw3PNGoJRDwAFqMcVZXWojJxm9D0gWpDpxzXXxYlt6iumvCkC7s8MosA7RfIJoNoXLLNNcMD6t7+CfznTb37PM5+P4R886u/mz00dtAnAeoRMyM9NNQRw2D01JXbbXVVF+t9dY+Z83112CHLfbYZJdt9tlop6322my37fbbcMct99x012333XjnrffefPft99+ABy744IQXbvjhiCeu+OKMN+7445BHLvnklFdu+eWYZ6755px37vnnoIcu+v/opJdu+umop6766qy37vrrsMcu++y012777bjnrvvuvPfu++/ABy/88MQXb/zxyCev/PLMN+/889BHL/301Fdv/fXYZ6/99tx37/334Icv/vjkl2/++einr/767Lfv/vvwxy///PTXb//9+Oev//789+///wAMoAAHSMACGvCACEygAhfIwAY68IEQjKAEJ0jBClrwghjMoAY3yMEOevCDIAyhCEdIwhKa8IQoTKEKV8jCFrrwhTCMoQxnSMMa2vCGOMyhDnfIwx768IdADKIQh0jEIhrxiEhMohKXyMQmOvGJUIyiFKdIxSpa8YpYzKIWt8jFLnrxi2AMoxjHSMb/MprxjGhMoxrXyMY2uvGNcIyjHOdIxzra8Y54zKMe98jHPvrxj4AMpCAHSchCGvKQiEykIhfJyEY68pGQjKQkJ0nJSlrykpjMpCY3yclOevKToAylKEdJylKa8pSoTKUqV8nKVrrylbCMpSxnScta2vKWuMylLnfJy1768pfADKYwh0nMYhrzmMhMpjKXycxmOvOZ0IymNKdJzWpa85rYzKY2t8nNbnrzm+AMpzjHSc5ymvOc6EynOtfJzna6853wjKc850nPetrznvjMpz73yc9++vOfAA2oQAdK0IIa9KAITahCF8rQhjr0oRCNqEQnStGKWvSiGM2oRjfK0Y56//SjIA2pSEdK0pKa9KQoTalKV8rS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", + "text/plain": [ + "" ] + }, + "execution_count": 10, + "metadata": { + "image/png": { + "width": 600 + } + }, + "output_type": "execute_result" + } + ], + "source": [ + "import urllib\n", + "\n", + "from IPython.display import Image\n", + "\n", + "# Get an image\n", + "request = urllib.request.urlopen(\n", + " \"https://raw.githubusercontent.com/neuml/txtai/master/demo.gif\"\n", + ")\n", + "\n", + "# Upsert new record having both text and an object\n", + "embeddings.upsert(\n", + " [\n", + " (\n", + " \"txtai\",\n", + " {\n", + " \"text\": \"txtai executes machine-learning workflows to transform data and build AI-powered semantic search applications.\",\n", + " \"object\": request.read(),\n", + " },\n", + " None,\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Query txtai for the most similar result to \"machine learning\" and get associated object\n", + "result = embeddings.search(\n", + " \"select object from txtai where similar('machine learning') limit 1\"\n", + ")[0][\"object\"]\n", + "\n", + "# Display image\n", + "Image(result.getvalue(), width=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "boEY-GSUsi_L" + }, + "source": [ + "# Topic modeling\n", + "\n", + "Topic modeling is enabled via semantic graphs. Semantic graphs, also known as knowledge graphs or semantic networks, build a graph network with semantic relationships connecting the nodes. In txtai, they can take advantage of the relationships inherently learned within an embeddings index." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "k7eRzturtCwr", + "outputId": "794d10d6-8463-4e8c-c1d1-0af59e97e59f" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "ekRIFk4uuoLN" - }, - "source": [ - "# Language model workflows\n", - "\n", - "Language model workflows, also known as semantic workflows, connect language models together to build intelligent applications.\n", - "\n", - "Workflows can run right alongside an embeddings instance, similar to a stored procedure in a relational database. Workflows can be written in either Python or YAML. We'll demonstrate how to write a workflow with YAML." + "data": { + "text/plain": [ + "[{'topic': 'confirmed_cases_us_5',\n", + " 'category': 'health',\n", + " 'text': 'US tops 5 million confirmed virus cases'},\n", + " {'topic': 'collapsed_iceberg_ice_intact',\n", + " 'category': 'climate',\n", + " 'text': \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\"},\n", + " {'topic': 'beijing_along_craft_tensions',\n", + " 'category': 'world politics',\n", + " 'text': 'Beijing mobilises invasion craft along coast as Taiwan tensions escalate'}]" ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create embeddings with a graph index\n", + "embeddings = Embeddings(\n", + " path=\"sentence-transformers/nli-mpnet-base-v2\",\n", + " content=True,\n", + " functions=[\n", + " {\"name\": \"graph\", \"function\": \"graph.attribute\"},\n", + " ],\n", + " expressions=[\n", + " {\"name\": \"category\", \"expression\": \"graph(indexid, 'category')\"},\n", + " {\"name\": \"topic\", \"expression\": \"graph(indexid, 'topic')\"},\n", + " ],\n", + " graph={\n", + " \"topics\": {\"categories\": [\"health\", \"climate\", \"finance\", \"world politics\"]}\n", + " },\n", + ")\n", + "\n", + "embeddings.index(data)\n", + "embeddings.search(\"select topic, category, text from txtai\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0VTB-LjExpfv" + }, + "source": [ + "When a graph index is enabled, topics are assigned to each of the entries in the embeddings instance. Topics are dynamically created using a sparse index over graph nodes grouped by [community detection algorithms](https://en.wikipedia.org/wiki/Community_structure).\n", + "\n", + "Topic categories are also be derived as shown above." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0aOJOxE3y4vD" + }, + "source": [ + "# Subindexes\n", + "\n", + "Subindexes can be configured for an embeddings. A single embeddings instance can have multiple subindexes each with different configurations.\n", + "\n", + "We'll build an embeddings index having both a keyword and dense index to demonstrate." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "id": "TOwwKw3w_eJG" + }, + "outputs": [], + "source": [ + "# Create embeddings with subindexes\n", + "embeddings = Embeddings(\n", + " content=True,\n", + " defaults=False,\n", + " indexes={\n", + " \"keyword\": {\"keyword\": True},\n", + " \"dense\": {\"path\": \"sentence-transformers/nli-mpnet-base-v2\"},\n", + " },\n", + ")\n", + "embeddings.index(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "M0HKb9mzxkL-", + "outputId": "16200bfc-715a-4dfe-89c4-cd6476c0425a" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "JFzSs_Wa012D", - "outputId": "055e4f6d-a324-47ce-e3be-5aae06b28651" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Overwriting embeddings.yml\n" - ] - } - ], - "source": [ - "%%writefile embeddings.yml\n", - "\n", - "# Embeddings instance\n", - "writable: true\n", - "embeddings:\n", - " path: sentence-transformers/nli-mpnet-base-v2\n", - " content: true\n", - " functions:\n", - " - {name: translation, argcount: 2, function: translation}\n", - "\n", - "# Translation pipeline\n", - "translation:\n", - "\n", - "# Workflow definitions\n", - "workflow:\n", - " search:\n", - " tasks:\n", - " - search\n", - " - action: translation\n", - " args:\n", - " target: fr\n", - " task: template\n", - " template: \"{text}\"" + "data": { + "text/plain": [ + "[]" ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "embeddings.search(\"feel good story\", limit=1, index=\"keyword\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "-SnA1s0kxw9x", + "outputId": "9f6d7cc6-7325-4ac4-ded0-aa0502d088e0" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "2WU0fCZasVNf" - }, - "source": [ - "The workflow above loads an embeddings index and defines a search workflow. The search workflow runs a search and then passes the results to a translation pipeline. The translation pipeline translates results to French." + "data": { + "text/plain": [ + "[{'id': '4',\n", + " 'text': 'Maine man wins $1M from $25 lottery ticket',\n", + " 'score': 0.08329027891159058}]" ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "embeddings.search(\"feel good story\", limit=1, index=\"dense\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "7vFe31Gax-0r" + }, + "source": [ + "Once again, this example demonstrates the difference between keyword and semantic search. The first search call uses the defined keyword index, the second uses the dense vector index." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "1M_OMEndzgnG" + }, + "source": [ + "# LLM orchestration\n", + "\n", + "txtai is an all-in-one embeddings database. It is the only vector database that also supports sparse indexes, graph networks and relational databases with inline SQL support. In addition to this, txtai has support for LLM orchestration.\n", + "\n", + "The [extractor pipeline](https://neuml.github.io/txtai/pipeline/text/extractor/) is txtai's spin on retrieval augmented generation (RAG). This pipeline extracts knowledge from content by joining a prompt, context data store and generative model together.\n", + "\n", + "The following example shows how a large language model (LLM) can use an embeddings database for context." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "vWX9Q6Iy0X3Z", + "outputId": "82f9f9cc-b7fb-4ee9-cd35-9b00c022f83a" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "ySOK3HDK1nOZ", - "outputId": "551f425d-8c99-4705-8dc8-7e23458a3ed5" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "['Maine homme gagne $1M Γ  partir de $25 billet de loterie']" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from txtai import Application\n", - "\n", - "# Build index\n", - "app = Application(\"embeddings.yml\")\n", - "app.add(data)\n", - "app.index()\n", - "\n", - "# Run workflow\n", - "list(app.workflow(\"search\", [\"select text from txtai where similar('feel good story') limit 1\"]))\n" + "data": { + "text/plain": [ + "{'answer': 'Canada', 'reference': 'da633124-33ff-58d6-8ecb-14f7a44c042a'}" ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import torch\n", + "from txtai.pipeline import Extractor\n", + "\n", + "\n", + "def prompt(question):\n", + " return [\n", + " {\n", + " \"query\": question,\n", + " \"question\": f\"\"\"\n", + "Answer the following question using the context below.\n", + "Question: {question}\n", + "Context:\n", + "\"\"\",\n", + " }\n", + " ]\n", + "\n", + "\n", + "# Create embeddings\n", + "embeddings = Embeddings(\n", + " path=\"sentence-transformers/nli-mpnet-base-v2\", content=True, autoid=\"uuid5\"\n", + ")\n", + "\n", + "# Create an index for the list of text\n", + "embeddings.index(data)\n", + "\n", + "# Create and run extractor instance\n", + "extractor = Extractor(\n", + " embeddings, \"google/flan-t5-large\", torch_dtype=torch.bfloat16, output=\"reference\"\n", + ")\n", + "extractor(prompt(\"What country is having issues with climate change?\"))[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "lqsZreJQuSfO" + }, + "source": [ + "The logic above first builds an embeddings index. It then loads a LLM and uses the embeddings index to drive a LLM prompt.\n", + "\n", + "The extractor pipeline can optionally return a reference to the id of the best matching record with the answer. That id can be used to resolve the full answer reference. Note that the embeddings above used an [uuid autosequence](https://neuml.github.io/txtai/embeddings/configuration/general/#autoid)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "ioC-gY4wwWVQ", + "outputId": "d6eab14a-83cd-434c-faa8-2afe285e842b" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "rhxBBaUO4znH" - }, - "source": [ - "SQL functions, in some cases, can accomplish the same thing as a workflow. The function below runs the translation pipeline as a function." + "data": { + "text/plain": [ + "[{'id': 'da633124-33ff-58d6-8ecb-14f7a44c042a',\n", + " 'text': \"Canada's last fully intact ice shelf has suddenly collapsed, forming a Manhattan-sized iceberg\"}]" ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uid = extractor(prompt(\"What country is having issues with climate change?\"))[0][\n", + " \"reference\"\n", + "]\n", + "embeddings.search(f\"select id, text from txtai where id = '{uid}'\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fwVMGqV2nHcP" + }, + "source": [ + "LLM inference can also be run standalone." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 36 }, + "id": "NAFMSJO-k8qW", + "outputId": "c2b07b49-f50d-4f74-a2fe-17bc699a91f1" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "hJAC430s4yIV", - "outputId": "8e0c4d6a-42d4-45e8-e79a-7872639c5512" + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'text': 'Maine homme gagne $1M Γ  partir de $25 billet de loterie'}]" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "app.search(\"select translation(text, 'fr') text from txtai where similar('feel good story') limit 1\")" + "text/plain": [ + "'national museum of american history'" ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from txtai.pipeline import LLM\n", + "\n", + "llm = LLM(\"google/flan-t5-large\", torch_dtype=torch.bfloat16)\n", + "llm(\"Where is one place you'd go in Washington, DC?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ekRIFk4uuoLN" + }, + "source": [ + "# Language model workflows\n", + "\n", + "Language model workflows, also known as semantic workflows, connect language models together to build intelligent applications.\n", + "\n", + "Workflows can run right alongside an embeddings instance, similar to a stored procedure in a relational database. Workflows can be written in either Python or YAML. We'll demonstrate how to write a workflow with YAML." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "JFzSs_Wa012D", + "outputId": "055e4f6d-a324-47ce-e3be-5aae06b28651" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "u5nHeC8MpX7k" - }, - "source": [ - "LLM chains with templates are also possible with workflows. Workflows are self-contained, they operate both with and without an associated embeddings instance. The following workflow uses a LLM to conditionally translate text to French and then detect the language of the text." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting embeddings.yml\n" + ] + } + ], + "source": [ + "%%writefile embeddings.yml\n", + "\n", + "# Embeddings instance\n", + "writable: true\n", + "embeddings:\n", + " path: sentence-transformers/nli-mpnet-base-v2\n", + " content: true\n", + " functions:\n", + " - {name: translation, argcount: 2, function: translation}\n", + "\n", + "# Translation pipeline\n", + "translation:\n", + "\n", + "# Workflow definitions\n", + "workflow:\n", + " search:\n", + " tasks:\n", + " - search\n", + " - action: translation\n", + " args:\n", + " target: fr\n", + " task: template\n", + " template: \"{text}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "2WU0fCZasVNf" + }, + "source": [ + "The workflow above loads an embeddings index and defines a search workflow. The search workflow runs a search and then passes the results to a translation pipeline. The translation pipeline translates results to French." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "ySOK3HDK1nOZ", + "outputId": "551f425d-8c99-4705-8dc8-7e23458a3ed5" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 21, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "zfwpSmTLnVU8", - "outputId": "154a34ff-2919-415b-a5b7-fb7c9f5b9cf1" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Overwriting workflow.yml\n" - ] - } - ], - "source": [ - "%%writefile workflow.yml\n", - "\n", - "sequences:\n", - " path: google/flan-t5-large\n", - " torch_dtype: torch.bfloat16\n", - "\n", - "workflow:\n", - " chain:\n", - " tasks:\n", - " - task: template\n", - " template: Translate '{statement}' to {language} if it's English\n", - " action: sequences\n", - " - task: template\n", - " template: What language is the following text? {text}\n", - " action: sequences" + "data": { + "text/plain": [ + "['Maine homme gagne $1M Γ  partir de $25 billet de loterie']" ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from txtai import Application\n", + "\n", + "# Build index\n", + "app = Application(\"embeddings.yml\")\n", + "app.add(data)\n", + "app.index()\n", + "\n", + "# Run workflow\n", + "list(\n", + " app.workflow(\n", + " \"search\", [\"select text from txtai where similar('feel good story') limit 1\"]\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rhxBBaUO4znH" + }, + "source": [ + "SQL functions, in some cases, can accomplish the same thing as a workflow. The function below runs the translation pipeline as a function." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "hJAC430s4yIV", + "outputId": "8e0c4d6a-42d4-45e8-e79a-7872639c5512" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "8mo2XSr9nXJH", - "outputId": "b9775570-4dd1-486e-f0c8-62f94fdb85b1" - }, - "outputs": [ - { - "data": { - "text/plain": [ - "['French', 'German']" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "inputs = [\n", - " {\"statement\": \"Hello, how are you\", \"language\": \"French\"},\n", - " {\"statement\": \"Hallo, wie geht's dir\", \"language\": \"French\"}\n", - "]\n", - "\n", - "app = Application(\"workflow.yml\")\n", - "list(app.workflow(\"chain\", inputs))" + "data": { + "text/plain": [ + "[{'text': 'Maine homme gagne $1M Γ  partir de $25 billet de loterie'}]" ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "app.search(\n", + " \"select translation(text, 'fr') text from txtai where similar('feel good story') limit 1\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "u5nHeC8MpX7k" + }, + "source": [ + "LLM chains with templates are also possible with workflows. Workflows are self-contained, they operate both with and without an associated embeddings instance. The following workflow uses a LLM to conditionally translate text to French and then detect the language of the text." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "zfwpSmTLnVU8", + "outputId": "154a34ff-2919-415b-a5b7-fb7c9f5b9cf1" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "aDIF3tYt6X0O" - }, - "source": [ - "# Wrapping up\n", - "\n", - "NLP is advancing at a rapid pace. Things not possible even a year ago are now possible. This notebook introduced txtai, an all-in-one embeddings database. The possibilities are limitless and we're excited to see what can be built on top of txtai!\n", - "\n", - "Visit the links below for more.\n", - "\n", - "[GitHub](https://github.com/neuml/txtai) | [Documentation](https://neuml.github.io/txtai) | [Examples](https://neuml.github.io/txtai/examples/)" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Overwriting workflow.yml\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "%%writefile workflow.yml\n", + "\n", + "sequences:\n", + " path: google/flan-t5-large\n", + " torch_dtype: torch.bfloat16\n", + "\n", + "workflow:\n", + " chain:\n", + " tasks:\n", + " - task: template\n", + " template: Translate '{statement}' to {language} if it's English\n", + " action: sequences\n", + " - task: template\n", + " template: What language is the following text? {text}\n", + " action: sequences" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { "colab": { - "gpuType": "T4", - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "base_uri": "https://localhost:8080/" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.12" + "id": "8mo2XSr9nXJH", + "outputId": "b9775570-4dd1-486e-f0c8-62f94fdb85b1" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "['French', 'German']" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" } + ], + "source": [ + "inputs = [\n", + " {\"statement\": \"Hello, how are you\", \"language\": \"French\"},\n", + " {\"statement\": \"Hallo, wie geht's dir\", \"language\": \"French\"},\n", + "]\n", + "\n", + "app = Application(\"workflow.yml\")\n", + "list(app.workflow(\"chain\", inputs))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aDIF3tYt6X0O" + }, + "source": [ + "# Wrapping up\n", + "\n", + "NLP is advancing at a rapid pace. Things not possible even a year ago are now possible. This notebook introduced txtai, an all-in-one embeddings database. The possibilities are limitless and we're excited to see what can be built on top of txtai!\n", + "\n", + "Visit the links below for more.\n", + "\n", + "[GitHub](https://github.com/neuml/txtai) | [Documentation](https://neuml.github.io/txtai) | [Examples](https://neuml.github.io/txtai/examples/)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/src/vdf_io/notebooks/aiven-qs.ipynb b/src/vdf_io/notebooks/aiven-qs.ipynb index 3387c40..3f4504c 100644 --- a/src/vdf_io/notebooks/aiven-qs.ipynb +++ b/src/vdf_io/notebooks/aiven-qs.ipynb @@ -33,7 +33,7 @@ }, "outputs": [], "source": [ - "from rich import print as rprint\n" + "from rich import print as rprint" ] }, { @@ -181,7 +181,10 @@ "auth_provider = PlainTextAuthProvider(\n", " os.environ.get(\"CASSANDRA_USER\"), os.environ.get(\"CASSANDRA_PASSWORD\")\n", ")\n", - "ssl_options = {\"ca_certs\": \"/Users/dhruvanand/Code/vector-io/aiven.pem\", \"cert_reqs\": ssl.CERT_REQUIRED}\n", + "ssl_options = {\n", + " \"ca_certs\": \"/Users/dhruvanand/Code/vector-io/aiven.pem\",\n", + " \"cert_reqs\": ssl.CERT_REQUIRED,\n", + "}\n", "CASSANDRA_URI = os.environ.get(\"CASSANDRA_URI\")\n", "CASSANDRA_URI, CASSANDRA_PORT = CASSANDRA_URI.split(\":\")\n", "with Cluster(\n", diff --git a/src/vdf_io/notebooks/astra_usage.ipynb b/src/vdf_io/notebooks/astra_usage.ipynb index 7d73948..2b43f7d 100644 --- a/src/vdf_io/notebooks/astra_usage.ipynb +++ b/src/vdf_io/notebooks/astra_usage.ipynb @@ -162,7 +162,7 @@ }, "outputs": [], "source": [ - "table = coll.find()['data']['documents']" + "table = coll.find()[\"data\"][\"documents\"]" ] }, { @@ -325,6 +325,7 @@ "source": [ "# convert list of dicts to pd.DataFrame\n", "import pandas as pd\n", + "\n", "df = pd.DataFrame(table)\n", "df.head()" ] @@ -359,7 +360,7 @@ } ], "source": [ - "len(resp['data']['documents'])" + "len(resp[\"data\"][\"documents\"])" ] }, { @@ -379,7 +380,7 @@ } ], "source": [ - "resp['data']['documents'][0].keys()" + "resp[\"data\"][\"documents\"][0].keys()" ] }, { @@ -497,14 +498,12 @@ "source": [ "# write to a new parquet file\n", "import pyarrow as pa\n", - "import pyarrow.parquet as pq\n", "\n", "table = pa.Table.from_pandas(coll.find().to_pandas())\n", "\n", "for r in coll.paginated_find():\n", " print(type(r), r.keys())\n", - " # append data into a parquet file\n", - " " + " # append data into a parquet file" ] }, { @@ -665,9 +664,9 @@ } ], "source": [ - "i=0\n", + "i = 0\n", "for r in tqdm(collection2.paginated_find()):\n", - " i+=1\n", + " i += 1\n", "print(i)" ] }, @@ -690,7 +689,7 @@ " break\n", " next_page_state = a[\"data\"][\"nextPageState\"]\n", " i += 1\n", - " print(i,len(id_set), tot_docs)\n", + " print(i, len(id_set), tot_docs)\n", "# len(a[\"data\"][\"documents\"])\n", "# len(id_set)" ] @@ -728,7 +727,7 @@ } ], "source": [ - "a['data']['documents'][0]" + "a[\"data\"][\"documents\"][0]" ] }, { @@ -748,7 +747,7 @@ } ], "source": [ - "len(collection2.find_one()['data']['document'][\"vector\"])" + "len(collection2.find_one()[\"data\"][\"document\"][\"vector\"])" ] }, { @@ -768,8 +767,8 @@ } ], "source": [ - "mr=db.collection(\"movie_reviews\")\n", - "mr.find_one()['data']['document'].keys()" + "mr = db.collection(\"movie_reviews\")\n", + "mr.find_one()[\"data\"][\"document\"].keys()" ] }, { diff --git a/src/vdf_io/notebooks/chroma-qs.ipynb b/src/vdf_io/notebooks/chroma-qs.ipynb index 38ecdd6..dd3a9f8 100644 --- a/src/vdf_io/notebooks/chroma-qs.ipynb +++ b/src/vdf_io/notebooks/chroma-qs.ipynb @@ -191,7 +191,7 @@ }, "outputs": [], "source": [ - "import chromadb\n" + "import chromadb" ] }, { @@ -200,9 +200,8 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# setup Chroma in-memory, for easy prototyping. Can add persistence easily!\n", - "client = chromadb.PersistentClient()\n" + "client = chromadb.PersistentClient()" ] }, { @@ -247,21 +246,23 @@ } ], "source": [ - "\n", "# Create collection. get_collection, get_or_create_collection, delete_collection also available!\n", "collection2 = client.get_or_create_collection(\"test\")\n", "\n", "# Add docs to the collection. Can also update and delete. Row-based API coming soon!\n", "collection2.add(\n", - " documents=[\"This is document1\", \"This is document2\"], # we handle tokenization, embedding, and indexing automatically. You can skip that and add your own embeddings as well\n", - " metadatas=[{\"source\": \"notion\"}, {\"source\": \"google-docs\"}], # filter on these!\n", - " ids=[\"doc1\", \"doc2\"], # unique for each doc\n", - " embeddings=[[1,2,3], [4,5,6]] # optional, we can also embed for you\n", + " documents=[\n", + " \"This is document1\",\n", + " \"This is document2\",\n", + " ], # we handle tokenization, embedding, and indexing automatically. You can skip that and add your own embeddings as well\n", + " metadatas=[{\"source\": \"notion\"}, {\"source\": \"google-docs\"}], # filter on these!\n", + " ids=[\"doc1\", \"doc2\"], # unique for each doc\n", + " embeddings=[[1, 2, 3], [4, 5, 6]], # optional, we can also embed for you\n", ")\n", "\n", "# Query/search 2 most similar results. You can also .get by id\n", "results = collection2.query(\n", - " query_embeddings=[[1,2,3]],\n", + " query_embeddings=[[1, 2, 3]],\n", " n_results=2,\n", " # where={\"metadata_field\": \"is_equal_to_this\"}, # optional filter\n", " # where_document={\"$contains\":\"search_string\"} # optional filter\n", @@ -304,7 +305,7 @@ }, "outputs": [], "source": [ - "coll=client.get_collection(\"test\")" + "coll = client.get_collection(\"test\")" ] }, { @@ -335,7 +336,7 @@ "metadata": {}, "outputs": [], "source": [ - "client.delete_collection(\"test\") # delete collection" + "client.delete_collection(\"test\") # delete collection" ] }, { @@ -357,7 +358,7 @@ } ], "source": [ - "client.list_collections() # list collections" + "client.list_collections() # list collections" ] }, { @@ -399,7 +400,7 @@ "metadata": {}, "outputs": [], "source": [ - "pclient = chromadb.PersistentClient(\"~/.chroma4\") # persistent client\n", + "pclient = chromadb.PersistentClient(\"~/.chroma4\") # persistent client\n", "\n", "pcol = pclient.create_collection(\"test3\")" ] @@ -414,8 +415,8 @@ " documents=[\"This is document1\", \"This is document2\"],\n", " metadatas=[{\"source\": \"notion\"}, {\"source\": \"google-docs\"}],\n", " ids=[\"doc1\", \"doc2\"],\n", - " embeddings=[[1,2,3], [4,5,6]]\n", - ")\n" + " embeddings=[[1, 2, 3], [4, 5, 6]],\n", + ")" ] }, { @@ -479,7 +480,9 @@ }, "outputs": [], "source": [ - "client2= chromadb.PersistentClient(\"/Users/dhruvanand/Code/vector-io/src/vdf_io/notebooks/chroma\")\n" + "client2 = chromadb.PersistentClient(\n", + " \"/Users/dhruvanand/Code/vector-io/src/vdf_io/notebooks/chroma\"\n", + ")" ] }, { @@ -559,7 +562,7 @@ }, "outputs": [], "source": [ - "coll2=client2.get_collection(\"vdf_2024_9-11\")" + "coll2 = client2.get_collection(\"vdf_2024_9-11\")" ] }, { @@ -653,7 +656,7 @@ " documents=[\"This is document1\", \"This is document2\"],\n", " metadatas=[{\"source\": \"notion\"}, {\"source\": \"google-docs\"}],\n", " ids=[\"doc5\", \"doc6\"],\n", - " embeddings=[[1,2,3], None]\n", + " embeddings=[[1, 2, 3], None],\n", ")" ] }, diff --git a/src/vdf_io/notebooks/deeplake.ipynb b/src/vdf_io/notebooks/deeplake.ipynb index f2a737b..0e6c561 100644 --- a/src/vdf_io/notebooks/deeplake.ipynb +++ b/src/vdf_io/notebooks/deeplake.ipynb @@ -156,7 +156,7 @@ "source": [ "import deeplake\n", "\n", - "ds = deeplake.load('hub://activeloop/coco-train')" + "ds = deeplake.load(\"hub://activeloop/coco-train\")" ] }, { diff --git a/src/vdf_io/notebooks/json_pandas.ipynb b/src/vdf_io/notebooks/json_pandas.ipynb index 0735d50..258dc92 100644 --- a/src/vdf_io/notebooks/json_pandas.ipynb +++ b/src/vdf_io/notebooks/json_pandas.ipynb @@ -17,34 +17,26 @@ "source": [ "import json\n", "import pandas as pd\n", - "js=[\n", + "\n", + "js = [\n", " {\n", " \"id\": 1,\n", " \"name\": \"John Doe\",\n", " \"age\": 30,\n", - " \"attribute\": {\n", - " \"height\": 176,\n", - " \"weight\": 80\n", - " }\n", + " \"attribute\": {\"height\": 176, \"weight\": 80},\n", " },\n", " {\n", " \"id\": 2,\n", " \"name\": \"Alice Smith\",\n", " \"age\": 28,\n", - " \"attribute\": {\n", - " \"height\": 167,\n", - " \"weight\": 55\n", - " }\n", + " \"attribute\": {\"height\": 167, \"weight\": 55},\n", " },\n", " {\n", " \"id\": 3,\n", " \"name\": \"Bob Johnson\",\n", " \"age\": 35,\n", - " \"attribute\": {\n", - " \"height\": 192,\n", - " \"weight\": 85\n", - " }\n", - " }\n", + " \"attribute\": {\"height\": 192, \"weight\": 85},\n", + " },\n", "]\n", "\n", "df = pd.read_json(json.dumps(js))" diff --git a/src/vdf_io/notebooks/jsonl_to_parquet.ipynb b/src/vdf_io/notebooks/jsonl_to_parquet.ipynb index e9b952f..6dae592 100644 --- a/src/vdf_io/notebooks/jsonl_to_parquet.ipynb +++ b/src/vdf_io/notebooks/jsonl_to_parquet.ipynb @@ -15,9 +15,7 @@ "outputs": [], "source": [ "# Importing required libraries\n", - "import pandas as pd\n", - "import pyarrow as pa\n", - "import pyarrow.parquet as pq" + "import pandas as pd" ] }, { @@ -35,7 +33,7 @@ "outputs": [], "source": [ "# Load JSONL File\n", - "jsonl_file = '/Users/dhruvanand/Code/datasets-dumps/shard-00000.jsonl 2'\n" + "jsonl_file = \"/Users/dhruvanand/Code/datasets-dumps/shard-00000.jsonl 2\"" ] }, { @@ -53,7 +51,9 @@ "outputs": [], "source": [ "# Convert JSONL to DataFrame\n", - "df = pd.read_json(jsonl_file, lines=True) # Convert the loaded jsonl data into a pandas DataFrame" + "df = pd.read_json(\n", + " jsonl_file, lines=True\n", + ") # Convert the loaded jsonl data into a pandas DataFrame" ] }, { @@ -172,7 +172,7 @@ } ], "source": [ - "df['metadata'].iloc[3]" + "df[\"metadata\"].iloc[3]" ] }, { @@ -213,9 +213,9 @@ "outputs": [], "source": [ "# Save DataFrame as Parquet\n", - "for comp in ['snappy', 'gzip', 'brotli', 'zstd']:\n", - " parquet_file = f'path_to_output_file-{comp}.parquet' # replace with your desired output parquet file path\n", - " df.to_parquet(parquet_file, engine='pyarrow', compression=comp)" + "for comp in [\"snappy\", \"gzip\", \"brotli\", \"zstd\"]:\n", + " parquet_file = f\"path_to_output_file-{comp}.parquet\" # replace with your desired output parquet file path\n", + " df.to_parquet(parquet_file, engine=\"pyarrow\", compression=comp)" ] }, { diff --git a/src/vdf_io/notebooks/jsonltgz_to_parquet.ipynb b/src/vdf_io/notebooks/jsonltgz_to_parquet.ipynb index ddf2d32..9c629ff 100644 --- a/src/vdf_io/notebooks/jsonltgz_to_parquet.ipynb +++ b/src/vdf_io/notebooks/jsonltgz_to_parquet.ipynb @@ -17,9 +17,7 @@ "outputs": [], "source": [ "# Importing required libraries\n", - "import pandas as pd\n", - "import pyarrow as pa\n", - "import pyarrow.parquet as pq" + "import pandas as pd" ] }, { @@ -71,7 +69,7 @@ "with open(jsonl_file) as f:\n", " for i, l in enumerate(f):\n", " pass\n", - " print(i)\n" + " print(i)" ] }, { @@ -120,7 +118,7 @@ } ], "source": [ - "df.iloc[0]['url']" + "df.iloc[0][\"url\"]" ] }, { diff --git a/src/vdf_io/notebooks/lance-qs.ipynb b/src/vdf_io/notebooks/lance-qs.ipynb index cce6883..ae499d8 100644 --- a/src/vdf_io/notebooks/lance-qs.ipynb +++ b/src/vdf_io/notebooks/lance-qs.ipynb @@ -78,6 +78,7 @@ "outputs": [], "source": [ "import lancedb\n", + "\n", "uri = \"~/.lancedb\"\n", "db = lancedb.connect(uri)" ] @@ -88,9 +89,13 @@ "metadata": {}, "outputs": [], "source": [ - "tbl = db.create_table(\"my_table\",\n", - " data=[{\"vector\": [3.1, 4.1], \"item\": \"foo\", \"price\": 10.0},\n", - " {\"vector\": [5.9, 26.5], \"item\": \"bar\", \"price\": 20.0}])" + "tbl = db.create_table(\n", + " \"my_table\",\n", + " data=[\n", + " {\"vector\": [3.1, 4.1], \"item\": \"foo\", \"price\": 10.0},\n", + " {\"vector\": [5.9, 26.5], \"item\": \"bar\", \"price\": 20.0},\n", + " ],\n", + ")" ] }, { @@ -112,15 +117,23 @@ }, "outputs": [], "source": [ - "\n", - "data = [{\"vector\": [1.3, 1.4], \"item\": \"fizz\", \"price\": 100.0},\n", - " {\"vector\": [9.5, 56.2], \"item\": \"buzz\", \"price\": 200.0}]\n", + "data = [\n", + " {\"vector\": [1.3, 1.4], \"item\": \"fizz\", \"price\": 100.0},\n", + " {\"vector\": [9.5, 56.2], \"item\": \"buzz\", \"price\": 200.0},\n", + "]\n", "tbl.add(data)\n", "\n", "# add 500 random items\n", "import random\n", "\n", - "data = [{\"vector\": [random.random(), random.random()], \"item\": \"item_{}\".format(i), \"price\": random.random()*100} for i in range(500)]\n", + "data = [\n", + " {\n", + " \"vector\": [random.random(), random.random()],\n", + " \"item\": \"item_{}\".format(i),\n", + " \"price\": random.random() * 100,\n", + " }\n", + " for i in range(500)\n", + "]\n", "tbl.add(data)" ] }, @@ -141,6 +154,7 @@ ], "source": [ "import pyarrow\n", + "\n", "tbl.schema, type(tbl.schema)\n", "for name in tbl.schema.names:\n", " if pyarrow.types.is_fixed_size_list(tbl.schema.field(name).type):\n", @@ -168,7 +182,10 @@ ], "source": [ "import pyarrow\n", - "tbl.schema.field(tbl.schema.names[0]).type is pyarrow.fixed_size_list(2, pyarrow.float64())\n" + "\n", + "tbl.schema.field(tbl.schema.names[0]).type is pyarrow.fixed_size_list(\n", + " 2, pyarrow.float64()\n", + ")" ] }, { diff --git a/src/vdf_io/notebooks/medium-articles.ipynb b/src/vdf_io/notebooks/medium-articles.ipynb index 27a85de..a379e22 100644 --- a/src/vdf_io/notebooks/medium-articles.ipynb +++ b/src/vdf_io/notebooks/medium-articles.ipynb @@ -207,7 +207,6 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", "import numpy as np\n", "from datasets import load_dataset\n", "import latentscope as ls" @@ -398,7 +397,7 @@ } ], "source": [ - "df.head()\n" + "df.head()" ] }, { @@ -438,7 +437,7 @@ "outputs": [], "source": [ "# Convert the numpy array of lists into a numpy array of numpy arrays\n", - "embeddings = np.array([np.array(embedding) for embedding in embeddings])\n" + "embeddings = np.array([np.array(embedding) for embedding in embeddings])" ] }, { @@ -550,8 +549,12 @@ } ], "source": [ - "\n", - "ls.import_embeddings(\"medium_articles\", embeddings, text_column=\"title\", model_id=\"openai-text-embedding-3-small\")" + "ls.import_embeddings(\n", + " \"medium_articles\",\n", + " embeddings,\n", + " text_column=\"title\",\n", + " model_id=\"openai-text-embedding-3-small\",\n", + ")" ] }, { @@ -560,7 +563,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "embeddings2 = df[\"title_vector\"].to_numpy()\n", "embeddings2 = np.array([np.array(embedding) for embedding in embeddings2])" ] @@ -581,9 +583,9 @@ } ], "source": [ - "# the model (facebook/dpr-ctx_encoder-single-nq-base) isn't in our supported list yet. \n", + "# the model (facebook/dpr-ctx_encoder-single-nq-base) isn't in our supported list yet.\n", "# we can still import the embeddings, but we won't be able to use the model for similarity search\n", - "ls.import_embeddings(\"medium_articles\", embeddings2, text_column=\"title\", model_id=\"\") " + "ls.import_embeddings(\"medium_articles\", embeddings2, text_column=\"title\", model_id=\"\")" ] }, { diff --git a/src/vdf_io/notebooks/mlx.ipynb b/src/vdf_io/notebooks/mlx.ipynb index 473009e..f404064 100644 --- a/src/vdf_io/notebooks/mlx.ipynb +++ b/src/vdf_io/notebooks/mlx.ipynb @@ -95,10 +95,11 @@ "\n", "\n", "from mlx_embedding_models.embedding import EmbeddingModel\n", + "\n", "model = EmbeddingModel.from_registry(\"bge-micro\")\n", "texts = [\n", " \"isn't it nice to be inside such a fancy computer\",\n", - " \"the horse raced past the barn fell\"\n", + " \"the horse raced past the barn fell\",\n", "]\n", "embs = model.encode(texts)\n", "print(embs.shape)\n", @@ -978,8 +979,8 @@ "source": [ "# display the embeddings side by side using zip\n", "from rich import print as rprint\n", - "rprint(list(zip(embs[0], embs2[0])))\n", - " " + "\n", + "rprint(list(zip(embs[0], embs2[0])))" ] }, { diff --git a/src/vdf_io/notebooks/qdrant-big.ipynb b/src/vdf_io/notebooks/qdrant-big.ipynb index c4d2981..74b06af 100644 --- a/src/vdf_io/notebooks/qdrant-big.ipynb +++ b/src/vdf_io/notebooks/qdrant-big.ipynb @@ -7,7 +7,7 @@ "outputs": [], "source": [ "import pandas as pd\n", - "from datasets import load_dataset\n" + "from datasets import load_dataset" ] }, { @@ -255,7 +255,6 @@ ], "source": [ "!pwd\n", - "import pandas as pd\n", "\n", "df = pd.read_parquet(\"../../../temp.parquet\")" ] @@ -317,7 +316,7 @@ } ], "source": [ - "df['title_vector']" + "df[\"title_vector\"]" ] }, { @@ -334,10 +333,7 @@ ] } ], - "source": [ - "from datasets import load_dataset\n", - "\n" - ] + "source": [] }, { "cell_type": "code", @@ -345,7 +341,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "ds = load_dataset(\"somewheresystems/dataclysm-pubmed\", split=\"train\", streaming=True)" ] }, @@ -375,8 +370,8 @@ } ], "source": [ - "type(df_take['title_embedding'].iloc[0])\n", - "type(df_take['title_embedding'].iloc[0][0])" + "type(df_take[\"title_embedding\"].iloc[0])\n", + "type(df_take[\"title_embedding\"].iloc[0][0])" ] }, { @@ -388,7 +383,9 @@ "import numpy as np\n", "\n", "\n", - "df_take['title_embedding'] = df_take['title_embedding'].apply(lambda x: np.array(x).flatten())" + "df_take[\"title_embedding\"] = df_take[\"title_embedding\"].apply(\n", + " lambda x: np.array(x).flatten()\n", + ")" ] }, { @@ -562,8 +559,8 @@ } ], "source": [ - "type(df_take['abstract_embedding'].iloc[0])\n", - "df_take['abstract_embedding'].iloc[0]" + "type(df_take[\"abstract_embedding\"].iloc[0])\n", + "df_take[\"abstract_embedding\"].iloc[0]" ] }, { @@ -587,6 +584,7 @@ ], "source": [ "from huggingface_hub import HfFileSystem\n", + "\n", "fs = HfFileSystem()\n", "fs.ls(\"datasets/aintech/vdf_20240125_130746_ac5a6_medium_articles\", detail=False)" ] @@ -606,7 +604,7 @@ } ], "source": [ - "from datasets import load_dataset\n" + "from datasets import load_dataset" ] }, { @@ -615,8 +613,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", - "\n", "ds = load_dataset(\"somewheresystems/dataclysm-arxiv\", split=\"train\", streaming=True)" ] }, @@ -665,6 +661,7 @@ ], "source": [ "import pandas as pd\n", + "\n", "df = pd.DataFrame(ds.take(1))\n", "df" ] @@ -700,7 +697,9 @@ } ], "source": [ - "ds2 = load_dataset(\"Qdrant/wolt-food-clip-ViT-B-32-embeddings\", split=\"train\", streaming=True)" + "ds2 = load_dataset(\n", + " \"Qdrant/wolt-food-clip-ViT-B-32-embeddings\", split=\"train\", streaming=True\n", + ")" ] }, { @@ -796,6 +795,7 @@ ], "source": [ "import pandas as pd\n", + "\n", "df2 = pd.DataFrame(ds2.take(2))\n", "df2" ] @@ -844,7 +844,7 @@ } ], "source": [ - "type(df2['cafe'].iloc[0])" + "type(df2[\"cafe\"].iloc[0])" ] }, { @@ -866,7 +866,7 @@ "source": [ "from PIL import Image\n", "\n", - "df2['image'].iloc[0]" + "df2[\"image\"].iloc[0]" ] }, { @@ -886,8 +886,8 @@ } ], "source": [ - "type(df2['vector'].iloc[0])\n", - "# check that the vector is a list of floats by parsing into list of floats\n" + "type(df2[\"vector\"].iloc[0])\n", + "# check that the vector is a list of floats by parsing into list of floats" ] }, { @@ -993,6 +993,7 @@ ], "source": [ "import pandas as pd\n", + "\n", "df = pd.DataFrame(ds.take(2))\n", "df" ] @@ -1043,7 +1044,7 @@ } ], "source": [ - "type(df['wit_features'].iloc[0])" + "type(df[\"wit_features\"].iloc[0])" ] }, { @@ -1063,7 +1064,7 @@ } ], "source": [ - "type(df['image'].iloc[0])" + "type(df[\"image\"].iloc[0])" ] }, { @@ -1112,9 +1113,7 @@ } ], "source": [ - "from PIL import Image\n", - "\n", - "isinstance(df['image'].iloc[0], Image.Image)" + "isinstance(df[\"image\"].iloc[0], Image.Image)" ] }, { @@ -1134,7 +1133,7 @@ } ], "source": [ - "str(df['image'].iloc[0])" + "str(df[\"image\"].iloc[0])" ] }, { @@ -1156,7 +1155,7 @@ } ], "source": [ - "Image.open(df['image'].iloc[0])" + "Image.open(df[\"image\"].iloc[0])" ] }, { @@ -1174,7 +1173,6 @@ } ], "source": [ - "\n", "ds3 = load_dataset(\"SciPhi/AgentSearch-V1\", split=\"train\", streaming=True)" ] }, diff --git a/src/vdf_io/notebooks/similar-words.ipynb b/src/vdf_io/notebooks/similar-words.ipynb index 1e29d1e..06b3ba5 100644 --- a/src/vdf_io/notebooks/similar-words.ipynb +++ b/src/vdf_io/notebooks/similar-words.ipynb @@ -6,8 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", - "from rich import print as rprint" + "import pandas as pd" ] }, { @@ -80,10 +79,7 @@ "metadata": {}, "outputs": [], "source": [ - "from io import StringIO\n", - "\n", - "\n", - "df = pd.read_csv(\"homophones.csv\", header=0,engine='python')" + "df = pd.read_csv(\"homophones.csv\", header=0, engine=\"python\")" ] }, { @@ -384,7 +380,9 @@ "metadata": {}, "outputs": [], "source": [ - "scope_df = pd.read_parquet(\"/Users/dhruvanand/Code/latent-scope/latentscope-working/homophones2/scopes/scopes-001.parquet\")" + "scope_df = pd.read_parquet(\n", + " \"/Users/dhruvanand/Code/latent-scope/latentscope-working/homophones2/scopes/scopes-001.parquet\"\n", + ")" ] }, { diff --git a/src/vdf_io/notebooks/tpuf-qs.ipynb b/src/vdf_io/notebooks/tpuf-qs.ipynb index 44f030c..8c21643 100644 --- a/src/vdf_io/notebooks/tpuf-qs.ipynb +++ b/src/vdf_io/notebooks/tpuf-qs.ipynb @@ -70,7 +70,7 @@ "metadata": {}, "outputs": [], "source": [ - "ns = tpuf.Namespace(\"namespace-name\")\n" + "ns = tpuf.Namespace(\"namespace-name\")" ] }, { @@ -99,9 +99,9 @@ "metadata": {}, "outputs": [], "source": [ - "import random,uuid\n", - "# use uuid\n", - "\n" + "import random\n", + "import uuid\n", + "# use uuid" ] }, { @@ -110,7 +110,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# Upsert vectors and attributes\n", "\n", "ns.upsert(\n", @@ -135,7 +134,7 @@ ], "source": [ "# upsert 20k random vectors\n", - "from random import random, randint, choice\n", + "from random import random, choice\n", "\n", "for i in tqdm(range(100)):\n", " # 10k random unique ids without replacement\n", @@ -261,11 +260,10 @@ } ], "source": [ - "from rich import print as rprint\n", "from tqdm import tqdm\n", "\n", - "for i,row in tqdm(enumerate(ns.vectors()), total=20000):\n", - " if i>10000:\n", + "for i, row in tqdm(enumerate(ns.vectors()), total=20000):\n", + " if i > 10000:\n", " break\n", " ns.delete(row.id)" ] @@ -354,7 +352,6 @@ } ], "source": [ - "\n", "ns3.approx_count()" ] }, diff --git a/src/vdf_io/notebooks/upsert_pinecone.ipynb b/src/vdf_io/notebooks/upsert_pinecone.ipynb index 0dd1d8b..ee1830c 100644 --- a/src/vdf_io/notebooks/upsert_pinecone.ipynb +++ b/src/vdf_io/notebooks/upsert_pinecone.ipynb @@ -17,14 +17,10 @@ } ], "source": [ - "import pandas as pd\n", - "import json\n", "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "from typing import List, Dict, Any\n", - "from rich import print as rprint\n", "\n", - "load_dotenv(find_dotenv(), override=True)\n" + "load_dotenv(find_dotenv(), override=True)" ] }, { @@ -42,7 +38,6 @@ } ], "source": [ - "import os\n", "import pinecone\n", "from dotenv import load_dotenv\n", "\n", @@ -134,7 +129,7 @@ " if len(all_ids) == successful_count:\n", " break\n", " print(\n", - " f\"dim={dim}, {len(all_ids)} unique ids found, out of {vec_count} total vectors in {i+1} tries\"\n", + " f\"dim={dim}, {len(all_ids)} unique ids found, out of {vec_count} total vectors in {i + 1} tries\"\n", " )\n", " # append the above to a csv file\n", "\n", @@ -1280,9 +1275,7 @@ "execution_count": 1, "metadata": {}, "outputs": [], - "source": [ - "from pinecone_datasets import load_dataset" - ] + "source": [] }, { "cell_type": "code", @@ -2076,7 +2069,7 @@ } ], "source": [ - "df_l[df_l[\"name\"] == \"amazon_toys_quora_all-MiniLM-L6-bm25\"].iloc[0].dense_model\n" + "df_l[df_l[\"name\"] == \"amazon_toys_quora_all-MiniLM-L6-bm25\"].iloc[0].dense_model" ] }, { @@ -4413,17 +4406,18 @@ ], "source": [ "from halo import Halo\n", - "with Halo(text=\"pc.list_indexes()\",spinner=\"dots\"):\n", + "\n", + "with Halo(text=\"pc.list_indexes()\", spinner=\"dots\"):\n", " list_index = pc.list_indexes().index_list[\"indexes\"]\n", "for index_dict in list_index:\n", " index = index_dict[\"name\"]\n", " index_obj = pc.Index(index)\n", - " print(index,index_obj.describe_index_stats())\n", + " print(index, index_obj.describe_index_stats())\n", " try:\n", " with Halo(spinner=\"dots\"):\n", " ids_list = [idx for idx in index_obj.list()]\n", " print(f\"{len(ids_list)=}\")\n", - " except Exception as e:\n", + " except Exception:\n", " print(f\"not supported for {index}\")" ] }, @@ -4727,12 +4721,11 @@ } ], "source": [ - "from tqdm.notebook import tqdm\n", - "\n", "dbpedia_id = pc.Index(\"dbpedia-entities\")\n", "\n", "for idx in tqdm(\n", - " dbpedia_id.list(limit=100), total=dbpedia_id.describe_index_stats()[\"total_vector_count\"]\n", + " dbpedia_id.list(limit=100),\n", + " total=dbpedia_id.describe_index_stats()[\"total_vector_count\"],\n", "):\n", " tqdm.write(f\"{len(idx)=} {idx=}\")" ] diff --git a/src/vdf_io/notebooks/vertex_export_sample.ipynb b/src/vdf_io/notebooks/vertex_export_sample.ipynb index ba28fbc..e13cb6f 100644 --- a/src/vdf_io/notebooks/vertex_export_sample.ipynb +++ b/src/vdf_io/notebooks/vertex_export_sample.ipynb @@ -50,7 +50,7 @@ "source": [ "import os\n", "\n", - "root_path = '..'\n", + "root_path = \"..\"\n", "os.chdir(root_path)\n", "os.getcwd()" ] @@ -73,8 +73,8 @@ ], "source": [ "# naming convention for all cloud resources\n", - "VERSION = \"pubv3\" # TODO\n", - "PREFIX = f'vvs-vectorio-{VERSION}' # TODO\n", + "VERSION = \"pubv3\" # TODO\n", + "PREFIX = f\"vvs-vectorio-{VERSION}\" # TODO\n", "\n", "print(f\"PREFIX = {PREFIX}\")" ] @@ -149,12 +149,12 @@ ], "source": [ "# staging GCS\n", - "GCP_PROJECTS = !gcloud config get-value project\n", - "PROJECT_ID = GCP_PROJECTS[0]\n", + "GCP_PROJECTS = !gcloud config get-value project\n", + "PROJECT_ID = GCP_PROJECTS[0]\n", "\n", "# GCS bucket and paths\n", - "BUCKET_NAME = f'{PREFIX}-{PROJECT_ID}'\n", - "BUCKET_URI = f'gs://{BUCKET_NAME}'\n", + "BUCKET_NAME = f\"{PREFIX}-{PROJECT_ID}\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", "\n", "config = !gsutil cat {BUCKET_URI}/config/notebook_env.py\n", "print(config.n)\n", @@ -189,13 +189,9 @@ ], "source": [ "import pandas as pd\n", - "import numpy as np\n", - "import itertools\n", - "import time \n", + "import time\n", "import json\n", - "import uuid\n", "\n", - "from pprint import pprint\n", "\n", "from google.cloud import aiplatform as aip\n", "from google.cloud import storage\n", @@ -203,15 +199,17 @@ "\n", "# logging\n", "import logging\n", + "\n", "logging.disable(logging.WARNING)\n", "\n", - "#python warning \n", + "# python warning\n", "import warnings\n", + "\n", "warnings.filterwarnings(\"ignore\")\n", "\n", - "print(f'BigQuery SDK version : {bigquery.__version__}')\n", - "print(f'Vertex AI SDK version : {aip.__version__}')\n", - "print(f'Cloud Storage SDK version : {storage.__version__}')" + "print(f\"BigQuery SDK version : {bigquery.__version__}\")\n", + "print(f\"Vertex AI SDK version : {aip.__version__}\")\n", + "print(f\"Cloud Storage SDK version : {storage.__version__}\")" ] }, { @@ -255,12 +253,10 @@ }, "outputs": [], "source": [ - "import sys\n", "import os\n", "\n", "# sys.path.append(\"..\")\n", - "from vdf_io.export_vdf.vertexai_vector_search_export import ExportVertexAIVectorSearch\n", - "from vdf_io.names import DBNames" + "from vdf_io.export_vdf.vertexai_vector_search_export import ExportVertexAIVectorSearch" ] }, { @@ -371,8 +367,8 @@ " \"index\": INDEX_DISPLAY_NAME,\n", " \"library_version\": VDF_VERSION,\n", " \"dir\": \".\",\n", - " \"model_name\":\"textembedding-gecko@001\",\n", - " \"max_vectors\": 5000\n", + " \"model_name\": \"textembedding-gecko@001\",\n", + " \"max_vectors\": 5000,\n", "}\n", "my_export_args" ] @@ -397,9 +393,7 @@ } ], "source": [ - "export_vvs = ExportVertexAIVectorSearch(\n", - " args=my_export_args \n", - ")\n", + "export_vvs = ExportVertexAIVectorSearch(args=my_export_args)\n", "\n", "export_vvs" ] @@ -673,12 +667,12 @@ } ], "source": [ - "if isinstance(my_export_args['index'], str):\n", + "if isinstance(my_export_args[\"index\"], str):\n", " file_path = f\"{VDF_EXPORT_DIR_PATH}/{my_export_args['index']}/1.parquet\"\n", - " \n", - "if isinstance(my_export_args['index'], list):\n", + "\n", + "if isinstance(my_export_args[\"index\"], list):\n", " file_path = f\"{VDF_EXPORT_DIR_PATH}/{my_export_args['index'][0]}/1.parquet\"\n", - " \n", + "\n", "print(f\"file_path: {file_path}\")\n", "\n", "test_parquet_df = pd.read_parquet(file_path)\n", @@ -716,7 +710,7 @@ }, "outputs": [], "source": [ - "ids_to_check = test_parquet_df['id'][:2].to_list()\n", + "ids_to_check = test_parquet_df[\"id\"][:2].to_list()\n", "# ids_to_check = ['2102980']\n", "# ids_to_check = ['36062183']\n", "# ids_to_check = ['48876786', '48821717']" @@ -743,8 +737,8 @@ ], "source": [ "read_response = my_index_endpoint.read_index_datapoints(\n", - " deployed_index_id=DEPLOYED_INDEX_ID, \n", - " ids = ids_to_check,\n", + " deployed_index_id=DEPLOYED_INDEX_ID,\n", + " ids=ids_to_check,\n", ")\n", "len(read_response)" ] diff --git a/src/vdf_io/notebooks/vertex_import_sample.ipynb b/src/vdf_io/notebooks/vertex_import_sample.ipynb index 01fa96d..3b70d44 100644 --- a/src/vdf_io/notebooks/vertex_import_sample.ipynb +++ b/src/vdf_io/notebooks/vertex_import_sample.ipynb @@ -30,7 +30,7 @@ "source": [ "import os\n", "\n", - "root_path = '..'\n", + "root_path = \"..\"\n", "os.chdir(root_path)\n", "os.getcwd()" ] @@ -73,8 +73,8 @@ ], "source": [ "# naming convention for all cloud resources\n", - "VERSION = \"pubv3\" # TODO\n", - "PREFIX = f'vvs-vectorio-{VERSION}' # TODO\n", + "VERSION = \"pubv3\" # TODO\n", + "PREFIX = f\"vvs-vectorio-{VERSION}\" # TODO\n", "\n", "print(f\"PREFIX = {PREFIX}\")" ] @@ -147,12 +147,12 @@ ], "source": [ "# staging GCS\n", - "GCP_PROJECTS = !gcloud config get-value project\n", - "PROJECT_ID = GCP_PROJECTS[0]\n", + "GCP_PROJECTS = !gcloud config get-value project\n", + "PROJECT_ID = GCP_PROJECTS[0]\n", "\n", "# GCS bucket and paths\n", - "BUCKET_NAME = f'{PREFIX}-{PROJECT_ID}'\n", - "BUCKET_URI = f'gs://{BUCKET_NAME}'\n", + "BUCKET_NAME = f\"{PREFIX}-{PROJECT_ID}\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", "\n", "config = !gsutil cat {BUCKET_URI}/config/notebook_env.py\n", "print(config.n)\n", @@ -213,11 +213,8 @@ ], "source": [ "import pandas as pd\n", - "import numpy as np\n", - "import itertools\n", - "import time \n", + "import time\n", "import json\n", - "import uuid\n", "\n", "from pprint import pprint\n", "\n", @@ -227,15 +224,17 @@ "\n", "# logging\n", "import logging\n", + "\n", "logging.disable(logging.WARNING)\n", "\n", - "#python warning \n", + "# python warning\n", "import warnings\n", + "\n", "warnings.filterwarnings(\"ignore\")\n", "\n", - "print(f'BigQuery SDK version : {bigquery.__version__}')\n", - "print(f'Vertex AI SDK version : {aip.__version__}')\n", - "print(f'Cloud Storage SDK version : {storage.__version__}')" + "print(f\"BigQuery SDK version : {bigquery.__version__}\")\n", + "print(f\"Vertex AI SDK version : {aip.__version__}\")\n", + "print(f\"Cloud Storage SDK version : {storage.__version__}\")" ] }, { @@ -279,7 +278,6 @@ }, "outputs": [], "source": [ - "import sys\n", "import os\n", "\n", "# sys.path.append(\"..\")\n", @@ -522,19 +520,19 @@ " \"version\": VDF_VERSION,\n", " \"exported_at\": TIMESTAMP_vdf,\n", " \"indexes\": {\n", - " \"soverflow_vvs_vectorio_pubv3\": [\n", - " {\n", - " \"data_path\": f\"{DATA_PATH}/soverflow_vvs_vectorio_pubv3\",\n", - " \"dimensions\": int(DIMENSIONS),\n", - " \"exported_vector_count\": 1000,\n", - " \"metric\": \"Dot\",\n", - " \"model_name\": \"textembedding-gecko@001\",\n", - " \"namespace\": \"\",\n", - " \"total_vector_count\": 1000,\n", - " \"vector_columns\": [\"vector\"]\n", - " }\n", - " ]\n", - " }\n", + " \"soverflow_vvs_vectorio_pubv3\": [\n", + " {\n", + " \"data_path\": f\"{DATA_PATH}/soverflow_vvs_vectorio_pubv3\",\n", + " \"dimensions\": int(DIMENSIONS),\n", + " \"exported_vector_count\": 1000,\n", + " \"metric\": \"Dot\",\n", + " \"model_name\": \"textembedding-gecko@001\",\n", + " \"namespace\": \"\",\n", + " \"total_vector_count\": 1000,\n", + " \"vector_columns\": [\"vector\"],\n", + " }\n", + " ]\n", + " },\n", "}\n", "pprint(my_vdf)" ] @@ -556,7 +554,7 @@ }, "outputs": [], "source": [ - "with open(f\"{TEST_VDF_META}\", 'w') as fp:\n", + "with open(f\"{TEST_VDF_META}\", \"w\") as fp:\n", " json.dump(my_vdf, fp)" ] }, @@ -706,7 +704,9 @@ } ], "source": [ - "TARGET_INDEX_ARG = \"new_index_vv4\" # INDEX_DISPLAY_NAME | DEPLOYED_INDEX_ID | INDEX_RESOURCE_NAME\n", + "TARGET_INDEX_ARG = (\n", + " \"new_index_vv4\" # INDEX_DISPLAY_NAME | DEPLOYED_INDEX_ID | INDEX_RESOURCE_NAME\n", + ")\n", "\n", "my_import_args = {\n", " \"project_id\": PROJECT_ID,\n", @@ -716,17 +716,12 @@ " \"dir\": DATA_PATH,\n", " \"filter_restricts\": [\n", " {\n", - " \"namespace\": \"tag\", # vertex VS namespace\n", + " \"namespace\": \"tag\", # vertex VS namespace\n", " \"allow_list\": [\"tag\"], # col name\n", " },\n", " ],\n", - " \"numeric_restricts\" : [\n", - " {\n", - " \"namespace\": \"score\", \n", - " \"data_type\": \"value_int\"\n", - " }\n", - " ],\n", - " \"crowding_tag\" : \"crowding_tag\",\n", + " \"numeric_restricts\": [{\"namespace\": \"score\", \"data_type\": \"value_int\"}],\n", + " \"crowding_tag\": \"crowding_tag\",\n", " \"create_new_index\": False,\n", " \"gcs_bucket\": BUCKET_NAME,\n", " \"machine_type\": \"e2-standard-16\",\n", @@ -788,9 +783,7 @@ } ], "source": [ - "import_vvs = ImportVertexAIVectorSearch(\n", - " args=my_import_args \n", - ")\n", + "import_vvs = ImportVertexAIVectorSearch(args=my_import_args)\n", "\n", "import_vvs" ] @@ -1005,7 +998,7 @@ } ], "source": [ - "ids_to_check = df_from_pq['id'][:3].to_list()\n", + "ids_to_check = df_from_pq[\"id\"][:3].to_list()\n", "\n", "ids_to_check" ] @@ -1031,7 +1024,7 @@ ], "source": [ "read_response = my_index_endpoint.read_index_datapoints(\n", - " deployed_index_id=DEPLOYED_INDEX_ID, \n", + " deployed_index_id=DEPLOYED_INDEX_ID,\n", " ids=ids_to_check,\n", ")\n", "len(read_response)" diff --git a/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb b/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb index 0ffda8e..53cbcc4 100644 --- a/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb +++ b/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb @@ -105,7 +105,7 @@ }, "outputs": [], "source": [ - "import os \n", + "import os\n", "import math\n", "import time\n", "import json\n", @@ -113,7 +113,6 @@ "import functools\n", "import numpy as np\n", "import pandas as pd\n", - "from pprint import pprint\n", "from datetime import datetime\n", "\n", "from google.cloud import aiplatform\n", @@ -123,10 +122,12 @@ "\n", "# logging\n", "import logging\n", + "\n", "logging.disable(logging.WARNING)\n", "\n", - "#python warning \n", + "# python warning\n", "import warnings\n", + "\n", "warnings.filterwarnings(\"ignore\")" ] }, @@ -152,12 +153,13 @@ "import pyarrow.parquet as pq\n", "from pyarrow import json as pj\n", "from concurrent.futures import ThreadPoolExecutor\n", - "from typing import Generator, List, Tuple, Dict, Any, Optional\n", + "from typing import Generator, List, Tuple, Any, Optional\n", "\n", "from vertexai.preview.language_models import TextEmbeddingModel\n", + "\n", "model = TextEmbeddingModel.from_pretrained(\"textembedding-gecko@001\")\n", "\n", - "os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'" + "os.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"2\"" ] }, { @@ -179,9 +181,9 @@ } ], "source": [ - "print(f'BigQuery SDK version : {bigquery.__version__}')\n", - "print(f'Vertex AI SDK version : {aiplatform.__version__}')\n", - "print(f'Cloud Storage SDK version : {storage.__version__}')" + "print(f\"BigQuery SDK version : {bigquery.__version__}\")\n", + "print(f\"Vertex AI SDK version : {aiplatform.__version__}\")\n", + "print(f\"Cloud Storage SDK version : {storage.__version__}\")" ] }, { @@ -206,7 +208,7 @@ "outputs": [], "source": [ "# create new gcs bucket, vs index, etc.?\n", - "CREATE_NEW_ASSETS = False " + "CREATE_NEW_ASSETS = False" ] }, { @@ -235,8 +237,8 @@ ], "source": [ "# naming convention for all cloud resources\n", - "VERSION = \"pubv3\" # TODO\n", - "PREFIX = f'vvs-vectorio-{VERSION}' # TODO\n", + "VERSION = \"pubv3\" # TODO\n", + "PREFIX = f\"vvs-vectorio-{VERSION}\" # TODO\n", "\n", "print(f\"PREFIX = {PREFIX}\")" ] @@ -268,8 +270,8 @@ ], "source": [ "# locations / regions for cloud resources\n", - "REGION = 'us-central1' \n", - "BQ_REGION = 'US'\n", + "REGION = \"us-central1\"\n", + "BQ_REGION = \"US\"\n", "\n", "print(f\"REGION = {REGION}\")\n", "print(f\"BQ_REGION = {BQ_REGION}\")" @@ -305,18 +307,18 @@ ], "source": [ "# let these ride\n", - "GCP_PROJECTS = !gcloud config get-value project\n", - "PROJECT_ID = GCP_PROJECTS[0]\n", + "GCP_PROJECTS = !gcloud config get-value project\n", + "PROJECT_ID = GCP_PROJECTS[0]\n", "\n", - "PROJECT_NUM = !gcloud projects describe $PROJECT_ID --format=\"value(projectNumber)\"\n", - "PROJECT_NUM = PROJECT_NUM[0]\n", + "PROJECT_NUM = !gcloud projects describe $PROJECT_ID --format=\"value(projectNumber)\"\n", + "PROJECT_NUM = PROJECT_NUM[0]\n", "\n", "# GCS bucket and paths\n", - "BUCKET_NAME = f'{PREFIX}-{PROJECT_ID}'\n", - "BUCKET_URI = f'gs://{BUCKET_NAME}'\n", + "BUCKET_NAME = f\"{PREFIX}-{PROJECT_ID}\"\n", + "BUCKET_URI = f\"gs://{BUCKET_NAME}\"\n", "\n", "# service account\n", - "VERTEX_SA = f'{PROJECT_NUM}-compute@developer.gserviceaccount.com'\n", + "VERTEX_SA = f\"{PROJECT_NUM}-compute@developer.gserviceaccount.com\"\n", "\n", "print(f\"PROJECT_ID = {PROJECT_ID}\")\n", "print(f\"PROJECT_NUM = {PROJECT_NUM}\")\n", @@ -363,21 +365,20 @@ ], "source": [ "if CREATE_NEW_ASSETS:\n", - " \n", " # create new bucket\n", " ! gsutil mb -l $REGION $BUCKET_URI\n", - " \n", + "\n", " # ### give Service account Admin to GCS\n", " # !gcloud projects add-iam-policy-binding $PROJECT_ID \\\n", " # --member=serviceAccount:$VERTEX_SA \\\n", " # --role=roles/storage.admin\n", - " \n", + "\n", " ### uncomment if org policy prevents granting Admin:\n", " # ! gsutil iam ch serviceAccount:{$VERTEX_SA}:roles/storage.objects.get $BUCKET_URI\n", " # ! gsutil iam ch serviceAccount:{$VERTEX_SA}:roles/storage.objects.create $BUCKET_URI\n", " # ! gsutil iam ch serviceAccount:{$VERTEX_SA}:roles/storage.objects.list $BUCKET_URI\n", - " \n", - " \n", + "\n", + "\n", "print(f\"{VERTEX_SA} should have access to {BUCKET_URI}\")" ] }, @@ -464,20 +465,18 @@ "\n", "\n", "def query_bigquery_chunks(\n", - " max_rows: int, \n", - " rows_per_chunk: int, \n", - " start_chunk: int = 0\n", + " max_rows: int, rows_per_chunk: int, start_chunk: int = 0\n", ") -> Generator[pd.DataFrame, Any, None]:\n", - " \n", + "\n", " for offset in range(start_chunk, max_rows, rows_per_chunk):\n", " query = QUERY_TEMPLATE.format(limit=rows_per_chunk, offset=offset)\n", " query_job = bq_client.query(query)\n", " rows = query_job.result()\n", " df = rows.to_dataframe()\n", " df[\"title_with_body\"] = df.title + \"\\n\" + df.body\n", - " df['tags_split_1'] = df['tags'].apply(lambda x: x.split('|', maxsplit=1)[0])\n", - " df['tags_split_2'] = df['tags'].apply(lambda x: x.rsplit('|', maxsplit=1)[-1])\n", - " df.drop(columns=[\"title\",\"body\",\"tags\"], inplace=True)\n", + " df[\"tags_split_1\"] = df[\"tags\"].apply(lambda x: x.split(\"|\", maxsplit=1)[0])\n", + " df[\"tags_split_2\"] = df[\"tags\"].apply(lambda x: x.rsplit(\"|\", maxsplit=1)[-1])\n", + " df.drop(columns=[\"title\", \"body\", \"tags\"], inplace=True)\n", " yield df" ] }, @@ -580,12 +579,7 @@ ], "source": [ "# Get a dataframe of 1000 rows for demonstration purposes\n", - "df_test = next(\n", - " query_bigquery_chunks(\n", - " max_rows=100, \n", - " rows_per_chunk=100\n", - " )\n", - ")\n", + "df_test = next(query_bigquery_chunks(max_rows=100, rows_per_chunk=100))\n", "\n", "# Examine the data\n", "print(f\"df shape: {df_test.shape}\")\n", @@ -741,19 +735,18 @@ " return [embedding.values for embedding in embeddings]\n", " except Exception:\n", " return [None for _ in range(len(sentences))]\n", - " \n", + "\n", + "\n", "# Generator function to yield batches of sentences\n", "def generate_batches(\n", - " sentences: List[str], \n", - " batch_size: int\n", + " sentences: List[str], batch_size: int\n", ") -> Generator[List[str], None, None]:\n", " for i in range(0, len(sentences), batch_size):\n", " yield sentences[i : i + batch_size]\n", - " \n", + "\n", + "\n", "def encode_text_to_embedding_batched(\n", - " sentences: List[str], \n", - " api_calls_per_second: int = 10, \n", - " batch_size: int = 5\n", + " sentences: List[str], api_calls_per_second: int = 10, batch_size: int = 5\n", ") -> Tuple[List[bool], np.ndarray]:\n", "\n", " embeddings_list: List[List[float]] = []\n", @@ -784,6 +777,7 @@ " )\n", " return is_successful, embeddings_list_successful\n", "\n", + "\n", "def create_emb_vector_files(\n", " bq_num_rows: int = 1000,\n", " bq_chunk_size: int = 100,\n", @@ -791,7 +785,7 @@ " start_chunk: int = 0,\n", " api_calls_per_sec: int = 50,\n", " items_per_request: int = 5,\n", - " emb_file_path: str = None\n", + " emb_file_path: str = None,\n", "):\n", " print(f\"bq_num_rows : {bq_num_rows}\")\n", " print(f\"bq_chunk_size : {bq_chunk_size}\")\n", @@ -800,31 +794,29 @@ " print(f\"api_calls_per_sec : {api_calls_per_sec}\")\n", " print(f\"items_per_request : {items_per_request}\")\n", " print(f\"emb_file_path : {emb_file_path}\")\n", - " \n", + "\n", " rows_list = []\n", - " \n", + "\n", " # Loop through each generated dataframe, convert\n", " for i, df in tqdm(\n", " enumerate(\n", " query_bigquery_chunks(\n", - " max_rows=bq_num_rows, \n", - " rows_per_chunk=bq_chunk_size, \n", - " start_chunk=start_chunk\n", + " max_rows=bq_num_rows,\n", + " rows_per_chunk=bq_chunk_size,\n", + " start_chunk=start_chunk,\n", " )\n", " ),\n", - " total=bq_num_chunks,# - start_chunk,\n", + " total=bq_num_chunks, # - start_chunk,\n", " position=-1,\n", " desc=\"Chunk of rows from BigQuery\",\n", " ):\n", - " \n", " print(f\"Starting: {i} of {bq_num_chunks} loops\")\n", - " \n", + "\n", " # Create a unique output file for each chunk\n", " chunk_path = emb_file_path.joinpath(\n", - " f\"{emb_file_path.stem}_{i+start_chunk}.json\"\n", + " f\"{emb_file_path.stem}_{i + start_chunk}.json\"\n", " )\n", " with open(chunk_path, \"a\") as f:\n", - " \n", " id_chunk = df.id\n", " scores_chunk = df.score\n", " tags_1_chunk = df.tags_split_1\n", @@ -832,7 +824,7 @@ "\n", " # Convert batch to embeddings\n", " is_successful, question_chunk_embeddings = encode_text_to_embedding_batched(\n", - " sentences=df.title_with_body.tolist(), #[:500]\n", + " sentences=df.title_with_body.tolist(), # [:500]\n", " api_calls_per_second=api_calls_per_sec,\n", " batch_size=items_per_request,\n", " )\n", @@ -841,21 +833,19 @@ " json.dumps(\n", " {\n", " \"id\": str(id),\n", - " \"embedding\": [\n", - " str(value) for value in embedding\n", - " ],\n", - " \"tag\": str(r_tag), # restricts_allow\n", - " \"score\": int(score), # numeric_restricts\n", - " \"crowding_tag\": str(c_tag)\n", + " \"embedding\": [str(value) for value in embedding],\n", + " \"tag\": str(r_tag), # restricts_allow\n", + " \"score\": int(score), # numeric_restricts\n", + " \"crowding_tag\": str(c_tag),\n", " }\n", " )\n", " + \"\\n\"\n", " for id, embedding, r_tag, score, c_tag in zip(\n", - " id_chunk[is_successful], \n", - " question_chunk_embeddings, \n", - " tags_1_chunk, \n", - " scores_chunk, \n", - " tags_2_chunk\n", + " id_chunk[is_successful],\n", + " question_chunk_embeddings,\n", + " tags_1_chunk,\n", + " scores_chunk,\n", + " tags_2_chunk,\n", " )\n", " ]\n", " f.writelines(embeddings_formatted)\n", @@ -869,11 +859,11 @@ " # Delete the DataFrame and any other large data structures\n", " del df\n", " gc.collect()\n", - " \n", + "\n", " print(\"loops complete...\\n\")\n", " print(f\"len(embeddings_formatted) : {len(embeddings_formatted)}\")\n", " print(f\"len(embeddings_formatted[0]) : {len(embeddings_formatted[0])}\")\n", - " \n", + "\n", " return embeddings_formatted[0]" ] }, @@ -940,7 +930,6 @@ ], "source": [ "import gc\n", - "import json\n", "import tempfile\n", "from pathlib import Path\n", "\n", @@ -967,11 +956,11 @@ } ], "source": [ - "BQ_NUM_ROWS = 5000 # position to stop\n", - "BQ_CHUNK_SIZE = 1000 # incrementation\n", - "NEXT_START = 2000 # position to start\n", + "BQ_NUM_ROWS = 5000 # position to stop\n", + "BQ_CHUNK_SIZE = 1000 # incrementation\n", + "NEXT_START = 2000 # position to start\n", "\n", - "BQ_NUM_CHUNKS = math.ceil(BQ_NUM_ROWS / BQ_CHUNK_SIZE)\n", + "BQ_NUM_CHUNKS = math.ceil(BQ_NUM_ROWS / BQ_CHUNK_SIZE)\n", "API_CALLS_PER_SEC = 300 / 60\n", "\n", "print(f\"BQ_NUM_CHUNKS : {BQ_NUM_CHUNKS}\")\n", @@ -1104,13 +1093,13 @@ ], "source": [ "sample_formatted_emb = create_emb_vector_files(\n", - " bq_num_rows = BQ_NUM_ROWS,\n", - " bq_chunk_size = BQ_CHUNK_SIZE,\n", - " bq_num_chunks = BQ_NUM_CHUNKS,\n", - " start_chunk = NEXT_START,\n", - " api_calls_per_sec = API_CALLS_PER_SEC,\n", - " items_per_request = 5,\n", - " emb_file_path = emb_json_file_path\n", + " bq_num_rows=BQ_NUM_ROWS,\n", + " bq_chunk_size=BQ_CHUNK_SIZE,\n", + " bq_num_chunks=BQ_NUM_CHUNKS,\n", + " start_chunk=NEXT_START,\n", + " api_calls_per_sec=API_CALLS_PER_SEC,\n", + " items_per_request=5,\n", + " emb_file_path=emb_json_file_path,\n", ")" ] }, @@ -1151,7 +1140,9 @@ } ], "source": [ - "REMOTE_GCS_FOLDER = f\"{BUCKET_URI}/{PREFIX}/embedding_indexes/{emb_json_file_path.stem}/\"\n", + "REMOTE_GCS_FOLDER = (\n", + " f\"{BUCKET_URI}/{PREFIX}/embedding_indexes/{emb_json_file_path.stem}/\"\n", + ")\n", "print(f\"REMOTE_GCS_FOLDER: {REMOTE_GCS_FOLDER}\")" ] }, @@ -1252,18 +1243,17 @@ "PARQUET_GCS_FILE_LIST = []\n", "\n", "for f in emb_json_file_path.iterdir():\n", - " \n", " local_f = os.path.abspath(str(f))\n", " dest_file = os.path.join(SO_PARQUET_GCS_DIR, f\"{f.stem}.parquet\")\n", - " \n", + "\n", " # print(f\"reading from: {local_f}\")\n", " table = pj.read_json(local_f)\n", - " \n", + "\n", " # print(f\"saving to: {dest_file}\")\n", " pq.write_table(table, dest_file)\n", - " \n", + "\n", " PARQUET_GCS_FILE_LIST.append(dest_file)\n", - " \n", + "\n", "print(f\"saved parquet files to: {SO_PARQUET_GCS_DIR}\\n\")\n", "PARQUET_GCS_FILE_LIST" ] @@ -1438,17 +1428,16 @@ "LOCAL_PARQUEST_FILE_LIST = []\n", "\n", "for file in PARQUET_GCS_FILE_LIST:\n", - "\n", - " file_name = file.rsplit('/', maxsplit=1)[-1]\n", + " file_name = file.rsplit(\"/\", maxsplit=1)[-1]\n", " print(file_name)\n", - " \n", + "\n", " LOCAL_PARQUET_FILE = f\"{LOCAL_TEST_DATA_DIR}/so_{file_name}\"\n", - " \n", + "\n", " df_tmp = pd.read_parquet(file)\n", " df_tmp.to_parquet(LOCAL_PARQUET_FILE)\n", - " \n", + "\n", " LOCAL_PARQUEST_FILE_LIST.append(LOCAL_PARQUET_FILE)\n", - " \n", + "\n", " # Delete the DataFrame and any other large data structures\n", " del df_tmp\n", " gc.collect()" @@ -1593,8 +1582,10 @@ "\n", "# if using exsiting index\n", "if not CREATE_NEW_VS_INDEX:\n", - " EXISTING_INDEX_ID = \"1081325705452584960\" # TODO\n", - " EXISTING_INDEX_NAME = f'projects/{PROJECT_NUM}/locations/{REGION}/indexes/{EXISTING_INDEX_ID}'\n", + " EXISTING_INDEX_ID = \"1081325705452584960\" # TODO\n", + " EXISTING_INDEX_NAME = (\n", + " f\"projects/{PROJECT_NUM}/locations/{REGION}/indexes/{EXISTING_INDEX_ID}\"\n", + " )\n", " print(f\"EXISTING_INDEX_NAME : {EXISTING_INDEX_NAME}\")" ] }, @@ -1616,17 +1607,17 @@ ], "source": [ "# specify VPC network or leave blank\n", - "VPC_NETWORK_NAME = \"\" # e.g., \"your-vpc-name\" | \"\"\n", + "VPC_NETWORK_NAME = \"\" # e.g., \"your-vpc-name\" | \"\"\n", "\n", "if VPC_NETWORK_NAME:\n", - " USE_PUBLIC_ENDPOINTS = False\n", + " USE_PUBLIC_ENDPOINTS = False\n", " # full VPC network name\n", - " VPC_NETWORK_FULL = f\"projects/{PROJECT_NUM}/global/networks/{VPC_NETWORK_NAME}\"\n", + " VPC_NETWORK_FULL = f\"projects/{PROJECT_NUM}/global/networks/{VPC_NETWORK_NAME}\"\n", " print(f\"VPC_NETWORK_NAME : {VPC_NETWORK_NAME}\")\n", " print(f\"VPC_NETWORK_FULL : {VPC_NETWORK_FULL}\")\n", "else:\n", - " USE_PUBLIC_ENDPOINTS = True\n", - " VPC_NETWORK_FULL = None\n", + " USE_PUBLIC_ENDPOINTS = True\n", + " VPC_NETWORK_FULL = None\n", "\n", "print(f\"USE_PUBLIC_ENDPOINTS = {USE_PUBLIC_ENDPOINTS}\")" ] @@ -1658,21 +1649,21 @@ "# =========================================================\n", "# ANN index config\n", "# =========================================================\n", - "DISPLAY_NAME = f\"soverflow_{PREFIX}\".replace(\"-\",\"_\")\n", - "DESCRIPTION = \"sample index for vectorio demo\"\n", - "APPROX_NEIGHBORS = 150\n", - "DISTANCE_MEASURE = \"DOT_PRODUCT_DISTANCE\"\n", + "DISPLAY_NAME = f\"soverflow_{PREFIX}\".replace(\"-\", \"_\")\n", + "DESCRIPTION = \"sample index for vectorio demo\"\n", + "APPROX_NEIGHBORS = 150\n", + "DISTANCE_MEASURE = \"DOT_PRODUCT_DISTANCE\"\n", "LEAF_NODE_EMB_COUNT = 500\n", "LEAF_SEARCH_PERCENT = 80\n", - "DIMENSIONS = 768\n", - "INDEX_UPDATE_METHOD = aipv1.Index.IndexUpdateMethod.STREAM_UPDATE # \"STREAM_UPDATE\"\n", - "INDEX_SHARD_SIZE = \"SHARD_SIZE_MEDIUM\"\n", + "DIMENSIONS = 768\n", + "INDEX_UPDATE_METHOD = aipv1.Index.IndexUpdateMethod.STREAM_UPDATE # \"STREAM_UPDATE\"\n", + "INDEX_SHARD_SIZE = \"SHARD_SIZE_MEDIUM\"\n", "\n", "# =========================================================\n", "# index endpoint config\n", "# =========================================================\n", - "ENDPOINT_DISPLAY_NAME = f'{DISPLAY_NAME}_endpoint'\n", - "ENDPOINT_DESCRIPTION = \"index endpoint for vectorio demo\"\n", + "ENDPOINT_DISPLAY_NAME = f\"{DISPLAY_NAME}_endpoint\"\n", + "ENDPOINT_DESCRIPTION = \"index endpoint for vectorio demo\"\n", "print(f\"ENDPOINT_DISPLAY_NAME : {ENDPOINT_DISPLAY_NAME}\")\n", "print(f\"ENDPOINT_DESCRIPTION : {ENDPOINT_DESCRIPTION}\")\n", "print(f\"USE_PUBLIC_ENDPOINTS : {USE_PUBLIC_ENDPOINTS}\")\n", @@ -1682,9 +1673,9 @@ "# =========================================================\n", "timestamp = datetime.now().strftime(\"%Y%m%d%H%M%S\")\n", "DEPLOYED_INDEX_ID = f\"{DISPLAY_NAME.replace('-', '_')}_{timestamp}\"\n", - "MACHINE_TYPE = \"e2-standard-16\"\n", - "MIN_REPLICAS = 1\n", - "MAX_REPLICAS = 1\n", + "MACHINE_TYPE = \"e2-standard-16\"\n", + "MIN_REPLICAS = 1\n", + "MAX_REPLICAS = 1\n", "print(f\"DEPLOYED_INDEX_ID : {DEPLOYED_INDEX_ID}\")\n", "print(f\"# characters (< 128) : {len(DEPLOYED_INDEX_ID)}\")\n", "print(f\"MACHINE_TYPE : {MACHINE_TYPE}\")\n", @@ -1699,9 +1690,7 @@ " \"project_id\": PROJECT_ID,\n", "}\n", "endpoint = \"{}-aiplatform.googleapis.com\".format(project_config[\"region\"])\n", - "index_client = aipv1.IndexServiceClient(\n", - " client_options=dict(api_endpoint=endpoint)\n", - ")" + "index_client = aipv1.IndexServiceClient(client_options=dict(api_endpoint=endpoint))" ] }, { @@ -1753,12 +1742,8 @@ "outputs": [], "source": [ "if CREATE_NEW_VS_INDEX:\n", - " \n", " # dummy embedding\n", - " init_embedding = {\n", - " \"id\": str(uuid.uuid4()),\n", - " \"embedding\": list(np.zeros(DIMENSIONS))\n", - " }\n", + " init_embedding = {\"id\": str(uuid.uuid4()), \"embedding\": list(np.zeros(DIMENSIONS))}\n", "\n", " # dump embedding to a local file\n", " with open(LOCAL_INIT_FILE, \"w\") as f:\n", @@ -1840,9 +1825,7 @@ " \"index_shard_size\": INDEX_SHARD_SIZE,\n", " \"index_update_method\": INDEX_UPDATE_METHOD,\n", " \"description\": DESCRIPTION,\n", - " \"labels\": {\n", - " \"prefix\": PREFIX\n", - " },\n", + " \"labels\": {\"prefix\": PREFIX},\n", " \"index_endpoint_display_name\": ENDPOINT_DISPLAY_NAME,\n", " \"index_endpoint_description\": ENDPOINT_DESCRIPTION,\n", " \"deployed_index_id\": DEPLOYED_INDEX_ID,\n", @@ -1865,9 +1848,7 @@ " number_value=VS_CONFIG_SPEC[\"leaf_node_embedding_count\"]\n", " ),\n", " \"leafNodesToSearchPercent\": struct_pb2.Value(\n", - " number_value=VS_CONFIG_SPEC[\n", - " \"leaf_nodes_to_search_percent\"\n", - " ]\n", + " number_value=VS_CONFIG_SPEC[\"leaf_nodes_to_search_percent\"]\n", " ),\n", " }\n", ")\n", @@ -1899,9 +1880,7 @@ "source": [ "VS_CONFIG = struct_pb2.Struct(\n", " fields={\n", - " \"dimensions\": struct_pb2.Value(\n", - " number_value=VS_CONFIG_SPEC[\"dimensions\"]\n", - " ),\n", + " \"dimensions\": struct_pb2.Value(number_value=VS_CONFIG_SPEC[\"dimensions\"]),\n", " \"approximateNeighborsCount\": struct_pb2.Value(\n", " number_value=VS_CONFIG_SPEC[\"approximate_neighbors_count\"]\n", " ),\n", @@ -1949,7 +1928,7 @@ " \"metadata\": struct_pb2.Value(struct_value=VS_METADATA),\n", " \"index_update_method\": VS_CONFIG_SPEC[\"index_update_method\"],\n", "}\n", - " \n", + "\n", "PARENT = f\"projects/{project_config['project_id']}/locations/{project_config['region']}\"" ] }, @@ -1994,28 +1973,27 @@ ], "source": [ "if CREATE_NEW_VS_INDEX:\n", - " \n", " print(f\"Creating new index: {VS_CONFIG_SPEC['index_display_name']} ...\")\n", " start = time.time()\n", " create_lro = index_client.create_index(parent=PARENT, index=INDEX_REQUEST)\n", - " \n", + "\n", " # Poll the operation until it's done successfullly.\n", " while True:\n", " if create_lro.done():\n", " break\n", " time.sleep(5)\n", - " \n", + "\n", " index = create_lro.result()\n", " my_vs_index = aiplatform.MatchingEngineIndex(index.name)\n", - " \n", + "\n", " end = time.time()\n", " print(f\"elapsed time: {round((end - start), 2)}\")\n", - " \n", + "\n", "else:\n", " my_vs_index = aiplatform.MatchingEngineIndex(EXISTING_INDEX_NAME)\n", - " \n", + "\n", "INDEX_RESOURCE_NAME = my_vs_index.resource_name\n", - "INDEX_DISPLAY_NAME = my_vs_index.display_name\n", + "INDEX_DISPLAY_NAME = my_vs_index.display_name\n", "\n", "print(f\"INDEX_RESOURCE_NAME : {INDEX_RESOURCE_NAME}\")\n", "print(f\"INDEX_DISPLAY_NAME : {INDEX_DISPLAY_NAME}\")" @@ -2058,7 +2036,7 @@ } ], "source": [ - "# get all index config to dictionary \n", + "# get all index config to dictionary\n", "my_vs_index.to_dict()" ] }, @@ -2104,8 +2082,8 @@ "\n", "# if using exsiting index endpoint\n", "if not CREATE_NEW_VS_INDEX_ENDPOINT:\n", - " EXISTING_ENDPOINT_ID = \"5739455095037231104\" # TODO\n", - " EXISTING_ENDPOINT_NAME = f'projects/{PROJECT_NUM}/locations/{REGION}/indexEndpoints/{EXISTING_ENDPOINT_ID}'\n", + " EXISTING_ENDPOINT_ID = \"5739455095037231104\" # TODO\n", + " EXISTING_ENDPOINT_NAME = f\"projects/{PROJECT_NUM}/locations/{REGION}/indexEndpoints/{EXISTING_ENDPOINT_ID}\"\n", " print(f\"EXISTING_ENDPOINT_NAME : {EXISTING_ENDPOINT_NAME}\")" ] }, @@ -2128,8 +2106,9 @@ ], "source": [ "if CREATE_NEW_VS_INDEX_ENDPOINT:\n", - " \n", - " print(f\"Creating new index endpoint: {VS_CONFIG_SPEC['index_endpoint_display_name']} ...\")\n", + " print(\n", + " f\"Creating new index endpoint: {VS_CONFIG_SPEC['index_endpoint_display_name']} ...\"\n", + " )\n", " start = time.time()\n", " my_index_endpoint = aiplatform.MatchingEngineIndexEndpoint.create(\n", " display_name=VS_CONFIG_SPEC[\"index_endpoint_display_name\"],\n", @@ -2140,11 +2119,11 @@ " )\n", " end = time.time()\n", " print(f\"elapsed time: {round((end - start), 2)}\")\n", - " \n", + "\n", "else:\n", " my_index_endpoint = aiplatform.MatchingEngineIndexEndpoint(EXISTING_ENDPOINT_NAME)\n", - " \n", - "ENDPOINT_DISPLAY_NAME = my_index_endpoint.display_name\n", + "\n", + "ENDPOINT_DISPLAY_NAME = my_index_endpoint.display_name\n", "ENDPOINT_RESOURCE_NAME = my_index_endpoint.resource_name\n", "\n", "print(f\"ENDPOINT_DISPLAY_NAME : {ENDPOINT_DISPLAY_NAME}\")\n", @@ -2255,22 +2234,21 @@ ], "source": [ "if DEPLOY_NEW_VS_INDEX:\n", - " \n", " print(f\"Deploying index to endpoint: {ENDPOINT_DISPLAY_NAME} ...\")\n", " start = time.time()\n", "\n", " deployed_index = my_index_endpoint.deploy_index(\n", " index=my_vs_index,\n", - " deployed_index_id=VS_CONFIG_SPEC['deployed_index_id'],\n", + " deployed_index_id=VS_CONFIG_SPEC[\"deployed_index_id\"],\n", " min_replica_count=MIN_REPLICAS,\n", " max_replica_count=MAX_REPLICAS,\n", " )\n", - " \n", + "\n", " end = time.time()\n", " print(f\"elapsed time: {round((end - start), 2)}\")\n", "else:\n", " deployed_index = aiplatform.MatchingEngineIndexEndpoint(EXISTING_ENDPOINT_NAME)\n", - " \n", + "\n", "PUBLIC_ENDPOINT_URL = deployed_index.public_endpoint_domain_name\n", "DEPLOYED_INDEX_ID_TEST = deployed_index.deployed_indexes[0].id\n", "\n", @@ -2296,7 +2274,7 @@ } ], "source": [ - "print(f\"Deployed indexes on the index endpoint:\")\n", + "print(\"Deployed indexes on the index endpoint:\")\n", "for d in my_index_endpoint.deployed_indexes:\n", " print(f\" {d.id}\")" ] @@ -2327,7 +2305,7 @@ } ], "source": [ - "MY_INDEX_ID = INDEX_RESOURCE_NAME.split(\"/\")[5]\n", + "MY_INDEX_ID = INDEX_RESOURCE_NAME.split(\"/\")[5]\n", "MY_INDEX_ENDPOINT_ID = ENDPOINT_RESOURCE_NAME.split(\"/\")[5]\n", "\n", "print(f\"MY_INDEX_ID = {MY_INDEX_ID}\")\n", @@ -2405,7 +2383,7 @@ } ], "source": [ - "LOCAL_PARQUEST_FILE_STR = '|'.join(LOCAL_PARQUEST_FILE_LIST)\n", + "LOCAL_PARQUEST_FILE_STR = \"|\".join(LOCAL_PARQUEST_FILE_LIST)\n", "LOCAL_PARQUEST_FILE_STR" ] }, diff --git a/src/vdf_io/notebooks/vespa-trial.ipynb b/src/vdf_io/notebooks/vespa-trial.ipynb index 3bca855..f3420cd 100644 --- a/src/vdf_io/notebooks/vespa-trial.ipynb +++ b/src/vdf_io/notebooks/vespa-trial.ipynb @@ -130,10 +130,8 @@ "outputs": [], "source": [ "from vespa.application import Vespa\n", - "from vespa.io import VespaQueryResponse\n", - "from vespa.exceptions import VespaError\n", "\n", - "app = Vespa(url=\"https://api.cord19.vespa.ai\",cert=None,vespa_cloud_secret_token=None)" + "app = Vespa(url=\"https://api.cord19.vespa.ai\", cert=None, vespa_cloud_secret_token=None)" ] }, { @@ -14660,9 +14658,7 @@ ] } ], - "source": [ - "from marqo.vespa.vespa_client import VespaClient" - ] + "source": [] }, { "cell_type": "code", diff --git a/src/vdf_io/notebooks/weaviate_fill.ipynb b/src/vdf_io/notebooks/weaviate_fill.ipynb index f6fe5f3..f4081f2 100644 --- a/src/vdf_io/notebooks/weaviate_fill.ipynb +++ b/src/vdf_io/notebooks/weaviate_fill.ipynb @@ -136,12 +136,7 @@ " name=\"TestArticle\",\n", " vectorizer_config=None,\n", " generative_config=None,\n", - " properties=[\n", - " wvcc.Property(\n", - " name=\"title\",\n", - " data_type=wvcc.DataType.TEXT\n", - " )\n", - " ]\n", + " properties=[wvcc.Property(name=\"title\", data_type=wvcc.DataType.TEXT)],\n", ")" ] }, @@ -160,14 +155,9 @@ "metadata": {}, "outputs": [], "source": [ - "collection.data.insert_many([\n", - " {\n", - " \"title\": \"The first article\"\n", - " },\n", - " {\n", - " \"title\": \"The second article\"\n", - " }\n", - "])" + "collection.data.insert_many(\n", + " [{\"title\": \"The first article\"}, {\"title\": \"The second article\"}]\n", + ")" ] }, { diff --git a/src/vdf_io/notebooks/wit-resnet.ipynb b/src/vdf_io/notebooks/wit-resnet.ipynb index 501699b..bca079f 100644 --- a/src/vdf_io/notebooks/wit-resnet.ipynb +++ b/src/vdf_io/notebooks/wit-resnet.ipynb @@ -23,7 +23,7 @@ } ], "source": [ - "%pip install --upgrade Pillow\n" + "%pip install --upgrade Pillow" ] }, { @@ -51,7 +51,6 @@ "image_url = \"/Users/dhruvanand/Code/vector-io/Scolopendra_gigantea.jpg\"\n", "\n", "\n", - "\n", "# Load the image as a torch tensor\n", "\n", "image_processor = ConvNextImageProcessor.from_pretrained(\"microsoft/resnet-50\")\n", @@ -217,7 +216,6 @@ ], "source": [ "from PIL import Image\n", - "import requests\n", "from transformers import ConvNextFeatureExtractor, FlaxResNetModel\n", "\n", "url = \"http://images.cocodataset.org/val2017/000000039769.jpg\"\n", diff --git a/src/vdf_io/scripts/reembed.py b/src/vdf_io/scripts/reembed.py index 85ddf48..2a6cbe5 100755 --- a/src/vdf_io/scripts/reembed.py +++ b/src/vdf_io/scripts/reembed.py @@ -225,7 +225,7 @@ def ask_for_text_column(args, file_path, df): # pick first non-null value non_null_value = df[col].dropna().iloc[0] if isinstance(non_null_value, str): - tqdm.write(f"{i+1}: {col}") + tqdm.write(f"{i + 1}: {col}") text_column_options[i + 1] = col choice_correctly_entered = False while not choice_correctly_entered: diff --git a/src/vdf_io/util.py b/src/vdf_io/util.py index 18554a8..ebeab58 100644 --- a/src/vdf_io/util.py +++ b/src/vdf_io/util.py @@ -336,7 +336,7 @@ def get_parquet_files(data_path, args, temp_file_paths=[], id_column=ID_COLUMN): return [ "hf://" + x for x in fs.glob( - f"datasets/{args.get('hf_dataset')}/{data_path if data_path!='.' else ''}/**.parquet" + f"datasets/{args.get('hf_dataset')}/{data_path if data_path != '.' else ''}/**.parquet" ) ] if not os.path.isdir(data_path):