用Agent Builder构建地图搜索

APPLICATION Dec 6, 2024

你刚刚抵达一座新城市,订好酒店,放下行李,准备开始探索。但是首先要去哪里呢?每个人都赞不绝口的时尚咖啡店?偏僻的博物馆?您打开应用程序,开始搜索……很快发现自己淹没在一大堆不相关的结果中。当然,那家米其林星级餐厅看起来很棒,但它离这里有一小时的路程!

传统搜索引擎经常会错过拼图中的关键部分:你当前的位置。当完美地点就在附近时,谁愿意浪费宝贵的时间穿越城镇?

在这篇博文中,我们将展示如何构建一个无服务器、位置感知的搜索引擎,该搜索引擎优先考虑邻近性,就像谷歌地图一样!

无论你的应用程序是用于查找最近的咖啡店、博物馆、理发店、酒店、音乐会还是其他任何地方,我们都会指导你创建超本地搜索体验。这意味着:

  • 相关结果:不再需要筛选数英里之外的选项。
  • 满意的用户:人们可以在需要的地方找到他们需要的东西。

最好的部分是什么?你可以使用 Google Cloud 的 Agent Builder 构建它。它是无服务器的,因此无需管理基础设施,无需安装软件,也无需支付许可费。(顺便说一句……trivago 已经在使用它了!)

来源

让我们开始实践吧!我们将使用 Agent Builder 为理发店构建一个搜索系统。可以将其视为创建一个专用的搜索微服务,可通过 REST API 访问,并可与你的应用集成。

1、项目内容

Agent Builder 使用一种称为检索增强生成 (RAG) 的技术。这意味着它将检索引擎与高级语言理解模型相结合。此外,它是完全托管的,因此你可以获得所有花哨的功能(矢量数据库、嵌入模型等),而无需任何设置麻烦。

这将使我们能够做什么?

  • 通过 REST API 发送搜索查询。
  • 返回超本地化的结果。想象一下搜索“理发店”,并且只获得距离你所在位置 5 公里以内的结果(纬度:52.3,经度:21.1):

而且不仅仅是固定距离!此搜索会根据用户所在的位置进行调整。如果他们从其他位置搜索,结果会相应更改 [纬度:52.2,经度:20.9] :

这可确保用户无论身在何处都能获得最相关的结果。但请记住,在实施任何位置感知功能之前,必须获得用户的明确许可才能访问其位置数据。这不仅可以确保遵守隐私法规,还可以与用户建立信任。清楚地解释你的应用需要位置访问权限的原因,并允许用户控制共享其位置的方式和时间。在这里,我们假设你已经涵盖了这些内容,并且你的应用能够在发送搜索请求时提供参考位置。

好了,说得够多了,让我们开始动手吧!任何工程师都知道,没有数据就没有乐趣。因此,我们将首先为我们的理发店搜索应用程序生成一些合成数据。

为此,我们将启动 Vertex AI Colab Enterprise。这为我们在浏览器中提供了一个协作编码环境。可以将其视为一个功能强大的笔记本,我们可以在其中编写和运行代码、试验数据并模拟我们的搜索应用程序。

准备好编码了吗?开始吧!

2、生成随机位置

好的,让我们开始创建我们的理发店之城!第一步是在定义的地理区域内生成一些随机位置。它们将代表我们虚构的理发店的位置。

我们需要决定:

  • 区域:我们希望理发店存在在哪里?特定的城市、地区还是整个国家?
  • 位置数量:我们想要模拟多少家理发店?

一旦我们有了这些参数,我们就可以使用下面的代码在我们选择的区域内生成随机坐标(纬度和经度)。这些坐标将成为我们合成数据的基础。

import random
import dataclasses

@dataclasses.dataclass
class Location:
        latitude: float
        longitude: float

