Pinecone のテストデータ生成方法について
Pinecone は、ベクトルデータベースの性能評価や検証を効果的に行うためのテストデータ作成手法に関する公式ドキュメントを発表しました。
キーポイント
テストデータの重要性と目的
ベクトルデータベースの真価を検証するためには、単なるランダムデータではなく、実際のユースケースを反映した質の高いテストデータが必要であることが強調されています。
実用的な作成手法の提供
ドキュメント内では、具体的なシナリオやデータ構造に基づいたテストデータの生成アプローチが解説されており、開発者が即座に適用可能なガイドラインを提供しています。
評価基準の明確化
適切なテストデータを用いることで、検索精度、レイテンシ、スケーラビリティなどの重要なパフォーマンス指標を正確に測定・比較できる仕組みが示されています。
影響分析・編集コメントを表示
影響分析
この情報は、ベクトルデータベース導入を検討・実装中の開発者にとって、システムのパフォーマンスを正しく評価するための重要なフレームワークを提供するものです。特に RAG(Retrieval-Augmented Generation)システムの構築において、テストデータの質が最終的な精度に直結するため、実務における品質保証の基準として機能します。
編集コメント
技術的な深みというよりは、実装フェーズにおける品質保証のベストプラクティスを示す実務的なドキュメントと言えます。
私は約1年前にPineconeのソリューションエンジニアとして入社しました。それ以来、私のいくつかのプロジェクトは、プラットフォームの異なる部分をテストするための内部ツールや例へと発展しました。この記事はそのシリーズの第1弾であり、後続の記事では、私のPinecone Index Migrator(インデックス移行ユーティリティ)、GPUベンチマーク用ユーティリティ、密ベクトル埋め込み生成器、そして密・疎・ハイブリッドインデックスに対するクエリの例について取り上げます。
これは規模が拡大するほど重要性が増す、一見単純そうに見えることから始まります:テストデータの生成です。私の業務の多くでは、実用的な規模で再現性のあるデータセットを作成し、再現性と精度のテストを支援できる程度に現実的であり、かつ実験間での再利用が可能であるような柔軟性を備えた方法が必要です。その結果、Parquetファイルの生成、埋め込みの作成、メタデータの準備、そしてデータをPineconeへのインポートを行うための一連のユーティリティが開発されました。
これらのユーティリティは以下のいくつかの部分に分割されています:
- Parquetファイル生成器
- ベクトル埋め込み生成器
- ローカルLLM(大規模言語モデル)を使用したParquet分類器
- カテゴリをPineconeバッチインポートの命名空間として利用するParquet再配置器
この記事では、このワークフローのうち最初の部分、つまりソースデータの生成と埋め込み・インポートのための準備に焦点を当てます。完全で実行可能なスクリプトはコンパニオンノートブックにあります。以下のスニペットは、行単位で議論する価値のある部分です。
なぜテストデータ生成が重要なのか
Pinecone の取り扱いを始めた際に私が最初に直面したことのひとつは、さまざまな種類のテスト用に異なるデータセットを作成する必要性でした。すべてのテストデータが同じではなく、すべてのデータセットサイズも同じではありません。
プラットフォームが「スケール」して動作すると言うのは簡単ですが、その言葉の意味はワークロードによって異なります。50 万ベクトルのテストと、1,500 万ベクトル、あるいは 1 億、5 億、10 億ベクトルのテストでは状況が全く異なります。それぞれのサイズには、取り込み(ingestion)、ストレージ、レイテンシ、フィルタリング、コスト、運用ワークフローに関する固有の問題群が生じます。
Hugging Face のデータセットや Kaggle のデータセットなど、優れた生データのソースは数多く存在します。これらは良い出発点となりますが、問題の全体像の一部に過ぎません。ベクトル検索のテストにおいては、依然としてベクトルの生成が必要です。そこで重要な問いが生じます。
ベクトルは実在するものである必要があるのか?
場合によっては不要です。書き込みスループットやストレージの挙動、クエリレイテンシのみをテストしているだけなら、ランダムなベクトルで十分です。スピードが唯一の評価対象であり、ベクトルの意味が重要ではないケースも私にはあります。しかし、多くのテストにおいては、再現率(recall)と精度(accuracy)が重要であり、そのためには埋め込み(embeddings)が実際のテキストを反映している必要があります。ランダムなベクトルではそこはカバーできません。
そこでこのプロジェクトでは、実在するテキストから現実的なデータセットを生成し、実際の埋め込みを作成し、Pinecone へ効率的にインポートできる形式ですべてを保存することを目的としました。
データセットの要件
この最初のユーティリティでは、必要に応じて再生成できる参照データセットが必要でした。また、検索動作をテストする際に結果について推論できるように、データはある程度馴染みのあるものであることも望みました。
私の要件は非常にシンプルでした:
- 広範なカバレッジを持つ実際のテキストデータセットを使用する
- Python を使用し、その実践的な練習の一部とする
- 出力を Parquet フォーマットで保存する
- 出力を Pinecone の一括インポートと互換性を持たせる
- ローカルでの埋め込み生成をサポートする
- 将来的に埋め込み処理を複数のマシンに分散可能にする
私は Hugging Face の stanford-oval/ccnews データセットを採用しました。これにより、広範なニュース記事のセットが得られ、タイトル、著者、ソース URL、公開日、カテゴリ情報などの有用なメタデータフィールドも含まれています。
Pinecone のインポート形式における構造はシンプルです:
- id(id)
- values(values)
- metadata(metadata)
id フィールドは一意のベクトル ID です。私は通常、構造化された ID を好みます。なぜなら、後でデバッグや運用タスクが容易になるからです。この例では UUID が使用されていますが、プレフィックス、ドキュメント ID、チャンク番号、または他の識別子を簡単に含めるように調整することも可能です。
values フィールドには密集ベクトル(dense vector)が含まれています。
メタデータフィールドには、ベクトルに関連する JSON メタデータが含まれています。バッチインポート用の Parquet ファイルを作成する際に注意すべき点は、メタデータフィールドをネストされた JSON オブジェクトではなく、JSON 文字列として記述しなければならないという点です。これは小さな細部ですが、インポート用ファイルを準備する際には重要な意味を持ちます。
エンベディングモデルの選択
この例では、mlx_embedding_models レジストリ内の bge-large というレジストリ名を通じてアクセスできる BAAI/bge-large-en-v1.5 を使用しました。
このモデルは 1024 次元の密ベクトルを生成します。Pinecone-hosted embedding models(Pinecone がホストするエンベディングモデル)を使用することも可能でしたが、本プロジェクトではローカルでのエンベディング生成を実験したかったのです。また、顧客が自らエンベディングを生成し、その結果得られたベクトルを Pinecone にロードするという、現場で頻繁に見られるワークフローをシミュレーションしたかったのです。
ここにはトレードオフが存在します。次元数の高いベクトルは特定のワークロードにおいて検索精度を向上させる可能性がありますが、一方で保存サイズが増大し、速度やコストに影響を与えることもあります。Pinecone は多くの異なるエンベディングモデルをサポートしているため、適切な選択はテスト内容に依存します。例えば、マルチモーダル検索を検証する場合は、CLIP などのモデルを使用するかもしれません。
このモデルが固定された選択肢であるわけではありません。重要なのは、後でモデルを差し替えられるよう、ワークフローが十分に柔軟であることです。
First Version: Stream, Embed, and Upsert Directly
最初のバージョンのスクリプトは、最も理解しやすいものです。これは Hugging Face から CC News データセットをストリームし、記事テキストをチャンク化し、ローカルで埋め込み(embeddings)を生成して、ベクトルを直接 Pinecone インデックスにアップサートします。
これは構成要素が少ないため、小規模なテストに適しています。スクリプトは一つのフロー内ですべての処理を行います:存在しない場合はインデックスを作成し、レコードをストリームし、使用できないまたは英語以外のレコードをスキップし、テキストをチャンク化し、埋め込みを生成し、メタデータを付与し、アップサートします。完全なスクリプトはノートブックにあります(section 1)。いくつかの詳細に注目する価値があります。
まず、スクリプトはメタデータのサイズを確認します。Pinecone はベクトルごとのメタデータペイロードに対して 40 KB の制限を課しており、これらのテストでは元のテキストをメタデータとして保存しているため、長い記事だとこの制限を超えてしまいます。チャンカー(chunker)は、シリアライズされたメタデータが収まるまで反復的にトリミングします:
while sys.getsizeof(json.dumps({"text": chunk})) >= MAX_JSON_BYTES and len(chunk) > 1000:
chunk = chunk[: int(len(chunk) * 0.9)]
次に、スクリプトは null(null)のメタデータ値を除外します。Pinecone は null 値を拒否するため、ソースデータセットには欠落したフィールドが含まれていますが、アップサートする前にこれらをフィルタリングしています:
batch_metadata.append({k: v for k, v in meta.items() if v is not None})
第三に、このスクリプトはバッチアップサートを実行します。ベクトルを一つずつ送信するのはパフォーマンスの観点から優れたパターンではありません。特にデータセットが大きくなると、ベクトルが溜まっていき、BATCH_SIZE に達するまで蓄積され、まとめてフラッシュされます。
小規模なテストでは、この直接アプローチはうまく機能します。しかし、データセットが大きくなるとこの手法は破綻し始めます。
ワークフローを分割した理由
直接アップサートを行うスクリプトはシンプルですが、数千万〜数億個のベクトルを対象とする場合、私が採用するアプローチではありません。
その理由は時間です。行処理、チャンキング(分割)、埋め込み生成、そしてアップサートがすべて一つのプロセス内で実行されると、埋め込み生成ステップがボトルネックとなります。数千個のベクトル程度であれば問題ありませんが、数百万個になると課題となり、数億個になるとすべての処理をインラインで行うのは不合理なほど長時間を要するようになります。
そこで私は作業を二つのステージに分割しました。最初のステージではテキストレコードを Parquet 形式で書き出し、二つ目のステージではそのファイルを読み込んで埋め込みを生成し、値列(values column)が埋め込まれた新しい Parquet ファイルを書き出します。
これにより柔軟性が得られます。ソースファイルを一度だけ生成すればよく、埋め込みの処理を複数のマシンに分散できます。状態追跡について過度に心配する必要もありません。各マシンが別々の Parquet ファイルセットを処理できるためです。
私の場合は、Apple M シリーズのマシンや NVIDIA GPU を搭載した ASUS ROG ラップトップなど、いくつかのマシンを利用できました。ただし、ローカルで実行する場合でも、クラウドの GPU インスタンスを使用する場合でも、RunPod などのサービスを利用する場合でも、この考え方は同じように機能します。
Second Version: Generate Parquet Without Embeddings
第二版のスクリプトは、同じ CC News データセットをストリーミングし、記事テキストをチャンク化して Parquet ファイルに書き出します。違いは、まだ埋め込みベクトル(embeddings)を生成しない点です。values カラムは空のリストとして書き込まれ、このプレースホルダーは後の埋め込みスクリプトによって埋められます。
ここでは Pandas ではなく PyArrow を使用しました。Parquet スキーマを明示的に定義したかったためです。特に values フィールドは LIST<FLOAT> として記述する必要があります。Pandas がそのカラムを汎用的なオブジェクト型と推測すると、後の Pinecone バルクインポートで問題が発生します:
PARQUET_SCHEMA = pa.schema([
pa.field("id", pa.string()),
pa.field("values", pa.list_(pa.float32())), # 現在は空;オフラインで埋められる
pa.field("metadata", pa.string()), # JSON 文字列、仕様により NULL 可能
])
各レコードはプレースホルダーを配置した状態で書き出され、メタデータはすでに JSON 文字列となっているため、インポート形式と一致します:
buffer.append({
"id": str(uuid.uuid4()),
"values": [], # プレースホルダー — オフライン埋め込みステップで埋められる
"metadata": metadata_str, # Pinecone Parquet インポートは JSON 文字列を期待する
})
完全なスクリプトはノートブックにあります (section 2)。あえて退屈なものにしていますが、それは良いことです — 一つの役割しか果たしません:生のデータセットレコードをチャンク化し、メタデータを豊富にした Parquet ファイルに変換することです。
パーティションサイズは設定可能です。私は通常、10,000 から 20,000 件程度のレコードをパーティションに保ちます。これは私がデータファイルを扱う傾向に合致しているためです。より大きな値にすることも可能で、場合によっては非常に大きくすることもできますが、小さなパーティションにすることで、マシン間で作業を分割しやすくなり、何か問題が発生した際に失敗したチャンクを再実行しやすくなります。
この段階では、Pinecone API キーやインデックス名、埋め込み次元は必要ありません。スクリプトは単にファイルの準備を行っているだけです。
第三版:Parquet から埋め込みを生成する
テキストのみを含む Parquet ファイルが揃えば、別の埋め込み生成スクリプトを実行できます。このスクリプトは各 Parquet ファイルを読み取り、メタデータからテキストを取り出し、埋め込みを生成して、values カラムに値が充填された新しい Parquet ファイルを書き出します。その出力は Pinecone へのバッチインポートの準備が整っています。
このスクリプトは実行時にデバイスを検出するため、各環境用に別バージョンを用意することなく、同じファイルを異なるマシンで実行できます。Apple Silicon では mlx_embedding_models を使用して Mac の GPU または Neural Engine で動作し、NVIDIA 環境では CUDA 上の sentence-transformers にフォールバックします。それ以外の場合は CPU にフォールバックします:
if torch.backends.mps.is_available():
DEVICE = "mps" # Apple Silicon — MLX on the Mac GPU / Neural Engine
elif torch.cuda.is_available():
DEVICE = "cuda" # sentence-transformers on CUDA
else:
DEVICE = "cpu" # CPU fallback
完全なスクリプトはノートブックにあります (section 3)。私が調整する主な点はバッチサイズです。CUDA および CPU のパスにおいて、バッチサイズは大きな差を生む可能性があり、適切な値は利用可能なメモリと使用されているモデルに依存します。
Pinecone へのバルクインポート
埋め込みスクリプトが完了した後、ID、密ベクトル、メタデータを含む Parquet ファイルが得られます。これはこのように出力を構造化した主な理由です。次のステップは、これらを Amazon S3 などのオブジェクトストレージにコピーし、Pinecone のバルクインポートを実行することです。小規模なテストでは直接アップサートでも問題ありませんが、大規模なケースではバルクインポートの方が高速で運用上も便利であり、大規模データセットに適しています。
全体の流れは以下のようになります:
これにより、異なるテストサイズ、モデル、ハードウェア構成に対して反復可能なプロセスが得られます。
学んだ教訓
この作業からいくつかの実践的な教訓が得られました。
第一に、ワークフローをモジュール化してください。直接アップサートするバージョンは学習や小規模なテストには役立ちますが、スケールが重要になってくると、分離された Parquet ワークフローの方がはるかに優れています。
第二に、何をテストしているかを明確にしてください。レイテンシまたは書き込みスループットのみをテストする場合、ランダムベクトルで十分かもしれません。しかし、リコールや回答の品質をテストする場合は、実際のコンテンツから作成された実在の埋め込みが必要です。
第三に、メタデータには早期から注意を払ってください。メタデータは後回しになりがちですが、フィルタリング、デバッグ、インポート形式、ストレージ制限に影響を与えます。この例では、後のテストを容易にするために元のテキストをメタデータに保存しましたが、その分、メタデータのサイズにも注意する必要がありました。
第四に、Parquet ファイルを作成する際は明示的なスキーマを使用してください。後から列が誤って推定されたことに気づくよりも、最初から正確に定義しておく方がはるかに優れています。
最後に、ハードウェアも重要です。適切なハードウェアがあれば、ローカルでの埋め込み生成は非常に実用的です。Apple Silicon、NVIDIA GPU、クラウド上の GPU インスタンス、CPU によるフォールバックなど、すべてが同じワークフローに組み込むことができますが、そのパフォーマンスは異なります。
まとめ
ベクトル検索用のテストデータ生成とは、単に行を作成するだけではありません。ソーステキスト、埋め込みモデル、ベクトルの次元、メタデータ、ファイル形式、インポートパスについて考え、埋め込みステップが単一のプロセスでボトルネックになるのではなく、複数のマシンにわたってスケーリングできるようにワークフローを構造化することを意味します。
次の記事では、このワークフローの埋め込み生成側により深く入り込み、どのようにして作業を複数のマシンに分割したか、またこの種のタスクにおいて Apple Silicon と NVIDIA GPU がどう比較されるかについて詳しく解説します。
原文を表示
I joined Pinecone as a Solutions Engineer about a year ago. Since then, several of my projects have turned into internal tools and examples for testing different parts of the platform. This article is the first in a series walking through them; later posts will cover my Pinecone Index Migrator, a GPU benchmarking utility, a dense vector embedding generator, and examples for querying dense, sparse, and hybrid indexes.
It starts with something that sounds simple but matters more at scale: generating test data. For most of my work I need a repeatable way to create datasets that are large enough to be useful, realistic enough to support recall and accuracy testing, and flexible enough to reuse across experiments. That led to a small collection of utilities for generating Parquet files, creating embeddings, preparing metadata, and importing the data into Pinecone.
The utilities are broken into a few parts:
- Parquet file generator
- Vector embedding generator
- Parquet categorizer using a local LLM
- Parquet repartitioner that uses categories as namespaces for Pinecone bulk import
This article focuses on the first part of that workflow: generating the source data and preparing it for embedding and import. The full, runnable scripts live in the companion notebook; the snippets below are the parts worth discussing in line.
Why Test Data Generation Matters
One of the first things I ran into when I started working with Pinecone was the need to create different datasets for different types of tests. Not all test data is the same, and not all dataset sizes are the same.
It is easy to say that a platform works "at scale," but that phrase means different things depending on the workload. A test with 500,000 vectors isn't the same as one with 15 million — or 100 million, 500 million, or a billion. Each size introduces its own set of problems around ingestion, storage, latency, filtering, cost, and operational workflow.
There are some great sources of raw data out there, including Hugging Face datasets and Kaggle datasets. Those are a good starting point, but they are only part of the problem. For vector search testing, I still need to generate vectors. That leads to an important question:
Do the vectors need to be real?
Sometimes no. If I am only testing write throughput, storage behavior, or query latency, random vectors are good enough — I've had use cases where speed was the only thing under evaluation and the meaning of the vector did not matter. But for most tests, recall and accuracy do matter, and that means the embeddings have to represent the actual text. Random vectors will not help there.
So for this project, I wanted to generate a realistic dataset from real text, create real embeddings, and store everything in a format that could be imported into Pinecone efficiently.
Requirements for the Dataset
For this first utility, I wanted a reference dataset that I could regenerate as needed. I also wanted the data to be familiar enough that I could reason about the results when testing search behavior.
My requirements were pretty straightforward:
- Use a real text dataset with broad coverage
- Use Python, partly for more hands-on practice with it
- Store the output in Parquet
- Make the output compatible with Pinecone bulk import
- Support local embedding generation
- Make it possible to split the embedding work across multiple machines later
I landed on the stanford-oval/ccnews dataset from Hugging Face. It gives me a broad set of news articles, and it includes useful metadata fields like title, author, source URL, published date, and category information.
For the Pinecone import format, the structure is simple:
- id\verb|id|
- values\verb|values|
- metadata\verb|metadata|
The id\verb|id|
field is the unique vector ID. I generally prefer structured IDs because they make debugging and operational tasks easier later. In this example, UUIDs are used, but this could easily be adjusted to include prefixes, document IDs, chunk numbers, or other identifiers.
The values field contains the dense vector.
The metadata field contains the JSON metadata associated with the vector. One thing to watch out for when writing Parquet for bulk import is that the metadata field should be written as a JSON string, not as a nested JSON object. That is a small detail, but it matters when you are preparing files for import.
Choosing an Embedding Model
For this example, I used BAAI/bge-large-en-v1.5, accessed through the bge-large\verb|bge-large| registry name in mlx_embedding_models\verb|mlx_embedding_models|.
This model produces 1024-dimensional dense vectors. I could have used Pinecone-hosted embedding models, but for this project I wanted to experiment with local embedding generation. I also wanted to simulate a workflow I see frequently in the field, where customers generate embeddings themselves and then load the resulting vectors into Pinecone.
There are tradeoffs here. Higher-dimensional vectors can improve retrieval quality in some workloads, but they also increase storage size and can affect speed and cost. Pinecone can support many different embedding models, so the right choice depends on what you are testing. If I were testing multimodal search, for example, I might use a model like CLIP instead.
The model isn't a fixed choice here; what matters is that the workflow stays flexible enough to swap models later.
First Version: Stream, Embed, and Upsert Directly
The first version of the script is the easiest one to understand. It streams the CC News dataset from Hugging Face, chunks the article text, generates embeddings locally, and upserts the vectors directly into a Pinecone index.
This is useful for smaller tests because there are fewer moving parts. The script does everything in one flow: create the index if it does not exist, stream records, skip unusable or non-English records, chunk the text, generate embeddings, attach metadata, and upsert. The full script is in the notebook (section 1). A few details are worth calling out.
First, the script checks the metadata size. Pinecone enforces a 40 KB limit on the metadata payload per vector, and since I am storing the original text in metadata for these tests, a long article can blow past it. The chunker trims iteratively until the serialized metadata fits:
while sys.getsizeof(json.dumps({"text": chunk})) >= MAX_JSON_BYTES and len(chunk) > 1000:
chunk = chunk[: int(len(chunk) * 0.9)]Second, the script drops null metadata values. Pinecone rejects null values, and the source dataset has missing fields, so I filter them out before upserting:
batch_metadata.append({k: v for k, v in meta.items() if v is not None})Third, the script batches upserts. Sending one vector at a time is not a great pattern for performance, especially once the dataset gets larger, so vectors accumulate until they reach BATCH_SIZE\verb|BATCH_SIZE| and flush together.
For a small test, this direct approach works well. But it starts to break down once the dataset gets bigger.
Why I Split the Workflow
The direct upsert script is simple, but it is not the approach I would use for tens or hundreds of millions of vectors.
The reason is time. When rows, chunking, embedding, and upserting all run in one process, the embedding step becomes the bottleneck. That's fine for a few thousand vectors, but at millions it becomes a problem, and at hundreds of millions, doing everything inline could take an unreasonable amount of time.
So I separated the work into two stages: the first writes the text records to Parquet, and the second reads those files, generates embeddings, and writes new Parquet files with the values column populated.
That gives me more flexibility. I can generate the source files once, then spread the embedding work across multiple machines. I do not have to worry as much about state tracking because each machine can process a separate set of Parquet files.
In my case, I had several machines available, including Apple M-Series machines and ASUS ROG laptops with NVIDIA GPUs. But the same idea works whether you run locally, use cloud GPU instances, or use a service like RunPod.
Second Version: Generate Parquet Without Embeddings
The second script streams the same CC News dataset, chunks the article text, and writes Parquet files. The difference is that it does not generate embeddings yet. The values column is written as an empty list, and that placeholder gets filled in later by the embedding script.
I used PyArrow instead of Pandas here because I wanted the Parquet schema to be explicit. In particular, the values field has to be written as LIST<FLOAT>. If Pandas infers that column as a generic object type, that breaks the Pinecone bulk import later:
PARQUET_SCHEMA = pa.schema([
pa.field("id", pa.string()),
pa.field("values", pa.list_(pa.float32())), # empty for now; filled offline
pa.field("metadata", pa.string()), # JSON string, nullable per spec
])Each record is written with the placeholder in place, and the metadata is already a JSON string so it matches the import format:
buffer.append({
"id": str(uuid.uuid4()),
"values": [], # placeholder — filled by the offline embedding step
"metadata": metadata_str, # Pinecone parquet import expects a JSON string
})The full script is in the notebook (section 2). It is intentionally boring, which is a good thing — it does one job: turn raw dataset records into chunked, metadata-rich Parquet files.
The partition size is configurable. I usually keep partitions around 10,000 to 20,000 records because that fits the way I tend to work with data files. You can go larger, and in some cases much larger, but smaller partitions make it easier to split work across machines and rerun failed chunks if something goes wrong.
For this stage, I do not need a Pinecone API key, index name, or embedding dimension. The script is only preparing files.
Third Version: Generate Embeddings From Parquet
Once I have the text-only Parquet files, I can run a separate embedding script. It reads each Parquet file, pulls the text out of the metadata, generates embeddings, and writes a new Parquet file with the values column populated. That output is ready for Pinecone bulk import.
The script detects the device at runtime, so the same file runs across different machines without a separate version for each environment. On Apple Silicon it uses mlx_embedding_models to run on the Mac GPU or Neural Engine; on NVIDIA it falls back to sentence-transformers on CUDA; otherwise it falls back to CPU:
if torch.backends.mps.is_available():
DEVICE = "mps" # Apple Silicon — MLX on the Mac GPU / Neural Engine
elif torch.cuda.is_available():
DEVICE = "cuda" # sentence-transformers on CUDA
else:
DEVICE = "cpu" # CPU fallbackThe full script is in the notebook (section 3). The main thing I need to tune is batch size. On CUDA and CPU paths, batch size can make a big difference, and the right value depends on available memory and the model being used.
Bulk Import Into Pinecone
After the embedding script finishes, I have Parquet files with IDs, dense vectors, and metadata — which is the whole reason for structuring the output this way. The next step is to copy them to object storage such as Amazon S3 and run Pinecone bulk import. For small tests, direct upsert is fine; for larger ones, bulk import is faster, more operationally convenient, and better suited to large datasets.
The overall workflow looks like this:
This gives me a repeatable process for different test sizes, models, and hardware setups.
Lessons Learned
A few practical lessons came out of this work.
First, keep the workflow modular. The direct upsert version is helpful for learning and smaller tests, but the separated Parquet workflow is much better once scale starts to matter.
Second, be clear about what you are testing. If you are only testing latency or write throughput, random vectors may be enough. If you are testing recall or answer quality, you need real embeddings created from real content.
Third, pay attention to metadata early. Metadata is easy to treat as an afterthought, but it affects filtering, debugging, import format, and storage limits. In this example, I stored the original text in metadata because it made later testing easier, but that also meant I had to watch metadata size.
Fourth, use explicit schemas when writing Parquet. It is much better to be precise up front than to find out later that a column was inferred incorrectly.
Finally, hardware matters. Local embedding generation can be surprisingly practical if you have the right hardware. Apple Silicon, NVIDIA GPUs, cloud GPU instances, and CPU fallback can all fit into the same workflow, but they will not perform the same.
Wrapping Up
Test data generation for vector search is not just about creating rows. It means thinking about the source text, the embedding model, the vector dimensions, the metadata, the file format, and the import path — and structuring the workflow so the embedding step can scale across machines instead of bottlenecking a single process.
In the next article, I'll go deeper into the embedding generation side of the workflow, including how I split the work across multiple machines and how Apple Silicon compared with NVIDIA GPUs for this type of task.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み