终于登场!?Firestore的全文搜索(向量搜索)

一直以来,我们只能依赖第三方服务来实现Firestore的全文搜索功能,
现在终于可以直接在Firestore中进行全文搜索了!!

这是一种通过向量搜索实现的全文搜索,本次将介绍其实现步骤。

前提

本次介绍的功能目前处于预览版。
正式发布后,下面的代码可能无法执行。
参考)基于向量嵌入的搜索:https://firebase.google.com/docs/firestore/vector-search

本次使用的Function或Vertex AI可能会产生使用费。
使用前请先行确认。
参考)Vertex AI:https://cloud.google.com/vertex-ai/generative-ai/pricing?hl=zh-cn

大致步骤

关于向量搜索,我们计划另撰一篇文章进行介绍,
但本次我们将使用Google提供的“Vertex AI”来计算用于向量搜索的向量。
此外,目前向量搜索仅支持Python或JavaScript(Node.js),
因此,我们将使用Google Cloud Function来执行搜索。

因此,为了进行搜索,需要以下准备工作:

  • 创建Google Cloud Function
  • 配置Vertex AI
  • 获取向量
  • 创建索引
  • 执行Firestore的向量搜索代码

我们将逐一详细介绍这些步骤。

创建Google Cloud Function

本次创建的Cloud Functions环境如下:

  • 2nd gen(第二代)
  • https触发器
  • Python3.12

我们还在运行时环境变量中设置了以下内容:

  • 名称:GOOGLE_CLOUD_PROJECT
  • 值:\<项目ID>(注意不是项目名称)

由于Vertex AI需要与Cloud Run绑定,因此在创建GCF时,会同时创建Cloud Run,我们使用了第二代(2nd gen)。

Google提供的Vertex AI设置