def generate_locations(number_of_locations, area_left_bottom_corner, area_right_top_corner):
    """
    Generates a list of random locations within a specified area.

    Args:
      number_of_locations: The number of locations to generate.
      area_left_bottom_corner: A Location object representing the bottom-left corner of the area.
      area_right_top_corner: A Location object representing the top-right corner of the area.

    Returns:
      A list of Location objects.
    """
    locations = []
    latitude_delta = area_right_top_corner.latitude - area_left_bottom_corner.latitude
    longitude_delta = area_right_top_corner.longitude - area_left_bottom_corner.longitude

    for _ in range(number_of_locations):
        new_location = Location(
            area_left_bottom_corner.latitude + latitude_delta * random.random(),
            area_left_bottom_corner.longitude + longitude_delta * random.random()
        )
        locations.append(new_location)

    return locations

我已经定义了我们的虚拟理发店景观的地理边界,以覆盖华沙:

areaLeftBottomCorner = Location(52.154996877313, 20.919926120993125);
areaRightTopCorner = Location(52.32715709266114, 21.13445849633365);

对于那些想要创建自己的虚拟城市的人,这里有一个关于如何获取定义该区域的坐标的快速提醒:

  • 转到 Google 地图:在浏览器中打开 map.google.com。
  • 导航到你的区域:缩放和平移,直到你看到所需的区域。
  • 单击以获取坐标:单击你想要标记区域一角的位置。纬度和经度将显示在屏幕底部的小框中。
  • 对另一个角落重复上述操作:对你所在区域的另一角执行相同操作。

现在我们已经定义了华沙边界,我们可以继续生成该区域内的随机位置。我们将生成 10,000 个位置:

no_of_locations = 10000
locations = generate_locations(no_of_locations, areaLeftBottomCorner, areaRightTopCorner);

3、生成理发店目录

太好了!我们有了随机坐标,但这些只是数字。为了使我们的数据更加真实和有用,我们将把这些坐标转换为实际地址。

如何实现?借助 Google Maps Geocoding API!这个方便的工具让我们可以获取纬度和经度值并将其转换为人类可读的地址。

想象一下,输入一个坐标如  (52.231958, 21.006725),然后返回一个地址如 Plac Defilad 1, 00-901 Warszawa, Poland。这对我们的搜索引擎来说更有帮助,对吧?

import requests

def get_address_from_coordinates(latitude, longitude, api_key):
  """
  Fetches the address for the given latitude and longitude using the Google Maps Geocoding API.

  Args:
    latitude: The latitude of the location.
    longitude: The longitude of the location.

  Returns:
    A string containing the formatted address, or None if the address could not be found.
  """
  geocode_url = f"https://maps.googleapis.com/maps/api/geocode/json?latlng={latitude},{longitude}&key={api_key}"
  response = requests.get(geocode_url)
  data = response.json()
  if data['status'] == 'OK':
    formatted_address = data['results'][0]['formatted_address']
    return formatted_address

与大多数 API 一样,Google Maps Geocoding API 需要 API 密钥才能跟踪和控制使用情况。将其视为访问服务时的唯一标识符。

以下是获取 API 密钥的方法:

  • 前往 Google Cloud Console:这是管理 Google Cloud 资源的中心枢纽。
  • 导航到 API 和服务部分:在这里可以找到 Google Cloud 提供的所有 API。
  • 选择 Google Maps Geocoding API:确保已为你的项目启用它。
  • 创建 API 密钥:你将找到创建新凭据的选项。生成 API 密钥并妥善保管!

获得 API 密钥后,你可以在向 Geocoding API 发出请求时将其包含在代码中。这允许 Google Cloud 验证你的请求并为你提供所需的地址信息。

是时候看看结果了。运行该代码将随机坐标转换为地址后,我们应该有一个如下所示的数据集:

接下来,我们将使用 Gemini 根据地址生成有创意且合适的名称。

想象一下,一家理发店位于“波兰华沙 Plac Defilad 1, 00–901” 。Gemini 可以建议使用“Defilada Barbers”或“Royal Cut”等名称。很酷,对吧?

