From 4d7db42259604d25ca27ba01f5c8cd636df98a0d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:13:47 +0000 Subject: [PATCH 1/2] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.6 β†’ v0.16.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.16.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c59f99..e056cbb 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.16.0 hooks: # Run the linter. - id: ruff From 2b408f1fd1d8241d86ea1299beaf1628faef7d3e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:13:57 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- archive/json-yaml.py | 5 +- src/vdf_io/export_vdf/astradb_export.py | 12 +- src/vdf_io/export_vdf/chroma_export.py | 4 +- src/vdf_io/export_vdf/kdbai_export.py | 14 +- src/vdf_io/export_vdf/lancedb_export.py | 8 +- src/vdf_io/export_vdf/milvus_export.py | 20 +- src/vdf_io/export_vdf/pinecone_export.py | 18 +- src/vdf_io/export_vdf/qdrant_export.py | 16 +- src/vdf_io/export_vdf/turbopuffer_export.py | 9 +- src/vdf_io/export_vdf/txtai_export.py | 2 +- src/vdf_io/export_vdf/vdb_export_cls.py | 19 +- .../vertexai_vector_search_export.py | 12 +- src/vdf_io/export_vdf/vespa_export.py | 7 +- src/vdf_io/export_vdf/weaviate_export.py | 2 +- src/vdf_io/export_vdf_cli.py | 11 +- src/vdf_io/import_vdf/astradb_import.py | 36 +- src/vdf_io/import_vdf/azureai_import.py | 4 +- src/vdf_io/import_vdf/chroma_import.py | 13 +- src/vdf_io/import_vdf/kdbai_import.py | 14 +- src/vdf_io/import_vdf/lancedb_import.py | 17 +- src/vdf_io/import_vdf/milvus_import.py | 15 +- src/vdf_io/import_vdf/pinecone_import.py | 10 +- src/vdf_io/import_vdf/qdrant_import.py | 40 +- src/vdf_io/import_vdf/turbopuffer_import.py | 10 +- src/vdf_io/import_vdf/vdf_import_cls.py | 10 +- .../vertexai_vector_search_import.py | 40 +- src/vdf_io/import_vdf_cli.py | 9 +- src/vdf_io/marqo_vespa_util.py | 27 +- src/vdf_io/meta_types.py | 15 +- .../notebooks/ get_data_from_json.ipynb | 3 +- .../notebooks/01_Introducing_txtai.ipynb | 2173 +++++++++-------- src/vdf_io/notebooks/aiven-qs.ipynb | 41 +- src/vdf_io/notebooks/astra_usage.ipynb | 29 +- src/vdf_io/notebooks/chroma-qs.ipynb | 39 +- src/vdf_io/notebooks/deeplake.ipynb | 2 +- src/vdf_io/notebooks/download_dataset.ipynb | 3 +- src/vdf_io/notebooks/json_pandas.ipynb | 21 +- src/vdf_io/notebooks/jsonl_to_parquet.ipynb | 18 +- .../notebooks/jsonltgz_to_parquet.ipynb | 8 +- .../notebooks/kdbai_end_to_end_vectorIO.ipynb | 1 + src/vdf_io/notebooks/lance-qs.ipynb | 33 +- src/vdf_io/notebooks/medium-articles.ipynb | 22 +- src/vdf_io/notebooks/mlx.ipynb | 7 +- src/vdf_io/notebooks/ny_embs.ipynb | 1 - src/vdf_io/notebooks/pc_upsert_sample.ipynb | 1 + src/vdf_io/notebooks/qdrant-big.ipynb | 58 +- src/vdf_io/notebooks/qdrant_datasets.ipynb | 3 +- src/vdf_io/notebooks/semantic_search.ipynb | 3 +- src/vdf_io/notebooks/similar-words.ipynb | 12 +- src/vdf_io/notebooks/test_filtering_pc.ipynb | 1 + .../notebooks/test_filtering_pc_log.ipynb | 1 + src/vdf_io/notebooks/tpuf-qs.ipynb | 20 +- src/vdf_io/notebooks/upsert_pinecone.ipynb | 38 +- .../notebooks/vertex_export_sample.ipynb | 64 +- .../notebooks/vertex_import_sample.ipynb | 89 +- .../vertex_quickstart_w_bq_datasets.ipynb | 315 ++- src/vdf_io/notebooks/vespa-trial.ipynb | 8 +- src/vdf_io/notebooks/weaviate_fill.ipynb | 19 +- src/vdf_io/notebooks/wit-resnet.ipynb | 10 +- src/vdf_io/scripts/check_for_updates.py | 2 +- src/vdf_io/scripts/consolidate_parquet.py | 9 +- src/vdf_io/scripts/count_rows.py | 5 +- src/vdf_io/scripts/count_rows_hf.py | 1 + src/vdf_io/scripts/get_id_list.py | 3 +- src/vdf_io/scripts/push_to_hub_vdf.py | 5 +- src/vdf_io/scripts/reembed.py | 24 +- src/vdf_io/util.py | 38 +- 67 files changed, 1768 insertions(+), 1781 deletions(-) diff --git a/archive/json-yaml.py b/archive/json-yaml.py index 5bf04a2..eba64b1 100644 --- a/archive/json-yaml.py +++ b/archive/json-yaml.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -from pathlib import Path +import json import os import sys -import json +from pathlib import Path + import yaml diff --git a/src/vdf_io/export_vdf/astradb_export.py b/src/vdf_io/export_vdf/astradb_export.py index 1d01d72..5c64ebb 100644 --- a/src/vdf_io/export_vdf/astradb_export.py +++ b/src/vdf_io/export_vdf/astradb_export.py @@ -2,21 +2,21 @@ import json import os import sys -from typing import Dict, List + from astrapy.db import AstraDB -from tqdm import tqdm -from cassandra.cluster import Cluster from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster from cassandra.query import SimpleStatement +from tqdm import tqdm from vdf_io.constants import DISK_SPACE_LIMIT +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import ( set_arg_from_input, set_arg_from_password, ) -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportAstraDB(ExportVDB): @@ -166,7 +166,7 @@ def get_data_from_cql(self): if self.args.get("collections") is None else self.args.get("collections").split(",") ) - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} self.paging_state = None for index_name in tqdm(index_names, desc="Fetching indexes"): # count rows using execute() @@ -255,7 +255,7 @@ def execute_select_all_once(self, index_name): def get_data(self): index_names = self.get_index_names() - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} self.total_imported_count = 0 for index_name in index_names: tqdm.write(f"Exporting collection: {index_name}") diff --git a/src/vdf_io/export_vdf/chroma_export.py b/src/vdf_io/export_vdf/chroma_export.py index 5450426..81d9b1a 100644 --- a/src/vdf_io/export_vdf/chroma_export.py +++ b/src/vdf_io/export_vdf/chroma_export.py @@ -1,14 +1,14 @@ import json import os import sys -from tqdm import tqdm import chromadb +from tqdm import tqdm from vdf_io.constants import DEFAULT_BATCH_SIZE, DISK_SPACE_LIMIT +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.names import DBNames from vdf_io.util import expand_shorthand_path, set_arg_from_input -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportChroma(ExportVDB): diff --git a/src/vdf_io/export_vdf/kdbai_export.py b/src/vdf_io/export_vdf/kdbai_export.py index ecaa84c..ee408c5 100644 --- a/src/vdf_io/export_vdf/kdbai_export.py +++ b/src/vdf_io/export_vdf/kdbai_export.py @@ -1,15 +1,14 @@ -import os -import json -from typing import Dict, List -from dotenv import load_dotenv import datetime -from tqdm import tqdm +import json +import os import kdbai_client as kdbai +from dotenv import load_dotenv +from tqdm import tqdm from vdf_io.export_vdf.vdb_export_cls import ExportVDB -from vdf_io.names import DBNames from vdf_io.meta_types import NamespaceMeta, VDFMeta +from vdf_io.names import DBNames from vdf_io.util import ( get_author_name, set_arg_from_input, @@ -17,7 +16,6 @@ standardize_metric, ) - load_dotenv() @@ -87,7 +85,7 @@ def get_data(self): table_names = self.get_all_index_names() else: table_names = self.args["tables"].split(",") - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} for table_name in tqdm(table_names, desc="Fetching indexes"): index_metas[table_name] = self.export_table(table_name) internal_metadata = VDFMeta( diff --git a/src/vdf_io/export_vdf/lancedb_export.py b/src/vdf_io/export_vdf/lancedb_export.py index 1e0a9d4..19c7b3e 100644 --- a/src/vdf_io/export_vdf/lancedb_export.py +++ b/src/vdf_io/export_vdf/lancedb_export.py @@ -1,15 +1,15 @@ import json import os -from typing import Dict, List + import lancedb import pandas as pd import pyarrow from tqdm import tqdm -from vdf_io.meta_types import NamespaceMeta +from vdf_io.export_vdf.vdb_export_cls import ExportVDB +from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input, set_arg_from_password -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportLanceDB(ExportVDB): @@ -83,7 +83,7 @@ def get_data(self): index_names = self.get_index_names() BATCH_SIZE = self.args["batch_size"] total = 0 - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} for index_name in index_names: namespace_metas = [] vectors_directory = self.create_vec_dir(index_name) diff --git a/src/vdf_io/export_vdf/milvus_export.py b/src/vdf_io/export_vdf/milvus_export.py index 85d0357..748df8c 100644 --- a/src/vdf_io/export_vdf/milvus_export.py +++ b/src/vdf_io/export_vdf/milvus_export.py @@ -1,21 +1,19 @@ -from typing import Dict, List -import os -import json import datetime -from tqdm import tqdm -from pymilvus import connections, utility, Collection +import json +import os +from pymilvus import Collection, connections, utility +from tqdm import tqdm from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.meta_types import NamespaceMeta, VDFMeta +from vdf_io.names import DBNames from vdf_io.util import ( get_author_name, set_arg_from_input, set_arg_from_password, standardize_metric, ) -from vdf_io.names import DBNames - MAX_FETCH_SIZE = 1_000 @@ -67,7 +65,7 @@ def export_vdb(cls, args): milvus_export.get_data() return milvus_export - def __init__(self, args: Dict): + def __init__(self, args: dict): """ Initialize the class. @@ -91,7 +89,7 @@ def get_data(self) -> bool: else: collection_names = self.args.get("collections").split(",") - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} for collection_name in tqdm(collection_names, desc="Fetching indexes"): index_meta = self.get_data_for_collection(collection_name) index_metas[collection_name] = index_meta @@ -111,10 +109,10 @@ def get_data(self) -> bool: print(json.dumps(internal_metadata.model_dump(), indent=4)) return True - def get_all_collection_names(self) -> List[str]: + def get_all_collection_names(self) -> list[str]: return utility.list_collections() - def get_data_for_collection(self, collection_name: str) -> List[NamespaceMeta]: + def get_data_for_collection(self, collection_name: str) -> list[NamespaceMeta]: vectors_directory = self.create_vec_dir(collection_name) try: diff --git a/src/vdf_io/export_vdf/pinecone_export.py b/src/vdf_io/export_vdf/pinecone_export.py index 2d365b3..a87d374 100644 --- a/src/vdf_io/export_vdf/pinecone_export.py +++ b/src/vdf_io/export_vdf/pinecone_export.py @@ -1,24 +1,24 @@ import argparse import datetime -import os import json +import os + import numpy as np -from tqdm import tqdm from halo import Halo - -from pinecone.grpc import PineconeGRPC as Pinecone from pinecone import Vector +from pinecone.grpc import PineconeGRPC as Pinecone +from tqdm import tqdm from vdf_io.constants import ID_COLUMN -from vdf_io.names import DBNames +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.meta_types import NamespaceMeta, VDFMeta +from vdf_io.names import DBNames from vdf_io.util import ( get_author_name, set_arg_from_input, set_arg_from_password, standardize_metric, ) -from vdf_io.export_vdf.vdb_export_cls import ExportVDB PINECONE_MAX_K = 10_000 MAX_TRIES_OVERALL = 150 @@ -281,7 +281,7 @@ def get_all_ids_from_index( ] if self.args["id_list_file"]: with open(self.args["id_list_file"]) as f: - return [line.strip() for line in f.readlines()] + return [line.strip() for line in f] if self.args.get("use_list_points", use_list_points_default): try: @@ -334,9 +334,7 @@ def get_all_ids_from_index( range_min = min(all_ids) - fetch_size range_max = max(all_ids) + 10 * fetch_size range_obj = range(range_min, range_max) - tqdm.write( - "Checking ids in range {} to {}".format(range_min, range_max) - ) + tqdm.write(f"Checking ids in range {range_min} to {range_max}") ids_to_fetch = [ x for x in list(range_obj) diff --git a/src/vdf_io/export_vdf/qdrant_export.py b/src/vdf_io/export_vdf/qdrant_export.py index 9f91da4..e4d96cf 100644 --- a/src/vdf_io/export_vdf/qdrant_export.py +++ b/src/vdf_io/export_vdf/qdrant_export.py @@ -1,14 +1,14 @@ import argparse import json -from typing import Dict, List -from qdrant_client import QdrantClient import os -from tqdm import tqdm + from dotenv import load_dotenv +from qdrant_client import QdrantClient +from tqdm import tqdm from vdf_io.export_vdf.vdb_export_cls import ExportVDB -from vdf_io.names import DBNames from vdf_io.meta_types import NamespaceMeta +from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input, set_arg_from_password load_dotenv() @@ -83,7 +83,7 @@ def __init__(self, args): prefer_grpc=self.args.get("prefer_grpc", True), ) - def get_all_index_names(self) -> List[str]: + def get_all_index_names(self) -> list[str]: """ Get all collection names from Qdrant """ @@ -91,7 +91,7 @@ def get_all_index_names(self) -> List[str]: collection_names = [collection.name for collection in collections] return collection_names - def get_index_names(self) -> List[str]: + def get_index_names(self) -> list[str]: """ Get collection names from args or all collection names """ @@ -101,7 +101,7 @@ def get_index_names(self) -> List[str]: def get_data(self): collection_names = self.get_index_names() - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} for collection_name in tqdm(collection_names, desc="Fetching indexes"): index_meta = self.get_data_for_collection(collection_name) index_metas[collection_name] = index_meta @@ -136,7 +136,7 @@ def try_scroll(self, fetch_size, collection_name, next_offset): ) return self.try_scroll((fetch_size * 2) // 3, collection_name, next_offset) - def get_data_for_collection(self, collection_name) -> List[NamespaceMeta]: + def get_data_for_collection(self, collection_name) -> list[NamespaceMeta]: vectors_directory = self.create_vec_dir(collection_name) total = self.client.get_collection(collection_name).vectors_count diff --git a/src/vdf_io/export_vdf/turbopuffer_export.py b/src/vdf_io/export_vdf/turbopuffer_export.py index 7564611..a3e7c25 100644 --- a/src/vdf_io/export_vdf/turbopuffer_export.py +++ b/src/vdf_io/export_vdf/turbopuffer_export.py @@ -1,14 +1,15 @@ import json import os import sys -from typing import Dict, List -from tqdm import tqdm + import turbopuffer as tpuf +from tqdm import tqdm + from vdf_io.constants import DISK_SPACE_LIMIT +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input, set_arg_from_password -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportTurbopuffer(ExportVDB): @@ -66,7 +67,7 @@ def get_data(self): ids = [] vectors = {} metadata = {} - index_metas: Dict[str, List[NamespaceMeta]] = {} + index_metas: dict[str, list[NamespaceMeta]] = {} self.total_imported_count = 0 for ns_name in tqdm( namespace_names, desc="Exporting turbopuffer namespaces (indexes)" diff --git a/src/vdf_io/export_vdf/txtai_export.py b/src/vdf_io/export_vdf/txtai_export.py index c76d073..ad51143 100644 --- a/src/vdf_io/export_vdf/txtai_export.py +++ b/src/vdf_io/export_vdf/txtai_export.py @@ -1,6 +1,6 @@ +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportTxtai(ExportVDB): diff --git a/src/vdf_io/export_vdf/vdb_export_cls.py b/src/vdf_io/export_vdf/vdb_export_cls.py index fbe8fcb..10f583d 100644 --- a/src/vdf_io/export_vdf/vdb_export_cls.py +++ b/src/vdf_io/export_vdf/vdb_export_cls.py @@ -1,15 +1,16 @@ from __future__ import annotations + +import abc import datetime -from typing import List -import pandas as pd import os -import abc -import pyarrow.parquet as pq + +import pandas as pd import pyarrow as pa +import pyarrow.parquet as pq +from vdf_io.constants import ID_COLUMN from vdf_io.meta_types import NamespaceMeta, VDFMeta from vdf_io.util import extract_data_hash, get_author_name, standardize_metric -from vdf_io.constants import ID_COLUMN class ExportVDB(abc.ABC): @@ -32,20 +33,18 @@ def __init__(self, args): os.makedirs(self.vdf_directory, exist_ok=True) @abc.abstractmethod - def get_index_names(self) -> List[str]: + def get_index_names(self) -> list[str]: """ Get index names from vector database """ # raise NotImplementedError() - pass @abc.abstractmethod - def get_all_index_names(self) -> List[str]: + def get_all_index_names(self) -> list[str]: """ Get all index names from vector database """ # raise NotImplementedError() - pass @abc.abstractmethod def get_data(self) -> ExportVDB: @@ -68,7 +67,7 @@ def save_vectors_to_parquet(self, vectors, metadata, vectors_directory): vectors_df = pd.DataFrame(list(vectors.items()), columns=[ID_COLUMN, "vector"]) if metadata: - metadata_list = [{**{ID_COLUMN: k}, **v} for k, v in metadata.items()] + metadata_list = [{ID_COLUMN: k, **v} for k, v in metadata.items()] metadata_df = pd.DataFrame.from_records(metadata_list) # Check for duplicate column names and rename as necessary diff --git a/src/vdf_io/export_vdf/vertexai_vector_search_export.py b/src/vdf_io/export_vdf/vertexai_vector_search_export.py index 4ecd461..53f1b78 100644 --- a/src/vdf_io/export_vdf/vertexai_vector_search_export.py +++ b/src/vdf_io/export_vdf/vertexai_vector_search_export.py @@ -2,18 +2,20 @@ Export data from vertex ai vector search index """ -import os import json -from tqdm import tqdm +import os import google.auth from google.cloud import aiplatform -from google.cloud.aiplatform import MatchingEngineIndex # as vs -from google.cloud.aiplatform import MatchingEngineIndexEndpoint # as vsep +from google.cloud.aiplatform import ( + MatchingEngineIndex, # as vs + MatchingEngineIndexEndpoint, # as vsep +) +from tqdm import tqdm +from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input, standardize_metric -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportVertexAIVectorSearch(ExportVDB): diff --git a/src/vdf_io/export_vdf/vespa_export.py b/src/vdf_io/export_vdf/vespa_export.py index 6e03b73..1b4fa3b 100644 --- a/src/vdf_io/export_vdf/vespa_export.py +++ b/src/vdf_io/export_vdf/vespa_export.py @@ -1,10 +1,9 @@ -from typing import List -from vdf_io.marqo_vespa_util import VespaClient from rich import print as rprint +from vdf_io.export_vdf.vdb_export_cls import ExportVDB +from vdf_io.marqo_vespa_util import VespaClient from vdf_io.names import DBNames from vdf_io.util import set_arg_from_input, set_arg_from_password -from vdf_io.export_vdf.vdb_export_cls import ExportVDB class ExportVespa(ExportVDB): @@ -72,7 +71,7 @@ def export_vdb(cls, args): def __init__(self, args): super().__init__(args) - def get_index_names(self) -> List[str]: + def get_index_names(self) -> list[str]: raise NotImplementedError() # not available in pyvespa def get_data(self): diff --git a/src/vdf_io/export_vdf/weaviate_export.py b/src/vdf_io/export_vdf/weaviate_export.py index 518dd3c..31b08a9 100644 --- a/src/vdf_io/export_vdf/weaviate_export.py +++ b/src/vdf_io/export_vdf/weaviate_export.py @@ -1,7 +1,7 @@ import os -from tqdm import tqdm import weaviate +from tqdm import tqdm from vdf_io.export_vdf.vdb_export_cls import ExportVDB from vdf_io.names import DBNames diff --git a/src/vdf_io/export_vdf_cli.py b/src/vdf_io/export_vdf_cli.py index f10830c..e1528dd 100755 --- a/src/vdf_io/export_vdf_cli.py +++ b/src/vdf_io/export_vdf_cli.py @@ -1,23 +1,22 @@ #!/usr/bin/env python3 import argparse +import importlib import os +import pkgutil import sys import time import traceback -from dotenv import find_dotenv, load_dotenv import warnings -import pkgutil -import importlib - import sentry_sdk +from dotenv import find_dotenv, load_dotenv from opentelemetry import trace from opentelemetry.propagate import set_global_textmap from opentelemetry.sdk.trace import TracerProvider from sentry_sdk.integrations.opentelemetry import ( - SentrySpanProcessor, SentryPropagator, + SentrySpanProcessor, ) import vdf_io @@ -25,7 +24,6 @@ from vdf_io.scripts.check_for_updates import check_for_updates from vdf_io.scripts.push_to_hub_vdf import push_to_hub - # Path to the directory containing all export modules package_dir = "vdf_io.export_vdf" @@ -96,7 +94,6 @@ def main(): finally: sentry_sdk.flush() sentry_sdk.flush() - return ARGS_ALLOWLIST = [ diff --git a/src/vdf_io/import_vdf/astradb_import.py b/src/vdf_io/import_vdf/astradb_import.py index 5d20dc0..8fba080 100644 --- a/src/vdf_io/import_vdf/astradb_import.py +++ b/src/vdf_io/import_vdf/astradb_import.py @@ -1,18 +1,17 @@ import argparse -from typing import Dict, List -from dotenv import load_dotenv -from tqdm import tqdm import concurrent.futures +import re from astrapy.db import AstraDB -from cassandra.cluster import Cluster from cassandra.auth import PlainTextAuthProvider +from cassandra.cluster import Cluster +from dotenv import load_dotenv +from tqdm import tqdm from vdf_io.constants import INT_MAX -from vdf_io.names import DBNames from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.meta_types import NamespaceMeta -import re +from vdf_io.names import DBNames from vdf_io.util import ( clean_documents, set_arg_from_input, @@ -109,8 +108,8 @@ def get_all_index_names_cql(self): def upsert_data(self, via_cql=False): self.total_imported_count = 0 max_hit = False - indexes_content: Dict[str, List[NamespaceMeta]] = self.vdf_meta["indexes"] - index_names: List[str] = list(indexes_content.keys()) + indexes_content: dict[str, list[NamespaceMeta]] = self.vdf_meta["indexes"] + index_names: list[str] = list(indexes_content.keys()) if len(index_names) == 0: raise ValueError("No indexes found in VDF_META.json") @@ -124,7 +123,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 +161,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 +207,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 +247,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/azureai_import.py b/src/vdf_io/import_vdf/azureai_import.py index f64cf6c..513adaa 100644 --- a/src/vdf_io/import_vdf/azureai_import.py +++ b/src/vdf_io/import_vdf/azureai_import.py @@ -1,13 +1,11 @@ from dotenv import load_dotenv - +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.names import DBNames from vdf_io.util import ( set_arg_from_input, set_arg_from_password, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB - load_dotenv() diff --git a/src/vdf_io/import_vdf/chroma_import.py b/src/vdf_io/import_vdf/chroma_import.py index 079898c..03d200d 100644 --- a/src/vdf_io/import_vdf/chroma_import.py +++ b/src/vdf_io/import_vdf/chroma_import.py @@ -1,10 +1,9 @@ -from typing import Dict, List +import chromadb from dotenv import load_dotenv from tqdm import tqdm -import chromadb - from vdf_io.constants import DEFAULT_BATCH_SIZE, INT_MAX +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import ( @@ -13,8 +12,6 @@ expand_shorthand_path, set_arg_from_input, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB - load_dotenv() @@ -105,8 +102,8 @@ def get_all_index_names(self): def upsert_data(self): max_hit = False self.total_imported_count = 0 - indexes_content: Dict[str, List[NamespaceMeta]] = self.vdf_meta["indexes"] - index_names: List[str] = list(indexes_content.keys()) + indexes_content: dict[str, list[NamespaceMeta]] = self.vdf_meta["indexes"] + index_names: list[str] = list(indexes_content.keys()) if len(index_names) == 0: raise ValueError("No indexes found in VDF_META.json") collections = self.get_all_index_names() @@ -123,7 +120,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..f772a2e 100644 --- a/src/vdf_io/import_vdf/kdbai_import.py +++ b/src/vdf_io/import_vdf/kdbai_import.py @@ -1,14 +1,12 @@ -from typing import Dict, List +import kdbai_client as kdbai +import pyarrow.parquet as pq from dotenv import load_dotenv from tqdm import tqdm -import pyarrow.parquet as pq - -import kdbai_client as kdbai from vdf_io.constants import INT_MAX -from vdf_io.names import DBNames from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.meta_types import NamespaceMeta +from vdf_io.names import DBNames from vdf_io.util import ( set_arg_from_input, set_arg_from_password, @@ -84,8 +82,8 @@ def compliant_name(self, name: str) -> str: def upsert_data(self): self.total_imported_count = 0 max_hit = False - indexes_content: Dict[str, List[NamespaceMeta]] = self.vdf_meta["indexes"] - index_names: List[str] = list(indexes_content.keys()) + indexes_content: dict[str, list[NamespaceMeta]] = self.vdf_meta["indexes"] + index_names: list[str] = list(indexes_content.keys()) if len(index_names) == 0: raise ValueError("No indexes found in VDF_META.json") @@ -99,7 +97,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..b5901dd 100644 --- a/src/vdf_io/import_vdf/lancedb_import.py +++ b/src/vdf_io/import_vdf/lancedb_import.py @@ -1,12 +1,11 @@ -from typing import Dict, List -from dotenv import load_dotenv +import lancedb import pandas as pd -from tqdm import tqdm import pyarrow.parquet as pq - -import lancedb +from dotenv import load_dotenv +from tqdm import tqdm from vdf_io.constants import DEFAULT_BATCH_SIZE, INT_MAX +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import ( @@ -15,8 +14,6 @@ set_arg_from_input, set_arg_from_password, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB - load_dotenv() @@ -71,8 +68,8 @@ def __init__(self, args): def upsert_data(self): max_hit = False self.total_imported_count = 0 - indexes_content: Dict[str, List[NamespaceMeta]] = self.vdf_meta["indexes"] - index_names: List[str] = list(indexes_content.keys()) + indexes_content: dict[str, list[NamespaceMeta]] = self.vdf_meta["indexes"] + index_names: list[str] = list(indexes_content.keys()) if len(index_names) == 0: raise ValueError("No indexes found in VDF_META.json") tables = self.db.table_names() @@ -89,7 +86,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..646a553 100644 --- a/src/vdf_io/import_vdf/milvus_import.py +++ b/src/vdf_io/import_vdf/milvus_import.py @@ -1,25 +1,24 @@ -from dotenv import load_dotenv -from tqdm import tqdm import json +from dotenv import load_dotenv from pymilvus import ( - connections, - utility, Collection, CollectionSchema, - FieldSchema, DataType, + FieldSchema, + connections, + utility, ) +from tqdm import tqdm from vdf_io.constants import INT_MAX +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.names import DBNames from vdf_io.util import ( set_arg_from_input, set_arg_from_password, standardize_metric_reverse, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB - load_dotenv() @@ -82,7 +81,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/pinecone_import.py b/src/vdf_io/import_vdf/pinecone_import.py index f579c1f..29ed9f6 100644 --- a/src/vdf_io/import_vdf/pinecone_import.py +++ b/src/vdf_io/import_vdf/pinecone_import.py @@ -1,20 +1,20 @@ import argparse -import pandas as pd -from tqdm import tqdm import os -from dotenv import load_dotenv +import pandas as pd +from dotenv import load_dotenv +from pinecone import PodSpec, ServerlessSpec, Vector from pinecone.grpc import PineconeGRPC as Pinecone -from pinecone import ServerlessSpec, PodSpec, Vector +from tqdm import tqdm from vdf_io.constants import INT_MAX +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.names import DBNames from vdf_io.util import ( set_arg_from_input, set_arg_from_password, standardize_metric_reverse, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB load_dotenv() diff --git a/src/vdf_io/import_vdf/qdrant_import.py b/src/vdf_io/import_vdf/qdrant_import.py index 65041d9..fbcce27 100644 --- a/src/vdf_io/import_vdf/qdrant_import.py +++ b/src/vdf_io/import_vdf/qdrant_import.py @@ -1,19 +1,20 @@ +import concurrent.futures import json -from dotenv import load_dotenv +from typing import Any + import numpy as np -from tqdm import tqdm +from dotenv import load_dotenv from grpc import RpcError -from typing import Any, Dict, List -from PIL import Image from halo import Halo - -import concurrent.futures - +from PIL import Image from qdrant_client import QdrantClient from qdrant_client.http.exceptions import UnexpectedResponse -from qdrant_client.http.models import VectorParams, Distance, PointStruct +from qdrant_client.http.models import Distance, PointStruct, VectorParams +from tqdm import tqdm from vdf_io.constants import INT_MAX +from vdf_io.import_vdf.vdf_import_cls import ImportVDB +from vdf_io.meta_types import NamespaceMeta from vdf_io.names import DBNames from vdf_io.util import ( expand_shorthand_path, @@ -21,8 +22,6 @@ set_arg_from_input, set_arg_from_password, ) -from vdf_io.import_vdf.vdf_import_cls import ImportVDB -from vdf_io.meta_types import NamespaceMeta load_dotenv() @@ -150,7 +149,7 @@ def upsert_data(self): max_hit = False self.total_imported_count = 0 # we know that the self.vdf_meta["indexes"] is a list - index_meta: Dict[str, List[NamespaceMeta]] = {} + index_meta: dict[str, list[NamespaceMeta]] = {} for index_name, index_meta in tqdm( self.vdf_meta["indexes"].items(), desc="Importing indexes" ): @@ -284,14 +283,14 @@ def get_nested_config(config, keys, default=None): self.make_metadata_qdrant_compliant(metadata) # union of all keys in vectors_all keys = set().union( - *[vectors_all[vec_col].keys() for vec_col in vectors_all.keys()] + *[vectors_all[vec_col].keys() for vec_col in vectors_all] ) points = [ PointStruct( id=get_qdrant_id_from_id(idx), vector={ vec_col: vectors_all[vec_col].get(idx, []) - for vec_col in vectors_all.keys() + for vec_col in vectors_all }, payload=metadata.get(idx, {}), ) @@ -313,12 +312,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..ff78d6f 100644 --- a/src/vdf_io/import_vdf/turbopuffer_import.py +++ b/src/vdf_io/import_vdf/turbopuffer_import.py @@ -1,7 +1,5 @@ -from typing import Dict, List -from tqdm import tqdm - import turbopuffer as tpuf +from tqdm import tqdm from vdf_io.constants import DEFAULT_BATCH_SIZE, INT_MAX from vdf_io.import_vdf.vdf_import_cls import ImportVDB @@ -55,8 +53,8 @@ def get_all_index_names(self): def upsert_data(self): self.total_imported_count = 0 - indexes_content: Dict[str, List[NamespaceMeta]] = self.vdf_meta["indexes"] - index_names: List[str] = list(indexes_content.keys()) + indexes_content: dict[str, list[NamespaceMeta]] = self.vdf_meta["indexes"] + index_names: list[str] = list(indexes_content.keys()) if len(index_names) == 0: raise ValueError("No indexes found in VDF_META.json") collections = self.get_all_index_names() @@ -73,7 +71,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/vdf_import_cls.py b/src/vdf_io/import_vdf/vdf_import_cls.py index 31ef4d6..a5016d6 100644 --- a/src/vdf_io/import_vdf/vdf_import_cls.py +++ b/src/vdf_io/import_vdf/vdf_import_cls.py @@ -1,15 +1,15 @@ +import abc import ast import datetime -from functools import lru_cache import json import os +from functools import lru_cache + import numpy as np -from packaging.version import Version -import abc -from tqdm import tqdm from halo import Halo - +from packaging.version import Version from qdrant_client.http.models import Distance +from tqdm import tqdm import vdf_io from vdf_io.constants import ID_COLUMN diff --git a/src/vdf_io/import_vdf/vertexai_vector_search_import.py b/src/vdf_io/import_vdf/vertexai_vector_search_import.py index 596870e..434e7e8 100644 --- a/src/vdf_io/import_vdf/vertexai_vector_search_import.py +++ b/src/vdf_io/import_vdf/vertexai_vector_search_import.py @@ -3,33 +3,31 @@ """ import argparse -from typing import Dict, List -import uuid +import itertools +import json import time +import uuid + +import google.api_core.exceptions as google_exceptions +import google.cloud.aiplatform_v1 as aipv1 import numpy as np -import json -import itertools -from tqdm import tqdm -from ratelimit import limits, sleep_and_retry -from backoff import on_exception, expo +from backoff import expo, on_exception # gcloud config set project $PROJECT_ID - users # SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] - # NEW from google.cloud import aiplatform as aip -import google.cloud.aiplatform_v1 as aipv1 from google.cloud import storage -from google.protobuf import struct_pb2 from google.cloud.aiplatform_v1.types.index import Index -from google.cloud.aiplatform_v1.types.index_endpoint import IndexEndpoint -from google.cloud.aiplatform_v1.types.index_endpoint import DeployedIndex -import google.api_core.exceptions as google_exceptions +from google.cloud.aiplatform_v1.types.index_endpoint import DeployedIndex, IndexEndpoint +from google.protobuf import struct_pb2 +from ratelimit import limits, sleep_and_retry +from tqdm import tqdm -from vdf_io.names import DBNames +from vdf_io.constants import ID_COLUMN, INT_MAX from vdf_io.import_vdf.vdf_import_cls import ImportVDB +from vdf_io.names import DBNames from vdf_io.util import read_parquet_progress, set_arg_from_input -from vdf_io.constants import ID_COLUMN, INT_MAX # exceptions @@ -196,7 +194,7 @@ def make_parser(cls, subparsers): action=argparse.BooleanOptionalAction, ) - def __init__(self, args: Dict) -> None: + def __init__(self, args: dict) -> None: super().__init__(args) self.DB_NAME_SLUG = DBNames.VERTEXAI self.project_id = self.args["project_id"] @@ -341,7 +339,6 @@ def __init__(self, args: Dict) -> None: ) except Exception as e: print(f"{self.gcs_bucket} bucket already exists {e}") - pass self.gcs_folder = "init_index" self.local_file_name = "embeddings_0.json" self.contents_delta_uri = ( @@ -464,7 +461,6 @@ def _get_index(self) -> Index: print( f"{self.index_name} not an existing display_name or resource_name: {e}" ) - pass if not indexes: try: # checking deployed indexes @@ -495,7 +491,6 @@ def _get_index(self) -> Index: indexes.append(d.index) except Exception as e: print(f"not an existing deployed_index: {e}") - pass if len(indexes) == 0: print(f"Index {self.index_name} not found") @@ -526,7 +521,6 @@ def _get_index_endpoint(self) -> IndexEndpoint: ] except Exception as e: print(f"{self.index_endpoint_name} not an existing index endpoint: {e}") - pass else: raise ResourceNotExistException("index_endpoint") @@ -540,7 +534,7 @@ def _get_index_endpoint(self) -> IndexEndpoint: ) return index_endpoint - def list_indexes(self) -> List[Index]: + def list_indexes(self) -> list[Index]: """ :return: @@ -550,7 +544,7 @@ def list_indexes(self) -> List[Index]: indexes = [response for response in page_result] return indexes - def list_index_endpoints(self) -> List[IndexEndpoint]: + def list_index_endpoints(self) -> list[IndexEndpoint]: """ :return: @@ -560,7 +554,7 @@ def list_index_endpoints(self) -> List[IndexEndpoint]: index_endpoints = [response for response in page_result] return index_endpoints - def list_deployed_indexes(self, endpoint_name: str = None) -> List[DeployedIndex]: + def list_deployed_indexes(self, endpoint_name: str = None) -> list[DeployedIndex]: """ :param endpoint_name: diff --git a/src/vdf_io/import_vdf_cli.py b/src/vdf_io/import_vdf_cli.py index 3db659a..3fc1d7b 100755 --- a/src/vdf_io/import_vdf_cli.py +++ b/src/vdf_io/import_vdf_cli.py @@ -5,22 +5,21 @@ import os import pkgutil import time -import warnings -from dotenv import find_dotenv, load_dotenv import traceback +import warnings import sentry_sdk +from dotenv import find_dotenv, load_dotenv from opentelemetry import trace from opentelemetry.propagate import set_global_textmap from opentelemetry.sdk.trace import TracerProvider -from sentry_sdk.integrations.opentelemetry import SentrySpanProcessor, SentryPropagator +from sentry_sdk.integrations.opentelemetry import SentryPropagator, SentrySpanProcessor import vdf_io from vdf_io.constants import ID_COLUMN +from vdf_io.import_vdf.vdf_import_cls import ImportVDB from vdf_io.scripts.check_for_updates import check_for_updates from vdf_io.util import set_arg_from_input -from vdf_io.import_vdf.vdf_import_cls import ImportVDB - load_dotenv(find_dotenv(), override=True) # Path to the directory containing all export modules diff --git a/src/vdf_io/marqo_vespa_util.py b/src/vdf_io/marqo_vespa_util.py index e3326c8..a0ad64b 100644 --- a/src/vdf_io/marqo_vespa_util.py +++ b/src/vdf_io/marqo_vespa_util.py @@ -1,5 +1,6 @@ import ssl -from typing import Any, Dict, List, Optional +from typing import Any + import httpx from pydantic import BaseModel, Field from rich import print as rprint @@ -7,14 +8,14 @@ class Document(BaseModel): id: str - fields: Dict[str, Any] + fields: dict[str, Any] class VisitDocumentsResponse(BaseModel): path_id: str = Field(alias="pathId") - documents: List[Document] + documents: list[Document] document_count: int = Field(alias="documentCount") - continuation: Optional[str] + continuation: str | None class VespaClient: @@ -24,8 +25,8 @@ def __init__( document_url: str, query_url: str, content_cluster_name: str, - cert_file: Optional[str] = None, - pk_file: Optional[str] = None, + cert_file: str | None = None, + pk_file: str | None = None, pool_size: int = 10, feed_pool_size: int = 10, get_pool_size: int = 10, @@ -55,7 +56,7 @@ def __init__( verify=True if cert_file else False, cert=(cert_file, pk_file) if cert_file else None, ) - except ssl.SSLError as e: # noqa: F821 + except ssl.SSLError as e: raise VespaError("Failed to create http client due to SSL error", cause=e) self.content_cluster_name = content_cluster_name self.feed_pool_size = feed_pool_size @@ -64,7 +65,7 @@ def __init__( self.partial_pool_size = partial_update_pool_size def get_all_documents( - self, schema: str, stream=False, continuation: Optional[str] = None + self, schema: str, stream=False, continuation: str | None = None ) -> VisitDocumentsResponse: """ Get all documents in a schema. @@ -85,7 +86,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: @@ -129,11 +130,11 @@ def __init__(cls, name, bases, attrs): if "__init__" not in attrs: def __init__( - self, message: Optional[str] = None, cause: Optional[Exception] = None + self, message: str | None = None, cause: Exception | None = None ): super(cls, self).__init__(message, cause) - setattr(cls, "__init__", __init__) + cls.__init__ = __init__ super().__init__(name, bases, attrs) @@ -142,9 +143,7 @@ class MarqoError(Exception, metaclass=MarqoErrorMeta): Base class for all Marqo errors. """ - def __init__( - self, message: Optional[str] = None, cause: Optional[Exception] = None - ): + def __init__(self, message: str | None = None, cause: Exception | None = None): super().__init__(message) self.message = message self.cause = cause diff --git a/src/vdf_io/meta_types.py b/src/vdf_io/meta_types.py index 91c111a..14635a4 100644 --- a/src/vdf_io/meta_types.py +++ b/src/vdf_io/meta_types.py @@ -1,5 +1,6 @@ +from typing import Any + from pydantic import BaseModel, ConfigDict -from typing import Any, Dict, List, Optional class NamespaceMeta(BaseModel): @@ -9,11 +10,11 @@ class NamespaceMeta(BaseModel): exported_vector_count: int dimensions: int model_name: str | None = None - model_map: Dict[str, Any] | None = None - vector_columns: List[str] = ["vector"] + model_map: dict[str, Any] | None = None + vector_columns: list[str] = ["vector"] data_path: str metric: str | None = None - index_config: Optional[Dict[Any, Any]] = None + index_config: dict[Any, Any] | None = None # schema_dict is a byte string schema_dict_str: str | None = None model_config = ConfigDict(protected_namespaces=()) @@ -21,9 +22,9 @@ class NamespaceMeta(BaseModel): class VDFMeta(BaseModel): version: str - file_structure: List[str] + file_structure: list[str] author: str exported_from: str - indexes: Dict[str, List[NamespaceMeta]] + indexes: dict[str, list[NamespaceMeta]] exported_at: str - id_column: Optional[str] = None + id_column: str | None = None diff --git a/src/vdf_io/notebooks/ get_data_from_json.ipynb b/src/vdf_io/notebooks/ get_data_from_json.ipynb index 6d14861..1e839e2 100644 --- a/src/vdf_io/notebooks/ get_data_from_json.ipynb +++ b/src/vdf_io/notebooks/ get_data_from_json.ipynb @@ -61,7 +61,6 @@ "source": [ "import json\n", "\n", - "\n", "file = open(\"/Users/dhruvanand/Downloads/medium_articles_2.json\")" ] }, @@ -400,8 +399,8 @@ } ], "source": [ - "import numpy as np\n", "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", "# calculate mean and variance of each vector column dimension\n", "\n", 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..89abcd7 100644 --- a/src/vdf_io/notebooks/aiven-qs.ipynb +++ b/src/vdf_io/notebooks/aiven-qs.ipynb @@ -20,7 +20,8 @@ ], "source": [ "import os\n", - "from dotenv import load_dotenv, find_dotenv\n", + "\n", + "from dotenv import find_dotenv, load_dotenv\n", "\n", "load_dotenv(find_dotenv(), override=True)" ] @@ -33,7 +34,7 @@ }, "outputs": [], "source": [ - "from rich import print as rprint\n" + "from rich import print as rprint" ] }, { @@ -174,6 +175,7 @@ ], "source": [ "import ssl\n", + "\n", "from cassandra.auth import PlainTextAuthProvider\n", "from cassandra.cluster import Cluster\n", "from cassandra.policies import DCAwareRoundRobinPolicy\n", @@ -181,23 +183,28 @@ "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", - " [CASSANDRA_URI],\n", - " port=CASSANDRA_PORT,\n", - " ssl_options=ssl_options,\n", - " auth_provider=auth_provider,\n", - " load_balancing_policy=DCAwareRoundRobinPolicy(local_dc=\"aiven\"),\n", - ") as cluster:\n", - " with cluster.connect() as session:\n", - " # print(\"Connected to cluster: %s\" % rprint(cluster.__dict__))\n", - " print(\"Connected to cluster: %s\" % cluster.metadata.cluster_name)\n", - " if cluster.metadata.token_map:\n", - " rprint(cluster.metadata.token_map.token_to_host_owner)\n", - " else:\n", - " print(\"No token map in use\")" + "with (\n", + " Cluster(\n", + " [CASSANDRA_URI],\n", + " port=CASSANDRA_PORT,\n", + " ssl_options=ssl_options,\n", + " auth_provider=auth_provider,\n", + " load_balancing_policy=DCAwareRoundRobinPolicy(local_dc=\"aiven\"),\n", + " ) as cluster,\n", + " cluster.connect() as session,\n", + "):\n", + " # print(\"Connected to cluster: %s\" % rprint(cluster.__dict__))\n", + " print(\"Connected to cluster: %s\" % cluster.metadata.cluster_name)\n", + " if cluster.metadata.token_map:\n", + " rprint(cluster.metadata.token_map.token_to_host_owner)\n", + " else:\n", + " print(\"No token map in use\")" ] }, { diff --git a/src/vdf_io/notebooks/astra_usage.ipynb b/src/vdf_io/notebooks/astra_usage.ipynb index 7d73948..9a4c9a7 100644 --- a/src/vdf_io/notebooks/astra_usage.ipynb +++ b/src/vdf_io/notebooks/astra_usage.ipynb @@ -17,6 +17,7 @@ ], "source": [ "import os\n", + "\n", "from astrapy.db import AstraDB\n", "\n", "# Initialization\n", @@ -126,8 +127,8 @@ ], "source": [ "from numpy import nan\n", - "from vdf_io.util import get_qdrant_id_from_id\n", "\n", + "from vdf_io.util import get_qdrant_id_from_id\n", "\n", "coll.upsert_many(\n", " documents=[\n", @@ -162,7 +163,7 @@ }, "outputs": [], "source": [ - "table = coll.find()['data']['documents']" + "table = coll.find()[\"data\"][\"documents\"]" ] }, { @@ -325,6 +326,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 +361,7 @@ } ], "source": [ - "len(resp['data']['documents'])" + "len(resp[\"data\"][\"documents\"])" ] }, { @@ -379,7 +381,7 @@ } ], "source": [ - "resp['data']['documents'][0].keys()" + "resp[\"data\"][\"documents\"][0].keys()" ] }, { @@ -497,14 +499,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" ] }, { @@ -567,6 +567,7 @@ ], "source": [ "from random import randint, random\n", + "\n", "from tqdm import tqdm\n", "\n", "\n", @@ -665,9 +666,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 +691,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 +729,7 @@ } ], "source": [ - "a['data']['documents'][0]" + "a[\"data\"][\"documents\"][0]" ] }, { @@ -748,7 +749,7 @@ } ], "source": [ - "len(collection2.find_one()['data']['document'][\"vector\"])" + "len(collection2.find_one()[\"data\"][\"document\"][\"vector\"])" ] }, { @@ -768,8 +769,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/download_dataset.ipynb b/src/vdf_io/notebooks/download_dataset.ipynb index 0d65267..0739f47 100644 --- a/src/vdf_io/notebooks/download_dataset.ipynb +++ b/src/vdf_io/notebooks/download_dataset.ipynb @@ -23,9 +23,10 @@ } ], "source": [ - "from datasets import load_dataset\n", "import os\n", "\n", + "from datasets import load_dataset\n", + "\n", "dataset = load_dataset(\"aintech/vdf_PC_ANN_Fashion-MNIST_d784_euclidean\")" ] }, diff --git a/src/vdf_io/notebooks/json_pandas.ipynb b/src/vdf_io/notebooks/json_pandas.ipynb index 0735d50..e990810 100644 --- a/src/vdf_io/notebooks/json_pandas.ipynb +++ b/src/vdf_io/notebooks/json_pandas.ipynb @@ -16,35 +16,28 @@ ], "source": [ "import json\n", + "\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/kdbai_end_to_end_vectorIO.ipynb b/src/vdf_io/notebooks/kdbai_end_to_end_vectorIO.ipynb index d3e63d7..1c7c98e 100644 --- a/src/vdf_io/notebooks/kdbai_end_to_end_vectorIO.ipynb +++ b/src/vdf_io/notebooks/kdbai_end_to_end_vectorIO.ipynb @@ -8,6 +8,7 @@ "outputs": [], "source": [ "from getpass import getpass\n", + "\n", "import kdbai_client as kdbai" ] }, diff --git a/src/vdf_io/notebooks/lance-qs.ipynb b/src/vdf_io/notebooks/lance-qs.ipynb index cce6883..1dcc1fe 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\": f\"item_{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..147aa91 100644 --- a/src/vdf_io/notebooks/medium-articles.ipynb +++ b/src/vdf_io/notebooks/medium-articles.ipynb @@ -207,10 +207,9 @@ "metadata": {}, "outputs": [], "source": [ - "import pandas as pd\n", + "import latentscope as ls\n", "import numpy as np\n", - "from datasets import load_dataset\n", - "import latentscope as ls" + "from datasets import load_dataset" ] }, { @@ -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/ny_embs.ipynb b/src/vdf_io/notebooks/ny_embs.ipynb index ab2b958..8739d5a 100644 --- a/src/vdf_io/notebooks/ny_embs.ipynb +++ b/src/vdf_io/notebooks/ny_embs.ipynb @@ -8,7 +8,6 @@ "source": [ "import pandas as pd\n", "\n", - "\n", "df = pd.read_csv(\n", " \"/Users/dhruvanand/Downloads/aint/llm/nyt_embs/articles_with_embeddings.csv\"\n", ")" diff --git a/src/vdf_io/notebooks/pc_upsert_sample.ipynb b/src/vdf_io/notebooks/pc_upsert_sample.ipynb index 422bf6f..de7b473 100644 --- a/src/vdf_io/notebooks/pc_upsert_sample.ipynb +++ b/src/vdf_io/notebooks/pc_upsert_sample.ipynb @@ -173,6 +173,7 @@ ], "source": [ "import os\n", + "\n", "import pinecone\n", "from dotenv import load_dotenv\n", "\n", diff --git a/src/vdf_io/notebooks/qdrant-big.ipynb b/src/vdf_io/notebooks/qdrant-big.ipynb index c4d2981..6b33106 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" ] }, { @@ -17,6 +17,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from qdrant_client import QdrantClient\n", "\n", "qdrant_client = QdrantClient(\n", @@ -255,7 +256,6 @@ ], "source": [ "!pwd\n", - "import pandas as pd\n", "\n", "df = pd.read_parquet(\"../../../temp.parquet\")" ] @@ -317,7 +317,7 @@ } ], "source": [ - "df['title_vector']" + "df[\"title_vector\"]" ] }, { @@ -334,10 +334,7 @@ ] } ], - "source": [ - "from datasets import load_dataset\n", - "\n" - ] + "source": [] }, { "cell_type": "code", @@ -345,7 +342,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "ds = load_dataset(\"somewheresystems/dataclysm-pubmed\", split=\"train\", streaming=True)" ] }, @@ -375,8 +371,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])" ] }, { @@ -387,8 +383,9 @@ "source": [ "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/qdrant_datasets.ipynb b/src/vdf_io/notebooks/qdrant_datasets.ipynb index 44ff85a..7bc5219 100644 --- a/src/vdf_io/notebooks/qdrant_datasets.ipynb +++ b/src/vdf_io/notebooks/qdrant_datasets.ipynb @@ -90,9 +90,10 @@ } ], "source": [ - "from datasets import load_dataset\n", "import os\n", "\n", + "from datasets import load_dataset\n", + "\n", "dataset = load_dataset(\n", " \"Qdrant/arxiv-titles-instructorxl-embeddings\", split=\"train\", streaming=True\n", ")" diff --git a/src/vdf_io/notebooks/semantic_search.ipynb b/src/vdf_io/notebooks/semantic_search.ipynb index 1c73c79..2908a27 100644 --- a/src/vdf_io/notebooks/semantic_search.ipynb +++ b/src/vdf_io/notebooks/semantic_search.ipynb @@ -572,8 +572,8 @@ } ], "source": [ - "from sentence_transformers import SentenceTransformer\n", "import torch\n", + "from sentence_transformers import SentenceTransformer\n", "\n", "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", "if device != \"cuda\":\n", @@ -689,6 +689,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "import pinecone\n", "\n", "# get api key from app.pinecone.io\n", 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/test_filtering_pc.ipynb b/src/vdf_io/notebooks/test_filtering_pc.ipynb index a22df30..a59dc64 100644 --- a/src/vdf_io/notebooks/test_filtering_pc.ipynb +++ b/src/vdf_io/notebooks/test_filtering_pc.ipynb @@ -16,6 +16,7 @@ ], "source": [ "import os\n", + "\n", "import pinecone\n", "from dotenv import load_dotenv\n", "\n", diff --git a/src/vdf_io/notebooks/test_filtering_pc_log.ipynb b/src/vdf_io/notebooks/test_filtering_pc_log.ipynb index a22df30..a59dc64 100644 --- a/src/vdf_io/notebooks/test_filtering_pc_log.ipynb +++ b/src/vdf_io/notebooks/test_filtering_pc_log.ipynb @@ -16,6 +16,7 @@ ], "source": [ "import os\n", + "\n", "import pinecone\n", "from dotenv import load_dotenv\n", "\n", diff --git a/src/vdf_io/notebooks/tpuf-qs.ipynb b/src/vdf_io/notebooks/tpuf-qs.ipynb index 44f030c..9532288 100644 --- a/src/vdf_io/notebooks/tpuf-qs.ipynb +++ b/src/vdf_io/notebooks/tpuf-qs.ipynb @@ -18,7 +18,7 @@ ], "source": [ "import turbopuffer as tpuf\n", - "from dotenv import load_dotenv, find_dotenv\n", + "from dotenv import find_dotenv, load_dotenv\n", "\n", "load_dotenv(find_dotenv())" ] @@ -31,7 +31,6 @@ "source": [ "import os\n", "\n", - "\n", "tpuf.api_key = os.environ[\"TURBOPUFFER_API_KEY\"]" ] }, @@ -70,7 +69,7 @@ "metadata": {}, "outputs": [], "source": [ - "ns = tpuf.Namespace(\"namespace-name\")\n" + "ns = tpuf.Namespace(\"namespace-name\")" ] }, { @@ -99,9 +98,9 @@ "metadata": {}, "outputs": [], "source": [ - "import random,uuid\n", - "# use uuid\n", - "\n" + "import random\n", + "import uuid\n", + "# use uuid" ] }, { @@ -110,7 +109,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# Upsert vectors and attributes\n", "\n", "ns.upsert(\n", @@ -135,7 +133,7 @@ ], "source": [ "# upsert 20k random vectors\n", - "from random import random, randint, choice\n", + "from random import choice, random\n", "\n", "for i in tqdm(range(100)):\n", " # 10k random unique ids without replacement\n", @@ -261,11 +259,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 +351,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..94fa38d 100644 --- a/src/vdf_io/notebooks/upsert_pinecone.ipynb +++ b/src/vdf_io/notebooks/upsert_pinecone.ipynb @@ -17,14 +17,11 @@ } ], "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" + "from dotenv import find_dotenv, load_dotenv\n", + "\n", + "load_dotenv(find_dotenv(), override=True)" ] }, { @@ -42,7 +39,6 @@ } ], "source": [ - "import os\n", "import pinecone\n", "from dotenv import load_dotenv\n", "\n", @@ -69,8 +65,9 @@ "source": [ "import csv\n", "import uuid\n", - "from tqdm.notebook import tqdm\n", + "\n", "import numpy as np\n", + "from tqdm.notebook import tqdm\n", "\n", "# delete index\n", "index_name = \"test-1\"\n", @@ -134,7 +131,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", @@ -1141,8 +1138,7 @@ "source": [ "# write a file with numbers from 0 to 3199, one per row\n", "with open(\"ids.txt\", \"w\") as f:\n", - " for i in range(3200):\n", - " f.write(f\"{i}\\n\")" + " f.writelines(f\"{i}\\n\" for i in range(3200))" ] }, { @@ -1280,9 +1276,7 @@ "execution_count": 1, "metadata": {}, "outputs": [], - "source": [ - "from pinecone_datasets import load_dataset" - ] + "source": [] }, { "cell_type": "code", @@ -2076,7 +2070,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" ] }, { @@ -3955,9 +3949,9 @@ "metadata": {}, "outputs": [], "source": [ - "from pinecone import Pinecone\n", "import os\n", "\n", + "from pinecone import Pinecone\n", "\n", "pc = Pinecone(api_key=os.environ.get(\"PINECONE_API_KEY\"))" ] @@ -4413,17 +4407,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 +4722,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..752182f 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", @@ -188,30 +188,26 @@ } ], "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import itertools\n", - "import time \n", "import json\n", - "import uuid\n", "\n", - "from pprint import pprint\n", + "# logging\n", + "import logging\n", + "import time\n", "\n", + "import pandas as pd\n", "from google.cloud import aiplatform as aip\n", - "from google.cloud import storage\n", - "from google.cloud import bigquery\n", + "from google.cloud import bigquery, storage\n", "\n", - "# logging\n", - "import logging\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 +251,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 +365,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 +391,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 +665,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 +708,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 +735,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..60651eb 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", @@ -212,30 +212,27 @@ } ], "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import itertools\n", - "import time \n", "import json\n", - "import uuid\n", "\n", + "# logging\n", + "import logging\n", + "import time\n", "from pprint import pprint\n", "\n", + "import pandas as pd\n", "from google.cloud import aiplatform as aip\n", - "from google.cloud import storage\n", - "from google.cloud import bigquery\n", + "from google.cloud import bigquery, storage\n", "\n", - "# logging\n", - "import logging\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 +276,6 @@ }, "outputs": [], "source": [ - "import sys\n", "import os\n", "\n", "# sys.path.append(\"..\")\n", @@ -522,19 +518,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 +552,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 +702,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 +714,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 +781,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 +996,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 +1022,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..487e87e 100644 --- a/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb +++ b/src/vdf_io/notebooks/vertex_quickstart_w_bq_datasets.ipynb @@ -105,28 +105,27 @@ }, "outputs": [], "source": [ - "import os \n", + "import functools\n", + "import json\n", + "\n", + "# logging\n", + "import logging\n", "import math\n", + "import os\n", "import time\n", - "import json\n", "import uuid\n", - "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", + "import numpy as np\n", + "import pandas as pd\n", + "from google.cloud import aiplatform, bigquery, storage\n", "from google.cloud import aiplatform_v1 as aipv1\n", - "from google.cloud import storage\n", - "from google.cloud import bigquery\n", "\n", - "# logging\n", - "import logging\n", "logging.disable(logging.WARNING)\n", "\n", - "#python warning \n", + "# python warning\n", "import warnings\n", + "\n", "warnings.filterwarnings(\"ignore\")" ] }, @@ -148,16 +147,18 @@ } ], "source": [ - "from tqdm.auto import tqdm\n", - "import pyarrow.parquet as pq\n", - "from pyarrow import json as pj\n", + "from collections.abc import Generator\n", "from concurrent.futures import ThreadPoolExecutor\n", - "from typing import Generator, List, Tuple, Dict, Any, Optional\n", + "from typing import Any\n", "\n", + "import pyarrow.parquet as pq\n", + "from pyarrow import json as pj\n", + "from tqdm.auto import tqdm\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 +180,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 +207,7 @@ "outputs": [], "source": [ "# create new gcs bucket, vs index, etc.?\n", - "CREATE_NEW_ASSETS = False " + "CREATE_NEW_ASSETS = False" ] }, { @@ -235,8 +236,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 +269,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 +306,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 +364,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 +464,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 +578,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", @@ -735,28 +728,27 @@ "outputs": [], "source": [ "# Define an embedding method that uses the model\n", - "def encode_texts_to_embeddings(sentences: List[str]) -> List[Optional[List[float]]]:\n", + "def encode_texts_to_embeddings(sentences: list[str]) -> list[list[float] | None]:\n", " try:\n", " embeddings = model.get_embeddings(sentences)\n", " 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", - ") -> Generator[List[str], None, None]:\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", - ") -> Tuple[List[bool], np.ndarray]:\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", + " embeddings_list: list[list[float]] = []\n", "\n", " # Prepare the batches using a generator\n", " batches = generate_batches(sentences, batch_size)\n", @@ -784,6 +776,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 +784,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 +793,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 +823,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 +832,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 +858,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 +929,6 @@ ], "source": [ "import gc\n", - "import json\n", "import tempfile\n", "from pathlib import Path\n", "\n", @@ -967,11 +955,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 +1092,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 +1139,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 +1242,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 +1427,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 +1581,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 +1606,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 +1648,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 +1672,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 +1689,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 +1741,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 +1824,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 +1847,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 +1879,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 +1927,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 +1972,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 +2035,7 @@ } ], "source": [ - "# get all index config to dictionary \n", + "# get all index config to dictionary\n", "my_vs_index.to_dict()" ] }, @@ -2104,8 +2081,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 +2105,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 +2118,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 +2233,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 +2273,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 +2304,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 +2382,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..4f99de3 100644 --- a/src/vdf_io/notebooks/weaviate_fill.ipynb +++ b/src/vdf_io/notebooks/weaviate_fill.ipynb @@ -89,6 +89,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "import weaviate\n", "from dotenv import load_dotenv\n", "\n", @@ -136,12 +137,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 +156,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..8832981 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" ] }, { @@ -41,17 +41,15 @@ ], "source": [ "import requests\n", - "from transformers import ResNetModel, ConvNextImageProcessor\n", - "\n", "import torch\n", "from PIL import Image\n", + "from transformers import ConvNextImageProcessor, ResNetModel\n", "\n", "model = ResNetModel.from_pretrained(\"microsoft/resnet-50\")\n", "# embed https://upload.wikimedia.org/wikipedia/commons/8/8b/Scolopendra_gigantea.jpg\n", "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 +215,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", @@ -271,8 +268,7 @@ } ], "source": [ - "import torch.nn as nn\n", - "\n", + "from torch import nn\n", "\n", "gap = nn.AdaptiveAvgPool2d((1, 1))\n", "x_reduced = gap(last_hidden_states)\n", diff --git a/src/vdf_io/scripts/check_for_updates.py b/src/vdf_io/scripts/check_for_updates.py index 4b0bc03..520e094 100644 --- a/src/vdf_io/scripts/check_for_updates.py +++ b/src/vdf_io/scripts/check_for_updates.py @@ -1,5 +1,5 @@ -import requests import pkg_resources as pkg +import requests import vdf_io diff --git a/src/vdf_io/scripts/consolidate_parquet.py b/src/vdf_io/scripts/consolidate_parquet.py index 9f11610..1649fc1 100755 --- a/src/vdf_io/scripts/consolidate_parquet.py +++ b/src/vdf_io/scripts/consolidate_parquet.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 +import argparse import json +import os +from collections import defaultdict + import pandas as pd import pyarrow.parquet as pq -import os -import argparse -from tqdm import tqdm from pyarrow import Table -from collections import defaultdict +from tqdm import tqdm def get_file_size_in_gb(file_path): diff --git a/src/vdf_io/scripts/count_rows.py b/src/vdf_io/scripts/count_rows.py index 0ee87a7..aa2bb9c 100755 --- a/src/vdf_io/scripts/count_rows.py +++ b/src/vdf_io/scripts/count_rows.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -import pyarrow.parquet as pq -import os import argparse +import os + +import pyarrow.parquet as pq def get_file_size_in_gb(file_path): diff --git a/src/vdf_io/scripts/count_rows_hf.py b/src/vdf_io/scripts/count_rows_hf.py index 6eb07c1..2f0211a 100644 --- a/src/vdf_io/scripts/count_rows_hf.py +++ b/src/vdf_io/scripts/count_rows_hf.py @@ -1,5 +1,6 @@ import os import sys + import requests headers = {"Authorization": f"Bearer {os.environ['HUGGING_FACE_TOKEN']}"} diff --git a/src/vdf_io/scripts/get_id_list.py b/src/vdf_io/scripts/get_id_list.py index cefc1e1..dfe8ea1 100755 --- a/src/vdf_io/scripts/get_id_list.py +++ b/src/vdf_io/scripts/get_id_list.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 import os + import pandas as pd -from vdf_io.util import expand_shorthand_path, read_parquet_progress from vdf_io.constants import ID_COLUMN +from vdf_io.util import expand_shorthand_path, read_parquet_progress # script to get list of ids from directory of parquet files diff --git a/src/vdf_io/scripts/push_to_hub_vdf.py b/src/vdf_io/scripts/push_to_hub_vdf.py index 1649b67..52379ea 100755 --- a/src/vdf_io/scripts/push_to_hub_vdf.py +++ b/src/vdf_io/scripts/push_to_hub_vdf.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -from getpass import getpass +import argparse import os +from getpass import getpass + from huggingface_hub import HfApi -import argparse def push_to_hub(export_obj, args): diff --git a/src/vdf_io/scripts/reembed.py b/src/vdf_io/scripts/reembed.py index 85ddf48..71be3a8 100755 --- a/src/vdf_io/scripts/reembed.py +++ b/src/vdf_io/scripts/reembed.py @@ -4,32 +4,32 @@ import datetime import json import os +import sys import time +import warnings + import litellm -from litellm import EmbeddingResponse import numpy as np +import pyarrow as pa +import pyarrow.parquet as pq import sentence_transformers +import torch +from dotenv import load_dotenv +from IPython.core import ultratb +from litellm import EmbeddingResponse +from mlx_embedding_models.embedding import EmbeddingModel +from sentence_transformers import SentenceTransformer from tenacity import ( retry, retry_if_exception_type, stop_after_attempt, wait_random_exponential, ) -import torch from tqdm import tqdm -from dotenv import load_dotenv -import sys -from IPython.core import ultratb -import warnings -import pyarrow as pa -import pyarrow.parquet as pq -from mlx_embedding_models.embedding import EmbeddingModel -from sentence_transformers import SentenceTransformer import vdf_io from vdf_io.constants import ID_COLUMN from vdf_io.meta_types import NamespaceMeta, VDFMeta - from vdf_io.util import ( get_author_name, get_final_data_path, @@ -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..7806d71 100644 --- a/src/vdf_io/util.py +++ b/src/vdf_io/util.py @@ -1,21 +1,20 @@ -from pathlib import Path -from collections import OrderedDict -from getpass import getpass import hashlib import json import os +import sys import time -from typing import Dict +from collections import OrderedDict +from getpass import getpass +from io import StringIO +from pathlib import Path from uuid import UUID + import numpy as np import pandas as pd -from io import StringIO -import sys -from tqdm import tqdm -from PIL import Image from halo import Halo - +from PIL import Image from qdrant_client.http.models import Distance +from tqdm import tqdm from vdf_io.constants import ID_COLUMN, INT_MAX from vdf_io.names import DBNames @@ -107,9 +106,7 @@ def set_arg_from_input( + (" " + str(list(choices)) + ": " if choices is not None else "") ) if len(inp) >= 2: - if inp[0] == '"' and inp[-1] == '"': - inp = inp[1:-1] - elif inp[0] == "'" and inp[-1] == "'": + if inp[0] == '"' and inp[-1] == '"' or inp[0] == "'" and inp[-1] == "'": inp = inp[1:-1] if inp == "": args[arg_name] = ( @@ -124,7 +121,6 @@ def set_arg_from_input( else: args[arg_name] = type_name(inp) break - return def set_arg_from_password(args, arg_name, prompt, env_var_name): @@ -135,7 +131,6 @@ def set_arg_from_password(args, arg_name, prompt, env_var_name): args[arg_name] = os.getenv(env_var_name) elif arg_name not in args or args[arg_name] is None: args[arg_name] = getpass(prompt) - return def expand_shorthand_path(shorthand_path): @@ -156,7 +151,7 @@ def expand_shorthand_path(shorthand_path): return str(full_path) -db_metric_to_standard_metric: Dict[str, Dict[str, Distance]] = { +db_metric_to_standard_metric: dict[str, dict[str, Distance]] = { DBNames.PINECONE: { "cosine": Distance.COSINE, "euclidean": Distance.EUCLID, @@ -316,10 +311,8 @@ def get_parquet_files(data_path, args, temp_file_paths=[], id_column=ID_COLUMN): if id_column not in df.columns: # remove all rows tqdm.write( - ( - f"ID column '{id_column}' not found in parquet file '{data_path}'." - f" Skipping split '{split}', config '{config}'." - ) + f"ID column '{id_column}' not found in parquet file '{data_path}'." + f" Skipping split '{split}', config '{config}'." ) continue total_rows_loaded += len(df) @@ -336,7 +329,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): @@ -422,8 +415,7 @@ def get_qdrant_id_from_id(idx): def read_parquet_progress(file_path, id_column, **kwargs): if file_path.startswith("hf://"): - from huggingface_hub import HfFileSystem - from huggingface_hub import hf_hub_download + from huggingface_hub import HfFileSystem, hf_hub_download fs = HfFileSystem() resolved_path = fs.resolve_path(file_path) @@ -461,8 +453,8 @@ def read_parquet_progress(file_path, id_column, **kwargs): "max_num_rows" in kwargs and (kwargs.get("max_num_rows", INT_MAX) or INT_MAX) < INT_MAX ): - from pyarrow.parquet import ParquetFile import pyarrow as pa + from pyarrow.parquet import ParquetFile pf = ParquetFile(file_path_to_be_read) first_ten_rows = next(pf.iter_batches(batch_size=kwargs["max_num_rows"]))