Skip to content

Conversation

@jhamon
Copy link
Collaborator

@jhamon jhamon commented Nov 5, 2025

Expose LSN Header Information in API Responses

Overview

This PR implements exposure of LSN (Log Sequence Number) header information from Pinecone API responses through a new _response_info attribute on response objects. This enables faster test suite execution by using LSN-based freshness checks instead of polling describe_index_stats().

Motivation

Integration tests currently rely on polling describe_index_stats() to verify data freshness, which is slow and inefficient. The Pinecone API includes LSN headers in responses that can be used to determine data freshness more efficiently:

  • x-pinecone-request-lsn: Committed LSN from write operations (upsert, delete)
  • x-pinecone-max-indexed-lsn: Reconciled LSN from read operations (query)

By extracting and exposing these headers, tests can use LSN-based polling to reduce test execution time significantly. Testing so far shows this will cut the time needed to run db data plane integration times down by half or more.

Changes

Core Implementation

Response Info Module

  • Created pinecone/utils/response_info.py with:
    • ResponseInfo TypedDict for structured response metadata
    • extract_response_info() function to extract and normalize raw headers
    • Fields: raw_headers (dictionary of all response headers normalized to lowercase)
    • Case-insensitive header matching
    • LSN extraction is handled by test utilities (lsn_utils) rather than in ResponseInfo

REST API Client Integration

  • Updated api_client.py and asyncio_api_client.py to automatically attach _response_info to db data plane response objects
  • Always attaches _response_info to ensure raw_headers are always available, even when LSN fields are not present

gRPC Integration

  • Updated grpc_runner.py to capture initial metadata from gRPC calls
  • Modified all parser functions in grpc/utils.py to accept optional initial_metadata parameter
  • Updated index_grpc.py to pass initial metadata to parser functions
  • Updated future.py to extract initial metadata from gRPC futures

Response Dataclasses

  • Created QueryResponse and UpsertResponse dataclasses in pinecone/db_data/dataclasses/
  • Added _response_info field to FetchResponse, FetchByMetadataResponse, QueryResponse, and UpsertResponse
  • All response dataclasses inherit from DictLike for dictionary-style access
  • _response_info is a required field (always present) with default {"raw_headers": {}}

Index Classes

  • Updated index.py and index_asyncio.py to:
    • Convert OpenAPI responses to dataclasses with _response_info attached
    • Handle async_req=True with ApplyResult wrapper for proper dataclass conversion
    • Extract _response_info from upsert_records() responses

Test Infrastructure

LSN Utilities

  • Created tests/integration/helpers/lsn_utils.py with helper functions for extracting LSN values
  • Created compatibility shim pinecone/utils/lsn_utils.py (deprecated)

Polling Helpers

  • Updated poll_until_lsn_reconciled() to use query operations for LSN-based freshness checks
  • Added poll_until_lsn_reconciled_async() for async tests
  • Falls back to old polling methods when LSN not available

Integration Test Updates

  • Updated multiple integration tests to use LSN-based polling:
    • test_query.py, test_upsert_dense.py, test_search_and_upsert_records.py
    • test_fetch.py, test_fetch_by_metadata.py, test_upsert_hybrid.py
    • test_query_namespaces.py, seed.py
    • Async versions: test_query.py (async)
  • Added assertions to verify _response_info is present when expected

Documentation

  • Created docs/maintainers/lsn-headers-discovery.md documenting discovered headers
  • Created scripts/inspect_lsn_headers.py for header discovery

Usage Examples

Accessing Response Info

The _response_info attribute is always available on all Index response objects:

from pinecone import Pinecone

pc = Pinecone(api_key="your-api-key")
index = pc.Index("my-index")

# Upsert operation - get committed LSN
upsert_response = index.upsert(
    vectors=[("id1", [0.1, 0.2, 0.3]), ("id2", [0.4, 0.5, 0.6])]
)

# Access raw headers (always present, contains all response headers)
raw_headers = upsert_response._response_info.get("raw_headers")
print(f"Raw headers: {raw_headers}")
# Example output: Raw headers: {
#   'x-pinecone-request-lsn': '12345',
#   'x-pinecone-api-version': '2025-10',
#   'content-type': 'application/json',
#   'server': 'envoy',
#   ...
# }

