Skip to content

[SABRA-2456] Add V2 Facets and Searchabilities API Support#179

Merged
esezen merged 22 commits into
masterfrom
SABRA-2456/cio-java
Jun 23, 2026
Merged

[SABRA-2456] Add V2 Facets and Searchabilities API Support#179
esezen merged 22 commits into
masterfrom
SABRA-2456/cio-java

Conversation

@aandrukhovich

Copy link
Copy Markdown
Contributor

Summary

This PR adds support for the v2 versions of the facets and searchabilities APIs, which include additional fields and capabilities not available in v1.

Key Changes

New Model Classes:

  • FacetConfigurationV2 - v2 facet configuration with path_in_metadata field support
  • SearchabilityV2 - Searchability configuration model

New Request Classes:

  • FacetConfigurationV2Request - Single facet v2 operations
  • FacetConfigurationsV2Request - Bulk facet v2 operations
  • SearchabilityV2Request - Single searchability operations
  • SearchabilitiesV2Request - Bulk searchability PATCH operations
  • SearchabilitiesV2GetRequest - Searchability list operations with filtering/pagination
  • SearchabilitiesV2DeleteRequest - Bulk searchability deletion

New API Methods in ConstructorIO:

Endpoint Method
GET /v2/facets retrieveFacetConfigurationsV2()
GET /v2/facets/{name} retrieveFacetConfigurationV2()
POST /v2/facets createFacetConfigurationV2()
PUT /v2/facets/{name} replaceFacetConfigurationV2()
PATCH /v2/facets/{name} updateFacetConfigurationV2()
PATCH /v2/facets updateFacetConfigurationsV2()
PUT /v2/facets replaceFacetConfigurationsV2()
DELETE /v2/facets/{name} deleteFacetConfigurationV2()
GET /v2/searchabilities retrieveSearchabilitiesV2()
GET /v2/searchabilities/{name} retrieveSearchabilityV2()
PATCH /v2/searchabilities/{name} createOrUpdateSearchabilityV2()
PATCH /v2/searchabilities createOrUpdateSearchabilitiesV2()
DELETE /v2/searchabilities/{name} deleteSearchabilityV2()
DELETE /v2/searchabilities deleteSearchabilitiesV2()

v2 API Differences from v1

  • Facets v2: Requires path_in_metadata field which specifies where in item metadata the facet data is located
  • Searchabilities v2: New endpoints for managing searchability configurations with support for skip_rebuild parameter

Test Plan

  • Unit tests for all model and request classes
  • Integration tests for all v2 API methods
  • Verify facet CRUD operations work correctly
  • Verify searchability CRUD operations work correctly
  • Verify bulk operations work correctly
  • Verify pagination and filtering work for list operations

@aandrukhovich aandrukhovich changed the title Add v2 versions of facets and searchabilities [SABRA-2456] Add v2 facets and searchabilities Dec 30, 2025
@aandrukhovich aandrukhovich changed the title [SABRA-2456] Add v2 facets and searchabilities [SABRA-2456] Add V2 Facets and Searchabilities API Support Dec 30, 2025
@constructor-claude-bedrock

This comment has been minimized.

Copilot AI review requested due to automatic review settings February 25, 2026 23:30
@constructor-claude-bedrock

This comment has been minimized.

This comment was marked as outdated.

@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment was marked as outdated.

@esezen esezen requested a review from a team as a code owner May 12, 2026 19:55
constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java:3949

  • Same pagination precedence issue here: offset is only included when request.getPage() == null, but page is only added when page > 0. If page is set to 0/negative, neither page nor offset will be sent. Recommend gating offset on a valid page value (or validating page values up-front).
            if (request.getPage() != null && request.getPage() > 0) {
                urlBuilder.addQueryParameter("page", request.getPage().toString());
            }
            if (request.getNumResultsPerPage() != null && request.getNumResultsPerPage() > 0) {
                urlBuilder.addQueryParameter(
                        "num_results_per_page", request.getNumResultsPerPage().toString());
            }
            if (request.getOffset() != null
                    && request.getOffset() >= 0
                    && request.getPage() == null) {
                urlBuilder.addQueryParameter("offset", request.getOffset().toString());
            }

constructorio-client/src/test/java/io/constructor/client/FacetConfigurationV2Test.java:177

  • This assertion also hard-codes the default section string ("Products"). Consider asserting ConstructorIO.DEFAULT_SECTION instead for consistency and future-proofing.
        FacetConfigurationsV2Request request =
                new FacetConfigurationsV2Request(Arrays.asList(config));

        assertEquals(1, request.getFacetConfigurations().size());
        assertEquals("Products", request.getSection());
    }

constructor-claude-bedrock[bot]

This comment was marked as outdated.

