1. 概览
在第一个代码实验室中,您将上传存储分区中的图片。这将生成一个文件创建事件,该事件将由某个函数处理。该函数将调用 Vision API 进行图片分析,并将结果保存在数据存储区中。

学习内容
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。



- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时更新。
- 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常情况下,您无需关注该字符串。在大多数 Codelab 中,您都需要引用项目 ID(通常用
PROJECT_ID标识)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且此 ID 在项目期间会一直保留。 - 此外,还有第三个值,即部分 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。若要关闭资源以避免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloud Shell
虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。
在 Google Cloud 控制台 中,点击右上角工具栏中的 Cloud Shell 图标:

预配和连接到环境应该只需要片刻时间。完成后,您应该会看到如下内容:

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5 GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。您在此 Codelab 中的所有工作都可以在浏览器中完成。您无需安装任何程序。
3. 启用 API
在本实验中,您将使用 Cloud Functions 和 Vision API,但首先需要在 Cloud 控制台中或使用 gcloud 启用它们。
如需在 Cloud 控制台中启用 Vision API,请在搜索栏中搜索 Cloud Vision API:

您将进入 Cloud Vision API 页面:

点击 ENABLE 按钮。
或者,您也可以使用 gcloud 命令行工具在 Cloud Shell 中启用该 API。
在 Cloud Shell 中,运行以下命令:
gcloud services enable vision.googleapis.com
您应该会看到操作已成功完成:
Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.
同时启用 Cloud Functions:
gcloud services enable cloudfunctions.googleapis.com
4. 创建存储分区(控制台)
创建一个存储分区来存储照片。您可以通过 Google Cloud Platform 控制台 ( console.cloud.google.com) 或使用 gsutil 命令行工具从 Cloud Shell 或本地开发环境中执行此操作。
前往“存储”
在“汉堡”菜单 (☰) 中,前往 Storage 页面。

为存储桶命名
点击 CREATE BUCKET 按钮。

点击 CONTINUE。
选择位置

在您选择的区域(此处为 Europe)中创建一个多区域存储分区。
点击 CONTINUE。
选择默认存储类别

为数据选择 Standard 存储类别。
点击 CONTINUE。
设置访问权限控制

由于您将使用可公开访问的图片,因此您希望存储在此存储分区中的所有图片都具有相同的统一访问权限控制。
选择 Uniform 访问权限控制选项。
点击 CONTINUE。
设置保护/加密

保留默认值 (Google-managed key)),因为您不会使用自己的加密密钥。
点击 CREATE,最终完成存储分区创建。
将 allUsers 添加为存储空间查看者
前往 Permissions 标签页:

向存储分区添加 allUsers 成员,并为其分配 Storage > Storage Object Viewer 角色,如下所示:

点击 SAVE。
5. 创建存储分区 (gsutil)
您还可以使用 Cloud Shell 中的 gsutil 命令行工具来创建存储分区。
在 Cloud Shell 中,为唯一的存储分区名称设置变量。Cloud Shell 已将 GOOGLE_CLOUD_PROJECT 设置为您的唯一项目 ID。您可以将其附加到存储分区名称中。
例如:
export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}
在欧洲创建标准多区域可用区:
gsutil mb -l EU gs://${BUCKET_PICTURES}
确保统一存储分区级访问权限:
gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}
公开该存储桶:
gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}
如果您前往控制台的 Cloud Storage 部分,应该会看到一个公开的 uploaded-pictures 存储分区:

测试您是否可以向存储分区上传图片,以及上传的图片是否公开可用,如上一步中所述。
6. 测试对存储分区的公开访问权限
返回存储分区浏览器,您会在列表中看到自己的存储分区,其中显示“公开”访问权限(包括一个警告标志,提醒您任何人都可以访问该存储分区的内容)。

您的存储分区现在可以接收图片了。
点击相应存储分区名称后,您会看到该存储分区的详细信息。

您可以在此处尝试点击 Upload files 按钮,测试是否可以将图片添加到相应存储分区。系统会显示一个文件选择器弹出式窗口,要求您选择一个文件。选择后,该文件将上传到您的存储分区,您将再次看到自动归因于此新文件的 public 访问权限。