我们将Function的Cloud Run与Vertex AI进行绑定。
(参考官方文档:https://cloud.google.com/run/docs/integrate/vertex-ai?authuser=3&hl=zh-cn

以下是步骤:

  • 点击右侧“Powered by Cloud Run”下的链接,跳转至Cloud Run
  • 点击“集成”标签
  • 点击“添加集成”
  • 选择“Vertex AI – 生成AI”,设置任意名称后点击“提交”
    ※但是,如果名称不符合一定的规则,可能会出现错误。
    如果没有特别的偏好,使用默认值即可。
  • 如果需要添加权限,按提示操作

计算用于向量搜索的向量

本次,我们以拥有者权限执行,因此没有进行权限添加等操作,
但在应用开发时,可能需要给执行Function的账户分配Vertex AI或Firestore的权限。

以下是使用Vertex AI计算向量并将数据存储到Firestore的代码。
(参考官方文档:https://firebase.google.com/docs/firestore/vector-search)

functions-framework==3.*
google-cloud-firestore
google-cloud-aiplatform

import functions_framework
import os
# firestore
from google.cloud import firestore
from google.cloud.firestore_v1.vector import Vector
# Vertex AI
import vertexai
from vertexai.language_models import TextEmbeddingModel

# 项目名(从环境变量获取)
MY_PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT")

# 计算传入字符串的向量值
def text_embedding(text: str) -> list:

    # 设置自己的位置
    vertexai.init(project=MY_PROJECT_ID, location="asia-northeast1")

    # 使用当前最新的AI模型“textembedding-gecko@003”进行向量计算
    model = TextEmbeddingModel.from_pretrained("textembedding-gecko@003")
    embeddings = model.get_embeddings([text])
    for embedding in embeddings:
        vector = embedding.values

    return Vector(vector)

# 主处理
# (函数名随意)
@functions_framework.http
def hello_http(request):

    # 从请求中获取文章摘要(description)
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'description' in request_json:
        description = request_json['description']
    elif request_args and 'description' in request_args:
        description = request_args['description']
    else:
        description = 'World'

    # 初始化Firestore客户端
    firestore_client = firestore.Client(project=MY_PROJECT_ID)
    # 引用集合(集合名字任意(如果没有集合请提前创建))
    collection = firestore_client.collection("article_collection")

    # 计算embedding
    embedding_vector = text_embedding(description)

    # 准备添加到Firestore的文档
    doc = {
        "description": description,
        "embedding_field": embedding_vector
    }
    # 添加文档
    collection.add(doc)

    return 'OK!'

为了简便,我们通过终端执行CLI测试命令来验证功能。

curl -m 70 -X POST https://asia-northeast1-python-tool-001.cloudfunctions.net/vector_chenge \
  -H "Authorization: bearer $(gcloud auth print-identity-token)" \
  -H "Content-Type: application/json" \
  -d '{ "description": "<任意字符串>"}'

如果执行成功,您应该会看到Firestore中存储了如下数据:

创建向量搜索的索引

似乎创建索引对于向量搜索是必需的。
本次我们通过控制台执行以下命令创建了索引。
(参考官方文档:https://firebase.google.com/docs/firestore/vector-search)

gcloud alpha firestore indexes composite create \
  --collection-group=article_collection \
  --query-scope=COLLECTION \
  --field-config field-path=embedding_field,vector-config='{"dimension":"768", "flat": "{}"}' \
  --database=<数据库ID>
  • collection-group:创建索引的集合名称
  • query-scope:创建索引的范围,可以指定多个集合(集合组)等。
  • field-path:存储向量的字段名称
  • vector-config:在dimension中设置向量的维度数(本次为768维)
  • database:指定目标数据库的ID。如果是default,则不需要此设置

执行后,Firestore中将创建如下索引:

实施Firestore的向量搜索代码

数据准备就绪后,我们实际进行搜索。

本次,我们准备了我的博客文章的摘要作为搜索目标数据。
全文太长,因此这里只展示了部分内容。

No.タイトル
1What to do when freezed.dart is not created When designing an immutable class using freezed, you may get a terminal “…
2What is Flutter’s pubspec.yaml? What it means and how to write it! YAML stands for YAML Ain’t Markup Language, a concise representation of data…
3What is MVVM, which we often hear about in app development? MVVM (Model-View-ViewModel) is the combination of an app’s logic and UI (user interface)…
4What is Flutter? An overview of Flutter What is Flutter? Flutter” is “useful for developing mobile apps…
5What is Riverpod, Flutter’s most major state management introduction! StatefulWidget, which we introduced before, is one of the functions that performs state management…

执行的代码如下:

import functions_framework
import os
# firestore
from google.cloud firestore
from google.cloud.firestore_v1.vector import Vector
from google.cloud.firestore_v1.base_vector_query import DistanceMeasure
# Vertex AI
import vertexai
from vertexai.language_models import TextEmbeddingModel

# 项目名(从环境变量获取)
MY_PROJECT_ID = os.environ.get("GOOGLE_CLOUD_PROJECT")

# 计算传入字符串的向量值
def text_embedding(text: str) -> list:

    # 设置自己的位置
    vertexai.init(project=MY_PROJECT_ID, location="asia-northeast1")

    # 使用当前最新的AI模型“textembedding-gecko@003”进行向量计算
    model = TextEmbeddingModel.from_pretrained("textembedding-gecko@003")
    embeddings = model.get_embeddings([text])
    for embedding in embeddings:
        vector = embedding.values

    return Vector(vector)

# 主处理
# (函数名随意)
@functions_framework.http
def hello_http(request):

    # 从请求中获取文章摘要(description)
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'target' in request_json:
        target = request_json['target']
    elif request_args and 'target' in request_args:
        target = request_args['target']
    else:
        target = 'World'

    # 初始化Firestore客户端
    firestore_client = firestore.Client(project=MY_PROJECT_ID)
    # 引用集合
    collection = firestore_client.collection("article_collection")

    # 计算embedding
    embedding_vector = text_embedding(target)

    # 执行向量搜索
    docs = collection.find_nearest(
        vector_field="embedding_field",
        query_vector=embedding_vector,
        distance_measure=DistanceMeasure.COSINE,
        limit=3
    ).get()

    # 以表格形式(这里用字符串形式)输出
    output = "Description \n"
    output += "-" * 50 + "\n"
    
    # 输出通过向量搜索获取的文档内容
    for doc in docs:
        doc_data = doc.to_dict()
        description = doc_data.get("description", "No description")
        # 将文档内容添加到字符串
        output += f"{description[:100]} \n"
    
    return output

我们也通过终端执行CLI测试命令来验证功能。
尝试搜索“About Riverpod”!

curl -m 70 -X POST https://asia-northeast1-python-tool-001.cloudfunctions.net/vector_search 
-H "Authorization: bearer $(gcloud auth print-identity-token)" 
-H "Content-Type: application/json" 
-d '{
  "target": "About Riverpod"
}'

执行结果

Description 
--------------------------------------------------
What is Riverpod, Flutter's most major state management introduction! StatefulWidget, which we introduced before, is one of the functions that performs state management…
What is Flutter? An overview of Flutter What is Flutter? Flutter" is "useful for developing mobile apps…
What is MVVM, which we often hear about in app development? MVVM (Model-View-ViewModel) is the combination of an app's logic and UI (user interface)…

虽然没有特别进行排序,但第一条就是关于Riverpod的文章!!
在本次的数据中,第二条和第三条是否为相关文章,我们无法判断。

最后

如果增加数据量,可能会更好地验证搜索精度。
考虑到向量数据的大小,如果按照float类型4字节计算,本次768维的情况下大约为768×4≈3KB。
由于文档限制为1MB,因此这个大小感觉还是有点大。

Firestore能够实现全文搜索,虽然目前还是预览版,但这是个好消息。
我们期待未来的发展。

タイトルとURLをコピーしました