import vertexai
from vertexai.generative_models import GenerativeModel, SafetySetting

vertexai.init(project="genai-app-builder", location="us-central1")
model = GenerativeModel(
        "gemini-1.5-flash-001",
    )

def generate_shop_name(address):
    """Generates a creative and relevant barber shop name based on the address.

    Args:
      address: The address of the barber shop.

    Returns:
      A string representing the generated barber shop name.
    """

    safety_settings = [
    SafetySetting(
        category=SafetySetting.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
        threshold=SafetySetting.HarmBlockThreshold.OFF
    ),
    SafetySetting(
        category=SafetySetting.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
        threshold=SafetySetting.HarmBlockThreshold.OFF
    ),
    SafetySetting(
        category=SafetySetting.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
        threshold=SafetySetting.HarmBlockThreshold.OFF
    ),
    SafetySetting(
        category=SafetySetting.HarmCategory.HARM_CATEGORY_HARASSMENT,
        threshold=SafetySetting.HarmBlockThreshold.OFF
    ),
    ]
    prompt = f" Generate exactly one creative and relevant name consisting of no more than 5 terms for a barber shop located at address {address}. The generated name should be suitable for use as a business name. DO NOT add any markdown characters."
    responses = model.generate_content(
        [prompt],
        stream=False,
        safety_settings = safety_settings
    )

    return responses.text

你明白了!将这些地址提供给 Gemini 后,我们应该会看到一些有创意的理发店名称弹出。输出可能如下所示:

现在,让我们将所有这些信息汇编成一个合适的理发店目录。在这里,我们将汇总我们生成的位置、地址和店铺名称。

关键在于:为了使这些数据与我们的搜索引擎(我们将使用 Agent Builder 构建)无缝协作,我们需要以特定方式构造 shop_address 属性。它需要是一个 JSON 对象,其中包含一个名为 address的键,用于保存实际的地址文本。

import json
import random
import datetime

def generate_shops(no_of_locations):
  """Generates JSON array of objects with user data and timestamps.

  Args:
    no_of_locations: The number of locations
  Returns:
    A JSON string representing an array of user objects.
  """
  data = []
  for i in range(no_of_locations):
    shop = {}  # Create an empty dictionary for each user
    shop["_id"] = f"shop_{i}"  # Add user_id as an attribute
    shop[f"shop_average_score"] = random.randrange(1, 5)
    shop["shop_latitude"] = locations[i].latitude
    shop["shop_longitude"] = locations[i].longitude
    shop["shop_address"] = {"address": get_address_from_coordinates(locations[i].latitude, locations[i].longitude)}
    shop["shop_name"] = generate_shop_name(shop["shop_address"])
    data.append(shop)
  return json.dumps(data, indent=2)

看看 shop_address 现在是一个 JSON 对象,其中嵌套了地址?此结构至关重要,因为它允许 Agent Builder 轻松识别和利用地址信息进行位置感知搜索。

是时候生成我们的理发店目录并将其保存到 JSONL 文件中了。此格式本质上是一个文本文件,其中每行都是一个有效的 JSON 对象,非常适合存储我们的结构化数据。

json_data = generate_shops(no_of_locations)

with open(f"shops_{no_of_locations}.json", "w") as f:
  f.write(json_data)

现在我们已经将理发店数据整齐地组织在 JSONL 文件中,是时候将其带入 BigQuery 了。

4、将数据导入BigQuery

你可能知道,BigQuery 是 Google Cloud 完全托管的无服务器数据仓库。它非常强大,可以处理各种数据,从结构化表格到 JSON 等半结构化格式。猜猜怎么着?它是我们理发店目录的完美归宿!

导入 JSONL 文件轻而易举。我们将使用 BigQuery 的 BigFrames 库,它允许我们使用熟悉的 Pandas DataFrames 与 BigQuery 进行交互。

