빠른 시작

빠른 시작

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

Cognica와 Python SDK 설치

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

pip install "cognica[server]"

현재 Python SDK를 통한 Cognica 설치는 Linux amd64만 지원하고 있습니다.

Cognica 서버를 실행하기 위해 cognica-init을 통해 실행 환경을 설정합니다.

cognica-init

인자 없이 실행하면 현재 경로에 데이터 저장을 위한 data 폴더와 실행을 위한 설정 파일이 포함된 conf 폴더가 생성됩니다.

실행 결과
The initial setup of Cognica is now complete.
Default Config File: conf/default.yaml
Data Path: data

이제 준비가 완료 되었으며 서버를 실행합니다.

cognica run server -c conf/default.yaml

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

실행 결과
[general] [info] [tid 153782] [migration.cpp:77] Collection '_sys.doc_db_index_usage_stats' has been successfully created.
[general] [info] [tid 153782] [grpc_service.cpp:46] Initializing gRPC service...
[general] [info] [tid 153782] [key_value_db_service.cpp:21] Initializing KeyValueDBService...
[general] [info] [tid 153782] [key_value_db_service.cpp:26] KeyValueDBService has been successfully initialized.
[general] [info] [tid 153782] [document_db_service.cpp:23] Initializing DocumentDBService...
[general] [info] [tid 153782] [document_db_service.cpp:28] DocumentDBService has been successfully initialized.
[general] [info] [tid 153782] [fts_analysis_pipeline_service.cpp:23] Initializing FTSAnalysisPipelineService...
[general] [info] [tid 153782] [fts_analysis_pipeline_service.cpp:29] FTSAnalysisPipelineService has been successfully initialized.
[general] [info] [tid 153782] [sentence_transformer_service.cpp:21] Initializing SentenceTransformerService...
[general] [debug] [tid 153928] [document_db_grpc.cpp:144] Query duration monitor is enabled.
[general] [info] [tid 153782] [sentence_transformer_service.cpp:32] SentenceTransformerService has been successfully initialized.
[general] [info] [tid 153782] [grpc_service.cpp:114] gRPC service has been successfully initialized.
[general] [info] [tid 153782] [grpc_service.cpp:117] Server listening on 0.0.0.0:10080

스키마 정의

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

from cognica 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"
            }
        }
    }

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

데이터 입력

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

from cognica 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")