在 Public 访问权限标签旁边,您还会看到一个小链接图标。点击该链接后,浏览器会前往相应图片的公开网址,该网址的格式如下:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
其中 BUCKET_NAME 是您为存储分区选择的全局唯一名称,后面是图片的相应文件名。
点击图片名称旁边的复选框后,DELETE 按钮会变为启用状态,您可以删除此第一张图片。
7. 创建函数
在此步骤中,您将创建一个对图片上传事件做出反应的函数。
访问 Google Cloud 控制台的 Cloud Functions 部分。访问该页面后,Cloud Functions 服务会自动启用。

点击 Create function。
选择名称(例如,picture-uploaded)和地区(请务必与存储分区的地区选择保持一致):

函数有两种类型:
- 可通过网址(即 Web API)调用的 HTTP 函数,
- 可由某些事件触发的后台函数。
您想创建一个后台函数,当新文件上传到 Cloud Storage 存储分区时触发该函数:

您对 Finalize/Create 事件类型感兴趣,该事件类型是指在存储分区中创建或更新文件时触发的事件:

选择之前创建的存储分区,以告知 Cloud Functions 在此特定存储分区中创建 / 更新文件时接收通知:

点击 Select 选择您之前创建的存储分区,然后点击 Save

在点击“下一步”之前,您可以展开并修改运行时、构建、连接和安全设置下的默认设置(256 MB 内存),并将其更新为 1 GB。

点击 Next 后,您可以调整运行时、源代码和入口点。
保留此函数的 Inline editor:

选择一个 Java 运行时,例如 Java 11:

源代码包含一个 Java 文件和一个提供各种元数据和依赖项的 pom.xml Maven 文件。
保留默认的代码段:它会记录上传的照片的文件名:

目前,出于测试目的,请将要执行的函数的名称保留为 Example。
点击 Deploy 以创建并部署函数。部署成功后,您应该会在函数列表中看到一个带有绿色圆圈的对勾标记:

8. 测试函数
在此步骤中,测试该函数是否会响应存储事件。
从“汉堡”菜单 (☰) 中,返回到 Storage 页面。
点击“图片”存储分区,然后点击 Upload files 上传图片。

在 Cloud 控制台中再次导航,前往 Logging > Logs Explorer 页面。
在 Log Fields 选择器中,选择 Cloud Function 以查看专门针对您的函数的日志。向下滚动浏览“日志字段”,您甚至可以选择特定函数,以便更精细地查看与函数相关的日志。选择 picture-uploaded 函数。
您应该会看到提及函数创建、函数开始和结束时间以及实际日志语句的日志项:

我们的日志语句显示:Processing file: pic-a-daily-architecture-events.png,这意味着与创建和存储此图片相关的事件确实已按预期触发。
9. 准备数据库
您将使用 Vision API 提供的图片信息存储到 Cloud Firestore 数据库中,该数据库是一种快速、全托管式、无服务器、云原生的 NoSQL 文档数据库。前往 Cloud 控制台的 Firestore 部分,准备数据库:

提供了两个选项:Native mode 或 Datastore mode。使用原生模式,该模式提供离线支持和实时同步等额外功能。
点击 SELECT NATIVE MODE。

选择一个多区域(此处为欧洲,但最好至少与您的函数和存储分区位于同一区域)。
点击 CREATE DATABASE 按钮。
创建数据库后,您应该会看到以下内容:

点击 + START COLLECTION 按钮,创建新的合集。
命名集合 pictures。

您无需创建文档。您将以编程方式添加这些标签,因为新图片会存储在 Cloud Storage 中并由 Vision API 进行分析。
点击 Save。
Firestore 会在新创建的集合中创建第一个默认文档,您可以放心地删除该文档,因为它不包含任何有用信息:

