빠른 시작

빠른 시작

Aeca를 SDK를 설치하고 사용 방법에 대한 내용을 설명합니다.

Aeca와 Python SDK 설치

pip를 통해 SDK를 설치합니다.

pip install aeca

이후 Docker를 통해 Aeca server를 실행합니다.

mkdir data
docker run --rm -it --name aeca \
    -p 10080:10080 -v $(pwd)/data:/app/data \
    aeca/aeca-server server

실행하면 다음과 같이 10080 포트를 통해 동작할 준비가 되었습니다.

실행 결과
Aeca v1.1.0

[2024-04-17T08:08:01.138] [general] [info] [1] Initializing gRPC service...
[2024-04-17T08:08:01.138] [general] [info] [1] Initializing KeyValueDBService...
[2024-04-17T08:08:01.138] [general] [info] [1] KeyValueDBService has been successfully initialized.
[2024-04-17T08:08:01.138] [general] [info] [1] Initializing DocumentDBService...
[2024-04-17T08:08:01.138] [general] [debug] [159] Long-running query monitor is enabled.
[2024-04-17T08:08:01.138] [general] [info] [1] DocumentDBService has been successfully initialized.
[2024-04-17T08:08:01.138] [general] [info] [1] Initializing FTSAnalysisPipelineService...
[2024-04-17T08:08:01.138] [general] [info] [1] FTSAnalysisPipelineService has been successfully initialized.
[2024-04-17T08:08:01.138] [general] [info] [1] Initializing SentenceTransformerService...
[2024-04-17T08:08:01.138] [general] [debug] [160] Long-running query monitor is enabled.
[2024-04-17T08:08:01.139] [general] [info] [1] SentenceTransformerService has been successfully initialized.
[2024-04-17T08:08:01.143] [general] [info] [1] gRPC service has been successfully initialized.
[2024-04-17T08:08:01.143] [general] [info] [1] Server listening on 0.0.0.0:10080 (Insecure)

스키마 정의

서버 연결을 위한 DocumentDB 객체를 생성합니다.

from aeca import Channel, DocumentDB
 
channel = Channel("localhost", 10080)
doc_db = DocumentDB(channel)

우리는 아래와 같이 정의된 데이터를 저장할 예정입니다.

  • doc_id: 문서의 아이디
  • content: 텍스트 데이터
  • embedding: content에 대한 embedding vector

이를 위해 다음과 같이 정의하여 test란 컬렉션을 생성합니다.

indexes = [
    {
        "name": "__primary_key__",
        "fields": [
            "doc_id"
        ],
        "unique": True,
        "index_type": "kPrimaryKey"
    },
    {
        "name": "sk_fts",
        "fields": [
            "doc_id",
            "content",
            "embedding"
        ],
        "unique": False,
        "index_type": "kFullTextSearchIndex",
        "options": {
            "doc_id": {
                "analyzer": {
                    "type": "KeywordAnalyzer"
                },
                "index_options": "doc_freqs"
            },
            "content": {
                "analyzer": {
                    "type": "StandardCJKAnalyzer",
                    "options": {
                        "tokenizer": "icu",
                        "ngram_filter": {
                            "min_size": 2,
                            "max_size": 4
                        }
                    }
                },
                "index_options": "offsets"
            },
            "embedding": {
                "analyzer": {
                    "type": "DenseVectorAnalyzer",
                    "options": {
                        "index_type": "HNSW",
                        "dims": 768,
                        "m": 64,
                        "ef_construction": 200,
                        "ef_search": 32,
                        "metric": "inner_product",
                        "normalize": True,
                        "shards": 1
                    }
                },
                "index_options": "doc_freqs"
            }
        }
    }
]
doc_db.create_collection("test", indexes=indexes)

위의 코드에서 indexes에는 두개의 색인이 정의되어 있습니다. 첫번째 색인은 primary key이며 doc_id 필드를 사용하여 색인이 생성되도록 정의 되었습니다.

    {
        "name": "__primary_key__",
        "fields": [
            "doc_id"
        ],
        "unique": True,
        "index_type": "kPrimaryKey"
    },

