uv-scripts/embeddings
Embeddings Generate embeddings for a Hugging Face dataset — text or images — with one command, on a cloud GPU, no infra. The output lands back on the Hub as a new dataset (or, with the Lance variant, as a searchable vector index you can query over hf:// without downloading). There is one simple default and two variants; they are separate single-file scripts because their dependencies (sentence-transformers vs vLLM vs Lance) are too different to share one env. Script Use it… See the full description on the dataset page: https://huggingface.co/datasets/uv-scripts/embeddings.
397
1# /// script2# requires-python = ">=3.10"3# dependencies = [4# "datasets",5# "sentence-transformers>=5.0.0",6# "torch",7# "numpy",8# "einops",9# "pyarrow",10# "pylance",11# "huggingface-hub",12# ]13# ///14"""15Embed a Hugging Face dataset and push it back as a Lance vector index — a Hub dataset that16IS a searchable vector database. Anyone you share it with can vector-search it over `hf://`17without downloading it:18 19 import lance20 ds = lance.dataset("hf://datasets/your-name/my-vecdb/vecdb.lance") # opens fast, no download21 hits = ds.to_table(nearest={"column": "vector", "q": query_vector, "k": 5})22 23Best for share-and-search over a corpus; for high-QPS serving, pull the dataset local first.24 25PROMPTS: documents are embedded with the model's known DOCUMENT convention (e5 → "passage: ",26nomic → "search_document: "; bge-en/bge-m3 → none). At SEARCH time, embed your query with the27matching QUERY prefix (printed at the end of the run) or retrieval quality silently drops.28Override the document prefix with --prompt '<prefix>' (or --prompt '' for none).29 30 hf jobs uv run --flavor l4x1 -s HF_TOKEN embed-to-lance.py \\31 stanfordnlp/imdb your-name/imdb-vecdb --column text --model BAAI/bge-base-en-v1.5 --private32"""33import argparse34import logging35import os36import re37import shutil38import sys39import time40import numpy as np41import pyarrow as pa42 43logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")44log = logging.getLogger("embed-to-lance")45 46 47def known_convention(model_id):48 """(query_prefix, doc_prefix) for common families (documented in model cards, not registered49 in sentence-transformers config). Same table as generate-embeddings.py; None = unknown."""50 m = model_id.lower()51 if "instruct" in m:52 return None53 if "nomic-embed-text" in m:54 return ("search_query: ", "search_document: ")55 if "bge-m3" in m:56 return ("", "")57 if re.search(r"(^|[/_-])e5([_-]|$)", m):58 return ("query: ", "passage: ")59 if "bge" in m and "-en" in m:60 return ("Represent this sentence for searching relevant passages: ", "")61 return None62 63 64def main():65 ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)66 ap.add_argument("input_dataset")67 ap.add_argument("output_repo")68 ap.add_argument("--column", default="text")69 ap.add_argument("--config", default=None, help="dataset config name (e.g. wikipedia needs one)")70 ap.add_argument("--split", default="train")71 ap.add_argument("--model", default="BAAI/bge-base-en-v1.5")72 ap.add_argument("--max-samples", type=int, default=None)73 ap.add_argument("--batch-size", type=int, default=64)74 ap.add_argument("--max-seq-len", type=int, default=512)75 ap.add_argument("--prompt", default=None,76 help="Document prefix to prepend (default: auto from the known-family table; "77 "pass '' to force none)")78 ap.add_argument("--private", action="store_true")79 args = ap.parse_args()80 81 import torch82 import lance83 from datasets import load_dataset84 from huggingface_hub import HfApi, login85 from sentence_transformers import SentenceTransformer86 87 if os.environ.get("HF_TOKEN"):88 login(token=os.environ["HF_TOKEN"])89 90 t_all = time.perf_counter()91 ds = load_dataset(args.input_dataset, args.config, split=args.split) if args.config \92 else load_dataset(args.input_dataset, split=args.split)93 if args.max_samples:94 ds = ds.select(range(min(args.max_samples, len(ds))))95 texts = [t if isinstance(t, str) and t.strip() else " " for t in ds[args.column]]96 n = len(texts)97 98 t_load = time.perf_counter()99 100 device = "cuda" if torch.cuda.is_available() else "cpu"101 model = SentenceTransformer(args.model, device=device, trust_remote_code=True)102 if getattr(model, "max_seq_length", None):103 model.max_seq_length = min(model.max_seq_length, args.max_seq_len)104 dim = model.get_sentence_embedding_dimension()105 106 # Document-side prompt: explicit --prompt wins (incl. '' for none), else the known-family107 # table; else None → encode_document() natively selects any REGISTERED document prompt108 # (and routes Router models by task).109 registered = {k: v for k, v in (getattr(model, "prompts", {}) or {}).items() if v}110 kc = known_convention(args.model)111 doc_prompt = args.prompt if args.prompt is not None else (kc[1] if kc else None)112 query_prompt = kc[0] if kc else registered.get("query", "")113 log.info(f"document prompt: {doc_prompt!r}" if doc_prompt114 else ("document prompt: native (registered)" if registered.get("document")115 else "document prompt: (none)"))116 117 t0 = time.perf_counter()118 encode_kwargs = {"prompt": doc_prompt} if doc_prompt is not None else {}119 emb = model.encode_document(texts, batch_size=args.batch_size, show_progress_bar=True,120 convert_to_numpy=True, normalize_embeddings=True,121 **encode_kwargs).astype(np.float32)122 log.info(f"embedded {n} rows in {time.perf_counter()-t0:.1f}s, dim={dim}")123 124 tbl = pa.table({125 "id": pa.array(range(n), pa.int64()),126 "text": pa.array([t[:2000] for t in texts]),127 "vector": pa.FixedSizeListArray.from_arrays(pa.array(emb.reshape(-1), pa.float32()), dim),128 })129 local = "vecdb.lance"130 if os.path.exists(local):131 shutil.rmtree(local)132 lds = lance.write_dataset(tbl, local, mode="overwrite")133 try:134 parts = max(1, min(256, int(np.sqrt(n))))135 lds.create_index("vector", index_type="IVF_PQ", num_partitions=parts,136 num_sub_vectors=max(1, dim // 16))137 log.info(f"built IVF_PQ index (partitions={parts})")138 except Exception as e:139 log.warning(f"index build skipped ({repr(e)[:120]}); flat search still works over hf://")140 141 # Retry the upload with an XET-disable fallback — a transient failure here would lose the142 # whole (paid) embedding run.143 api = HfApi()144 api.create_repo(args.output_repo, repo_type="dataset", private=args.private, exist_ok=True)145 max_retries = 3146 for attempt in range(1, max_retries + 1):147 try:148 if attempt > 1:149 log.warning("Disabling XET (fallback to HTTP upload)")150 os.environ["HF_HUB_DISABLE_XET"] = "1"151 api.upload_folder(folder_path=local, path_in_repo="vecdb.lance",152 repo_id=args.output_repo, repo_type="dataset")153 break154 except Exception as e:155 log.error(f"Upload attempt {attempt}/{max_retries} failed: {e}")156 if attempt < max_retries:157 delay = 30 * (2 ** (attempt - 1))158 log.info(f"Retrying in {delay}s...")159 time.sleep(delay)160 else:161 log.error("All upload attempts failed. Results are lost.")162 sys.exit(1)163 total_s = time.perf_counter() - t_all164 import json as _json165 log.info("ROUNDTRIP " + _json.dumps({166 "input": args.input_dataset, "n": n, "dim": dim, "model": args.model,167 "gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else "cpu",168 "batch_size": args.batch_size, "load_s": round(t_load - t_all, 1),169 "total_roundtrip_s": round(total_s, 1), "rows_per_s_end_to_end": round(n / total_s, 1),170 "hf_path": f"hf://datasets/{args.output_repo}/vecdb.lance"}))171 log.info(f"✅ {n} rows → searchable vector DB in {total_s/60:.1f} min "172 f"(load→embed→index→push). hf://datasets/{args.output_repo}/vecdb.lance")173 if query_prompt or registered.get("query"):174 log.info("⚠️ At search time, embed queries with the QUERY convention — mismatched prompts "175 "degrade retrieval. Easiest: model.encode_query([your_query])"176 + (f", or explicitly: model.encode([{query_prompt!r} + your_query])" if query_prompt else "."))177 178 179if __name__ == "__main__":180 main()181 