import pandas as pd

# Load the JSON data from the file
with open(f"shops_{no_of_locations}.json", "r") as f:
  data = json.load(f)

# Create a Pandas DataFrame from the loaded data
df = pd.DataFrame(data)
print(df.head())  # Print the first 5 rows
import bigframes
import bigframes.pandas

BQ_TABLE_URI = f"{PROJECT_ID}.{BQ_DATASET_ID}.{BQ_TABLE_NAME}"

session = bigframes.connect(
    bigframes.BigQueryOptions(
        project=PROJECT_ID,
        location=BQ_LOCATION,
    )
)

df.to_gbq(BQ_TABLE_URI, if_exists="replace")

以下是它的要点:

  • 将 JSONL 文件读入 Pandas DataFrame:这给出在 Python 中,我们以结构化的方式表示我们的数据。
  • 使用 df.to_gbq() 将 DataFrame 加载到 BigQuery 中:此函数负责在 BigQuery 中创建新表(如果不存在)并使用 DataFrame 中的数据填充它。

就是这样!我们的理发店目录将整齐地保存在 BigQuery 中,随时可以进行索引和搜索。访问 BigQuery 预览你的表格!

虽然我必须逐步完成这些数据生成步骤以进​​行演示,但你可能已经准备好了数据目录。

5、使用Agent Builder构建搜索体验

这就是 Agent Builder 的真正优势所在。使用 BigQuery 中的数据,构建强大的搜索体验非常快。

以下是开始使用 Agent Builder 的方法:

  • 转到 Google Cloud 控制台:这是所有 Google Cloud 的指挥中心。
  • 查找 Agent Builder:可以在控制台的搜索栏中轻松搜索它。
  • 单击“创建应用程序”:这将启动创建新搜索应用程序的过程。

Agent Builder 非常灵活。它可以帮助你构建针对特定行业(如零售、媒体或医疗保健)进行微调的专业搜索应用程序。但对于我们的理发店搜索,我们将从通用搜索应用程序开始。这为我们提供了坚实的基础,并允许我们探索 Agent Builder 的核心功能。

好吧,让我们将搜索应用程序变为现实!

首先,我们需要给它起个名字。这是你在 Agent Builder 环境中识别应用程序的方式。选择一些描述性强且令人难忘的名字,例如 WarsawBarberSearchHyperlocalBarberFinder

接下来,你需要选择应用程序的托管位置以及索引数据的存储位置。这对于性能和数据驻留原因很重要。选择地理位置靠近用户的位置或数据主要所在的位置。

为应用程序命名并选择位置后,你就可以继续下一步:连接数据!

现在,让我们讨论一下 Agent Builder 如何组织数据。它使用“数据存储”——将它们视为专门设计用于保存你想要搜索的信息的容器。

选择 BigQuery 作为你想要索引的数据源:

快到了!现在是时候将 BigQuery 中的数据与 Agent Builder 中的新搜索应用程序连接起来了。

以下是我们需要做的:

  • 指定 BigQuery 表:告诉 Agent Builder 我们的理发店目录在 BigQuery 中的确切位置(数据集 ID 和表名)
  • 声明自定义结构:我们需要告诉 Agent Builder 我们使用自定义结构。这只是意味着我们没有使用为专门的搜索应用程序(如零售或医疗保健应用程序)设计的预定义架构。

通过提供这些信息,Agent Builder 可以了解在哪里可以找到我们的数据以及数据是如何组织的。

最后一步是微调 Agent Builder 理解和使用数据的方式。

我们可以这样做:

  • 标记要索引的属性:我们将告诉 Agent Builder 哪些属性对于搜索很重要(可索引)。
  • 控制响应内容:我们可以选择要包含在搜索结果中的属性(可检索)。这有助于我们保持响应简洁和相关。
  • 分配预定义属性:Agent Builder 有一些内置属性(关键属性),如果我们要使用其内置 UI,这些属性会很有用。例如,我们可以将 shop_name 分配给预定义的 title 属性。将其视为具有预定义关键属性的购物车模板,用于在搜索结果列表中呈现结果。