constructor-claude-bedrock[bot]

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java:3853

  • Bulk facet v2 replace (replaceFacetConfigurationsV2) similarly skips validation of each facet's type (and potentially other required fields like path_in_metadata). Since this is a full replace (PUT), failing fast client-side with clear validation errors will be more consistent with createFacetConfigurationV2 / replaceFacetConfigurationV2 and avoid avoidable API rejections.
                            .addQueryParameter("section", facetConfigurationsV2Request.getSection())
                            .build();

            Map<String, Object> bodyMap = new HashMap<>();
            bodyMap.put("facets", facetConfigurationsV2Request.getFacetConfigurations());
            String params = new Gson().toJson(bodyMap);

@esezen esezen requested a review from a team May 14, 2026 13:14
esezen
esezen previously approved these changes Jun 4, 2026

@esezen esezen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need one more review

@esezen esezen requested a review from a team June 4, 2026 18:46
HHHindawy
HHHindawy previously approved these changes Jun 22, 2026

@HHHindawy HHHindawy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved, with a small suggestion 🙏

Also, @esezen looks like Quizzes tests are failing, but it's unrelated to this PR.

* @throws IllegalArgumentException if request is null
* @throws ConstructorException if the request fails
*/
public String replaceFacetConfigurationsV2(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we also run validateFacetConfigurationV2Type here on every facet configuration?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch, added it. replaceFacetConfigurationsV2 now runs validateFacetConfigurationV2Type on each facet configuration, matching the single replace/create endpoints (bulk replace is a full replace, so type is required). Left the PATCH update methods as-is since type is optional for partial updates. Added a test covering the invalid-type case. (commit 9d3eb34e)

esezen added 2 commits June 22, 2026 14:58
Run validateFacetConfigurationV2Type on each facet configuration in
replaceFacetConfigurationsV2 for parity with the single-replace and
create endpoints, since bulk replace is a full replace where type is
required.
@esezen esezen dismissed stale reviews from HHHindawy and themself via 9d3eb34 June 22, 2026 19:33

@constructor-claude-bedrock constructor-claude-bedrock Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This PR adds v2 Facets and Searchabilities API support with new model/request classes and 14 new methods in ConstructorIO. The implementation is generally well-structured with good test coverage, but there are several important issues around HTTP client consistency, Gson instantiation, and API design that should be addressed.

Inline comments: 9 discussions added

Overall Assessment: ⚠️ Needs Work

RequestBody.create(params, MediaType.parse("application/json; charset=utf-8"));
Request request = this.makeAuthorizedRequestBuilder().url(url).post(body).build();

Response response = client.newCall(request).execute();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: All mutating facet v2 methods (createFacetConfigurationV2, replaceFacetConfigurationV2, updateFacetConfigurationV2, updateFacetConfigurationsV2, replaceFacetConfigurationsV2, deleteFacetConfigurationV2) use client.newCall(...) instead of clientWithRetry.newCall(...). The read methods (retrieveFacetConfigurationsV2, retrieveFacetConfigurationV2) correctly use clientWithRetry. This is inconsistent with the existing v1 patterns (e.g., createFacetConfiguration, replaceFacetConfiguration) which also use client, but it's worth verifying whether mutating v2 endpoints benefit from retry-on-connection-failure or not. The v2 searchability write/delete methods also use client. If the intent is that only GETs use clientWithRetry, this should be documented; otherwise all methods should consistently use clientWithRetry.

.addQueryParameter("section", facetConfigurationV2Request.getSection())
.build();

String params = new Gson().toJson(facetConfigurationV2Request.getFacetConfiguration());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: new Gson() is instantiated inline on every single request call — there are 7 such instantiations across the new methods (createFacetConfigurationV2, replaceFacetConfigurationV2, updateFacetConfigurationV2, updateFacetConfigurationsV2, replaceFacetConfigurationsV2, createOrUpdateSearchabilitiesV2, deleteSearchabilitiesV2). Gson instances are thread-safe and expensive to construct. A shared static private static final Gson GSON = new Gson() already exists implicitly via the GSON_WITHOUT_SEARCHABILITY_NAME builder approach; just add a plain private static final Gson GSON = new Gson() constant and reuse it for all serialization, similar to the existing pre-existing GSON_WITHOUT_SEARCHABILITY_NAME pattern.

client.newBuilder().retryOnConnectionFailure(true).build();

/** Default section used when no section is specified */
public static final String DEFAULT_SECTION = "Products";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: DEFAULT_SECTION is declared public static final, making it part of the public API surface. If it needs to be public (it is referenced in the request classes and tests), that's fine, but consider whether this hardcoded string belongs as a constant in ConstructorIO or in a shared constants class. More importantly, the existing v1 code uses "Products" as a hardcoded string literal in multiple places without this constant — to maintain consistency either all callers should be updated to use DEFAULT_SECTION or this constant should stay private/package-private.


try {
HttpUrl url = this.makeUrl(Arrays.asList("v2", "facets"));
url =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The URL construction in createFacetConfigurationV2 uses a two-step pattern (assign url, then reassign url = url.newBuilder()...build()) while every other v2 method uses the single-chain pattern directly. This is inconsistent within the PR itself. The replaceFacetConfigurationV2, updateFacetConfigurationV2 etc. all use the single-chain form which is cleaner. Use the single-chain pattern here too:

HttpUrl url = this.makeUrl(Arrays.asList("v2", "facets"))
        .newBuilder()
        .addQueryParameter("section", facetConfigurationV2Request.getSection())
        .build();


List<FacetConfigurationV2> facetConfigurations =
facetConfigurationsV2Request.getFacetConfigurations();
if (facetConfigurations != null) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: In replaceFacetConfigurationsV2, the type validation loop is guarded by if (facetConfigurations != null) — meaning a null list silently passes validation and proceeds to build a request with a null body for the "facets" key. Compare with FacetConfigurationsV2Request's constructor, which already rejects a null list at construction time. If the list can never be null at this point, the null check is redundant noise. If it can be null (via setFacetConfigurations(null)), then calling replaceFacetConfigurationsV2 with a null list should throw IllegalArgumentException just like createFacetConfigurationV2 does for a missing type. Either remove the null guard and rely on the constructor invariant, or add an explicit null check and throw.

.addQueryParameter("section", facetConfigurationsV2Request.getSection())
.build();

Map<String, Object> bodyMap = new HashMap<>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The deleteSearchabilitiesV2 method builds its request body by iterating over request.getSearchabilityNames() and constructing Map<String, String> objects with a single "name" key per entry. While correct, this is unnecessarily verbose. A simpler and more readable alternative is to pass the list directly with a wrapper, or use an existing model. More pragmatically: the body creation pattern {"searchabilities": [{"name": "x"}, ...]} differs from how the PATCH bulk operation sends full objects. If the DELETE API truly only needs names (and not full objects), this is fine — but it's worth adding a comment explaining the body format.

})
.create();