# Extract LSN from raw headers using test utilities (for testing/polling)
from tests.integration.helpers.lsn_utils import extract_lsn_committed
lsn_committed = extract_lsn_committed(raw_headers)
print(f"Committed LSN: {lsn_committed}")
# Example output: Committed LSN: 12345

# Query operation
query_response = index.query(
    vector=[0.1, 0.2, 0.3],
    top_k=10
)

# Access raw headers
raw_headers = query_response._response_info.get("raw_headers")
print(f"Raw headers: {raw_headers}")
# Example output: Raw headers: {
#   'x-pinecone-max-indexed-lsn': '12345',
#   'x-pinecone-api-version': '2025-10',
#   'content-type': 'application/json',
#   ...
# }

# Extract LSN from raw headers using test utilities
from tests.integration.helpers.lsn_utils import extract_lsn_reconciled
lsn_reconciled = extract_lsn_reconciled(raw_headers)
print(f"Reconciled LSN: {lsn_reconciled}")
# Example output: Reconciled LSN: 12345

# Fetch operation - response info always available
fetch_response = index.fetch(ids=["id1", "id2"])
print(f"Response info: {fetch_response._response_info}")
# Example output:
# Response info: {
#   'raw_headers': {
#     'x-pinecone-max-indexed-lsn': '12345',
#     'x-pinecone-api-version': '2025-10',
#     'content-type': 'application/json',
#     ...
#   }
# }

Dictionary-Style Access

All response dataclasses inherit from DictLike, enabling dictionary-style access:

query_response = index.query(vector=[...], top_k=10)

# Attribute access (existing)
matches = query_response.matches

# Dictionary-style access (new)
matches = query_response["matches"]

# Response info access
response_info = query_response._response_info
# Example: {'raw_headers': {'x-pinecone-max-indexed-lsn': '12345', 'x-pinecone-api-version': '2025-10', 'content-type': 'application/json', ...}}

Technical Details

Response Info Flow

  1. REST API:

    • HTTP headers → api_client.py extracts → attaches _response_info to OpenAPI model → Index classes convert to dataclasses
  2. gRPC:

    • Initial metadata → grpc_runner.py captures → parser functions extract → attach _response_info to response objects

Backward Compatibility

  • All existing method signatures remain unchanged
  • _response_info is always present on response objects (required field)
  • raw_headers in _response_info always contains response headers (may be empty dict if no headers)
  • Test utilities (poll_until_lsn_reconciled, poll_until_lsn_reconciled_async) accept _response_info directly and extract LSN internally
  • Response objects maintain all existing attributes and behavior

Type Safety

  • Added proper type hints for _response_info fields
  • Updated return type annotations to reflect dataclass usage
  • Added type: ignore comments where necessary (e.g., ApplyResult wrapping)

Dataclass Enhancements

  • All response dataclasses now inherit from DictLike for dictionary-style access
  • QueryResponse and UpsertResponse are new dataclasses replacing OpenAPI models
  • _response_info field: ResponseInfo = field(default_factory=lambda: cast(ResponseInfo, {"raw_headers": {}}), repr=True, compare=False)
    • Always present (required field)
    • repr=True for all response dataclasses to aid debugging
    • raw_headers always contains response headers (may be empty dict)
    • ResponseInfo only contains raw_headers

Testing

Unit Tests

  • ✅ All gRPC upsert tests pass (32/32)
  • ✅ All unit tests pass (340+ passed)
  • ✅ Created unit tests for extract_response_info() function
  • ✅ Created unit tests for LSN utility functions

Integration Tests

  • ✅ Updated integration tests to use LSN-based polling
  • ✅ 38 integration tests pass
  • ✅ LSN-based polling working correctly (faster test execution)
  • _response_info assertions added to verify LSN data is present

Breaking Changes

None - This is a backward-compatible enhancement.