我们将在集合中以编程方式创建的文档将包含 4 个字段:
- name(字符串):上传的照片的文件名,也是相应文档的键
- 标签(字符串数组):Vision API 识别出的商品的标签
- color(字符串):主色的十六进制颜色代码(例如,#ab12ef)
- created(日期):相应图片元数据的存储时间戳
- thumbnail(布尔值):一个可选字段,如果已为此图片生成缩略图,则此字段会显示并为 true
由于我们将在 Firestore 中搜索有缩略图的图片,并按创建日期进行排序,因此需要创建搜索索引。
您可以在 Cloud Shell 中使用以下命令创建索引:
gcloud firestore indexes composite create \
--collection-group=pictures \
--field-config field-path=thumbnail,order=descending \
--field-config field-path=created,order=descending
您也可以在 Cloud 控制台中执行此操作,方法是点击左侧导航列中的 Indexes,然后创建复合索引,如下所示:

点击 Create。创建索引可能需要几分钟时间。
10. 更新函数
返回到 Functions 页面,更新函数以调用 Vision API 来分析我们的图片,并将元数据存储在 Firestore 中。
在“汉堡”菜单 (☰) 中,前往 Cloud Functions 部分,点击函数名称,选择 Source 标签页,然后点击 EDIT 按钮。
首先,修改列出 Java 函数依赖项的 pom.xml 文件。更新代码以添加 Cloud Vision API Maven 依赖项:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cloudfunctions</groupId>
<artifactId>gcs-function</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>1.0.4</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-firestore</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-vision</artifactId>
</dependency>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
</dependency>
</dependencies>
<!-- Required for Java 11 functions in the inline editor -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<excludes>
<exclude>.google/</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
现在,依赖项已更新到最新版本,接下来,您将通过使用自定义代码更新 Example.java 文件来处理函数的代码。
将鼠标移至 Example.java 文件上,然后点击铅笔图标。将软件包名称和文件名替换为 src/main/java/fn/ImageAnalysis.java。
将 ImageAnalysis.java 中的代码替换为以下代码。我们将在下一步中对此进行说明。
package fn;
import com.google.cloud.functions.*;
import com.google.cloud.vision.v1.*;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.cloud.firestore.*;
import com.google.api.core.ApiFuture;
import java.io.*;
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import java.util.logging.Logger;
import fn.ImageAnalysis.GCSEvent;
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
private static final Logger logger = Logger.getLogger(ImageAnalysis.class.getName());
@Override
public void accept(GCSEvent event, Context context)
throws IOException, InterruptedException, ExecutionException {
String fileName = event.name;
String bucketName = event.bucket;
logger.info("New picture uploaded " + fileName);
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
List<AnnotateImageRequest> requests = new ArrayList<>();
ImageSource imageSource = ImageSource.newBuilder()
.setGcsImageUri("gs://" + bucketName + "/" + fileName)
.build();
Image image = Image.newBuilder()
.setSource(imageSource)
.build();
Feature featureLabel = Feature.newBuilder()
.setType(Type.LABEL_DETECTION)
.build();
Feature featureImageProps = Feature.newBuilder()
.setType(Type.IMAGE_PROPERTIES)
.build();
Feature featureSafeSearch = Feature.newBuilder()
.setType(Type.SAFE_SEARCH_DETECTION)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(featureLabel)
.addFeatures(featureImageProps)
.addFeatures(featureSafeSearch)
.setImage(image)
.build();
requests.add(request);
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
if (responses.size() == 0) {
logger.info("No response received from Vision API.");
return;
}
AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
logger.info("Error: " + response.getError().getMessage());
return;
}
List<String> labels = response.getLabelAnnotationsList().stream()
.map(annotation -> annotation.getDescription())
.collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
logger.info("- " + label);
}
String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
ColorInfo colorInfo = colorsAnn.getColors(0);
mainColor = rgbHex(
colorInfo.getColor().getRed(),
colorInfo.getColor().getGreen(),
colorInfo.getColor().getBlue());
logger.info("Color: " + mainColor);
}
boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();
isSafe = Stream.of(
safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
safeSearch.getSpoof(), safeSearch.getViolence())
.allMatch( likelihood ->
likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
);
logger.info("Safe? " + isSafe);
}
// Saving result to Firestore
if (isSafe) {
FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
Firestore pictureStore = firestoreOptions.getService();
DocumentReference doc = pictureStore.collection("pictures").document(fileName);
Map<String, Object> data = new HashMap<>();
data.put("labels", labels);
data.put("color", mainColor);
data.put("created", new Date());
ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());
logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}
}
}
private static String rgbHex(float red, float green, float blue) {
return String.format("#%02x%02x%02x", (int)red, (int)green, (int)blue);
}
public static class GCSEvent {
String bucket;
String name;
}
}

