Files
tasteby/backend-java/src/main/java/com/tasteby/util/HangulSimilarity.java
joungmin 3815221535 feat(util): #348 isNameSimilar 한국어 자모 + Sørensen-Dice
- HangulSimilarity 유틸 신규
  - decompose: Unicode NFD 분해 (한글 음절 → 초성/중성/종성)
  - 공백·구두점 제거 + 소문자화
  - bigram multiset 기반 Sørensen-Dice 계수
  - 빈 입력/포함 관계 가드
- RestaurantController.isNameSimilar 임계값 0.45 (이전 Jaccard 0.4와 유사 보수성)
- 기존 normalize 헬퍼 제거 (HangulSimilarity 내부로 이동)

DDG/DTO/UNIQUE는 별도 후속:
- 외부 검색 API 선정 (Naver/Kakao/Google CSE)
- RestaurantUpdateDTO + @Valid
- google_place_id 중복 정리 후 UNIQUE 제약

설계서: docs/design/348-name-similarity/README.md

Refs: #348 (Developer 단계)
2026-06-15 16:10:44 +09:00

68 lines
2.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.tasteby.util;
import java.text.Normalizer;
import java.util.HashMap;
import java.util.Map;
/**
* #348 — 한국어 자모 분해(Unicode NFD) + Sørensen-Dice bigram 유사도.
*
* 음절 단위 Jaccard보다 짧은 한국어 이름에 정확. 예:
* similarity("스타벅스 강남", "스타벅스 강남점") ≈ 0.85+
* similarity("스타벅스 강남", "스타벅스 종로") ≈ 0.55~0.85
* similarity("스타벅스", "맥도날드") < 0.20
*
* 공백/구두점은 제거하고 소문자화한 뒤 NFD 분해.
*/
public final class HangulSimilarity {
private HangulSimilarity() {}
/** 공백/구두점 제거 + 소문자화 + NFD 분해(한글 음절 → 자모). */
public static String decompose(String s) {
if (s == null || s.isEmpty()) return "";
String stripped = s.replaceAll("[\\\\-_()\\[\\]【】]", "").toLowerCase();
return Normalizer.normalize(stripped, Normalizer.Form.NFD);
}
/**
* Sørensen-Dice 계수 (bigram multiset 기반). 0.0~1.0.
* 동일 문자열 → 1.0. 빈 입력 → 0.0.
*/
public static double similarity(String a, String b) {
String da = decompose(a);
String db = decompose(b);
if (da.isEmpty() || db.isEmpty()) return 0.0;
if (da.equals(db)) return 1.0;
// 포함 관계는 강한 신호로 1.0 처리 (기존 동작과 일관)
if (da.contains(db) || db.contains(da)) return 1.0;
if (da.length() < 2 || db.length() < 2) {
return 0.0;
}
Map<String, Integer> bigramsA = bigrams(da);
Map<String, Integer> bigramsB = bigrams(db);
int common = 0;
for (var e : bigramsA.entrySet()) {
Integer countB = bigramsB.get(e.getKey());
if (countB != null) {
common += Math.min(e.getValue(), countB);
}
}
int sizeA = da.length() - 1;
int sizeB = db.length() - 1;
return (2.0 * common) / (sizeA + sizeB);
}
private static Map<String, Integer> bigrams(String s) {
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < s.length() - 1; i++) {
String gram = s.substring(i, i + 2);
map.merge(gram, 1, Integer::sum);
}
return map;
}
}