Search
Duplicate

Hybrid Search 데모 - 인물 Profile 검색

문서번호 : 11-2172434

Document Information

최초 작성일 : 2024.02.06
최종 수정일 : 2024.02.13
이 문서는 아래 버전을 기준으로 작성되었습니다.
SinglestoreDB : 8.5.4
CPU : 4 MEMORY : 16 GB Node : MA - 1개 LEAF - 1개 Partition : 4 Python 3.8

Goal

Hybrid Search 를 이용하여 검색 문장에 가장 적합한 인물 profile Top3를 검색 한다.
Hybrid Search = Semantic Search + Lexical Search
Semantic Search : 인물 profile data 와 Search 문장을 각각 임베딩 하여 벡터 데이터 생성. 그 후 벡터 함수인 dot_product 를 이용하여 Search 문장의 벡터와 가장 유사한 프로필 벡터 데이터 검색 = Similar Search 결과 확인
Lexical Search : 인물 data와 Search 문장을 각각 형태소 분석, 형태소 분석 칼럼을 Fulltext index 로 지정. 그 후 Match against 구문을 사용하여 Search 문장과 유사한 프로필 문장 검색 = Text Search 결과 확인
Hybrid Search : Semantic Search 와 Lexical Search 의 결과 값을 미리 정한 알고리즘에 따라 reordering 하여 결과 도출.
Hybrid Search Demo(profile Search) 구성도

결과

Step

1.
테이블 생성 & 인물 Profile 데이터(Json data) Ingestion
Embedding → 벡터 데이터
Tokenize → Fulltext Search 데이터
2.
Hybrid Search를 이용하여 인물 Profile 검색
Vector Similarity Search + Fulltext Search → Reordering 하여 결과 도출

DDL 및 DML

DDL 및 DML 쿼리는 아래 파이썬 코드 (crt_prftbl.py, hybrd_srch.py) 에도 포함되어 있습니다.
1.
crt_prftbl.py : 테이블 생성 & 인물 Profile 데이터 적재
2.
hybrd_srch.py : 인물 검색 결과를 hybrid Search 의 결과로 도출

테이블 생성

