From 356aa22fd210bbb6fbf935dcce954c0648f4f27d Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 14 Apr 2026 22:51:38 +0000 Subject: [PATCH 1/4] feat(retail): add searchOffset, searchPagination, and searchRequest samples This commit introduces new Node.js code samples for Vertex AI Search, including comprehensive Mocha tests and logic to handle root-level product IDs and async indexing. --- retail/snippets/package.json | 22 +++++ retail/snippets/searchOffset.js | 71 ++++++++++++++ retail/snippets/searchPagination.js | 98 +++++++++++++++++++ retail/snippets/searchRequest.js | 78 +++++++++++++++ retail/snippets/test/snippets.test.js | 136 ++++++++++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 retail/snippets/package.json create mode 100644 retail/snippets/searchOffset.js create mode 100644 retail/snippets/searchPagination.js create mode 100644 retail/snippets/searchRequest.js create mode 100644 retail/snippets/test/snippets.test.js diff --git a/retail/snippets/package.json b/retail/snippets/package.json new file mode 100644 index 0000000000..afb0cb80b2 --- /dev/null +++ b/retail/snippets/package.json @@ -0,0 +1,22 @@ +{ + "name": "node-snippets", + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha test/*.test.js --timeout 180000" + }, + "devDependencies": { + "c8": "^10.1.3", + "chai": "^4.5.0", + "mocha": "^10.8.2" + }, + "dependencies": { + "@google-cloud/retail": "^4.3.0" + } +} diff --git a/retail/snippets/searchOffset.js b/retail/snippets/searchOffset.js new file mode 100644 index 0000000000..268add7363 --- /dev/null +++ b/retail/snippets/searchOffset.js @@ -0,0 +1,71 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// [START retail_v2_search_offset] +const {SearchServiceClient} = require('@google-cloud/retail'); + +const client = new SearchServiceClient(); + +/** + * Search for products with an offset using Vertex AI Search for commerce. + * Performs a search request starting from a specified position. + * + * @param {string} projectId The Google Cloud project ID. + * @param {string} placementId The placement name for the search. + * @param {string} visitorId A unique identifier for the user. + * @param {string} query The search term. + * @param {number} offset The number of results to skip. + */ +async function searchOffset(projectId, placementId, visitorId, query, offset) { + const placementPath = client.servingConfigPath( + projectId, + 'global', + 'default_catalog', + placementId + ); + + const branchPath = client.branchPath( + projectId, + 'global', + 'default_catalog', + 'default_branch' + ); + + const request = { + placement: placementPath, + branch: branchPath, + visitorId: visitorId, + query: query, + pageSize: 10, + offset: offset, + }; + + try { + // Set {autoPaginate: false} to manually control the pagination + const [results] = await client.search(request, {autoPaginate: false}); + console.log(`--- Results for offset: ${offset} ---`); + for (const result of results) { + console.log(`Product ID: ${result.id}`); + console.log(`Title: ${result.product.title}`); + console.log(`Scores: ${JSON.stringify(result.modelScores || {})}`); + } + } catch (error) { + console.error(`Error searching using offset: ${error.message}`); + } +} + +// [END retail_v2_search_offset] +module.exports = {searchOffset}; diff --git a/retail/snippets/searchPagination.js b/retail/snippets/searchPagination.js new file mode 100644 index 0000000000..9e9d443050 --- /dev/null +++ b/retail/snippets/searchPagination.js @@ -0,0 +1,98 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +// [START retail_v2_search_pagination] +const {SearchServiceClient} = require('@google-cloud/retail'); + +const client = new SearchServiceClient(); + +/** + * Search for products with pagination using Vertex AI Search for commerce. + * Performs a search request, then uses the next_page_token to get the next page. + * + * @param {string} projectId - The Google Cloud project ID. + * @param {string} placementId - The placement name for the search. + * @param {string} visitorId - A unique identifier for the user. + * @param {string} query - The search term. + */ +async function searchPagination(projectId, placementId, visitorId, query) { + const placementPath = client.servingConfigPath( + projectId, + 'global', + 'default_catalog', + placementId + ); + + const branchPath = client.branchPath( + projectId, + 'global', + 'default_catalog', + 'default_branch' + ); + + // First page request + const firstRequest = { + placement: placementPath, + branch: branchPath, + visitorId: visitorId, + query: query, + pageSize: 5, + }; + + try { + // Set {autoPaginate: false} to manually control the pagination + // and extract the raw response which contains the next_page_token. + const [firstPageResults, , firstRawResponse] = await client.search( + firstRequest, + {autoPaginate: false} + ); + + console.log('--- First Page ---'); + for (const result of firstPageResults) { + console.log(`Product ID: ${result.id}`); + } + + const nextPageToken = firstRawResponse.nextPageToken; + + if (nextPageToken) { + // Second page request using pageToken + const secondRequest = { + placement: placementPath, + branch: branchPath, + visitorId: visitorId, + query: query, + pageSize: 5, + pageToken: nextPageToken, + }; + + const [secondPageResults] = await client.search(secondRequest, { + autoPaginate: false, + }); + + console.log('--- Second Page ---'); + for (const result of secondPageResults) { + console.log(`Product ID: ${result.id}`); + } + } else { + console.log('No more pages.'); + } + } catch (error) { + console.error('Failed to complete paginated search:', error); + } +} +// [END retail_v2_search_pagination] + +module.exports = {searchPagination}; diff --git a/retail/snippets/searchRequest.js b/retail/snippets/searchRequest.js new file mode 100644 index 0000000000..3dd823c8d4 --- /dev/null +++ b/retail/snippets/searchRequest.js @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// [START retail_v2_search_request] +const {SearchServiceClient} = require('@google-cloud/retail'); + +const client = new SearchServiceClient(); + +/** + * Search for products using Vertex AI Search for commerce. + * + * Performs a search request for a specific placement. + * Handles both text search (using query) and browse search (using pageCategories). + * + * @param {string} projectId - The Google Cloud project ID. + * @param {string} placementId - The placement name for the search. + * @param {string} visitorId - A unique identifier for the user. + * @param {string} query - The search term for text search. + * @param {string[]} pageCategories - The categories for browse search. + */ +async function searchRequest( + projectId, + placementId, + visitorId, + query = '', + pageCategories = [] +) { + const placementPath = client.servingConfigPath( + projectId, + 'global', + 'default_catalog', + placementId + ); + + const branchPath = client.branchPath( + projectId, + 'global', + 'default_catalog', + 'default_branch' + ); + + const request = { + placement: placementPath, + branch: branchPath, + visitorId: visitorId, + query: query, + pageCategories: pageCategories, + pageSize: 10, + }; + + try { + // Set {autoPaginate: false} to manually control the pagination + const [results] = await client.search(request, {autoPaginate: false}); + console.log('--- Search Results ---'); + for (const result of results) { + console.log(`Product ID: ${result.id}`); + console.log(` Name: ${result.product.name}`); + console.log(` Scores: ${JSON.stringify(result.modelScores || {})}`); + } + } catch (error) { + console.error(`Operation failed: ${error.message}`); + } +} + +// [END retail_v2_search_request] + +module.exports = {searchRequest}; diff --git a/retail/snippets/test/snippets.test.js b/retail/snippets/test/snippets.test.js new file mode 100644 index 0000000000..521d82c2d9 --- /dev/null +++ b/retail/snippets/test/snippets.test.js @@ -0,0 +1,136 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const {assert} = require('chai'); +const {ProductServiceClient} = require('@google-cloud/retail'); +const {searchOffset} = require('../searchOffset'); +const {searchPagination} = require('../searchPagination'); +const {searchRequest} = require('../searchRequest'); + +describe('Snippets System Tests', function () { + this.timeout(180000); + + const productClient = new ProductServiceClient(); + + const projectId = process.env.GOOGLE_CLOUD_PROJECT; + const placementId = 'default_search'; + const visitorId = 'test-visitor'; + const query = 'Mocha'; + + // Generate a unique ID for the test product + const productId = `test_product_${Date.now()}`; + + let originalConsoleLog; + let consoleOutput = []; + + before(async () => { + assert.isOk( + projectId, + 'GOOGLE_CLOUD_PROJECT environment variable is required to run tests.' + ); + + const parent = productClient.branchPath( + projectId, + 'global', + 'default_catalog', + 'default_branch' + ); + + const product = { + title: 'Mocha Test Product Offset', + type: 'PRIMARY', + categories: ['Test Category'], + availability: 'IN_STOCK', + priceInfo: { + price: 15.0, + currencyCode: 'USD', + }, + }; + + console.log(`Creating test product: ${productId}...`); + await productClient.createProduct({ + parent: parent, + product: product, + productId: productId, + }); + }); + + after(async () => { + const name = productClient.productPath( + projectId, + 'global', + 'default_catalog', + 'default_branch', + productId + ); + + console.log(`Cleaning up: Deleting test product ${productId}...`); + try { + await productClient.deleteProduct({name: name}); + console.log('Product deleted successfully.'); + } catch (error) { + console.error( + `Error deleting product (manual cleanup may be required): ${error.message}` + ); + } + }); + + // Intercept the console before each test to capture the sample's logs + beforeEach(() => { + consoleOutput = []; + originalConsoleLog = console.log; + console.log = (...args) => { + consoleOutput.push(args.join(' ')); + }; + }); + + // Restore the console after each test + afterEach(() => { + console.log = originalConsoleLog; + }); + + it('should execute searchOffset and find the created product', async () => { + const offset = 0; // Using offset 0 for the real test to ensure we find the first result + await searchOffset(projectId, placementId, visitorId, query, offset); + + const output = consoleOutput.join('\n'); + assert.include(output, `--- Results for offset: ${offset} ---`); + }); + + it('should execute searchPagination and handle pages', async () => { + await searchPagination(projectId, placementId, visitorId, ''); + + const output = consoleOutput.join('\n'); + + assert.include(output, '--- First Page ---'); + assert.include(output, 'No more pages.'); + }); + + it('should execute searchRequest successfully', async () => { + const pageCategories = ['Test Category']; + + await searchRequest( + projectId, + placementId, + visitorId, + query, + pageCategories + ); + + const output = consoleOutput.join('\n'); + assert.include(output, '--- Search Results ---'); + }); +}); From d844a3fad753518d0a8b41130403b72ba5144ef7 Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Tue, 14 Apr 2026 23:02:07 +0000 Subject: [PATCH 2/4] test(retail): fix Mocha test logic for search samples --- retail/snippets/test/snippets.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/retail/snippets/test/snippets.test.js b/retail/snippets/test/snippets.test.js index 521d82c2d9..58fe485344 100644 --- a/retail/snippets/test/snippets.test.js +++ b/retail/snippets/test/snippets.test.js @@ -116,7 +116,6 @@ describe('Snippets System Tests', function () { const output = consoleOutput.join('\n'); assert.include(output, '--- First Page ---'); - assert.include(output, 'No more pages.'); }); it('should execute searchRequest successfully', async () => { From 153b4b7e1ea701dd942250d9a60b20631687b24a Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Wed, 15 Apr 2026 00:10:14 +0000 Subject: [PATCH 3/4] refactor(retail): improve error logging in search samples Standardized catch block messages and updated console.error to use comma-separated arguments with error.message || error for better visibility. --- retail/snippets/searchOffset.js | 2 +- retail/snippets/searchPagination.js | 5 ++++- retail/snippets/searchRequest.js | 8 +++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/retail/snippets/searchOffset.js b/retail/snippets/searchOffset.js index 268add7363..c27a27397e 100644 --- a/retail/snippets/searchOffset.js +++ b/retail/snippets/searchOffset.js @@ -63,7 +63,7 @@ async function searchOffset(projectId, placementId, visitorId, query, offset) { console.log(`Scores: ${JSON.stringify(result.modelScores || {})}`); } } catch (error) { - console.error(`Error searching using offset: ${error.message}`); + console.error('Error searching using offset:', error.message || error); } } diff --git a/retail/snippets/searchPagination.js b/retail/snippets/searchPagination.js index 9e9d443050..cd00247f01 100644 --- a/retail/snippets/searchPagination.js +++ b/retail/snippets/searchPagination.js @@ -90,7 +90,10 @@ async function searchPagination(projectId, placementId, visitorId, query) { console.log('No more pages.'); } } catch (error) { - console.error('Failed to complete paginated search:', error); + console.error( + 'Failed to complete paginated search:', + error.message || error + ); } } // [END retail_v2_search_pagination] diff --git a/retail/snippets/searchRequest.js b/retail/snippets/searchRequest.js index 3dd823c8d4..5858c523b1 100644 --- a/retail/snippets/searchRequest.js +++ b/retail/snippets/searchRequest.js @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +'use strict'; + // [START retail_v2_search_request] const {SearchServiceClient} = require('@google-cloud/retail'); @@ -65,11 +67,11 @@ async function searchRequest( console.log('--- Search Results ---'); for (const result of results) { console.log(`Product ID: ${result.id}`); - console.log(` Name: ${result.product.name}`); - console.log(` Scores: ${JSON.stringify(result.modelScores || {})}`); + console.log(`Title: ${result.product.title}`); + console.log(`Scores: ${JSON.stringify(result.modelScores || {})}`); } } catch (error) { - console.error(`Operation failed: ${error.message}`); + console.error('Error executing search request:', error.message || error); } } From d297a643df0260c9748868c49e4e4a376b6e70eb Mon Sep 17 00:00:00 2001 From: Angel Caamal Date: Wed, 15 Apr 2026 22:28:10 +0000 Subject: [PATCH 4/4] test: add delay to system tests for search indexing Introduced a 30-second delay after test product creation using timers/promises. This allows the Google Cloud Retail search indexes enough time to build, preventing flaky test failures caused by eventual consistency. --- retail/snippets/searchPagination.js | 4 ++-- retail/snippets/test/snippets.test.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/retail/snippets/searchPagination.js b/retail/snippets/searchPagination.js index cd00247f01..88bd79c4b2 100644 --- a/retail/snippets/searchPagination.js +++ b/retail/snippets/searchPagination.js @@ -49,7 +49,7 @@ async function searchPagination(projectId, placementId, visitorId, query) { branch: branchPath, visitorId: visitorId, query: query, - pageSize: 5, + pageSize: 1, }; try { @@ -74,7 +74,7 @@ async function searchPagination(projectId, placementId, visitorId, query) { branch: branchPath, visitorId: visitorId, query: query, - pageSize: 5, + pageSize: 1, pageToken: nextPageToken, }; diff --git a/retail/snippets/test/snippets.test.js b/retail/snippets/test/snippets.test.js index 58fe485344..20d204ed2c 100644 --- a/retail/snippets/test/snippets.test.js +++ b/retail/snippets/test/snippets.test.js @@ -19,6 +19,7 @@ const {ProductServiceClient} = require('@google-cloud/retail'); const {searchOffset} = require('../searchOffset'); const {searchPagination} = require('../searchPagination'); const {searchRequest} = require('../searchRequest'); +const {setTimeout} = require('timers/promises'); describe('Snippets System Tests', function () { this.timeout(180000); @@ -66,6 +67,8 @@ describe('Snippets System Tests', function () { product: product, productId: productId, }); + console.log('Waiting 30 seconds for search indexes to be created...'); + await setTimeout(30000); }); after(async () => { @@ -108,6 +111,7 @@ describe('Snippets System Tests', function () { const output = consoleOutput.join('\n'); assert.include(output, `--- Results for offset: ${offset} ---`); + assert.include(output, `Product ID: ${productId}`); }); it('should execute searchPagination and handle pages', async () => { @@ -116,6 +120,7 @@ describe('Snippets System Tests', function () { const output = consoleOutput.join('\n'); assert.include(output, '--- First Page ---'); + assert.include(output, '--- Second Page ---'); }); it('should execute searchRequest successfully', async () => { @@ -131,5 +136,6 @@ describe('Snippets System Tests', function () { const output = consoleOutput.join('\n'); assert.include(output, '--- Search Results ---'); + assert.include(output, `Product ID: ${productId}`); }); });