Skip to content

Commit 52c1cf2

Browse files
committed
refactor(vectorstore): introduce Step Builder pattern for PineconeVectorStore
Replace the existing builder implementation with a type-safe Step Builder pattern to enforce required field initialization in a specific order. This change: - Introduces a new builder() method that guides users through required fields - Deprecates the old builder method with all parameters - Makes field configuration more explicit and type-safe - Updates all usage points to use the new builder pattern The required fields must now be provided in sequence: 1. embeddingModel 2. apiKey 3. projectId 4. environment 5. indexName This change improves API usability by preventing parameter confusion and ensuring all required fields are set before optional configuration.
1 parent d7fe07b commit 52c1cf2

File tree

4 files changed

+195
-17
lines changed

4 files changed

+195
-17
lines changed

spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vectorstore/pinecone/PineconeVectorStoreAutoConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ public PineconeVectorStore vectorStore(EmbeddingModel embeddingModel, PineconeVe
5555
ObjectProvider<VectorStoreObservationConvention> customObservationConvention,
5656
BatchingStrategy batchingStrategy) {
5757

58-
return PineconeVectorStore
59-
.builder(embeddingModel, properties.getApiKey(), properties.getProjectId(), properties.getEnvironment(),
60-
properties.getIndexName())
58+
return PineconeVectorStore.builder(embeddingModel)
59+
.apiKey(properties.getApiKey())
60+
.projectId(properties.getProjectId())
61+
.environment(properties.getEnvironment())
62+
.indexName(properties.getIndexName())
6163
.namespace(properties.getNamespace())
6264
.contentFieldName(properties.getContentFieldName())
6365
.distanceMetadataFieldName(properties.getDistanceMetadataFieldName())

vector-stores/spring-ai-pinecone-store/src/main/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStore.java

Lines changed: 179 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,10 @@ public PineconeVectorStore(PineconeVectorStoreConfig config, EmbeddingModel embe
112112
public PineconeVectorStore(PineconeVectorStoreConfig config, EmbeddingModel embeddingModel,
113113
ObservationRegistry observationRegistry, VectorStoreObservationConvention customObservationConvention,
114114
BatchingStrategy batchingStrategy) {
115-
this(builder(embeddingModel, config.clientConfig.getApiKey(), config.clientConfig.getProjectName(),
116-
config.clientConfig.getEnvironment(), config.connectionConfig.getIndexName())
115+
this(builder(embeddingModel).apiKey(config.clientConfig.getApiKey())
116+
.projectId(config.clientConfig.getProjectName())
117+
.environment(config.clientConfig.getEnvironment())
118+
.indexName(config.connectionConfig.getIndexName())
117119
.namespace(config.namespace)
118120
.contentFieldName(config.contentFieldName)
119121
.distanceMetadataFieldName(config.distanceMetadataFieldName)
@@ -155,12 +157,48 @@ protected PineconeVectorStore(Builder builder) {
155157
/**
156158
* Creates a new builder instance for configuring a PineconeVectorStore.
157159
* @return A new PineconeBuilder instance
160+
* @deprecated use {@link #builder(EmbeddingModel)} instead.
158161
*/
162+
@Deprecated(forRemoval = true, since = "1.0.0-M6")
159163
public static Builder builder(EmbeddingModel embeddingModel, String apiKey, String projectId, String environment,
160164
String indexName) {
161165
return new Builder(embeddingModel, apiKey, projectId, environment, indexName);
162166
}
163167

168+
/**
169+
* Creates a new builder for constructing a PineconeVectorStore instance. This builder
170+
* implements a type-safe step pattern that guides users through the required
171+
* configuration fields in a specific order, followed by optional configurations.
172+
*
173+
* Required fields must be provided in this sequence:
174+
* <ol>
175+
* <li>embeddingModel (provided to this method)</li>
176+
* <li>apiKey</li>
177+
* <li>projectId</li>
178+
* <li>environment</li>
179+
* <li>indexName</li>
180+
* </ol>
181+
*
182+
* After all required fields are set, optional configurations can be added using the
183+
* fluent builder pattern.
184+
*
185+
* Example usage: <pre>{@code
186+
* PineconeVectorStore store = PineconeVectorStore.builder(embeddingModel)
187+
* .apiKey("your-api-key")
188+
* .projectId("your-project")
189+
* .environment("your-env")
190+
* .indexName("your-index")
191+
* .namespace("optional") // optional configuration
192+
* .build();
193+
* }</pre>
194+
* @param embeddingModel the embedding model to use for vector transformations
195+
* @return the first step of the builder requiring API key configuration
196+
* @throws IllegalArgumentException if embeddingModel is null
197+
*/
198+
public static Builder.BuilderWithApiKey builder(EmbeddingModel embeddingModel) {
199+
return Builder.StepBuilder.start(embeddingModel);
200+
}
201+
164202
/**
165203
* Adds a list of documents to the vector store based on the namespace.
166204
* @param documents The list of documents to be added.
@@ -336,18 +374,41 @@ public VectorStoreObservationContext.Builder createObservationContextBuilder(Str
336374
}
337375

338376
/**
339-
* Builder class for creating PineconeVectorStore instances.
377+
* Builder class for creating {@link PineconeVectorStore} instances. This implements a
378+
* type-safe step builder pattern to ensure all required fields are provided in a
379+
* specific order before optional configuration.
380+
*
381+
* The required fields must be provided in this sequence: 1. embeddingModel (via
382+
* builder method) 2. apiKey 3. projectId 4. environment 5. indexName
383+
*
384+
* After all required fields are set, optional configurations can be provided using
385+
* the fluent builder pattern.
386+
*
387+
* Example usage: <pre>{@code
388+
* PineconeVectorStore store = PineconeVectorStore.builder(embeddingModel)
389+
* .apiKey("your-api-key")
390+
* .projectId("your-project")
391+
* .environment("your-env")
392+
* .indexName("your-index")
393+
* .namespace("optional") // optional configuration
394+
* .build();
395+
* }</pre>
340396
*/
341397
public static class Builder extends AbstractVectorStoreBuilder<Builder> {
342398

399+
/** Required field for Pinecone API authentication */
343400
private final String apiKey;
344401

402+
/** Required field identifying the Pinecone project */
345403
private final String projectId;
346404

405+
/** Required field specifying the Pinecone environment (e.g. "gcp-starter") */
347406
private final String environment;
348407

408+
/** Required field specifying the Pinecone index name */
349409
private final String indexName;
350410

411+
// Optional fields with default values
351412
private String namespace = "";
352413

353414
private String contentFieldName = CONTENT_FIELD_NAME;
@@ -361,18 +422,127 @@ public static class Builder extends AbstractVectorStoreBuilder<Builder> {
361422
private Builder(EmbeddingModel embeddingModel, String apiKey, String projectId, String environment,
362423
String indexName) {
363424
super(embeddingModel);
364-
365-
Assert.hasText(apiKey, "ApiKey must not be null or empty");
366-
Assert.hasText(projectId, "ProjectId must not be null or empty");
367-
Assert.hasText(environment, "Environment must not be null or empty");
368-
Assert.hasText(indexName, "IndexName must not be null or empty");
369-
370425
this.apiKey = apiKey;
371426
this.projectId = projectId;
372427
this.environment = environment;
373428
this.indexName = indexName;
374429
}
375430

431+
/**
432+
* First step interface requiring API key configuration.
433+
*/
434+
public interface BuilderWithApiKey {
435+
436+
/**
437+
* Sets the Pinecone API key and moves to project ID configuration.
438+
* @param apiKey The Pinecone API key
439+
* @return The next builder step for project ID
440+
* @throws IllegalArgumentException if apiKey is null or empty
441+
*/
442+
BuilderWithProjectId apiKey(String apiKey);
443+
444+
}
445+
446+
/**
447+
* Second step interface requiring project ID configuration.
448+
*/
449+
public interface BuilderWithProjectId {
450+
451+
/**
452+
* Sets the project ID and moves to environment configuration.
453+
* @param projectId The Pinecone project ID
454+
* @return The next builder step for environment
455+
* @throws IllegalArgumentException if projectId is null or empty
456+
*/
457+
BuilderWithEnvironment projectId(String projectId);
458+
459+
}
460+
461+
/**
462+
* Third step interface requiring environment configuration.
463+
*/
464+
public interface BuilderWithEnvironment {
465+
466+
/**
467+
* Sets the environment and moves to index name configuration.
468+
* @param environment The Pinecone environment
469+
* @return The next builder step for index name
470+
* @throws IllegalArgumentException if environment is null or empty
471+
*/
472+
BuilderWithIndexName environment(String environment);
473+
474+
}
475+
476+
/**
477+
* Final step interface requiring index name configuration.
478+
*/
479+
public interface BuilderWithIndexName {
480+
481+
/**
482+
* Sets the index name and returns the builder for optional configuration.
483+
* @param indexName The Pinecone index name
484+
* @return The builder for optional configurations
485+
* @throws IllegalArgumentException if indexName is null or empty
486+
*/
487+
Builder indexName(String indexName);
488+
489+
}
490+
491+
/**
492+
* Internal implementation of the step builder pattern using records for
493+
* immutability. Each step maintains the state from previous steps and implements
494+
* the corresponding interface to ensure type safety and proper sequencing of the
495+
* build steps.
496+
*/
497+
public static class StepBuilder {
498+
499+
private record ApiKeyStep(EmbeddingModel embeddingModel) implements BuilderWithApiKey {
500+
@Override
501+
public BuilderWithProjectId apiKey(String apiKey) {
502+
Assert.hasText(apiKey, "ApiKey must not be null or empty");
503+
return new ProjectIdStep(embeddingModel, apiKey);
504+
}
505+
}
506+
507+
private record ProjectIdStep(EmbeddingModel embeddingModel, String apiKey) implements BuilderWithProjectId {
508+
@Override
509+
public BuilderWithEnvironment projectId(String projectId) {
510+
Assert.hasText(projectId, "ProjectId must not be null or empty");
511+
return new EnvironmentStep(embeddingModel, apiKey, projectId);
512+
}
513+
}
514+
515+
private record EnvironmentStep(EmbeddingModel embeddingModel, String apiKey,
516+
String projectId) implements BuilderWithEnvironment {
517+
@Override
518+
public BuilderWithIndexName environment(String environment) {
519+
Assert.hasText(environment, "Environment must not be null or empty");
520+
return new IndexNameStep(embeddingModel, apiKey, projectId, environment);
521+
}
522+
}
523+
524+
private record IndexNameStep(EmbeddingModel embeddingModel, String apiKey, String projectId,
525+
String environment) implements BuilderWithIndexName {
526+
@Override
527+
public Builder indexName(String indexName) {
528+
Assert.hasText(indexName, "IndexName must not be null or empty");
529+
return new Builder(embeddingModel, apiKey, projectId, environment, indexName);
530+
}
531+
}
532+
533+
/**
534+
* Initiates the step builder sequence with the embedding model.
535+
* @param embeddingModel The embedding model to use
536+
* @return The first step for API key configuration
537+
* @throws IllegalArgumentException if embeddingModel is null
538+
*/
539+
static BuilderWithApiKey start(EmbeddingModel embeddingModel) {
540+
Assert.notNull(embeddingModel, "EmbeddingModel must not be null");
541+
return new ApiKeyStep(embeddingModel);
542+
}
543+
544+
}
545+
376546
/**
377547
* Sets the Pinecone namespace. Note: The free-tier (gcp-starter) doesn't support
378548
* Namespaces.

vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreIT.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,12 @@ public static class TestApplication {
291291
@Bean
292292
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
293293
String apikey = System.getenv("PINECONE_API_KEY");
294-
return PineconeVectorStore
295-
.builder(embeddingModel, apikey, PINECONE_PROJECT_ID, PINECONE_ENVIRONMENT, PINECONE_INDEX_NAME)
294+
295+
return PineconeVectorStore.builder(embeddingModel)
296+
.apiKey(apikey)
297+
.projectId(PINECONE_PROJECT_ID)
298+
.environment(PINECONE_ENVIRONMENT)
299+
.indexName(PINECONE_INDEX_NAME)
296300
.namespace(PINECONE_NAMESPACE)
297301
.contentFieldName(CUSTOM_CONTENT_FIELD_NAME)
298302
.build();

vector-stores/spring-ai-pinecone-store/src/test/java/org/springframework/ai/vectorstore/pinecone/PineconeVectorStoreObservationIT.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,11 @@ public TestObservationRegistry observationRegistry() {
189189

190190
@Bean
191191
public VectorStore vectorStore(EmbeddingModel embeddingModel, ObservationRegistry observationRegistry) {
192-
return PineconeVectorStore
193-
.builder(embeddingModel, System.getenv("PINECONE_API_KEY"), PINECONE_PROJECT_ID, PINECONE_ENVIRONMENT,
194-
PINECONE_INDEX_NAME)
192+
return PineconeVectorStore.builder(embeddingModel)
193+
.apiKey(System.getenv("PINECONE_API_KEY"))
194+
.projectId(PINECONE_PROJECT_ID)
195+
.environment(PINECONE_ENVIRONMENT)
196+
.indexName(PINECONE_INDEX_NAME)
195197
.namespace(PINECONE_NAMESPACE)
196198
.contentFieldName(CUSTOM_CONTENT_FIELD_NAME)
197199
.observationRegistry(observationRegistry)

0 commit comments

Comments
 (0)