From 21ede49529d42ab44a8fb962ec42f5e4d11bd112 Mon Sep 17 00:00:00 2001 From: Rashin Arab <3806658+rasharab@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:15:21 -0700 Subject: [PATCH 1/3] feat: add response metadata fields for read units, upserted count, and m Agent-Id: agent-99b8efc1-3adb-4099-8683-03d0a3351bed Linked-Note-Id: b55dbb0b-4a64-437e-93bd-f29d287c9b0c --- .../io/pinecone/configs/ResponseMetadata.java | 60 +++++++++ .../configs/ResponseMetadataInterceptor.java | 71 +++++++++++ .../configs/ResponseMetadataTest.java | 116 ++++++++++++++++++ 3 files changed, 247 insertions(+) diff --git a/src/main/java/io/pinecone/configs/ResponseMetadata.java b/src/main/java/io/pinecone/configs/ResponseMetadata.java index 3b4f14dd..60001540 100644 --- a/src/main/java/io/pinecone/configs/ResponseMetadata.java +++ b/src/main/java/io/pinecone/configs/ResponseMetadata.java @@ -9,6 +9,9 @@ *
  • {@link #getClientDurationMs()} - Total round-trip time measured by the client
  • *
  • {@link #getServerDurationMs()} - Server processing time from x-pinecone-response-duration-ms header
  • *
  • {@link #getNetworkOverheadMs()} - Computed network + serialization overhead
  • + *
  • {@link #getReadUnits()} - Number of read units consumed (query, fetch, list operations)
  • + *
  • {@link #getUpsertedCount()} - Number of records upserted (upsert operations)
  • + *
  • {@link #getMatchedRecords()} - Number of records matched (update operations)
  • * * *

    Example usage with a listener: @@ -34,6 +37,9 @@ public class ResponseMetadata { private final String status; private final String grpcStatusCode; private final String errorType; + private final Integer readUnits; + private final Integer upsertedCount; + private final Integer matchedRecords; private ResponseMetadata(Builder builder) { this.operationName = builder.operationName; @@ -45,6 +51,9 @@ private ResponseMetadata(Builder builder) { this.status = builder.status; this.grpcStatusCode = builder.grpcStatusCode; this.errorType = builder.errorType; + this.readUnits = builder.readUnits; + this.upsertedCount = builder.upsertedCount; + this.matchedRecords = builder.matchedRecords; } /** @@ -134,6 +143,30 @@ public String getErrorType() { return errorType; } + /** + * Returns the number of read units consumed by the operation, or null if not applicable. + * Populated for query, fetch, and list operations. + */ + public Integer getReadUnits() { + return readUnits; + } + + /** + * Returns the number of records upserted, or null if not applicable. + * Populated for upsert operations. + */ + public Integer getUpsertedCount() { + return upsertedCount; + } + + /** + * Returns the number of records matched by the operation, or null if not applicable. + * Populated for update operations. + */ + public Integer getMatchedRecords() { + return matchedRecords; + } + /** * Returns true if the operation was successful. */ @@ -162,6 +195,15 @@ public String toString() { if (errorType != null) { sb.append(", errorType=").append(errorType); } + if (readUnits != null) { + sb.append(", readUnits=").append(readUnits); + } + if (upsertedCount != null) { + sb.append(", upsertedCount=").append(upsertedCount); + } + if (matchedRecords != null) { + sb.append(", matchedRecords=").append(matchedRecords); + } sb.append("}"); return sb.toString(); } @@ -176,6 +218,9 @@ public static class Builder { private String status = "success"; private String grpcStatusCode = "OK"; private String errorType; + private Integer readUnits; + private Integer upsertedCount; + private Integer matchedRecords; public Builder operationName(String operationName) { this.operationName = operationName; @@ -222,6 +267,21 @@ public Builder errorType(String errorType) { return this; } + public Builder readUnits(Integer readUnits) { + this.readUnits = readUnits; + return this; + } + + public Builder upsertedCount(Integer upsertedCount) { + this.upsertedCount = upsertedCount; + return this; + } + + public Builder matchedRecords(Integer matchedRecords) { + this.matchedRecords = matchedRecords; + return this; + } + public ResponseMetadata build() { return new ResponseMetadata(this); } diff --git a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java index 12774d14..c7baccf7 100644 --- a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java +++ b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java @@ -75,6 +75,9 @@ public void start(Listener responseListener, Metadata headers) { responseListener) { private Metadata initialHeaders; + private Integer readUnits; + private Integer upsertedCount; + private Integer matchedRecords; @Override public void onHeaders(Metadata headers) { @@ -86,6 +89,19 @@ public void onHeaders(Metadata headers) { super.onHeaders(headers); } + @Override + public void onMessage(RespT message) { + // Extract response body fields via reflection + try { + readUnits = extractReadUnits(message); + upsertedCount = extractUpsertedCount(message); + matchedRecords = extractMatchedRecords(message); + } catch (Exception e) { + logger.debug("Could not extract response body fields", e); + } + super.onMessage(message); + } + @Override public void onClose(Status status, Metadata trailers) { try { @@ -116,6 +132,9 @@ public void onClose(Status status, Metadata trailers) { .status(statusStr) .grpcStatusCode(status.getCode().name()) .errorType(errorType) + .readUnits(readUnits) + .upsertedCount(upsertedCount) + .matchedRecords(matchedRecords) .build(); invokeListener(metadata); @@ -159,6 +178,58 @@ private String extractNamespace(T message) { } } + private Integer extractReadUnits(T message) { + // Extract read units from QueryResponse, FetchResponse, ListResponse via hasUsage()/getUsage()/getReadUnits() + try { + java.lang.reflect.Method hasUsage = message.getClass().getMethod("hasUsage"); + Boolean has = (Boolean) hasUsage.invoke(message); + if (Boolean.TRUE.equals(has)) { + java.lang.reflect.Method getUsage = message.getClass().getMethod("getUsage"); + Object usage = getUsage.invoke(message); + java.lang.reflect.Method getReadUnits = usage.getClass().getMethod("getReadUnits"); + Object result = getReadUnits.invoke(usage); + return ((Number) result).intValue(); + } + } catch (NoSuchMethodException e) { + // Response type does not have usage field + } catch (Exception e) { + logger.debug("Could not extract read units from response", e); + } + return null; + } + + private Integer extractUpsertedCount(T message) { + // Extract upserted count from UpsertResponse via getUpsertedCount() + try { + java.lang.reflect.Method getUpsertedCount = message.getClass().getMethod("getUpsertedCount"); + Object result = getUpsertedCount.invoke(message); + return ((Number) result).intValue(); + } catch (NoSuchMethodException e) { + // Response type does not have upsertedCount field + } catch (Exception e) { + logger.debug("Could not extract upserted count from response", e); + } + return null; + } + + private Integer extractMatchedRecords(T message) { + // Extract matched records from UpdateResponse via hasMatchedRecords()/getMatchedRecords() + try { + java.lang.reflect.Method hasMatchedRecords = message.getClass().getMethod("hasMatchedRecords"); + Boolean has = (Boolean) hasMatchedRecords.invoke(message); + if (Boolean.TRUE.equals(has)) { + java.lang.reflect.Method getMatchedRecords = message.getClass().getMethod("getMatchedRecords"); + Object result = getMatchedRecords.invoke(message); + return ((Number) result).intValue(); + } + } catch (NoSuchMethodException e) { + // Response type does not have matchedRecords field + } catch (Exception e) { + logger.debug("Could not extract matched records from response", e); + } + return null; + } + private String mapGrpcStatusToErrorType(Status.Code code) { switch (code) { case INVALID_ARGUMENT: diff --git a/src/test/java/io/pinecone/configs/ResponseMetadataTest.java b/src/test/java/io/pinecone/configs/ResponseMetadataTest.java index 1695dd4e..5547e2a0 100644 --- a/src/test/java/io/pinecone/configs/ResponseMetadataTest.java +++ b/src/test/java/io/pinecone/configs/ResponseMetadataTest.java @@ -17,6 +17,9 @@ void testBuilderWithAllFields() { .serverDurationMs(100L) .status("success") .grpcStatusCode("OK") + .readUnits(5) + .upsertedCount(10) + .matchedRecords(3) .build(); assertEquals("query", metadata.getOperationName()); @@ -29,6 +32,9 @@ void testBuilderWithAllFields() { assertEquals("OK", metadata.getGrpcStatusCode()); assertTrue(metadata.isSuccess()); assertNull(metadata.getErrorType()); + assertEquals(5, metadata.getReadUnits()); + assertEquals(10, metadata.getUpsertedCount()); + assertEquals(3, metadata.getMatchedRecords()); } @Test @@ -180,5 +186,115 @@ void testAllErrorTypes() { assertFalse(metadata.isSuccess()); } } + + @Test + void testReadUnitsNullByDefault() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("query") + .build(); + + assertNull(metadata.getReadUnits()); + } + + @Test + void testReadUnitsWithValue() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("query") + .readUnits(10) + .build(); + + assertEquals(10, metadata.getReadUnits()); + } + + @Test + void testUpsertedCountNullByDefault() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("upsert") + .build(); + + assertNull(metadata.getUpsertedCount()); + } + + @Test + void testUpsertedCountWithValue() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("upsert") + .upsertedCount(100) + .build(); + + assertEquals(100, metadata.getUpsertedCount()); + } + + @Test + void testMatchedRecordsNullByDefault() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("update") + .build(); + + assertNull(metadata.getMatchedRecords()); + } + + @Test + void testMatchedRecordsWithValue() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("update") + .matchedRecords(42) + .build(); + + assertEquals(42, metadata.getMatchedRecords()); + } + + @Test + void testToStringWithReadUnits() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("query") + .indexName("my-index") + .clientDurationMs(100) + .readUnits(5) + .build(); + + String str = metadata.toString(); + assertTrue(str.contains("readUnits=5")); + } + + @Test + void testToStringWithUpsertedCount() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("upsert") + .indexName("my-index") + .clientDurationMs(100) + .upsertedCount(50) + .build(); + + String str = metadata.toString(); + assertTrue(str.contains("upsertedCount=50")); + } + + @Test + void testToStringWithMatchedRecords() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("update") + .indexName("my-index") + .clientDurationMs(100) + .matchedRecords(7) + .build(); + + String str = metadata.toString(); + assertTrue(str.contains("matchedRecords=7")); + } + + @Test + void testToStringOmitsNullResponseBodyFields() { + ResponseMetadata metadata = ResponseMetadata.builder() + .operationName("delete") + .indexName("my-index") + .clientDurationMs(100) + .build(); + + String str = metadata.toString(); + assertFalse(str.contains("readUnits")); + assertFalse(str.contains("upsertedCount")); + assertFalse(str.contains("matchedRecords")); + } } From a401a7d49ad343cc9a75894e768cb37a59f7aeb3 Mon Sep 17 00:00:00 2001 From: Rashin Arab <3806658+rasharab@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:19:35 -0700 Subject: [PATCH 2/3] fix: add List operation to tracked operations in ResponseMetadataInterce Agent-Id: agent-e63157ed-432d-4883-9f3e-1895a0b24e7a --- .../java/io/pinecone/configs/ResponseMetadataInterceptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java index c7baccf7..e3fb7139 100644 --- a/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java +++ b/src/main/java/io/pinecone/configs/ResponseMetadataInterceptor.java @@ -21,7 +21,7 @@ public class ResponseMetadataInterceptor implements ClientInterceptor { // Operations to track (matches VectorService RPC method names) private static final Set TRACKED_OPERATIONS = new HashSet<>(Arrays.asList( - "Upsert", "Query", "Fetch", "Update", "Delete" + "Upsert", "Query", "Fetch", "Update", "Delete", "List" )); private final ResponseMetadataListener listener; From a164deeb6f77fecb194e1638307067bfb71e614d Mon Sep 17 00:00:00 2001 From: Rashin Arab <3806658+rasharab@users.noreply.github.com> Date: Fri, 27 Mar 2026 11:22:40 -0700 Subject: [PATCH 3/3] docs: update README with usage and operation-result metadata fields Agent-Id: agent-3f21d110-2d0f-42e3-aae2-b6331803d85a --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 4b07f152..6ecae0a3 100644 --- a/README.md +++ b/README.md @@ -168,12 +168,24 @@ Pinecone pinecone = new Pinecone.Builder("PINECONE_API_KEY") metadata.getClientDurationMs(), metadata.getServerDurationMs(), metadata.getNetworkOverheadMs()); + + // Log usage and operation-result fields when available + if (metadata.getReadUnits() != null) { + System.out.printf(" Read units consumed: %d%n", metadata.getReadUnits()); + } + if (metadata.getUpsertedCount() != null) { + System.out.printf(" Upserted count: %d%n", metadata.getUpsertedCount()); + } + if (metadata.getMatchedRecords() != null) { + System.out.printf(" Matched records: %d%n", metadata.getMatchedRecords()); + } }) .build(); Index index = pinecone.getIndexConnection("example-index"); index.query(5, Arrays.asList(1.0f, 2.0f, 3.0f)); // Output: Operation: query | Client: 45ms | Server: 32ms | Network: 13ms +// Read units consumed: 5 ``` The `ResponseMetadata` object provides: @@ -189,6 +201,9 @@ The `ResponseMetadata` object provides: | `isSuccess()` | Whether the operation succeeded | | `getGrpcStatusCode()` | gRPC status code | | `getErrorType()` | Error category when failed | +| `getReadUnits()` | Read units consumed (available on query, fetch, list) | +| `getUpsertedCount()` | Number of vectors upserted (available on upsert) | +| `getMatchedRecords()` | Number of matched records (available on update) | For a complete OpenTelemetry integration example with Prometheus and Grafana, see the [java-otel-metrics example](examples/java-otel-metrics/).