11. 探索函数
让我们仔细看看各个有趣的部分。
首先,我们在 Maven pom.xml 文件中添加特定依赖项。Google Java 客户端库发布了 Bill-of-Materials(BOM),以消除任何依赖项冲突。使用此功能后,您无需为各个 Google 客户端库指定任何版本
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.1.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后,我们为 Vision API 准备一个客户端:
...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...
现在,我们来了解一下函数的结构。我们从传入的事件中捕获感兴趣的字段,并将它们映射到我们定义的 GCSEvent 结构:
...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
@Override
public void accept(GCSEvent event, Context context)
throws IOException, InterruptedException,
ExecutionException {
...
public static class GCSEvent {
String bucket;
String name;
}
请注意签名,以及我们如何检索触发 Cloud Functions 函数的文件和存储分区的名称。
以下是事件载荷的示例,供您参考:
{
"bucket":"uploaded-pictures",
"contentType":"image/png",
"crc32c":"efhgyA==",
"etag":"CKqB956MmucCEAE=",
"generation":"1579795336773802",
"id":"uploaded-pictures/Screenshot.png/1579795336773802",
"kind":"storage#object",
"md5Hash":"PN8Hukfrt6C7IyhZ8d3gfQ==",
"mediaLink":"https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/Screenshot.png?generation=1579795336773802&alt=media",
"metageneration":"1",
"name":"Screenshot.png",
"selfLink":"https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/Screenshot.png",
"size":"173557",
"storageClass":"STANDARD",
"timeCreated":"2020-01-23T16:02:16.773Z",
"timeStorageClassUpdated":"2020-01-23T16:02:16.773Z",
"updated":"2020-01-23T16:02:16.773Z"
}
我们准备通过 Vision 客户端发送的请求:
ImageSource imageSource = ImageSource.newBuilder()
.setGcsImageUri("gs://" + bucketName + "/" + fileName)
.build();
Image image = Image.newBuilder()
.setSource(imageSource)
.build();
Feature featureLabel = Feature.newBuilder()
.setType(Type.LABEL_DETECTION)
.build();
Feature featureImageProps = Feature.newBuilder()
.setType(Type.IMAGE_PROPERTIES)
.build();
Feature featureSafeSearch = Feature.newBuilder()
.setType(Type.SAFE_SEARCH_DETECTION)
.build();
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
.addFeatures(featureLabel)
.addFeatures(featureImageProps)
.addFeatures(featureSafeSearch)
.setImage(image)
.build();
我们要求 Vision API 具备 3 项关键功能:
- 标签检测:了解这些图片中的内容
- 图片属性:提供图片的有趣属性(我们感兴趣的是图片的主色)
- 安全搜索:了解图片是否可以安全显示(不应包含成人 / 医疗 / 少儿不宜 / 暴力内容)
此时,我们可以调用 Vision API:
...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result =
vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...
以下是 Vision API 的响应示例,供您参考:
{
"faceAnnotations": [],
"landmarkAnnotations": [],
"logoAnnotations": [],
"labelAnnotations": [
{
"locations": [],
"properties": [],
"mid": "/m/01yrx",
"locale": "",
"description": "Cat",
"score": 0.9959855675697327,
"confidence": 0,
"topicality": 0.9959855675697327,
"boundingPoly": null
},
✄ - - - ✄
],
"textAnnotations": [],
"localizedObjectAnnotations": [],
"safeSearchAnnotation": {
"adult": "VERY_UNLIKELY",
"spoof": "UNLIKELY",
"medical": "VERY_UNLIKELY",
"violence": "VERY_UNLIKELY",
"racy": "VERY_UNLIKELY",
"adultConfidence": 0,
"spoofConfidence": 0,
"medicalConfidence": 0,
"violenceConfidence": 0,
"racyConfidence": 0,
"nsfwConfidence": 0
},
"imagePropertiesAnnotation": {
"dominantColors": {
"colors": [
{
"color": {
"red": 203,
"green": 201,
"blue": 201,
"alpha": null
},
"score": 0.4175916016101837,
"pixelFraction": 0.44456374645233154
},
✄ - - - ✄
]
}
},
"error": null,
"cropHintsAnnotation": {
"cropHints": [
{
"boundingPoly": {
"vertices": [
{ "x": 0, "y": 118 },
{ "x": 1177, "y": 118 },
{ "x": 1177, "y": 783 },
{ "x": 0, "y": 783 }
],
"normalizedVertices": []
},
"confidence": 0.41695669293403625,
"importanceFraction": 1
}
]
},
"fullTextAnnotation": null,
"webDetection": null,
"productSearchResults": null,
"context": null
}
如果没有返回错误,我们可以继续操作,这就是我们使用此 if 块的原因:
AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
logger.info("Error: " + response.getError().getMessage());
return;
}
我们将获取图片中识别出的事物、类别或主题的标签:
List<String> labels = response.getLabelAnnotationsList().stream()
.map(annotation -> annotation.getDescription())
.collect(Collectors.toList());
logger.info("Annotations found:");
for (String label: labels) {
logger.info("- " + label);
}
我们希望了解图片的主色调:
String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
DominantColorsAnnotation colorsAnn =
imgProps.getDominantColors();
ColorInfo colorInfo = colorsAnn.getColors(0);
mainColor = rgbHex(
colorInfo.getColor().getRed(),
colorInfo.getColor().getGreen(),
colorInfo.getColor().getBlue());
logger.info("Color: " + mainColor);
}
我们还使用了一个实用函数,将红色 / 绿色 / 蓝色值转换为可在 CSS 样式表中使用的十六进制颜色代码。
我们来检查一下图片是否可以安全显示:
boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
SafeSearchAnnotation safeSearch =
response.getSafeSearchAnnotation();
isSafe = Stream.of(
safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
safeSearch.getSpoof(), safeSearch.getViolence())
.allMatch( likelihood ->
likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
);
logger.info("Safe? " + isSafe);
}
我们会检查成人 / 恶搞 / 医疗 / 暴力 / 少儿不宜属性,看看它们是否不太可能或非常可能。
如果安全搜索的结果没问题,我们可以将元数据存储在 Firestore 中:
if (isSafe) {
FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
Firestore pictureStore = firestoreOptions.getService();
DocumentReference doc = pictureStore.collection("pictures").document(fileName);
Map<String, Object> data = new HashMap<>();
data.put("labels", labels);
data.put("color", mainColor);
data.put("created", new Date());
ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());
logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}
12. 部署函数
部署函数所需的时间。

