빠른 시작
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_id
는 KeywordAnalyzer로 정의하여 exact matching이 가능하고 content
는 FTS가 가능한 StandardAnalyzer로 정의 되었습니다. 마지막으로 embedding
은 DenseVectorAnalyzer로 정의되어 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")