private static final Set<String> VALID_FACET_V2_TYPES =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: VALID_FACET_V2_TYPES uses LinkedHashSet (ordered iteration), but a plain HashSet is sufficient here since the only operation performed is contains(). The existing v1 facet type validation uses "multiple".equals(type) || "hierarchical".equals(type) || "range".equals(type). Consider using Set.of("multiple", "hierarchical", "range") (Java 9+) or Collections.unmodifiableSet(new HashSet<>(...)) for a clearly immutable, unordered set with O(1) lookup. Also, updateFacetConfigurationsV2 (PATCH) deliberately skips type validation, while replaceFacetConfigurationsV2 (PUT) validates it — this asymmetry is correct for a partial-update vs full-replace distinction, but a brief comment explaining why PATCH skips type validation would improve readability.

@SerializedName("range_inclusive")
private String rangeInclusive;

@SerializedName("range_limits")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important Issue: rangeLimits is typed as List<Number> in FacetConfigurationV2, whereas the existing FacetConfiguration model uses List<String>. This inconsistency is surprising — if both models represent range limits from the same underlying API concept, they should be typed consistently. If the v2 API genuinely returns numeric values (integers/floats) while v1 returns strings, this needs a comment explaining the API contract difference. Also note that Number is an abstract type; the actual runtime type from Gson deserialization will be Double (for all JSON numbers), which could cause surprising behavior if callers cast to Integer.

public class ConstructorIOSearchabilityV2Test {

private static String token = System.getenv("TEST_API_TOKEN");
private static String apiKey = System.getenv("TEST_CATALOG_FACETS_V2_API_KEY");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Both ConstructorIOFacetConfigurationV2Test and ConstructorIOSearchabilityV2Test use the same env variable TEST_CATALOG_FACETS_V2_API_KEY for what is a Searchability test. This reuse is fine if a single test index supports both Facets v2 and Searchabilities v2, but the variable name is misleading for the Searchability test class. Consider renaming the env variable to something neutral like TEST_CATALOG_V2_API_KEY, or introducing a dedicated TEST_CATALOG_SEARCHABILITIES_V2_API_KEY secret for clarity. Also, the workflow file at .github/workflows/run-tests.yml only adds TEST_CATALOG_FACETS_V2_API_KEY — if a separate key is ever needed for searchability tests, it will need to be added there too.

@HHHindawy HHHindawy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

@esezen esezen merged commit 6003285 into master Jun 23, 2026
5 checks passed
@esezen esezen deleted the SABRA-2456/cio-java branch June 23, 2026 17:35
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.

4 participants