全文検索:アーキテクチャと設計
Pinecone は Lucene 構文、BM25 スコアリング、多言語トークナイゼーションなどを備えた本格的な全文検索機能をベクトルデータベースに統合し、エージェントの複雑なタスク実行能力を強化した。
キーポイント
高度な全文検索機能の追加
Lucene 構文クエリ、多フィールドスキーマ、BM25 スコアリング、18 か国語のトークナイゼーション、ステミング、句一致検索などの機能を Pinecone に実装した。
エージェントによる複雑なデータ取得の実現
エージェントが顧客サポート記録から特定の感情(例:待ち時間への不満)を検索し、特定キーワードを除外したり地理的・時間的条件を適用したりする複合クエリを実行可能にした。
セマンティック検索と全文検索の統合
ベクトル検索(意味的)、テキスト検索(lexical)、メタデータフィルタリングを単一の API 内で連携させ、開発者が複雑なクエリを容易に記述できる環境を提供する。
スプースインデックスの進化
2023 年からサポートされている最適化されたスプースインデックス(トークン ID と重みのリスト)を基盤とし、非標準的なトークナイザーや独自モデルの導入も可能にする柔軟性を維持している。
エージェントによる複雑なクエリの実現
Agents はテキスト検索、ベクトル検索、メタデータフィルタを単一 API で統合して実行できるため、待機時間などの特定の感情を検出しながら除外条件や地理的制約を組み合わせた高度なタスクを完遂できます。
Tantivy 採用の理由と利点
Lucene 構文の親和性、18 カ国語の標準サポート、RAG ワークロードに最適化された性能、および Rust によるパフォーマンスにより、開発者が独自にトークナイザーや重み付けロジックを実装する手間を省いています。
新しいテキストマッチフィルタ演算子
$match_phrase(完全一致)、$match_all(全単語任意順序)、$match_any(いずれかの単語)という 3 つの演算子が追加され、既存のメタデータフィルタと組み合わせて柔軟な検索条件を構築できます。
影響分析・編集コメントを表示
影響分析
この発表は、ベクトルデータベースが従来のキーワード検索や意味的検索だけでなく、複雑な自然言語処理タスクを単一プラットフォームで完結させる重要な転換点となったことを示しています。特に AI エージェントの分野において、高精度なデータ取得と論理的なフィルタリングを可能にする基盤技術として、RAG(Retrieval-Augmented Generation)システムの信頼性と実用性を大幅に高める可能性があります。
編集コメント
ベクトルデータベースの進化において、検索精度と柔軟性を高めるための「全文検索機能」の実装は、AI エージェントが現実世界の複雑なタスクをこなす上で不可欠なステップです。特に、意味的検索とキーワード検索のハイブリッド化により、RAG システムの信頼性が一段階向上するでしょう。
Pinecone は現在、広範な機能セットを備えた全文検索をサポートしています。これには、Lucene 構文による完全なクエリ、マルチフィールドスキーマ、BM25 スコアリング、18 か国語でのトークン化、ステミングおよびストップワードの除去、フレーズマッチング検索、ベクトル検索のためのテキスト一致に基づく新しいフィルターなどが含まれます。
クエリ:Lucene 構文を使用したタイトル/本文全体でのキーワード検索(BM25 ランキング)
index = pc.preview.index(name="transcripts")
response = index.documents.search(
namespace="animal-kingdom",
score_by=[
{
"type": "query_string",
"query": 'body:("nice marmot")'
}
],
top_k=10,
include_fields=["title"],
)
for match in response.matches:
print(match._id, match._score)
なぜこれが重要なのか:エージェント
エージェントはタスクの完了、コードの記述、質問への回答を必要とします。必要なデータを正確に取得するためには、異なる種類のフィルターとスコアリングを組み合わせて行う必要があります。テキストはその大きな部分を占めています。
例えば、エージェントは顧客サポートの通訳記録を探索して新たな洞察を得るかもしれません。待ち時間に対する不満を示すユーザーとのやり取りを探しますが、「wait」や「waiting」という単語を含むやり取りは既にフラグが立てられており、それらを除外したいと考えています。また、特定の地理的範囲と時間帯のみを対象とする必要があります。このような複雑なクエリを構成する点ではコーディングエージェントは非常に優れていますが、必要なのは、内部的にセマンティック(ベクトル)、テキスト、メタデータフィルタリングを単一の API で連携させて実行できるエンジンです。詳細な API の例については以下をご覧ください。
Pinecone の検索機能の強化について
2023 年以来、Pinecone は最適化されたスパースインデックスを通じてテキスト検索をサポートしています。内部では、スパースインデックスは文書をトークン ID と重みのリストとしてエンコードします。ID は通常トークナイザーから取得され、重みは BM25 などのヒューリスティックに基づきます。この機能を一般メカニズムとして公開したことで、開発者は非標準のトークナイザーを差し込むこと、独自の語彙モデルを訓練すること、あるいは単純なトークン重みのヒューリスティックに代えて Pinecone のスパースモデル を使用することが可能になりました。
つまり、スパースインデックスを使えばより多くのことが可能になりますが、その分、ユーザー側でより多くの作業を行う必要があります!
ユーザー(およびエージェント)は、トークン化、重み付け、単語数の管理、ランキングのヒューリスティックを自分自身で行う必要がありました。彼らが求めているのは、データサイエンスプロジェクトというよりは、むしろ検索テキストボックスのような感覚です。フルテキスト検索がまさにそれを実現します。
Under the hood: Tantivy
私たちは、ベクトルやメタデータに加えて高度なテキスト機能のために Tantivy ライブラリを統合しました。自社開発も検討しましたが、以下の 4 つの理由から見送ることにしました。これらは、この製品を利用する開発者にとって最も重要と思われる順に並べています。
- 親しみやすい Lucene 構文。検索の専門家やエージェントはすでにこれを熟知しています。コーディングエージェントに対して Pinecone クエリにテキスト検索句を追加させる際、通常は最初の試行で正しく実行されます。
- 18 の言語をそのままサポート。以下の言語に対する成熟したコミュニティ維持型のトークン化と語幹抽出が利用可能です。ゼロからこれを実現するのは難しく、かつ永遠に完了しない作業です。
- RAG(Retrieval-Augmented Generation)向けのクエリに最適化されています。私たちのテストでは、Tantivy は長いクエリや大きな top_k 値において良好なパフォーマンスを発揮することが示されました。これは RAG やエージェントのワークロードを支配する領域であり、Lucene が最も最適化されていない領域でもあります。
- Rust とスラブ。Tantivy は Rust ライブラリ(私たちも同様)であり、Tantivy のセグメントは Pinecone スラブに明確に対応します。
Advanced text search features
Query syntax
Pinecone は Tantivy のクエリパーサーを通じて Lucene 構文をサポートしています。最も一般的な演算子は以下の通りです。完全なリストについては ドキュメント を参照してください。
Feature Syntax Included docs Scoring
Single term fox Docs containing the term BM25 for the term
フレーズ "quick brown" 完全な隣接フレーズを含むドキュメント
BM25 をフレーズを単一の用語として適用
ブール AND quick AND dog すべての節に一致するドキュメント
各節の BM25 スコアの合計
ブール OR fox OR dog 少なくとも一つの節に一致するドキュメント
一致した節からの BM25 スコアの合計
ブール NOT science NOT algorithms 正の節には一致し、否定された節には一致しないドキュメント
正の節に対する BM25; 否定された節は何も寄与しない
フレーズスロップ "quick fox"~2 用語が互いに約 N 位置以内で出現するドキュメント
フレーズに対する BM25; ウィンドウ内のすべての出現は TF に等しくカウントされる
項ブースト fox^3 ブーストなしの節と同じ
節の BM25 をブースト係数で乗算
フィールドスコーピングはクエリ自体に記述されます:body:("nice marmot") または title:(...) OR body:(...)。他のランクタイプやより詳細なクエリ構文を含む完全なリクエストとレスポンスの参照については、パブリックプレビュードキュメント をご覧ください。ドキュメント検索がパブリックプレビュー期間中である間、すべてのコントロールプレーンおよびデータプレーンの呼び出しには、ヘッダー X-Pinecone-Api-Version: 2026-01.alpha が必要です。
テキストマッチ:新しいフィルタ演算子
Pinecone のメタデータフィルタ言語はすでに豊富です — $eq / $ne、$gt / $gte / $lt / $lte、$in / $nin、$exists、そして論理結合子の $and / $or / $not があり、エンジンはこの構文をベクトル検索と並行して効率的に評価します。全文検索では、テキスト検索可能なフィールドに対して動作する新しいフィルタ演算子が 3 つ追加されます:$match_phrase、$match_all、および $match_any です。それぞれは、インデックス作成時にフィールドがトークン化されたのと同じ方法でオペランドをトークン化し、フィールドが一致するかどうかに基づいてレコードを含めたり除外したりします。
各テキスト述語はフィルタ節です:レコードを保持する場合に true を返し、除外する場合に false を返します。スコアは score_by から得られ、フィルタからは得られません。
filter = {"body": {$match_phrase: "machine learning"}} # 完全な隣接フレーズ
filter = {"body": {$match_all: "machine learning"}} # すべての用語、任意の順序
filter = {"body": {$match_any: "ml ai neural"}} # いずれかの用語
これらは既存のメタデータ演算子と $and / $or / $not を介して組み合わせられるため、単一の filter ブロック内でテキストマッチとメタデータの述語を混合することができます。次のセクションでは、これをベクトルランキングと組み合わせて説明します。
ベクトルランキングとテキストマッチフィルターの組み合わせ
ドキュメントはフィルターを通過するか、しないかのどちらかです。通過した場合、そのスコアは完全に score_by によって決定されます。同じ filter ブロックは、すべてのランク付け器(dense_vector、sparse_vector、text(単一フィールド上の BM25)、または query_string(Lucen クエリ構文による複数フィールドの BM25))と併用可能です。
導入部で取り上げたエージェントの例——「待ち時間に関する新たな不満信号を検索するが、すでに wait や waiting に言及しているレコードは除外し、EU 顧客に限定し、最近のレコードのみを対象とする」——という要件も、単一の検索クエリとして実現できます。
ハイブリッド検索における BYO-vector(Bring Your Own Vector)機能はパブリックプレビュー版です。エンベディング(またはスパースベクトル)を自分で計算し、それを score_by にテキストマッチフィルターと共に渡します。現在、ドキュメント形状のインデックスには semantic_text や統合推論フィールドは存在しません。
from pinecone import Pinecone
pc = Pinecone(api_key="YOUR_API_KEY")
Index schema (set when the index was created):
from pinecone.preview import SchemaBuilder
schema = (
SchemaBuilder()
.add_dense_vector_field("embedding", dimension=1536, metric="cosine")
.add_string_field("body", full_text_search={"language": "en"})
.add_string_field("geo", filterable=True)
.add_float_field("timestamp", filterable=True) # epoch seconds
.build()
)
1. 自然言語による検索意図を埋め込む。
query_embedding = compute_embedding("your wait times are too long")
2. 単一の検索を実行:密ベクトルが生存した文書をランク付けする。
index = pc.preview.index(name="support-conversations")
response = index.documents.search(
namespace="__default__",
filter={
"$and": [
{"$not": {"body": {"$match_any": "wait waiting"}}},
{"geo": {"$eq": "EU"}},
{"timestamp": {"$gte": 1767225600}} # 2026-01-01T00:00:00Z
]
},
score_by=[{
"type": "dense_vector",
"field": "embedding",
"values": query_embedding,
}],
top_k=10,
include_fields=["body", "geo", "timestamp"],
)
$match_any\verb|$match_any| は、その演算子をインデックス化されたフィールドと同じ方法でトークン化する。つまり、「wait\verb|wait| waiting\verb|waiting|」というフィルタは、body\verb|body| フィールド内にこれらのトークンのいずれかを含む文書に一致する。
トークナイザー・パイプライン
ステミング(語幹抽出)とストップワードのフィルタリングは、各テキストフィールドごとにオプトイン式であり、デフォルトでは両方ともオフになっている。これらは、インデックス作成時にフィールドの full_text_search\verb|full_text_search| ブロックで設定する必要がある(例:full_text_search={"language": "en", "stemming": True, "stop_words": True}\verb|full_text_search={"language": "en", "stemming": True, "stop_words": True}|)。指定しない場合、サーバーのデフォルトが適用され、フィールドはトークン化されて小文字化されるのみとなる。
ステミングは 18 の言語で利用可能です:アラビア語、デンマーク語、ドイツ語、ギリシャ語、英語、スペイン語、フィンランド語、フランス語、ハンガリー語、イタリア語、オランダ語、ノルウェー語、ポルトガル語、ルーマニア語、ロシア語、スウェーデン語、タミル語、トルコ語。
ストップワードフィルタリングは、英語、デンマーク語、ドイツ語、フィンランド語、フランス語、ハンガリー語、イタリア語、オランダ語、ノルウェー語、ポルトガル語、ロシア語、スウェーデン語で利用可能です。アラビア語、ギリシャ語、ルーマニア語、タミル語、トルコ語にはストップワードのサポートはありません。
両方のフィルタが有効になっている場合、インデックス作成時およびクエリ実行時に各フィールドに適用される分析チェーンは以下の通りです:
- SimpleTokenizer → RemoveLongFilter(max_term_len) → LowerCaser → StopWordFilter (stop_words が有効な場合) → Stemmer (ステミングが有効な場合)
BM25 スコアリング
Pinecone は、テキストランキングのために一般的な BM25 アルゴリズムを使用しています。各(クエリ,ドキュメント)ペアに対して、スコアは両者に共通する用語の出現頻度 (TF: Term Frequency) と逆文書頻度 (IDF: Inverse Document Frequency) から計算されます。TF はドキュメント内の用語の単純なカウントです。標準的な IDF は検索対象のコパス全体に適用されるグローバル値であり、以下の式で与えられます。
IDF(t)=log (1+(N−n+0.5)/(n+0.5))
ここで、N はドキュメントの総数、n は用語 t を含むドキュメント数を表します。
理想的には、これらのドキュメントレベルの統計情報を保存し、クエリに応答する際に IDF 値を単に取得できるようにしたいと考えています。以下では、スラブベースアーキテクチャにおいて、成長・動的なデータセットでこれをどのように実現するかを説明します。
デザインの選択
ドキュメント順序
テキスト検索においてドキュメント順序は非常に重要です。これは投稿リストの圧縮方法、インデックスをたどるクエリの動作、および可能な最適化を制御します。一般的な選択肢には、挿入時刻、クラスタまたはトピック、権威性や人気度などがあり、それぞれが異なる目標(挿入スループット、インデックスサイズ、クエリレイテンシ)の最適化に焦点を当てています。
スラブは不変バッチからオフラインで構築されるため、任意の順序を選択する自由があります。ストリーミング挿入の制約を維持する必要がないため、書き込み時の再順序付けコストは低くなります。
レコードごとにベクトルとテキストの両方が存在する場合、空間におけるベクトルの幾何学的位置を活用してテキストレコードの順序を決定します。ベクトルは高速なベクトル検索のために IVF クラスタでグループ化されるため、近接するベクトルは意味的に類似したレコードに属します。これにより、ベクトル検索、メタデータフィルタリング、およびテキスト検索が同じ順序を共有し、ある段階からのスコアや述語が変換なしで次の段階に引き継がれます。
この順序付けも投稿リストを効果的に圧縮します。類似したドキュメントが近接する ID に割り当てられると、各投稿リスト内の連続するドキュメント ID 間のギャップが狭まり、差分符号化されたリストのサイズが小さくなります。この効果は情報検索の文献においてよく確立されています [1, 2, 3, 4]。私たちが採用する IVF ベースの順序付けは、これらの論文が推奨するクラスタリングの安価な近似です。ベクトル検索のためにすでに必要としている順序付けを行うことで、圧縮効果という副次的な利益を得ることができます。
成長・動的データにおける BM25 の統計情報
正確な BM25 計算には、インデックス全体にわたるグローバルな用語統計が必要です。しかし、データセットは成長し変化しており、Pinecone の スラブベースアーキテクチャ は、データをスラブ間で分割し、クエリ実行時にスラブレベルの結果をマージすることでクエリに応答します。スラブ間での一貫したスコアリングを実現するために、クエリに含まれる用語のスラブレベルの用語統計を単一の統計オブジェクトにマージし、そのオブジェクトを各スラブへ渡してスコアリングを行います。このマージ処理は高速です。なぜなら、対象となるのはクエリ用語の統計のみだからです。
インデックスのすべてのスラブが単一のクエリ実行エンジンに存在する場合、マージされた統計情報はインデックス全体をカバーし、スコアは正規化されたグローバル BM25 ランキングと一致します。複数の実行エンジンにまたがるスラブを持つインデックスの場合、マシン間での高コストな散乱・集約(scatter-gather)を回避するため、統計情報のマージは実行エンジンレベルのみで行います。その結果得られるスコアは単一のグローバルコーパスに対して近似値となりますが、BM25 は用語頻度に対して対数的であるため、この摂動によってランキングが大幅に変わることはほとんどありません。分散検索におけるローカル統計情報とグローバル統計情報のトレードオフについては 1990 年代半ばから研究が進められており [5]、シャードごとの統計情報は実運用中の検索エンジンでは標準的な慣行となっています [6]。意味のある歪みは非常に小さなサンプルの場合にのみ現れます。
例:ベンチマーク
データセット。 Wikipedia(英語、20231101 ダンプ)、各記事はタイトル(平均約 3 語)と本文(平均約 470 語)の 2 フィールドレコードに縮小されています。5 つのコーパスサイズ(1k, 10k, 100k, 1M、および完全な 6.4M 記事)に対して同じクエリスイートを実行しました。
クエリ。
- BM25 セット:20 のカテゴリにわたる 5,000 件の Lucene 構文クエリ(単一用語、フレーズ、スロップ、用語ブースト、クロスフィールド AND/OR、4〜8 クラージスを組み合わせた複雑なブール演算)。これは RAG やエージェントワークロードが生成する長くて多クラージスのクエリを模倣するように生成されたものです。
- フィルター付きセット:500 件の BM25 クエリそれぞれにテキスト一致フィルター(タイトルおよび/または本文に対する $match_phrase / $match_all / $match_any、一部はメタデータ述語と組み合わされている)をペアにしたもの。フィルターの選択性は約 1 ドキュメントから約 60% のコーパスまで幅広く、曲線の非常に選択的な側面と非常に寛容な側面の両方を検証します。
セットアップ。 コーパスサイズごとに Pinecone インデックスを 1 つずつ作成し、b1 デッドicated ノードタイプを使用し、シャード数およびレプリカ数はそれぞれ 1 です。レイテンシはクライアント側で測定されます。クライアントとインデックスは同じリージョン(AWS us-east-1)内に配置されています。Top-k は 100 です。クエリは順次、一度に 1 つずつ送信されます。レイテンシはバッチ処理のスループットではなく、クエリごとのエンドツーエンドのレイテンシを反映しています。
サイズ | BM25 p50 (ms) | BM25 p99 | Filter p50 (ms) | Filter p99
---|---|---|---|---
1k | 6.0 | 9.6 | 6.1 | 9.6
10k | 6.0 | 9.7 | 6.2 | 52.3
100k | 6.6 | 12.2 | 6.6 | 50.3
1M | 9.1 | 28.3 | 9.4 | 60.0
6.4M | 22.7 | 84.0 | 15.8 | 136.0
ベンチマーク手法として(パブリック API の保証ではなく)、正確性を検証するために、すべての (クエリ,ドキュメント) ペアを標準的な(単一マシン、グローバル)BM25 数式でスコアリングし、厳密にランク付けする外部の全探索ベースラインに対して測定を行います。再現率(Recall)は、top-k = 100 における標準的な |返却された結果 ∩ 正確な上位 k| / |正確な上位 k| で定義されます。
ベンチマークコーパス全体において、単純な BM25 およびテキストマッチフィルタ付きの BM25 クエリは、1,000 ドキュメントから数百万ドキュメントに及ぶインデックス上で、平均再現率を 95% を大きく上回る値で達成しています。わずかな差が生じるのは、上位 k 件の境界において多数のドキュメントが同一の BM25 スコアを持つクエリの場合です。このようなケースでは、上位 k 件に含まれるドキュメントの特定は、スラブ間の走査順序に依存し、その順序は任意であり、ブルートフォース走査順序とは異なる可能性があります。
文書検索機能は現在パブリックプレビュー中です。Python SDK では pc.preview モジュールを通じて利用可能ですが、すべてのリクエストにはヘッダー X-Pinecone-Api-Version: 2026-01.alpha を含める必要があります。
pip install --upgrade pinecone
from pinecone import Pinecone
from pinecone.preview import SchemaBuilder
pc = Pinecone(api_key="YOUR_API_KEY")
1. インデックススキーマの定義:2 つの全文検索用文字列フィールド
schema = (
SchemaBuilder()
.add_string_field("title", full_text_search={"language": "en"})
.add_string_field("body", full_text_search={"language": "en"})
.build()
)
2. サーバーレスプレビューインデックスの作成。status.ready になるまで待機してから続行する。
pc.preview.indexes.create(
name="quickstart",
schema=schema,
read_capacity={"mode": "OnDemand"},
)
3. デフォルト名前空間にドキュメントをアップサートする。
index = pc.preview.index(name="quickstart")
index.documents.batch_upsert(
namespace="__default__",
documents=[
{"_id": "1", "title": "The Big Lebowski", "body": "nice marmot"},
{"_id": "2", "title": "Fargo", "body": "wood chipper"},
],
)
4. Lucene クエリ構文を使用して両方のフィールドを横断検索する。
response = index.documents.search(
namespace="__default__",
top_k=10,
score_by=[{"type": "query_string", "query": 'body:("nice marmot")'}],
include_fields=["title"],
)
for match in response.matches:
print(match._id, match._score)
同等の REST コール:
curl -X POST "https://<index-host>/documents/search" \
-H "Api-Key: $PINECONE_API_KEY" \
-H "X-Pinecone-Api-Version: 2026-01.alpha" \
-H "Content-Type: application/json" \
-d '{
"namespace": "__default__",
"top_k": 10,
"score_by": [{"type": "query_string", "query": "body:(\"nice marmot\")"}],
"include_fields": ["title"]
}'
完全なリファレンスについては、ドキュメントを参照してください。
参考文献
- Blandford と Blelloch, 『文書再順序付けによるインデックス圧縮』, DCC 2002.
- Silvestri, 『ドキュメント識別子割り当て問題の整理』, ECIR 2007.
- Yan, Ding, Suel, 『最適化された文書順序に基づく逆インデックス圧縮とクエリ処理』, WWW 2009.
- Dhulipala ら, 『再帰的グラフ二分割によるグラフおよびインデックスの圧縮』, KDD 2016.
- Viles と French, 『分散情報検索システムにおけるコレクション全体情報の伝達』, SIGIR 1995.
- Elastic, 『実用的な BM25 — パート 1: Elasticsearch におけるシャードが関連性スコアリングに与える影響』。
原文を表示
Pinecone now supports full text search with a broad set of features: full Lucene-syntax queries, multi-field schemas, BM25 scoring, tokenization in 18 languages, stemming and stop-word removal, phrase matching search, new filters based on text match for vector search, and more.
# Query: keyword search across title/body using Lucene query syntax (BM25 ranking)
index = pc.preview.index(name="transcripts")
response = index.documents.search(
namespace="animal-kingdom",
score_by=[
{
"type": "query_string",
"query": 'body:("nice marmot")'
}
],
top_k=10,
include_fields=["title"],
)
for match in response.matches:
print(match._id, match._score)Why this matters: agents
Agents need to complete tasks, write code, and answer questions. To retrieve the exact data needed, they have to combine different kinds of filters and scoring. Text is a big part of that.
For example, an agent might search through customer support transcriptions for new insights. It looks for user interactions that indicate frustration with wait times — but it has already flagged interactions that contain the words "wait" or "waiting" and wants to exclude those. And, it needs to consider only a specific geographic and time period. Coding agents are great at composing complex queries like this. What they need is an engine that can execute them — one that internally links semantic (vector), text, and metadata filtering in a single API. See below for a detailed API example.
How this enhances Pinecone's search capabilities
Since 2023, Pinecone has supported text search via optimized sparse indexes. Under the hood, sparse indexes encode documents as a list of token IDs and weights — IDs typically come from a tokenizer, weights from a heuristic like BM25. We exposed this as a general mechanism so developers could plug in non-standard tokenizers, train their own lexical models, or use Pinecone's sparse models instead of naive token weight heuristics.
So, sparse indexes let you do more. But they also *require* you to do more!
Users (and agents) had to deal with tokenization, weighting, maintaining word counts, and ranking heuristics themselves. They needed something that feels more like a search text box and less like a data science project. Full text search is that.
Under the hood: Tantivy
We integrated the Tantivy library for advanced text functionality alongside vectors and metadata. We considered building it in-house and decided against it, for four reasons — ordered by what we think matters most to a developer using this:
- Familiar Lucene syntax. Most search experts already know it, and agents know it too. When we ask a coding agent to add a text search clause to a Pinecone query, it usually gets it right on the first try.
- 18 languages out of the box. Mature, community-maintained tokenization and stemming for the languages listed below. Getting this right from scratch is hard and never finishes.
- Tuned for RAG-shaped queries. Our testing showed Tantivy performs well on long queries and large top_k\verb|top_k| values — the regimes that dominate RAG and agent workloads, and the regimes Lucene is least optimized for.
- Rust and slabs. Tantivy is a Rust library (so are we), and Tantivy segments map cleanly onto Pinecone slabs.
Advanced text search features
Query syntax
Pinecone supports Lucene query syntax via Tantivy's query parser. The most common operators are below; see the docs for the full list.
Feature Syntax Included docs Scoring
Single term fox Docs containing the term BM25 for the term
Phrase "quick brown" Docs containing the exact adjacent phrase BM25 over the phrase as a single term
Boolean AND quick AND dog Docs matching every clause Sum of clause BM25 scores
Boolean OR fox OR dog Docs matching at least one clause Sum of BM25 scores from clauses that matched
Boolean NOT science NOT algorithms Docs matching the positive clause and not the negated one BM25 for the positive clause; the negated clause contributes nothing
Phrase slop "quick fox"~2 Docs where the terms appear within ~N positions of each other BM25 over the phrase; every in-window occurrence counts equally toward TF
Term boost fox^3 Same as the unboosted clause Clause BM25 multiplied by the boost factor
Field scoping is written into the query itself: body:("nice marmot")\verb|body:("nice marmot")| or title:(...) OR body:(...)\verb|title:(...) OR body:(...)| . For the full request and response reference, including other ranker types and more query syntax details, see the public preview docs. All control- and data-plane calls require the header X-Pinecone-Api-Version: 2026-01.alpha\verb|X-Pinecone-Api-Version: 2026-01.alpha| while document search is in public preview.
Text match: new filter operations
Pinecone's metadata filter language is already rich — $eq\verb|$eq| / $ne\verb|$ne|, $gt\verb|$gt| / $gte\verb|$gte| / $lt\verb|$lt| / $lte\verb|$lte|, $in\verb|$in| / $nin\verb|$nin|, $exists\verb|$exists|, and the boolean combinators $and\verb|$and| / $or\verb|$or| / $not\verb|$not| — and the engine evaluates it efficiently alongside vector search. Full text search adds three new filter operators that work against any text-searchable field: $match_phrase\verb|$match_phrase|, $match_all\verb|$match_all|, and $match_any\verb|$match_any|. Each one tokenizes the operand the same way the field is tokenized at index time, then includes or excludes a record based on whether the field matches.
# Each text predicate is a filter clause: it returns true (keep the record)
# or false (drop it). Score comes from score_by, never from the filter.
filter = {"body": {"$match_phrase": "machine learning"}} # exact adjacent phrase
filter = {"body": {"$match_all": "machine learning"}} # all terms, any order
filter = {"body": {"$match_any": "ml ai neural"}} # any termThese compose with the existing metadata operators via $and\verb|$and|, $or\verb|$or|, and $not\verb|$not|, so a single filter\verb|filter| block can mix text-match and metadata predicates. The next section pairs that with vector ranking.
Combining vector ranking with a text-match filter
A document either passes the filter or it doesn't; if it passes, its score comes entirely from score_by\verb|score_by|. The same filter\verb|filter| block works with every ranker — dense_vector\verb|dense_vector|, sparse_vector\verb|sparse_vector|, text\verb|text| (BM25 on a single field), or query_string\verb|query_string| (multi-field BM25 with Lucene query syntax).
Returning to the agent example from the intro — "find new frustration signals about wait times, but exclude records that already mention wait\verb|wait| or waiting\verb|waiting|, restrict to EU customers, and only look at recent records" — that becomes a single search:
Public preview is BYO-vector for hybrid: you compute the embedding (or sparse vector) yourself, then pass it in score_by\verb|score_by| alongside any text-match filters. There is no semantic_text\verb|semantic_text| / integrated-inference field on document-shaped indexes today.
from pinecone import Pinecone
pc = Pinecone(api_key="YOUR_API_KEY")
# Index schema (set when the index was created):
# from pinecone.preview import SchemaBuilder
# schema = (
# SchemaBuilder()
# .add_dense_vector_field("embedding", dimension=1536, metric="cosine")
# .add_string_field("body", full_text_search={"language": "en"})
# .add_string_field("geo", filterable=True)
# .add_float_field("timestamp", filterable=True) # epoch seconds
# .build()
# )
# 1. Embed the natural-language search intent.
query_embedding = compute_embedding("your wait times are too long")
# 2. Run a single search: the dense vector ranks the surviving documents.
index = pc.preview.index(name="support-conversations")
response = index.documents.search(
namespace="__default__",
filter={
"$and": [
{"$not": {"body": {"$match_any": "wait waiting"}}},
{"geo": {"$eq": "EU"}},
{"timestamp": {"$gte": 1767225600}} # 2026-01-01T00:00:00Z
]
},
score_by=[{
"type": "dense_vector",
"field": "embedding",
"values": query_embedding,
}],
top_k=10,
include_fields=["body", "geo", "timestamp"],
)$match_any\verb|$match_any| tokenizes its operand the same way the indexed field was tokenized, so the "wait\verb|wait| waiting\verb|waiting|" filter will match any document containing any of these tokens in the body\verb|body| field.
Tokenizer pipeline
Stemming and stop-word filtering are opt-in per text field — both off by default. Configure them on the field's full_text_search\verb|full_text_search| block at index creation (for example, full_text_search={"language": "en", "stemming": True, "stop_words": True}\verb|full_text_search={"language": "en", "stemming": True, "stop_words": True}|). When omitted, server defaults apply and the field is tokenized and lowercased only.
Stemming is available for 18 languages: Arabic, Danish, German, Greek, English, Spanish, Finnish, French, Hungarian, Italian, Dutch, Norwegian, Portuguese, Romanian, Russian, Swedish, Tamil, and Turkish.
Stop-word filtering is available for English, Danish, German, Finnish, French, Hungarian, Italian, Dutch, Norwegian, Portuguese, Russian, and Swedish. There is no stop word support for Arabic, Greek, Romanian, Tamil, and Turkish
The analysis chain applied to each field at index and query time, when both filters are enabled, is:
- SimpleTokenizer → RemoveLongFilter(max_term_len) → LowerCaser → StopWordFilter (when stop_words is enabled) → Stemmer (when stemming is enabled)
BM25 scoring
Pinecone uses the common BM25 algorithm for text ranking. For each (query, document) pair, the score is computed from the term frequencies (TF) and inverse document frequencies (IDF) of the terms common to both. TFs are simple counts of the terms in the document. The canonical IDF is global to the searched corpus, given by
IDF(t)=log (1+N−n+0.5n+0.5)\text{IDF}(t) = \log\!\left(1 + \frac{N - n + 0.5}{n + 0.5}\right)
where NN is the total number of documents and nn is the number of documents containing term tt.
Ideally, we want to store these document-level statistics so that when we answer queries we simply retrieve the IDF values. We describe below how we make this work with a growing and dynamic dataset in our slab-based architecture.
Design choices
Document ordering
Document order matters a lot in text search. It controls how posting lists compress, how queries traverse the index, and which optimizations are possible. Common choices include time of insertion, cluster or topic, and authority or popularity — each optimizing for a different goal: insertion throughput, index size, or query latency.
Slabs are built offline from immutable batches, so we are free to pick any order. There is no streaming-insertion constraint to preserve, so the reordering costs are low at write time.
When both vectors and text exist per record, we take advantage of the geometric location of vectors in space to inform the ordering of text records. Vectors are grouped by IVF clusters for fast vector search, so nearby vectors belong to semantically similar records. Vector search, metadata filtering, and text search then share the same ordering — scores and predicates from one stage feed the next without translation.
This ordering also compresses posting lists well. When similar documents land on nearby IDs, the gaps between consecutive document IDs in each posting list shrink, and delta-encoded lists become smaller. The effect is well established in the information retrieval literature [1, 2, 3, 4]. Our IVF-based ordering is a cheap approximation of the clustering those papers recommend: we get the compression benefit as a side effect of the ordering we already need for vector search.
BM25 statistics for growing and dynamic data
Exact BM25 needs global term statistics across the whole index. The dataset is growing and changing, though, and Pinecone's slab-based architecture serves queries by partitioning data across slabs and merging slab-level results at query time. To score consistently across slabs, we merge slab-level term statistics for the terms in the query into a single statistics object, then pass that object to every slab as it scores. The merge is fast because it only touches statistics for the query terms.
When all slabs of an index reside on a single query executor, the merged statistics cover the entire index — scores match the canonical global-BM25 ranking. For indexes whose slabs span multiple executors, we avoid the costly scatter-gather across machines and merge statistics only at the executor level. The resulting scores are approximate relative to a single global corpus, but BM25 is logarithmic in term frequencies, so the perturbation barely moves rankings. The tradeoff between local and global statistics in distributed retrieval has been studied since the mid-1990s [5], and per-shard statistics are standard practice in production search engines [6]. Meaningful distortions only show up on very small samples.
Example benchmarks
Dataset. Wikipedia (English, 20231101 dump), every article reduced to a two-field record of title (avg ~3 words) and body (avg ~470 words). We ran the same query suite against five corpus sizes — 1k, 10k, 100k, 1M, and the full 6.4M articles.
Queries.
- BM25 set: 5,000 Lucene-syntax queries spanning 20 categories — single terms, phrases, slop, term boosts, cross-field AND/OR, complex booleans combining 4–8 clauses. Generated to mirror the long, multi-clause queries that RAG and agent workloads produce.
- Filtered set: 500 BM25 queries each paired with a text-match filter ($match_phrase / $match_all / $match_any over title and/or body, some combined with metadata predicates). Filter selectivity ranges from ~1 doc to ~60% of the corpus, exercising both the highly-selective and the broadly-permissive ends of the curve.
Setup. A single Pinecone index per corpus size, b1 dedicated node type, 1 shard / 1 replica. Latency is measured client side. Client and index are in the same region (AWS us-east-1). Top-k = 100. Queries are sent sequentially, one at a time. Latency reflects end-to-end per-query latency, not batching throughput.
SizeBM25 p50 (ms)BM25 p99Filter p50 (ms)Filter p99
1k6.09.66.19.6
10k6.09.76.252.3
100k6.612.26.650.3
1M9.128.39.460.0
6.4M22.784.015.8136.0
As a benchmark methodology — not a public-API guarantee — we measure accuracy against an external brute-force baseline that scores every (query, doc) pair with the canonical (single-machine, global) BM25 formula and ranks them exactly. Recall is the standard ∣returned∩exact_top_k∣/∣exact_top_k∣|\verb|returned| ∩ \verb|exact_top_k|| / |\verb|exact_top_k||at top-k = 100.
Across our benchmark corpora, both plain BM25 and BM25-with-text-match-filter queries deliver mean recall well above 95% on indexes from 1k up to millions of documents. The small gap is concentrated in queries where many documents share an identical BM25 score at the top-k boundary. In such cases, the identity of the documents ending in the top-k depends on the scan order between slabs that can be arbitrary, and potentially different than the brute-force scan order.
Document search is in public preview. The Python SDK exposes it under pc.preview, and every request must include the header X-Pinecone-Api-Version: 2026-01.alpha.
pip install --upgrade pineconefrom pinecone import Pinecone
from pinecone.preview import SchemaBuilder
pc = Pinecone(api_key="YOUR_API_KEY")
# 1. Define the index schema: two FTS string fields.
schema = (
SchemaBuilder()
.add_string_field("title", full_text_search={"language": "en"})
.add_string_field("body", full_text_search={"language": "en"})
.build()
)
# 2. Create a serverless preview index. Wait for status.ready before continuing.
pc.preview.indexes.create(
name="quickstart",
schema=schema,
read_capacity={"mode": "OnDemand"},
)
# 3. Upsert documents to the default namespace.
index = pc.preview.index(name="quickstart")
index.documents.batch_upsert(
namespace="__default__",
documents=[
{"_id": "1", "title": "The Big Lebowski", "body": "nice marmot"},
{"_id": "2", "title": "Fargo", "body": "wood chipper"},
],
)
# 4. Search across both fields with Lucene query syntax.
response = index.documents.search(
namespace="__default__",
top_k=10,
score_by=[{"type": "query_string", "query": 'body:("nice marmot")'}],
include_fields=["title"],
)
for match in response.matches:
print(match._id, match._score)The equivalent REST call:
curl -X POST "https://<index-host>/documents/search" \
-H "Api-Key: $PINECONE_API_KEY" \
-H "X-Pinecone-Api-Version: 2026-01.alpha" \
-H "Content-Type: application/json" \
-d '{
"namespace": "__default__",
"top_k": 10,
"score_by": [{"type": "query_string", "query": "body:(\"nice marmot\")"}],
"include_fields": ["title"]
}'For the full reference, see the docs.
References
- Blandford and Blelloch, Index Compression through Document Reordering, DCC 2002.
- Silvestri, Sorting Out the Document Identifier Assignment Problem, ECIR 2007.
- Yan, Ding, and Suel, Inverted Index Compression and Query Processing with Optimized Document Ordering, WWW 2009.
- Dhulipala et al., Compressing Graphs and Indexes with Recursive Graph Bisection, KDD 2016.
- Viles and French, Dissemination of Collection Wide Information in a Distributed Information Retrieval System, SIGIR 1995.
- Elastic, Practical BM25 — Part 1: How Shards Affect Relevance Scoring in Elasticsearch.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み