但这是最重要的部分:Agent Builder 自动识别出我们的 shop_address 属性属于“地理位置”类型,这对于构建我们的位置感知搜索至关重要。

单击“创建”按钮,Agent Builder 便开始运行。它会设置你的搜索应用程序,最重要的是,开始索引你的 BigQuery 表。Agent Builder 会在此分析你的数据、识别关键信息并对其进行组织,以便进行高效搜索。

我们的搜索微服务现已准备就绪!我们可以从内置 UI 中预览其工作方式:

我们的搜索微服务已准备就绪。最好的部分是什么?Agent Builder 提供了一个内置 UI,我们可以在其中预览其工作方式。

此 UI 是了解搜索应用程序行为的绝佳方式。你可以输入查询、查看结果,甚至可以探索不同设置如何影响搜索体验。

现在,我知道我们的理发店数据相当基础。我们有姓名、地​​址和分数,但没有太多富文本描述或图像。但想象一下,如果我们有关于每个理发店的更详细信息 — 比如提供的服务、客户评论、理发照片等。在这种情况下,此内置 UI 将是一个更强大的工具。它将使我们能够以最小的努力快速将功能齐全的搜索界面添加到我们的应用程序中。

现在,我们将使用内置 UI 快速预览我们的搜索引擎的运行情况。但请记住,此 UI 只是与你的搜索应用程序交互的一种方式。我们还将探索如何使用 REST API 以编程方式发送查询和检索结果,从而为你提供将搜索集成到应用程序中的最大灵活性。

由于该 shop_address 字段,我们的搜索应用程序现在在技术上可以感知位置。但我们需要做更多的事情才能在搜索时激活它。我们需要告诉 Agent Builder 在处理搜索查询时使用位置信息作为过滤器。

为此,我们将以编程方式与我们的搜索应用程序交互。我创建了一个名为 DatastoreService 的自定义类,其中包含搜索方法。此方法允许我们将搜索查询发送到我们的应用程序并指定各种参数,包括位置过滤器。

from google.auth import default, transport
import requests

class DatastoreService:
    def __init__(self):
        creds, project_id = default()
        auth_req = transport.requests.Request()  # Use google.auth here
        creds.refresh(auth_req)
        access_token = creds.token
        self.access_token = access_token

    def search(self, project_id, app_engine, query, distance = 100, ref_lat = 52.31089273830141, ref_lon = 21.117887809721072):
        # Define API endpoint and headers
        url = f"https://discoveryengine.googleapis.com/v1alpha/projects/{project_id}/locations/global/collections/default_collection/engines/{app_engine}/servingConfigs/default_search:search"
        headers = {
            "Authorization": f"Bearer {self.access_token}",
            "Content-Type": "application/json"
        }

        data = {
            "query": f"{query}",

            "pageSize": 50, ## how many results showuld be deisplayed
            "queryExpansionSpec": {"condition": "AUTO"},
            "spellCorrectionSpec": {"mode": "AUTO"},
            "contentSearchSpec": {
                "summarySpec": {},
                "snippetSpec": {"returnSnippet": False},
                "extractiveContentSpec": {"maxExtractiveAnswerCount": 1}  ## how many extractive answers per result
            },
            ##"filter": f"shop_address:GEO_DISTANCE(\"Elsnerów 01, Warszawa, Poland\", {distance})"
            "filter": f"shop_address:GEO_DISTANCE({ref_lat},{ref_lon}, {distance})"

        }

        # Make POST request
        response = requests.post(url, headers=headers, json=data)
        return response.json()

filter 属性是位置感知搜索的魔力所在发生了!