点击 DEPLOY 按钮,新版本将部署,您可以看到进度:

13. 再次测试该函数
成功部署函数后,您将向 Cloud Storage 发布一张图片,看看我们的函数是否会被调用、Vision API 会返回什么内容,以及元数据是否会存储在 Firestore 中。
返回 Cloud Storage,然后点击我们在实验开始时创建的存储分区:

进入存储分区详情页面后,点击 Upload files 按钮上传图片。

在“汉堡”菜单 (☰) 中,前往 Logging > Logs 探索器。
在 Log Fields 选择器中,选择 Cloud Function 以查看专门针对您的函数的日志。向下滚动浏览“日志字段”,您甚至可以选择特定函数,以便更精细地查看与函数相关的日志。选择 picture-uploaded 函数。

事实上,在日志列表中,我可以看到我们的函数已被调用:

日志会指明函数执行的开始和结束时间。在两者之间,我们可以看到使用 console.log() 语句放入函数中的日志。我们看到:
- 触发函数的事件的详细信息,
- Vision API 调用的原始结果,
- 我们上传的图片中找到的标签,
- 主色信息,
- 图片是否可以安全显示,
- 最终,有关相应图片的所有元数据都已存储在 Firestore 中。

再次从“汉堡”菜单 (☰) 中前往 Firestore 部分。在 Data 子部分(默认显示)中,您应该会看到 pictures 集合中添加了一个新文档,该文档与您刚刚上传的图片相对应:

14. 清理(可选)
如果您不打算继续学习本系列中的其他实验,可以清理资源,以节省费用,并践行良好的云资源管理实践。您可以按如下方式逐个清理资源。
删除存储分区:
gsutil rb gs://${BUCKET_PICTURES}
删除函数:
gcloud functions delete picture-uploaded --region europe-west1 -q
通过从集合中选择“删除集合”来删除 Firestore 集合:

或者,您也可以删除整个项目:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
15. 恭喜!
恭喜!您已成功实现项目的第一个关键服务!
所学内容
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore