개요
최근 사내 서버 비용 관리 대시보드를 만들면서 겪었던 문제를 Fuzz Matching 라이브러리인 rapidfuzz를 통해 해결한 경험을 공유하려고 합니다.
데이터 정규화 난제 …
사내 인프라팀에서 관리하는 DB와, 자산팀에서 제공하는 가격 API를 연동하여
이번 달 입고된 서버의 총 가격 을 계산해야 하는 상황입니다.
그런데 데이터가 심상치 않습니다..
- DB (엔지니어 입력값): 총무팀으로부터 전달된 서버 스펙 정보가 저장됩니다. (약어와 축약어가 난무합니다.)
ssd wd 1T*2(웨스턴 디지털 SSD 1테라 2개)intel nic 10g(인텔 10G 네트워크 카드)
- API (시스템 표준값): 제조사 풀네임과 대괄호 등이 포함된 정형화된 데이터입니다.
[SSD]Western Digital Blue 1T[10G NIC]Intel X550-T2
문제점
단순히 if db_name == api_name:으로는 절대 매칭되지 않습니다.
정규표현식(re)을 쓰자니 wd가 western digital인지, 순서가 섞여있는지 모든 케이스를 예외처리하다가 밤을 새울 것 같습니다.
이때 알게된 것이 바로 유사도 검사(Fuzz Matching) 입니다.
RapidFuzz
파이썬에는 FuzzyWuzzy라는 유명한 라이브러리가 있지만, 속도 이슈와 라이선스 문제(GPL)가 있었습니다.
이를 개선하여 MIT 라이센스이면서, C++로 작성되어 엄창나게 빠른 라이브러리가 바로 RapidFuzz입니다.
설치
pip install rapidfuzz
[실습] 유사도 측정해보기
먼저 간단한 문자열 비교부터 해보겠습니다.
from rapidfuzz import fuzz
# Case 1: 단순 비율 비교 (Ratio)
# wd와 Western Digital은 글자 길이가 너무 달라서 점수가 낮게 나옵니다.
s1 = "wd ssd 1t"
s2 = "Western Digital SSD 1T"
print(f"Ratio: {fuzz.ratio(s1, s2)}")
# 결과: Ratio: 19.354838709677423 (매칭 실패로 간주될 확률 높음)
# Case 2: 부분 집합 비율 비교 (Token Set Ratio)
# 이 방식은 문자열을 공백 기준으로 쪼개고(Token), 교집합을 찾아 비교함
# 순서가 바뀌어도, 중간에 다른 단어가 껴도 매칭이 잘된다는 특징이 있음
print(f"Token Set Ratio: {fuzz.token_set_ratio(s1=s1,s2=s2)}")
# 결과: 25.80645161290323 (여전히 낮음 ... 이유는 'wd' != 'Western Digital'이기 때문임.)
[실전 구현] 전처리 + 매칭 로직
이제 실제 프로젝트에 적용했던 방식을 알아보겠습니다.
동의어 사전(Synonym Dict) 을 이용하여 약어를 풀네임으로 치환한 뒤, rapidfuzz를 돌리는 방식입니다.
# 동의어 사전: 실무에서 자주 쓰는 약어를 정의
SYNONYMS = {
"wd": "western digital",
"nic": "nic", # 그대로 유지
"dell": "dell",
"ent": "enterprise"
}
def normalize_text(text):
"""텍스트를 소문자로 변환하고, 동의어를 치환합니다."""
text = text.lower()
for key, value in SYNONYMS.items():
# 단어 경계(\b)를 기준으로 치환 (wd -> western digital)
text = re.sub(r'\b' + re.escape(key) + r'\b', value, text)
return text
def find_best_match(user_input, catalog_list):
"""
RapidFuzz를 이용해 가장 적합한 모델을 찾습니다.
"""
# 1. 입력값 정규화
clean_input = normalize_text(user_input)
# 2. 비교 대상(Catalog)의 이름들만 추출하여 리스트 생성
# (실제로는 catalog 로딩 시 미리 정규화해두면 더 빠릅니다)
choices = {idx: normalize_text(item["name"]) for idx, item in enumerate(catalog_list)}
# 3. extractOne: 가장 유사한 하나를 추출
# scorer=fuzz.token_set_ratio: 순서가 섞여도 잘 찾음
result = process.extractOne(clean_input, choices, scorer=fuzz.token_set_ratio)
if result:
match_text, score, key = result
# 임계값(Threshold) 설정: 50점 이상만 매칭으로 인정
if score >= 50:
matched_item = catalog_list[key]
return matched_item, score
return None, 0
테스트 및 결과
작성한 로직을 돌려볼게요.
print(f"{'Input Data':<20} | {'Matched Name':<30} | {'Score':<5} | {'Price'}")
print("-" * 75)
for inp in db_inputs:
matched_item, score = find_best_match(inp, api_catalog)
if matched_item:
print(f"{inp:<20} | {matched_item['name']:<30} | {score:<5.1f} | {matched_item['price']:,}원")
else:
print(f"{inp:<20} | {'(매칭 실패)':<30} | {score:<5.1f} | -")
실행 결과
---------------------------------------------------------------------------
ssd wd 1T | [SSD]Western Digital Blue 1T | 84.0 | 100,000원
samsung 500g ssd | [SSD]Samsung 870 EVO 500G | 63.4 | 80,000원
intel 10g nic | [10G NIC]Intel X550-T2 | 57.1 | 250,000원
hpe DL20 Gen11 | (매칭 실패) | 0.0 | -
마무리
이번 프로젝트를 통해 “이미 있는 도구를 잘 쓰자” 는 교훈을 얻었습니다.
- 정규식: 패턴이 명확할 때 좋음 (이메일, 전화번호).
- Fuzzy Matching (RapidFuzz): 오타, 약어, 순서 변경 등 “비정형 텍스트” 비교에 최강.