##"filter": f"shop_address:GEO_DISTANCE(\"Elsnerów 01, Warszawa, Poland\", {distance})"
"filter": f"shop_address:GEO_DISTANCE({ref_lat},{ref_lon}, {distance})"

让我们分解一下它的工作原理:

  • shop_address:GEO_DISTANCE(...):这告诉 Agent Builder 将地理距离过滤器应用于我们数据中的 shop_address 属性。
  • 指定位置的两种方法:我们可以以文本形式提供特定地址(如“Elsnerów 01,Warszawa,Poland”);或者,我们可以使用 ref_latref_lon 提供纬度和经度坐标。
  • distance:这是我们以公里为单位指定搜索半径的地方。

因此,如果我们发送距离为 10 公里的搜索查询,Agent Builder 将仅返回位于指定位置 10 公里半径范围内的理发店的结果:

6、在地图上可视化结果

为了证明它确实有效,让我们在地图上可视化结果!我非常喜欢 Bokeh 库。这是一个多功能的 Python 库,允许我们创建交互式图表,包括地图。它具有出色的工具,可用于在 Google 地图上显示自定义点,这正是我们所需要的。

!pip install bokeh
from bokeh.io import output_notebook
output_notebook()
bokeh_width, bokeh_height = 500,400

拥有一个专门用于在地图上绘制搜索结果的函数也很有帮助。

以下是我创建此辅助函数的方法:

函数名称: plot_search_results

输入参数:

  • search_term:查询字符串(例如,“理发店”、“理发”)
  • latitude:参考位置的纬度
  • longitude:参考位置的经度
  • distance:搜索半径(以米为单位)
from bokeh.models import ColumnDataSource
from bokeh.models import GMapOptions, HoverTool

def plot_within_distance(search_term = "Barber", distance = 1000, lat = 52.31 , lon = 21.11):
    search_service = DatastoreService()
    resp = search_service.search(PROJECT_ID, search_app_name, search_term, False, distance, lat, lon)

    points = []
    try:
      resp['results']
    except Exception:
      ##print("Results not found")
      return
    for r in resp['results']:
      a = {
          "id": r['document']['id'],
          "name": r['document']['structData']['shop_name'],
          "address": r['document']['structData']['shop_address']['address'],
          "lat": r['document']['structData']['shop_latitude'],
          "lon": r['document']['structData']['shop_longitude']
      }

      points.append(a)

    df = pd.DataFrame(points)
    pp = plot(df, points[0]['lat'], points[0]['lon'])

现在是游戏时间!现在我们已经准备好了所有东西——数据、搜索应用程序和可视化工具——我们终于可以看到我们的位置感知搜索引擎在运行了。

通过这种交互式探索,我们可以更深入地了解位置感知搜索引擎的行为方式,以及如何对其进行优化,以便为用户提供最相关、最有用的结果。希望你喜欢。在评论中分享它如何适用于你的用例!

7、结束语

在这篇博文中,我向你展示了如何使用 BigQuery 和 Google Cloud 的 Agent Builder 构建无服务器、位置感知的搜索引擎。

我首先强调了不相关搜索结果的常见挫败感,特别是当接近性是关键因素时。然后,我介绍了 Agent Builder 作为创建类似于 Google Maps 的自然语言、位置感知搜索体验的解决方案,并以 trivago 为成功示例。

我概述了使用我使用 Python 和 Google Maps API 生成的合成数据为理发店构建搜索系统的过程。然后,我将这些数据导入 BigQuery 并使用 Agent Builder 对其进行索引。我强调了使用 Agent Builder 设置搜索应用程序的简易性及其识别地理位置属性的能力。

最后,我演示了如何使用 REST API 以编程方式与搜索应用程序交互。最后,我通过在地图上可视化位置感知搜索结果来模拟最终应用程序。


原文链接:Serverless, Location-Aware Search for web and mobile apps with Agent Builder & BigQuery

汇智网翻译整理,转载请标明出处

Tags