Response Type Changes

  • QueryResponse and UpsertResponse are now dataclasses instead of OpenAPI models
  • Impact: Minimal - dataclasses are compatible for attribute access and dictionary-style access (via DictLike)
  • Mitigation: Public API exports remain the same (from pinecone import QueryResponse, UpsertResponse)
  • Note: If users were doing isinstance() checks against OpenAPI models, they should still work when importing from pinecone

New Attribute

  • _response_info is added to all Index response objects (QueryResponse, UpsertResponse, FetchResponse, FetchByMetadataResponse)
  • Impact: Minimal - it's a required attribute with underscore prefix (indicates internal use)
  • Mitigation: Underscore prefix indicates it's not part of the public API contract
  • Note: _response_info is always present and contains raw_headers.

Compatibility Notes

  • All response dataclasses inherit from DictLike, enabling dictionary-style access (response['matches'])
  • Attribute access remains unchanged (response.matches, response.namespace, etc.)
  • OpenAPI-specific methods like to_dict() were not part of the public API

Related Issues

  • Enables faster test suite execution through LSN-based polling
  • Provides foundation for future LSN-based features

@jhamon jhamon marked this pull request as ready for review November 6, 2025 16:54
@jhamon jhamon merged commit d8d68bf into release-candidate/2025-10 Nov 6, 2025
22 checks passed
@jhamon jhamon deleted the jhamon/expose-lsn branch November 6, 2025 17:00
jobs:
dependency-matrix-grpc:
name: GRPC py3.9/py3.10
name: GRPC py3.10/py3.10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both versions are 3.10?

jhamon added a commit that referenced this pull request Nov 18, 2025
⚠️ **Python 3.9 is no longer supported.** The SDK now requires Python 3.10 or later. Python 3.9 reached end-of-life on October 2, 2025. Users must upgrade to Python 3.10+ to continue using the SDK.

⚠️ **Namespace parameter default behavior changed.** The SDK no longer applies default values for the `namespace` parameter in GRPC methods. When `namespace=None`, the parameter is omitted from requests, allowing the API to handle namespace defaults appropriately. This change affects `upsert_from_dataframe` methods in GRPC clients. The API is moving toward `"__default__"` as the default namespace value, and this change ensures the SDK doesn't override API defaults.

Note: The official SDK package was renamed last year from `pinecone-client` to `pinecone` beginning in version 5.1.0.  Please remove `pinecone-client` from your project dependencies and add `pinecone` instead to get  the latest updates if upgrading from earlier versions.

You can now configure dedicated read nodes for your serverless indexes, giving you more control over query performance and capacity planning. By default, serverless indexes use OnDemand read capacity, which automatically scales based on demand. With dedicated read capacity, you can allocate specific read nodes with manual scaling control.

**Create an index with dedicated read capacity:**

```python
from pinecone import (
    Pinecone,
    ServerlessSpec,
    CloudProvider,
    AwsRegion,
    Metric
)

pc = Pinecone()

pc.create_index(
    name='my-index',
    dimension=1536,
    metric=Metric.COSINE,
    spec=ServerlessSpec(
        cloud=CloudProvider.AWS,
        region=AwsRegion.US_EAST_1,
        read_capacity={
            "mode": "Dedicated",
            "dedicated": {
                "node_type": "t1",
                "scaling": "Manual",
                "manual": {
                    "shards": 2,
                    "replicas": 2
                }
            }
        }
    )
)
```

**Configure read capacity on an existing index:**

You can switch between OnDemand and Dedicated modes, or adjust the number of shards and replicas for dedicated read capacity:

```python
from pinecone import Pinecone

pc = Pinecone()

pc.configure_index(
    name='my-index',
    read_capacity={"mode": "OnDemand"}
)

pc.configure_index(
    name='my-index',
    read_capacity={
        "mode": "Dedicated",
        "dedicated": {
            "node_type": "t1",
            "scaling": "Manual",
            "manual": {
                "shards": 3,
                "replicas": 2
            }
        }
    }
)

pc.configure_index(
    name='my-index',
    read_capacity={
        "mode": "Dedicated",
        "dedicated": {
            "node_type": "t1",
            "scaling": "Manual",
            "manual": {
                "shards": 4,
                "replicas": 3
            }
        }
    }
)
```