vector데이터 칼럼 : Embedding 모델의 차원에 맞게, vector 데이터 타입 차원 설정
vector Index : ANN 서치 활용 (Demo 에서는 IVF_FLAT 인덱스 사용)
형태소 분석 칼럼 : Toknizer로 형태소 분석 후 적재, Fulltext index로 지정
create table if not exists prf_tbl ( id_num int(11) not null, profile json, token text, # 형태소 칼럼 vec vector(768) not null, # vector 칼럼 (768 차원) vector index vec_idx1 (vec) index_options '{{"index_type":"IVF_FLAT", "nlist":2, "nprobe":1}}', # vector 인덱스 primary key (id_num), fulltext(token) # fulltext 인덱스 );
SQL
복사

Hybrid Search 쿼리

벡터 데이터 & 임베딩한 검색 문장의 Similarity Search 결과 : S1
검색 문장 형태소 Serach 의 MATCH AGAINST 결과 : S2
Hybrid Search Score : Reordering Score = (S1 + S2) / 2
Demo 에서 의 Reordering은, 임의로 각 결과의 합의 1/2 로 설정함
Reordering 의 알고리즘은 자체적으로 선정 필요함
select profile, # 인물 Profile 출력 vec <*> json_array_pack(<Embedding 한 검색 문장>) as s1, # Similarity Score match(token) against (<검색 문장 형태소 분석 결과>) as s2, # Fulltext Search Score (s1+s2)/2 as score # Hybrid Search Score from prf_tbl order by score desc limit 3
SQL
복사

데모 코드

인물 Profile 데이터

가상의 인물 Json 프로필 10개
profile_data.txt
4.5KB
UTF-8 character set 사용

테이블 생성 & 인물 Profile 데이터(Json data) Ingestion

파이썬 코드 : crt_prftbl.py

Embedding 모델 : “huggingface” 의 “jhgan/ko-sroberta-multitask” 사용
Multi lingual 모델 & 한글 Embedding 중 Similarity Score 가 높은 모델 사용
# 라이브러리 import singlestoredb as s2 import json from langchain.embeddings import HuggingFaceEmbeddings import torch from konlpy.tag import Okt # DB 접속정보 HOST = 'localhost' PORT = 3306 USER = '<유저>' PASSWORD = '<패스워드>' DATABASE = '<데이터 베이스>' TABLE = 'prf_tbl' # DB 연결 conn = s2.connect( host=HOST, user=USER, password=PASSWORD, database=DATABASE ) # 테이블 생성 tbl_sql = f""" create table if not exists {TABLE} ( id_num int(11) not null, profile json, token text, vec vector(768) not null, vector index vec_idx1 (vec) index_options '{{"index_type":"IVF_FLAT", "nlist":2, "nprobe":1}}', primary key (id_num), fulltext(token) );""" cur = conn.cursor() cur.execute(tbl_sql) # 데이터 ingestion 쿼리 isrt_sql = f"INSERT INTO {TABLE} (id_num, profile, token, vec) VALUES (%s, %s, %s, %s)" # 형태소분석 okt = Okt() # 임베딩 벡터 정규화함수 def normalize_vector(vector): vector_tensor = torch.tensor(vector, dtype=torch.float32) vector_tensor = vector_tensor.unsqueeze(0) # 차원 추가 norm = torch.norm(vector_tensor, p=2, dim=1, keepdim=True) # L2 정규화 normalized_vector = vector_tensor / norm return normalized_vector.tolist()[0] # 리스트로 변환하여 반환 # huggingfaces embeddings = HuggingFaceEmbeddings(model_name="jhgan/ko-sroberta-multitask") # 프로필 데이터 삽입 with open('profiles1.txt', 'r', encoding='utf-8') as file: id_num = 0 # id for line in file: id_num += 1 profile_data = json.loads(line) # json 데이터파일 load profile_values = ' '.join(profile_data.values()) # json 의 value 값만 추출 tokens = okt.morphs(profile_values) # Tokenize, 형태소 분석 tokens = ' '.join(tokens) profile_vector = embeddings.embed_query(profile_values) # Embedding profile_vector = normalize_vector(profile_vector) # 정규화 profile_vector = str(profile_vector) profile_json = json.dumps(profile_data) # json profile 데이터 cur.execute(isrt_sql, (id_num, profile_json, tokens, profile_vector)) # 데이터 ingestion optmz_sql = f"optimize table {TABLE} full" cur.execute(optmz_sql) conn.commit() conn.close() print("=" * 40) print(f"{id_num} rows 데이터ingestion 완료") print("=" * 40)
Python
복사

Hybrid Search를 이용하여 인물 Profile 검색

파이썬 코드 : hybrd_srch.py

# 라이브러리 import singlestoredb as s2 import json from langchain.embeddings import HuggingFaceEmbeddings import torch from konlpy.tag import Okt # DB 접속정보 HOST = 'localhost' PORT = 3306 USER = '<유저>' PASSWORD = '<패스워드>' DATABASE = '<데이터 베이스>' # DB 연결 conn = s2.connect( host=HOST, user=USER, password=PASSWORD, database=DATABASE ) cur = conn.cursor() # 검색 print("=" * 80) qstn = input("프로필 검색: ") print("=" * 80 + "\n") # 형태소분석 okt = Okt() tokens = okt.morphs(qstn) tokens = ' '.join(tokens) # 임베딩 벡터 정규화함수 def normalize_vector(vector): vector_tensor = torch.tensor(vector, dtype=torch.float32) vector_tensor = vector_tensor.unsqueeze(0) # 차원 추가 norm = torch.norm(vector_tensor, p=2, dim=1, keepdim=True) # L2 정규화 normalized_vector = vector_tensor / norm return normalized_vector.tolist()[0] # 리스트로 변환하여 반환 # huggingfaces embeddings = HuggingFaceEmbeddings(model_name="jhgan/ko-sroberta-multitask") # 프로필 검색문장 Embedding qstn_vector = embeddings.embed_query(qstn) qstn_vector = normalize_vector(qstn_vector) qstn_vector = str(qstn_vector) # ----------- Demo 코드 실행 시 제외 (vector index 동작 확인용 코드) : Begin ----------- # ANN 인덱스 검색 확인쿼리 xpln_sql = f""" explain select profile, vec <*> json_array_pack(%s) as s1, match(token) against (%s) as s2, (s1+s2)/2 as score from prf_tbl order by s1 desc limit 3 """ cur.execute(xpln_sql, (qstn_vector, tokens,)) xpln_rslt = cur.fetchall() # explain 쿼리 확인 for rs in xpln_rslt : print(rs) # ----------- Demo 코드 실행 시 제외 (vector index 동작 확인용 코드) : End ----------- # Search 쿼리 srch_sql = f""" select profile, vec <*> json_array_pack(%s) as s1, match(token) against (%s) as s2, (s1+s2)/2 as score from prf_tbl order by score desc limit 3 """ cur.execute(srch_sql, (qstn_vector, tokens,)) results = cur.fetchall() # 결과 출력 & 확인 separator = "*" * 30 output = [separator, f'Question : {qstn}', f'Token : {tokens}'] i = 0 for res in results: i += 1 output.append("-----------") output.append(f"#{i}#") for key, val in res[0].items(): output.append(f"{key} : {val}") output.extend([f"Total Score: {res[3]}", f"Vec score : {res[1]}", f"Text score : {res[2]}"]) output.append(separator) print('\n'.join(output))
Python
복사

ANN Index Search 확인

hybrd_srch.py 파이썬 코드의 explain 쿼리 확인

explain쿼리를 통해 ANN 동작을 확인하기 위한 코드로서, 데모 코드 실행 시에는 해당 코드 제외
# ANN 인덱스 검색 확인쿼리 xpln_sql = f""" explain select profile, vec <*> json_array_pack(%s) as s1, match(token) against (%s) as s2, (s1+s2)/2 as score from prf_tbl order by s1 desc limit 3 """ cur.execute(xpln_sql, (qstn_vector, tokens,)) xpln_rslt = cur.fetchall() # explain 쿼리 확인 for rs in xpln_rslt : print(rs)
Python
복사

vector Index 사용 확인

vector index 동작 확인 : INTERNAL_VECTOR_SEARCH
('Project [remote_0.profile, remote_0.s1, remote_0.s2, remote_0.score]',) ('TopSort limit:5 [remote_0.s1 DESC]',) ('Gather partitions:all alias:remote_0 parallelism_level:segment',) ("Project [prf_tbl.profile, DOT_PRODUCT(prf_tbl.vec,JSON_ARRAY_PACK('[-0.08343324065208435, 0.008895773440599442, 0.04481888562440872, -0.0775635838508606, 0.0565468966960907, -0.00961839035153389, -0.04823928698897362, -0.03210621327161789, -0.048955123871564865, -0.04540256783366203, 0.055186785757541656, 0.00972767360508442, 0.04668761417269707, -0.023273343220353127, 0.04557356610894203, 0.06394023448228836, -0.004792636726051569, -0.01738288626074791, -0.011180720292031765, 0.008260964415967464, -0.01...]",) ("TopSort limit:5 [DOT_PRODUCT(prf_tbl.vec,JSON_ARRAY_PACK('[-0.08343324065208435, 0.008895773440599442, 0.04481888562440872, -0.0775635838508606, 0.0565468966960907, -0.00961839035153389, -0.04823928698897362, -0.03210621327161789, -0.048955123871564865, -0.04540256783366203, 0.055186785757541656, 0.00972767360508442, 0.04668761417269707, -0.023273343220353127, 0.04557356610894203, 0.06394023448228836, -0.004792636726051569, -0.01738288626074791, -0.011180720292031765, 0.008260964415967464, -0.013191510923206806,...]",) ("ColumnStoreFilter [INTERNAL_VECTOR_SEARCH(0, (JSON_ARRAY_PACK('[-0.08343324065208435, 0.008895773440599442, 0.04481888562440872, -0.0775635838508606, 0.0565468966960907, -0.00961839035153389, -0.04823928698897362, -0.03210621327161789, -0.048955123871564865, -0.04540256783366203, 0.055186785757541656, 0.00972767360508442, 0.04668761417269707, -0.023273343220353127, 0.04557356610894203, 0.06394023448228836, -0.004792636726051569, -0.01738288626074791, -0.011180720292031765, 0.008260964415967464, -0.0131915109232068...]",) ('ColumnStoreScan v_test.prf_tbl, SORT KEY __UNORDERED () table_type:sharded_columnstore',)
Bash
복사

데모 실행 & 전체 결과 확인

제의 & 제안 (데모 결과를 기준으로)
Fulltext Search(Lexical search) Score 비중을 높인다면, 보다 변별력 있는 결과가 도출 될 것으로 판단됨
Total score가 특정 Score 미만인 경우는 결과에서 제외. 아래 Hybrid Search 결과 예시에서는 0.25 미만의 결과 제외하여, 각 검색 문장에서 얻고자 하는 프로필(요리사, 의사) 만 가져오도록 한다.

crt_prftbl.py 실행

테이블 확인

select * from prf_tbl order by 1 limit 5;
SQL
복사

Hybrid Search 예시 1

검색 문장 : “한식 요리를 하고 서울에 있는 레스토랑에서 일하는 요리사
결과

Hybrid Search 예시 2

검색 문장 : “연세대학교 출신의 전문의 세브란스 병원에서 근무하는 의사
결과

References

History

일자
작성자
비고
2024.02.06
min
최초 작성
2024.02.13
min
문구 추가, 수정