다음은 Full-Text Search(이하 FTS)를 위한 색인입니다. 각각의 필드별로 다른 analyzer가 정의되어 있는 것을 확인할 수 있습니다. doc_idKeywordAnalyzer로 정의하여 exact matching이 가능하고 content는 FTS가 가능한 StandardAnalyzer로 정의 되었습니다. 마지막으로 embeddingDenseVectorAnalyzer로 정의되어 vector search가 가능하게 구성 되었습니다.

    {
        "name": "sk_fts",
        "fields": [
            "doc_id",
            "content",
            "embedding"
        ],
        "unique": False,
        "index_type": "kFullTextSearchIndex",
        "options": {
            "doc_id": {
                "analyzer": {
                    "type": "KeywordAnalyzer"
                },
                "index_options": "doc_freqs"
            },
            "content": {
                "analyzer": {
                    "type": "StandardCJKAnalyzer",
                    "options": {
                        "tokenizer": "icu",
                        "ngram_filter": {
                            "min_size": 2,
                            "max_size": 4
                        }
                    }
                },
                "index_options": "offsets"
            },
            "embedding": {
                "analyzer": {
                    "type": "DenseVectorAnalyzer",
                    "options": {
                        "index_type": "HNSW",
                        "dims": 768,
                        "m": 64,
                        "ef_construction": 200,
                        "ef_search": 32,
                        "metric": "inner_product",
                        "normalize": True,
                        "shards": 1
                    }
                },
                "index_options": "doc_freqs"
            }
        }
    }

스키마 정의애 대한 상세한 내용은 색인 정의를 위한 스키마에서 확인할 수 있습니다.

데이터 입력

이제 데이터를 입력할 컬렉션이 준비되었습니다. Aeca는 ML 모델 서빙을 위한 기능이 준비되어 있고 아래와 같이 Huggingface hub의 모델명 (opens in a new tab)을 지정하여 SentenceTransformers (opens in a new tab)의 모델을 바로 사용할 수 있습니다.

from aeca import SentenceTransformerEncoder
 
encoder = SentenceTransformerEncoder(channel, "ko-sbert-sts")
 
sentences = [
    "남자가 달걀을 그릇에 깨어 넣고 있다.",
    "한 남자가 기타를 치고 있다.",
    "목표물이 총으로 맞고 있다.",
    "남자가 냄비에 국물을 부어 넣고 있다."
]
embeddings = encoder.encode(sentences)
 
docs = []
for doc_id, (sentence, embedding) in enumerate(zip(sentences, embeddings)):
    doc = {
        "doc_id": doc_id,
        "content": sentence,
        "embedding": embedding.tolist()
    }
    docs.append(doc)
doc_db.insert("test", docs)

ID, 문장과 임베딩된 벡터를 생성하고 insert 함수를 통해 데이터를 삽입합니다.

데이터 입력과 관련한 자세한 내용은 데이터 입력과 관리에서 확인할 수 있습니다.

검색

데이터가 삽입되고 검색할 준비가 완료 되었습니다. 이제 위에서 정의한 스키마를 고려하여 적절한 쿼리 문법을 작성합니다.

query = "그 여자는 달걀 하나를 그릇에 깨어 넣었다."
query_embedding = encoder.encode(query)
query_embed = ", ".join([str(e) for e in query_embedding[0]])
search_query = {
    "$search": {
        "query": f"(content:({query}))^0.2 AND (embedding:[{query_embed}])^20"
    },
    "$hint": "sk_fts",
    "$project": [
        "doc_id",
        "content",
        {"_meta": "$unnest"}
    ]
}
 
df = doc_db.find("test", search_query)
print(df)

우리는 FTS를 위한 sk_fts라는 색인을 생성했습니다. 이를 FTS 검색을 수행하게 위해 $search를 정의합니다. 여기에 사용되는 문법은 lucene (opens in a new tab)과 유사합니다.

    "$search": {
        "query": f"(content:({query}))^0.2 AND (embedding:[{query_embed}])^20"
    },

쿼리의 내용을 하나씩 살펴보면

  • content:({query}): StandardCJKAnalyzer를 통해 ngram으로 분리된 content 필드의 텍스트 데이터를 FTS로 검색합니다.
  • embedding:[{query_embed}]: query로부터 임베딩하여 생성된 query_embed를 사용하여 HNSW (opens in a new tab)를 이용하여 유사 벡터를 찾습니다.
  • ^0.2^20: 각각의 항목에 대해 boosting 점수를 부여합니다. 이 예시는 vector search 통해 나온 결과물을 더 중요하게 다루고 있습니다.
  • AND: FTS와 vector search를 병합하여 hybrid search를 진행합니다.

이어서 $project를 통해 출력할 필드를 선택합니다. 여기서 내부에서 계산된 점수를 출력하기 위해 _meta를 명시하고 있고 이 데이터는 계층적이기 때문에 $unnest를 통해 1차원 필드로 변환시킵니다.

    "$project": [
        "doc_id",
        "content",
        {"_meta": "$unnest"}
    ]

이제 find 함수를 통해 생성한 쿼리를 전달하고 실행하면 다음과 같은 결과물을 얻을 수 있습니다.

   _meta.doc_id  _meta.score  content                          doc_id
0             0     0.313446  남자가 달걀을 그릇에 깨어 넣고 있다.    0

검색과 관련한 자세한 내용은 검색에서 확인할 수 있습니다.

정리

이제 삽입하고 검색했던 데이터는 drop_collection을 통해 삭제할 수 있습니다.

doc_db.drop_collection("test")