When you change read capacity configuration, the index will transition to the new configuration. You can use `describe_index` to check the status of the transition.

See [PR #528](#528) for details.

You can now fetch vectors using metadata filters instead of vector IDs. This is especially useful when you need to retrieve vectors based on their metadata properties.

```python
from pinecone import Pinecone

pc = Pinecone()
index = pc.Index(host="your-index-host")

response = index.fetch_by_metadata(
    filter={'genre': {'$in': ['comedy', 'drama']}, 'year': {'$eq': 2019}},
    namespace='my_namespace',
    limit=50
)
print(f"Found {len(response.vectors)} vectors")

for vec_id, vector in response.vectors.items():
    print(f"ID: {vec_id}, Metadata: {vector.metadata}")
```

**Pagination support:**

When fetching large numbers of vectors, you can use pagination tokens to retrieve results in batches:

```python
response = index.fetch_by_metadata(
    filter={'status': 'active'},
    limit=100
)

if response.pagination and response.pagination.next:
    next_response = index.fetch_by_metadata(
        filter={'status': 'active'},
        pagination_token=response.pagination.next,
        limit=100
    )
```

The update method used to require a vector id to be passed, but now you have the option to pass a metadata filter instead. This is useful for bulk metadata updates across many vectors.

There is also a dry_run option that allows you to preview the number of vectors that would be changed by the update before performing the operation.

```python
from pinecone import Pinecone

pc = Pinecone()
index = pc.Index(host="your-index-host")

response = index.update(
    set_metadata={'status': 'active'},
    filter={'genre': {'$eq': 'drama'}},
    dry_run=True
)
print(f"Would update {response.matched_records} vectors")

response = index.update(
    set_metadata={'status': 'active'},
    filter={'genre': {'$eq': 'drama'}}
)
```

A new `FilterBuilder` utility class provides a type-safe, fluent interface for constructing metadata filters. While perhaps a bit verbose, it can help prevent common errors like misspelled operator names and provides better IDE support.

When you chain `.build()` onto the `FilterBuilder` it will emit a python dictionary representing the filter. Methods that take metadata filters as arguments will continue to accept dictionaries as before.

```python
from pinecone import Pinecone, FilterBuilder

pc = Pinecone()
index = pc.Index(host="your-index-host")

filter1 = FilterBuilder().eq("genre", "drama").build()

filter2 = (FilterBuilder().eq("genre", "drama") &
           FilterBuilder().gt("year", 2020)).build()

filter3 = (FilterBuilder().eq("genre", "comedy") |
           FilterBuilder().eq("genre", "drama")).build()

filter4 = ((FilterBuilder().eq("genre", "drama") &
            FilterBuilder().gte("year", 2020)) |
           (FilterBuilder().eq("genre", "comedy") &
            FilterBuilder().lt("year", 2000))).build()

response = index.fetch_by_metadata(filter=filter2, limit=50)

index.update(
    set_metadata={'status': 'archived'},
    filter=filter3
)
```

The FilterBuilder supports all Pinecone filter operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in_`, `nin`, and `exists`. Compound expressions are build with and `&` and or `|`.

See [PR #529](#529) for `fetch_by_metadata`, [PR #544](#544) for `update()` with filter, and [PR #531](#531) for FilterBuilder.

You can now create namespaces in serverless indexes directly from the SDK:

```python
from pinecone import Pinecone

pc = Pinecone()
index = pc.Index(host="your-index-host")

namespace = index.create_namespace(name="my-namespace")
print(f"Created namespace: {namespace.name}, Vector count: {namespace.vector_count}")

namespace = index.create_namespace(
    name="my-namespace",
    schema={
        "fields": {
            "genre": {"filterable": True},
            "year": {"filterable": True}
        }
    }
)
```

**Note:** This operation is not supported for pod-based indexes.

See [PR #532](#532) for details.

For sparse indexes with integrated embedding configured to use the `pinecone-sparse-english-v0` model, you can now specify which terms must be present in search results:

```python
from pinecone import Pinecone, SearchQuery

pc = Pinecone()
index = pc.Index(host="your-index-host")

response = index.search(
    namespace="my-namespace",
    query=SearchQuery(
        inputs={"text": "Apple corporation"},
        top_k=10,
        match_terms={
            "strategy": "all",
            "terms": ["apple", "corporation"]
        }
    )
)
```

The `match_terms` parameter ensures that all specified terms must be present in the text of each search hit. Terms are normalized and tokenized before matching, and order does not matter.

See [PR #530](#530) for details.

**Update API keys, projects, and organizations:**

```python
from pinecone import Admin

admin = Admin() # Auth with PINECONE_CLIENT_ID and PINECONE_CLIENT_SECRET

api_key = admin.api_key.update(
    api_key_id='my-api-key-id',
    name='updated-api-key-name',
    roles=['ProjectEditor', 'DataPlaneEditor']
)

project = admin.project.update(
    project_id='my-project-id',
    name='updated-project-name',
    max_pods=10,
    force_encryption_with_cmek=True
)

organization = admin.organization.update(
    organization_id='my-org-id',
    name='updated-organization-name'
)
```

**Delete organizations:**

```python
from pinecone import Admin

admin = Admin()

admin.organization.delete(organization_id='my-org-id')
```

See [PR #527](#527) and [PR #543](#543) for details.

You can now configure which metadata fields are filterable when creating serverless indexes. This helps optimize performance by only indexing metadata fields that you plan to use for filtering:

```python
from pinecone import (
    Pinecone,
    ServerlessSpec,
    CloudProvider,
    AwsRegion,
    Metric
)

pc = Pinecone()

pc.create_index(
    name='my-index',
    dimension=1536,
    metric=Metric.COSINE,
    spec=ServerlessSpec(
        cloud=CloudProvider.AWS,
        region=AwsRegion.US_EAST_1,
        schema={
            "genre": {"filterable": True},
            "year": {"filterable": True},
            "rating": {"filterable": True}
        }
    )
)
```

When using schemas, only fields marked as `filterable: True` in the schema can be used in metadata filters.

See [PR #528](#528) for details.

The SDK now exposes header information from API responses. This information is available in response objects via the `_response_info` attribute and can be useful for debugging and monitoring.

```python
from pinecone import Pinecone

pc = Pinecone()
index = pc.Index(host="your-index-host")

response = index.query(
    vector=[0.1, 0.2, 0.3, ...],
    top_k=10,
    namespace='my_namespace'
)

for k, v in response._response_info.get('raw_headers').items():
    print(f"{k}: {v}")
```

See [PR #539](#539) for details.

We've replaced Python's standard library `json` module with `orjson`, a fast JSON library written in Rust. This provides significant performance improvements for both serialization and deserialization of request payloads:

- **Serialization (dumps)**: 10-23x faster depending on payload size
- **Deserialization (loads)**: 4-7x faster depending on payload size

These improvements are especially beneficial for:
- High-throughput applications making many API calls
- Applications handling large vector payloads
- Real-time applications where latency matters

No code changes are required - the API remains the same, and you'll automatically benefit from these performance improvements.

See [PR #556](#556) for details.

We've optimized gRPC response parsing by replacing `json_format.MessageToDict` with direct protobuf field access. This optimization provides approximately 2x faster response parsing for gRPC operations.

Special thanks to [@yorickvP](https://github.com/yorickvP) for surfacing the `json_format.MessageToDict` refactor opportunity. While we didn't merge the specific PR, yorick's insight led us to implement a similar optimization that significantly improves gRPC performance.

See [PR #553](#553) for details.

- **Type hints and IDE support**: Comprehensive type hints throughout the SDK improve IDE autocomplete and type checking. The SDK now uses Python 3.10+ type syntax throughout.
- **Documentation**: Updated docstrings with RST formatting and code examples for better developer experience.
- **Dependency updates**: Updated protobuf to 5.29.5 to address security vulnerabilities. Updated `pinecone-plugin-assistant` to version 3.0.1.
- **Build system**: Migrated from poetry to uv for faster dependency management.

- [@yorickvP](https://github.com/yorickvP) - Thanks for surfacing the gRPC response parsing optimization opportunity!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants