드디어 등장!? 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=ko

대략적인 절차

벡터 검색에 대해서는 별도의 기사를 작성할 예정이지만,
이번에는 벡터 검색에 사용할 벡터의 계산에 Google이 제공하는 “Vertex AI”를 사용합니다.
또한, 벡터 검색은 현재 Python 또는 JavaScript(Node.js)로만 수행할 수 있기 때문에,
이번에는 Google Cloud Function을 사용하여 검색을 수행합니다.

따라서, 검색을 수행하기 위해서는 다음과 같은 준비가 필요합니다.

  • Google Cloud Function의 생성
  • Vertex AI의 설정
  • 벡터의 획득
  • 인덱스의 생성
  • Firestore에서의 벡터 검색 실행 코드

각각의 절차를 자세히 설명합니다.

Google Cloud Function의 생성

이번에 생성한 Cloud Functions의 환경은 다음과 같습니다.

  • 2nd gen(제2세대)
  • https 트리거
  • Python3.12

또한 런타임 환경 변수에 다음을 설정했습니다.

 이름:GOOGLE_CLOUD_PROJECT
 값 :<프로젝트 ID>(프로젝트 이름이 아님에 주의)

Vertex AI는 Cloud Run과 연동해야 하기 때문에, GCF 생성 시 Cloud Run이 생성되는,
2nd gen(제2세대)를 사용하고 있습니다.

Google이 제공하는 Vertex AI의 설정

Function의 Cloud Run에 Vertex AI를 연동합니다.
(참고 공식:https://cloud.google.com/run/docs/integrate/vertex-ai?authuser=3&hl=ko

참고로 여기에도 절차를 올려두겠습니다.

  1. 오른쪽에 있는 “Powered by Cloud Run” 아래의 링크를 클릭하고, Cloud Run으로 이동
  2. “통합” 탭을 클릭
  3. “통합 추가”를 클릭
  4. “Vertex AI – 생성 AI”를 클릭하고, 임의의 이름을 설정하여 “submit”을 클릭
    ※단, 이름은 어느 정도 규칙에 맞는 이름이 아니면 오류가 발생합니다.
     특히 구애가 없으면 초기값 그대로 OK입니다.
  5. 권한 등의 추가 요청이 있을 경우 승인

벡터 검색에 사용할 벡터 계산

이번에는 소유자 권한으로 실행했기 때문에 권한 추가 등을 수행하지 않았지만,
앱 개발 시 등은 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:

    # location은 각자의 로케이션을 설정
    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.제목
1freezed.dart가 생성되지 않을 때의 대처 방법 freezed를 사용하여 이뮤터블한 클래스를 설계할 때, 터미널에서 「…
2Flutter의 pubspec.yaml이란? 의미와 작성 방법을 소개!! YAML은 YAML Ain’t Markup Language의 약어로, 데이터를 간결하게 표현하는 …
3앱 개발에서 자주 듣는 MVVM이란 무엇인가? MVVM(Model-View-ViewModel)이란, 앱의 로직과 UI(사용자 인터페이스)를 나누어, 개발의 효율성과 유지 보수성을 높이는
4Flutter란?? Flutter의 개요를 설명합니다. 「Flutter」에 대해서, “모바일 앱을 개발할 때 편리!”라는 인식은 있을지 모르지만, 무엇이 편리하고 무엇이 인기를 끌고 있는 것일까요?
5Riverpod이란? Flutter의 가장 메이저한 상태 관리를 소개!! 이전에 소개한 「StatefulWidget」도 상태 관리를 수행하는 기능의 하나이지만, 여러 화면이나 기능을 갖춘 앱을 구현하는 경우, 관리

실행한 코드는 다음과 같습니다.

import functions_framework
import os
# firestore
from google.cloud import 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:

    # location은 각자의 로케이션을 설정
    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 테스트 명령을 실행하여 동작을 확인했습니다.
「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": "Riverpod에 대해"
}'

실행 결과

Description 
--------------------------------------------------
Riverpod이란? Flutter의 가장 메이저한 상태 관리를 소개!! 이전에 소개한 「StatefulWidget」도 상태 관리를 수행하는 기능의 하나이지만, 여러 화면이나 기능을 갖춘 앱을 구현하는 경우, 관리 
Flutter란?? Flutter의 개요를 설명합니다. 「Flutter」에 대해서, "모바일 앱을 개발할 때 편리!"라는 인식은 있을지 모르지만, 무엇이 편리하고 무엇이 인기를 끌고 있는 것일까요? 
앱 개발에서 자주 듣는 MVVM이란 무엇인가? MVVM(Model-View-ViewModel)이란, 앱의 로직과 UI(사용자 인터페이스)를 나누어, 개발의 효율성과 유지 보수성을 높이는 것

특히 정렬은 하지 않았지만, 첫 번째로 Riverpod 기사가 나왔습니다!!
이번 데이터에서는 두 번째와 세 번째에 대해, 가까운 것이 선택되었는지 판단할 수 없었습니다.

마지막으로

좀 더 데이터 양을 늘려서 검증하는 것이 검색 정확도를 검증할 수 있을 것 같습니다.
벡터 데이터 크기는 float를 4바이트로 하면, 이번 경우 768차원이므로 768✕4≒3KB입니다.
문서의 제한이 1MB이므로 조금 크게 느껴집니다.

Firestore에서 전면 검색이 가능해진 것은, 아직 프리뷰 버전이지만 좋은 소식입니다.
앞으로의 동향도 기대하고 싶습니다.

제목과 URL을 복사했습니다