Skip to content

Commit ff323f9

Browse files
committed
feat(retail): add Vertex AI Search for commerce snippets
- Introduce search_request, search_pagination, and search_offset samples. - Use the correct "Vertex AI Search for commerce" product naming. - Hardcode catalog and branch IDs to "default_catalog" and "default_branch" to maximize sample readability. - Include unit tests with mocks and a shared conftest.py. - Add a README.md detailing API prerequisites, ADC setup, and required IAM roles. - Ensure all samples and tests are PEP8 compliant and formatted with Black.
1 parent 3828909 commit ff323f9

10 files changed

Lines changed: 470 additions & 0 deletions

retail/snippets/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Vertex AI Search for commerce Samples
2+
3+
This directory contains Python samples for [Vertex AI Search for commerce](https://cloud.google.com/retail/docs/search-basic#search).
4+
5+
## Prerequisites
6+
7+
To run these samples, you must have:
8+
9+
1. **A Google Cloud Project** with the [Vertex AI Search for commerce API](https://console.cloud.google.com/apis/library/retail.googleapis.com) enabled.
10+
2. **Vertex AI Search for commerce** set up with a valid catalog and serving configuration (placement).
11+
3. **Authentication**: These samples use [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc).
12+
- If running locally, you can set up ADC by running:
13+
```bash
14+
gcloud auth application-default login
15+
```
16+
4. **IAM Roles**: The service account or user running the samples needs the `roles/retail.viewer` (Retail Viewer) role or higher.
17+
18+
## Samples
19+
20+
- **[search_request.py](search_request.py)**: Basic search request showing both text search and browse search (using categories).
21+
- **[search_pagination.py](search_pagination.py)**: Shows how to use `next_page_token` to paginate through search results.
22+
- **[search_offset.py](search_offset.py)**: Shows how to use `offset` to skip a specified number of results.
23+
24+
## Documentation
25+
26+
For more information, see the [Vertex AI Search for commerce documentation](https://docs.cloud.google.com/retail/docs/search-basic#search).

retail/snippets/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import os
2+
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def project_id() -> str:
8+
"""Get the Google Cloud project ID from the environment."""
9+
project_id = os.environ.get("BUILD_SPECIFIC_GCLOUD_PROJECT")
10+
if not project_id:
11+
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
12+
return project_id
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pytest
2+
pytest-xdist
3+
mock
4+
google-cloud-retail>=2.10.0
5+
google-api-core

retail/snippets/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
google-cloud-retail>=2.10.0

retail/snippets/search_offset.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# [START retail_v2_search_offset]
2+
import sys
3+
4+
from google.api_core import exceptions
5+
from google.cloud import retail_v2
6+
7+
client = retail_v2.SearchServiceClient()
8+
9+
10+
def search_offset(
11+
project_id: str,
12+
placement_id: str,
13+
visitor_id: str,
14+
query: str,
15+
offset: int,
16+
) -> None:
17+
"""Search for products with an offset using Vertex AI Search for commerce.
18+
19+
Performs a search request starting from a specified position.
20+
21+
Args:
22+
project_id: The Google Cloud project ID.
23+
placement_id: The placement name for the search.
24+
visitor_id: A unique identifier for the user.
25+
query: The search term.
26+
offset: The number of results to skip.
27+
"""
28+
placement_path = client.serving_config_path(
29+
project=project_id,
30+
location="global",
31+
catalog="default_catalog",
32+
serving_config=placement_id,
33+
)
34+
35+
branch_path = client.branch_path(
36+
project=project_id,
37+
location="global",
38+
catalog="default_catalog",
39+
branch="default_branch",
40+
)
41+
42+
request = retail_v2.SearchRequest(
43+
placement=placement_path,
44+
branch=branch_path,
45+
visitor_id=visitor_id,
46+
query=query,
47+
page_size=10,
48+
offset=offset,
49+
)
50+
51+
try:
52+
response = client.search(request=request)
53+
54+
print(f"--- Results for offset: {offset} ---")
55+
for result in response:
56+
product = result.product
57+
print(f"Product ID: {product.id}")
58+
print(f" Title: {product.title}")
59+
print(f" Scores: {result.model_scores}")
60+
61+
except exceptions.GoogleAPICallError as e:
62+
print(f"error: {e.message}", file=sys.stderr)
63+
64+
65+
# [END retail_v2_search_offset]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from unittest import mock
2+
3+
from google.cloud import retail_v2
4+
import pytest
5+
6+
from search_offset import search_offset
7+
8+
9+
@pytest.fixture
10+
def test_config(project_id):
11+
return {
12+
"project_id": project_id,
13+
"placement_id": "default_placement",
14+
"visitor_id": "test_visitor",
15+
}
16+
17+
18+
@mock.patch.object(retail_v2.SearchServiceClient, "search")
19+
def test_search_offset(mock_search, test_config, capsys):
20+
# Mock result
21+
mock_product = mock.Mock()
22+
mock_product.id = "product_at_offset"
23+
mock_product.title = "Offset Title"
24+
25+
mock_result = mock.Mock()
26+
mock_result.product = mock_product
27+
28+
mock_search.return_value = [mock_result]
29+
30+
search_offset(
31+
project_id=test_config["project_id"],
32+
placement_id=test_config["placement_id"],
33+
visitor_id=test_config["visitor_id"],
34+
query="test query",
35+
offset=10,
36+
)
37+
38+
out, _ = capsys.readouterr()
39+
assert "--- Results for offset: 10 ---" in out
40+
assert "Product ID: product_at_offset" in out
41+
42+
# Verify call request
43+
args, kwargs = mock_search.call_args
44+
request = kwargs.get("request") or args[0]
45+
assert request.offset == 10
46+
assert request.page_size == 10
47+
assert request.query == "test query"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# [START retail_v2_search_pagination]
2+
import sys
3+
4+
from google.api_core import exceptions
5+
from google.cloud import retail_v2
6+
7+
client = retail_v2.SearchServiceClient()
8+
9+
10+
def search_pagination(
11+
project_id: str,
12+
placement_id: str,
13+
visitor_id: str,
14+
query: str,
15+
) -> None:
16+
"""Search for products with pagination using Vertex AI Search for commerce.
17+
18+
Performs a search request, then uses the next_page_token to get the next page.
19+
20+
Args:
21+
project_id: The Google Cloud project ID.
22+
placement_id: The placement name for the search.
23+
visitor_id: A unique identifier for the user.
24+
query: The search term.
25+
"""
26+
placement_path = client.serving_config_path(
27+
project=project_id,
28+
location="global",
29+
catalog="default_catalog",
30+
serving_config=placement_id,
31+
)
32+
33+
branch_path = client.branch_path(
34+
project=project_id,
35+
location="global",
36+
catalog="default_catalog",
37+
branch="default_branch",
38+
)
39+
40+
# First page request
41+
first_request = retail_v2.SearchRequest(
42+
placement=placement_path,
43+
branch=branch_path,
44+
visitor_id=visitor_id,
45+
query=query,
46+
page_size=5,
47+
)
48+
49+
try:
50+
first_response = client.search(request=first_request)
51+
print("--- First Page ---")
52+
for result in first_response:
53+
print(f"Product ID: {result.product.id}")
54+
55+
next_page_token = first_response.next_page_token
56+
57+
if next_page_token:
58+
# Second page request using page_token
59+
second_request = retail_v2.SearchRequest(
60+
placement=placement_path,
61+
branch=branch_path,
62+
visitor_id=visitor_id,
63+
query=query,
64+
page_size=5,
65+
page_token=next_page_token,
66+
)
67+
second_response = client.search(request=second_request)
68+
print("\n--- Second Page ---")
69+
for result in second_response:
70+
print(f"Product ID: {result.product.id}")
71+
else:
72+
print("\nNo more pages.")
73+
74+
except exceptions.GoogleAPICallError as e:
75+
print(f"error: {e.message}", file=sys.stderr)
76+
77+
78+
# [END retail_v2_search_pagination]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
from unittest import mock
2+
3+
from google.cloud import retail_v2
4+
import pytest
5+
6+
from search_pagination import search_pagination
7+
8+
9+
@pytest.fixture
10+
def test_config(project_id):
11+
return {
12+
"project_id": project_id,
13+
"placement_id": "default_placement",
14+
"visitor_id": "test_visitor",
15+
}
16+
17+
18+
@mock.patch.object(retail_v2.SearchServiceClient, "search")
19+
def test_search_pagination(mock_search, test_config, capsys):
20+
# Mock first response
21+
mock_product_1 = mock.Mock()
22+
mock_product_1.id = "product_1"
23+
24+
mock_result_1 = mock.Mock()
25+
mock_result_1.product = mock_product_1
26+
27+
mock_first_response = mock.MagicMock()
28+
mock_first_response.next_page_token = "token_for_page_2"
29+
mock_first_response.__iter__.return_value = [mock_result_1]
30+
31+
# Mock second response
32+
mock_product_2 = mock.Mock()
33+
mock_product_2.id = "product_2"
34+
35+
mock_result_2 = mock.Mock()
36+
mock_result_2.product = mock_product_2
37+
38+
mock_second_response = mock.MagicMock()
39+
mock_second_response.next_page_token = ""
40+
mock_second_response.__iter__.return_value = [mock_result_2]
41+
42+
mock_search.side_effect = [mock_first_response, mock_second_response]
43+
44+
search_pagination(
45+
project_id=test_config["project_id"],
46+
placement_id=test_config["placement_id"],
47+
visitor_id=test_config["visitor_id"],
48+
query="test query",
49+
)
50+
51+
out, _ = capsys.readouterr()
52+
assert "--- First Page ---" in out
53+
assert "Product ID: product_1" in out
54+
assert "--- Second Page ---" in out
55+
assert "Product ID: product_2" in out
56+
57+
# Verify calls
58+
assert mock_search.call_count == 2
59+
60+
# Check first call request
61+
first_call_request = mock_search.call_args_list[0].kwargs["request"]
62+
assert first_call_request.page_size == 5
63+
assert not first_call_request.page_token
64+
65+
# Check second call request
66+
second_call_request = mock_search.call_args_list[1].kwargs["request"]
67+
assert second_call_request.page_size == 5
68+
assert second_call_request.page_token == "token_for_page_2"

retail/snippets/search_request.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# [START retail_v2_search_request]
2+
import sys
3+
from typing import List
4+
5+
from google.api_core import exceptions
6+
from google.cloud import retail_v2
7+
8+
client = retail_v2.SearchServiceClient()
9+
10+
11+
def search_request(
12+
project_id: str,
13+
placement_id: str,
14+
visitor_id: str,
15+
query: str = "",
16+
page_categories: List[str] = None,
17+
) -> None:
18+
"""Search for products using Vertex AI Search for commerce.
19+
20+
Performs a search request for a specific placement.
21+
Handles both text search (using query) and browse search (using page_categories).
22+
23+
Args:
24+
project_id: The Google Cloud project ID.
25+
placement_id: The placement name for the search.
26+
visitor_id: A unique identifier for the user.
27+
query: The search term for text search.
28+
page_categories: The categories for browse search.
29+
"""
30+
placement_path = client.serving_config_path(
31+
project=project_id,
32+
location="global",
33+
catalog="default_catalog",
34+
serving_config=placement_id,
35+
)
36+
37+
branch_path = client.branch_path(
38+
project=project_id,
39+
location="global",
40+
catalog="default_catalog",
41+
branch="default_branch",
42+
)
43+
44+
request = retail_v2.SearchRequest(
45+
placement=placement_path,
46+
branch=branch_path,
47+
visitor_id=visitor_id,
48+
query=query,
49+
page_categories=page_categories or [],
50+
page_size=10,
51+
)
52+
53+
try:
54+
response = client.search(request=request)
55+
56+
for result in response:
57+
product = result.product
58+
print(f"Product ID: {product.id}")
59+
print(f" Title: {product.title}")
60+
scores = dict(result.model_scores.items())
61+
print(f" Scores: {scores}")
62+
63+
except exceptions.GoogleAPICallError as e:
64+
print(f"error: {e.message}", file=sys.stderr)
65+
print(
66+
f"Troubleshooting Context: Project: {project_id}, Catalog: default_catalog",
67+
file=sys.stderr,
68+
)
69+
70+
71+
# [END retail_v2_search_request]

0 commit comments

Comments
 (0)