Skip to content

Commit 59b9f5f

Browse files
committed
Add support to create sparse serverless index method (#175)
## Problem Add support to create sparse serverless index method ## Solution As a part of this PR, added following changes: 1. add createSparseServelessIndex() that doesn't accept dimension and metric as compared to the traditional createServerlessIndex() 2. removed a validation to check for values (part of vector) being null or empty at the time of upsert since values can be an empty array of floats for sparse vector 3. metric is now optional for creating serverless index and will default to cosine for dense index 4. added integration tests ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [X] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update - [ ] Infrastructure change (CI configs, etc) - [ ] Non-code change (docs, etc) - [ ] None of the above: (explain here) ## Test Plan Added integration tests
1 parent 0f9a483 commit 59b9f5f

File tree

6 files changed

+214
-41
lines changed

6 files changed

+214
-41
lines changed

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ Operations related to the building and managing of Pinecone indexes are called [
162162
You can use the Java SDK to create two types of indexes: [serverless indexes](https://docs.pinecone.io/guides/indexes/understanding-indexes#serverless-indexes) (recommended for most use cases) and
163163
[pod-based indexes](https://docs.pinecone.io/guides/indexes/understanding-indexes#pod-based-indexes) (recommended for high-throughput use cases).
164164

165-
### Create a serverless index
165+
### Create a dense serverless index
166166

167167
The following is an example of creating a serverless index in the `us-west-2` region of AWS. For more information on
168168
serverless and regional availability, see [Understanding indexes](https://docs.pinecone.io/guides/indexes/understanding-indexes#serverless-indexes).
@@ -187,6 +187,31 @@ tags.put("env", "test");
187187
IndexModel indexModel = pinecone.createServerlessIndex(indexName, similarityMetric, dimension, cloud, region, DeletionProtection.ENABLED, tags);
188188
```
189189

190+
### Create a sparse serverless index
191+
192+
The following is an example of creating a sparse serverless index in the `us-east-1` region of AWS. For more information on
193+
serverless and regional availability, see [Understanding indexes](https://docs.pinecone.io/guides/indexes/sparse-indexes).
194+
195+
```java
196+
import io.pinecone.clients.Pinecone;
197+
import org.openapitools.db_control.client.model.IndexModel;
198+
import org.openapitools.db_control.client.model.DeletionProtection;
199+
import java.util.HashMap;
200+
...
201+
202+
Pinecone pinecone = new Pinecone.Builder("PINECONE_API_KEY").build();
203+
204+
String indexName = "example-index";
205+
int dimension = 1538;
206+
String cloud = "aws";
207+
String region = "us-east-1";
208+
HashMap<String, String> tags = new HashMap<>();
209+
tags.put("env", "test");
210+
String vectorType = "sparse";
211+
212+
IndexModel indexModel = pinecone.createSparseServelessIndex(indexName, cloud, region, DeletionProtection.ENABLED, tags, vectorType);
213+
```
214+
190215
### Create a pod index
191216

192217
The following is a minimal example of creating a pod-based index. For all the possible configuration options, see
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package io.pinecone.integration.controlPlane.serverless;
2+
3+
import io.pinecone.clients.Index;
4+
import io.pinecone.clients.Pinecone;
5+
import io.pinecone.exceptions.PineconeNotFoundException;
6+
import io.pinecone.helpers.RandomStringBuilder;
7+
import io.pinecone.proto.UpsertResponse;
8+
import io.pinecone.unsigned_indices_model.QueryResponseWithUnsignedIndices;
9+
import org.junit.jupiter.api.*;
10+
import org.openapitools.db_control.client.model.DeletionProtection;
11+
import org.openapitools.db_control.client.model.IndexModel;
12+
13+
import java.util.*;
14+
15+
import static io.pinecone.helpers.TestUtilities.waitUntilIndexIsReady;
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
19+
public class SparseIndexTest {
20+
static String indexName;
21+
static Pinecone pinecone;
22+
23+
@BeforeAll
24+
public static void setUp() throws InterruptedException {
25+
indexName = RandomStringBuilder.build("sparse-index", 8);
26+
pinecone = new Pinecone
27+
.Builder(System.getenv("PINECONE_API_KEY"))
28+
.withSourceTag("pinecone_test")
29+
.build();
30+
}
31+
32+
@Test
33+
@Order(1)
34+
public void createSparseIndex() {
35+
Map<String, String> tags = new HashMap<>();
36+
tags.put("env", "test");
37+
38+
// Create sparse Index
39+
IndexModel indexModel = pinecone.createSparseServelessIndex(indexName,
40+
"aws",
41+
"us-east-1",
42+
DeletionProtection.ENABLED,
43+
tags,
44+
"sparse");
45+
46+
assertNotNull(indexModel);
47+
assertEquals(indexName, indexModel.getName());
48+
assertEquals(IndexModel.MetricEnum.DOTPRODUCT, indexModel.getMetric());
49+
assertEquals(indexModel.getDeletionProtection(), DeletionProtection.ENABLED);
50+
assertEquals(indexModel.getTags(), tags);
51+
assertEquals(indexModel.getVectorType(), "sparse");
52+
}
53+
54+
@Test
55+
@Order(2)
56+
public void configureSparseIndex() throws InterruptedException {
57+
String key = "flag";
58+
String value = "internal";
59+
Map<String, String> tags = new HashMap<>();
60+
tags.put(key, value);
61+
62+
// Wait until index is ready
63+
waitUntilIndexIsReady(pinecone, indexName, 200000);
64+
65+
// Disable deletion protection and add more index tags
66+
pinecone.configureServerlessIndex(indexName, DeletionProtection.DISABLED, tags);
67+
Thread.sleep(7000);
68+
69+
// Describe index to confirm deletion protection is disabled
70+
IndexModel indexModel = pinecone.describeIndex(indexName);
71+
assertEquals(indexModel.getDeletionProtection(), DeletionProtection.DISABLED);
72+
assert indexModel.getTags() != null;
73+
assertEquals(indexModel.getTags().get(key), value);
74+
}
75+
76+
@Disabled
77+
// @Order(3)
78+
public void upsertAndQueryVectors() {
79+
Index index = pinecone.getIndexConnection(indexName);
80+
String id = "v1";
81+
ArrayList<Long> indices = new ArrayList<>();
82+
indices.add(1L);
83+
indices.add(2L);
84+
85+
ArrayList<Float> values = new ArrayList<>();
86+
values.add(1f);
87+
values.add(2f);
88+
89+
UpsertResponse upsertResponse = index.upsert("v1", Collections.emptyList(), indices, values, null, "");
90+
assertEquals(upsertResponse.getUpsertedCount(), 1);
91+
92+
// Query by vector id
93+
QueryResponseWithUnsignedIndices queryResponse = index.queryByVectorId(1, id, true, false);
94+
assertEquals(queryResponse.getMatchesList().size(), 1);
95+
assertEquals(queryResponse.getMatches(0).getId(), id);
96+
assertEquals(queryResponse.getMatches(0).getSparseValuesWithUnsignedIndices().getIndicesWithUnsigned32IntList(), indices);
97+
assertEquals(queryResponse.getMatches(0).getSparseValuesWithUnsignedIndices().getValuesList(), values);
98+
}
99+
100+
@Test
101+
@Order(4)
102+
public void deleteSparseIndex() throws InterruptedException {
103+
// Delete sparse index
104+
pinecone.deleteIndex(indexName);
105+
Thread.sleep(5000);
106+
107+
// Confirm the index is deleted by calling describe index which should return resource not found
108+
assertThrows(PineconeNotFoundException.class, () -> pinecone.describeIndex(indexName));
109+
}
110+
}

src/integration/java/io/pinecone/integration/dataPlane/UpsertErrorTest.java

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,7 @@ public void upsertWithApiKeyMissingSyncTest() {
4848
index.upsert(null, values);
4949
fail("Expecting invalid upsert request exception");
5050
} catch (PineconeException expected) {
51-
assertEquals(expected.getMessage(), "Invalid upsert request. Please ensure that both id and values are provided.");
52-
}
53-
}
54-
55-
@Test
56-
public void upsertWhenValuesMissingSyncTest() {
57-
try {
58-
index.upsert("some_id", null);
59-
fail("Expecting invalid upsert request exception");
60-
} catch (PineconeException expected) {
61-
assertEquals(expected.getMessage(), "Invalid upsert request. Please ensure that both id and values are provided.");
51+
assertEquals(expected.getMessage(), "Invalid upsert request. Please ensure that id is provided.");
6252
}
6353
}
6454

@@ -126,17 +116,7 @@ public void upsertWithApiKeyMissingFutureTest() {
126116
asyncIndex.upsert(null, values);
127117
fail("Expecting invalid upsert request exception");
128118
} catch (PineconeException expected) {
129-
assertTrue(expected.getMessage().contains("ensure that both id and values are provided."));
130-
}
131-
}
132-
133-
@Test
134-
public void upsertWhenValuesMissingFutureTest() {
135-
try {
136-
asyncIndex.upsert("some_id", null);
137-
fail("Expecting invalid upsert request exception");
138-
} catch (PineconeException expected) {
139-
assertTrue(expected.getMessage().contains("ensure that both id and values are provided."));
119+
assertTrue(expected.getMessage().contains("ensure that id is provided."));
140120
}
141121
}
142122

src/main/java/io/pinecone/clients/Pinecone.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ public IndexModel createServerlessIndex(String indexName,
8787
}
8888

8989
if (metric == null || metric.isEmpty()) {
90-
throw new PineconeValidationException("Metric cannot be null or empty. Must be one of " + Arrays.toString(CreateIndexRequest.MetricEnum.values()));
90+
metric = "cosine";
9191
}
92+
9293
try {
9394
CreateIndexRequest.MetricEnum.fromValue(metric.toLowerCase());
9495
} catch (IllegalArgumentException e) {
@@ -142,6 +143,78 @@ public IndexModel createServerlessIndex(String indexName,
142143
return indexModel;
143144
}
144145

146+
/**
147+
* Creates a new sparse serverless index.
148+
* <p>
149+
* Example:
150+
* <pre>{@code
151+
* client.createServerlessIndex("YOUR-INDEX", "cosine", 1536, "aws", "us-west-2", DeletionProtection.ENABLED);
152+
* }</pre>
153+
*
154+
* @param indexName The name of the index to be created.
155+
* @param cloud The cloud provider for the index.
156+
* @param region The cloud region for the index.
157+
* @param deletionProtection Enable or disable deletion protection for the index.
158+
* @param tags A map of tags to associate with the Index.
159+
* @param vectorType The metric type for the index. Must be one of "cosine", "euclidean", or "dotproduct".
160+
* @return {@link IndexModel} representing the created serverless index.
161+
* @throws PineconeException if the API encounters an error during index creation or if any of the arguments are invalid.
162+
*/
163+
public IndexModel createSparseServelessIndex(String indexName,
164+
String cloud,
165+
String region,
166+
DeletionProtection deletionProtection,
167+
Map<String, String> tags,
168+
String vectorType) throws PineconeException {
169+
if (indexName == null || indexName.isEmpty()) {
170+
throw new PineconeValidationException("Index name cannot be null or empty");
171+
}
172+
173+
if (cloud == null || cloud.isEmpty()) {
174+
throw new PineconeValidationException("Cloud cannot be null or empty. Must be one of " + Arrays.toString(ServerlessSpec.CloudEnum.values()));
175+
}
176+
177+
try {
178+
ServerlessSpec.CloudEnum.fromValue(cloud.toLowerCase());
179+
} catch (IllegalArgumentException e) {
180+
throw new PineconeValidationException("Cloud cannot be null or empty. Must be one of " + Arrays.toString(ServerlessSpec.CloudEnum.values()));
181+
}
182+
183+
if (region == null || region.isEmpty()) {
184+
throw new PineconeValidationException("Region cannot be null or empty");
185+
}
186+
187+
if(!vectorType.equalsIgnoreCase("sparse") && !vectorType.equalsIgnoreCase("dense")) {
188+
throw new PineconeValidationException("vectorType must be sparse or dense");
189+
}
190+
191+
// Convert user string for "cloud" arg into ServerlessSpec.CloudEnum
192+
ServerlessSpec.CloudEnum cloudProvider = ServerlessSpec.CloudEnum.fromValue(cloud.toLowerCase());
193+
194+
ServerlessSpec serverlessSpec = new ServerlessSpec().cloud(cloudProvider).region(region);
195+
IndexSpec createServerlessIndexRequestSpec = new IndexSpec().serverless(serverlessSpec);
196+
197+
IndexModel indexModel = null;
198+
199+
try {
200+
CreateIndexRequest createIndexRequest = new CreateIndexRequest()
201+
.name(indexName)
202+
.metric(CreateIndexRequest.MetricEnum.DOTPRODUCT)
203+
.spec(createServerlessIndexRequestSpec)
204+
.deletionProtection(deletionProtection)
205+
.vectorType(vectorType);
206+
207+
if(tags != null && !tags.isEmpty()) {
208+
createIndexRequest.tags(tags);
209+
}
210+
211+
indexModel = manageIndexesApi.createIndex(createIndexRequest);
212+
} catch (ApiException apiException) {
213+
handleApiException(apiException);
214+
}
215+
return indexModel;
216+
}
217+
145218
/**
146219
* Overload for creating a new pods index with environment and podType, the minimum required parameters.
147220
* <p>

src/main/java/io/pinecone/commons/IndexInterface.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,10 @@ default Vector buildUpsertVector(String id,
103103
List<Long> sparseIndices,
104104
List<Float> sparseValues,
105105
Struct metadata) {
106-
if (id == null || id.isEmpty() || values == null || values.isEmpty()) {
107-
throw new PineconeValidationException("Invalid upsert request. Please ensure that both id and values are " +
108-
"provided.");
106+
if (id == null || id.isEmpty()) {
107+
throw new PineconeValidationException("Invalid upsert request. Please ensure that id is provided.");
109108
}
110109

111-
112110
Vector.Builder vectorBuilder = Vector.newBuilder()
113111
.setId(id)
114112
.addAllValues(values);

src/test/java/io/pinecone/PineconeIndexOperationsTest.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,6 @@ public void testCreateServerlessIndex() throws IOException {
7676
() -> client.createServerlessIndex(null, "cosine", 3, "aws", "us-west-2", DeletionProtection.DISABLED, Collections.EMPTY_MAP));
7777
assertEquals("Index name cannot be null or empty", thrownNullIndexName.getMessage());
7878

79-
PineconeValidationException thrownEmptyMetric = assertThrows(PineconeValidationException.class,
80-
() -> client.createServerlessIndex("testServerlessIndex", "", 3, "aws", "us-west-2", DeletionProtection.DISABLED, Collections.EMPTY_MAP));
81-
assertEquals("Metric cannot be null or empty. Must be one of " + Arrays.toString(IndexModel.MetricEnum.values()), thrownEmptyMetric.getMessage());
82-
83-
PineconeValidationException thrownInvalidMetric = assertThrows(PineconeValidationException.class,
84-
() -> client.createServerlessIndex("testServerlessIndex", "blah", 3, "aws", "us-west-2", DeletionProtection.DISABLED, Collections.EMPTY_MAP));
85-
assertEquals(String.format("Metric cannot be null or empty. Must be one of " + Arrays.toString(IndexModel.MetricEnum.values())), thrownInvalidMetric.getMessage());
86-
87-
PineconeValidationException thrownNullMetric = assertThrows(PineconeValidationException.class,
88-
() -> client.createServerlessIndex("testServerlessIndex", null, 3, "aws", "us-west-2", DeletionProtection.DISABLED, Collections.EMPTY_MAP));
89-
assertEquals("Metric cannot be null or empty. Must be one of " + Arrays.toString(IndexModel.MetricEnum.values()),
90-
thrownNullMetric.getMessage());
91-
9279
PineconeValidationException thrownNegativeDimension = assertThrows(PineconeValidationException.class,
9380
() -> client.createServerlessIndex("testServerlessIndex", "cosine", -3, "aws", "us-west-2", DeletionProtection.DISABLED, Collections.EMPTY_MAP));
9481
assertEquals("Dimension must be greater than 0. See limits for more info: https://docs.pinecone.io/reference/limits", thrownNegativeDimension.getMessage());

0 commit comments

Comments
 (0)