kangda-robot-backend/ruoyi-fastapi-backend/utils/match_service.py
2025-12-24 18:41:42 +08:00

262 lines
9.5 KiB
Python
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.

import re
import jieba
from typing import List, Tuple, Optional
from loguru import logger
class MatchService:
def __init__(self):
self._colloquial_map = {
'': '怎么',
'咋办': '怎么办',
'咋样': '怎么样',
'': '什么',
'啥子': '什么',
'啥时候': '什么时候',
'啥地方': '什么地方',
'哪儿': '哪里',
'咋个': '怎么',
'咋整': '怎么办',
'咋回事': '怎么回事',
'咋回事儿': '怎么回事',
'咋样': '怎么样',
'啥人': '什么人',
'啥东西': '什么东西',
'啥玩意': '什么东西',
'啥情况': '什么情况',
'咋样': '怎么样',
'咋办': '怎么办',
'咋弄': '怎么办',
'咋整': '怎么办',
'咋回事': '怎么回事',
'咋回事儿': '怎么回事',
'咋样': '怎么样',
'': '什么',
'啥子': '什么',
'啥时候': '什么时候',
'啥地方': '什么地方',
'啥人': '什么人',
'啥东西': '什么东西',
'啥玩意': '什么东西',
'啥情况': '什么情况',
'啥事': '什么事',
'啥事儿': '什么事',
'啥时候': '什么时候',
'啥地方': '什么地方',
'哪儿': '哪里',
'哪儿个': '哪个',
'': '怎么',
'咋办': '怎么办',
'咋样': '怎么样',
'咋个': '怎么',
'咋整': '怎么办',
'咋弄': '怎么办',
'咋回事': '怎么回事',
'咋回事儿': '怎么回事',
'咋样': '怎么样',
'': '什么',
'啥子': '什么',
'啥时候': '什么时候',
'啥地方': '什么地方',
'啥人': '什么人',
'啥东西': '什么东西',
'啥玩意': '什么东西',
'啥情况': '什么情况',
'啥事': '什么事',
'啥事儿': '什么事',
}
self._stop_words = {
'', '', '', '', '', '', '', '', '', '', '', '',
'一个', '', '', '', '', '', '', '', '', '', '', '没有',
'', '', '自己', '', '', '', '', '', '', '', '', '',
'', '', '', '', '', '', '', '', '', '哎哟', '哎呀',
'请问', '请问一下', '请问一下', '请问一下', '我想知道', '我想问',
'帮我', '帮我看', '帮我看一下', '帮我查', '帮我查一下',
'能不能', '可不可以', '是否', '有没有', '是不是',
'麻烦', '麻烦问一下', '麻烦您', '麻烦帮我',
'请问', '请问一下', '请问问',
'我们', '你们', '他们', '她们', '它们',
'这个', '那个', '这些', '那些',
'这种', '那种', '这类', '那类',
'你好', '您好', '大家好', '', '哈喽', '哈罗',
'早上好', '上午好', '下午好', '晚上好',
'hi', 'hello', 'hey', '', '哈喽',
'怎么样', '如何', '怎样',
'关于', '对于', '至于',
'以及', '或者', '还是',
'因为', '所以', '但是', '不过',
'如果', '要是', '假如',
'虽然', '尽管',
'而且', '并且', '同时',
'然后', '接着', '之后',
'首先', '其次', '最后',
'总之', '总的来说',
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '',
'哎哟', '哎呀', '哎嘿', '哎呦', '哎呀',
'', '', '', '', '', '', '', '嗯哼', '哼哼', '呵呵', '嘿嘿',
'嘻嘻', '哈哈', '哇塞', '哇哦', '天哪', '天啊', '哎呀呀',
'', '', '嗯嗯', '哦哦', '啊啊', '嗯啊', '哦啊',
}
self._question_words = {
'什么', '怎么', '如何', '为什么', '哪儿', '哪里', '哪个', '',
'多少', '', '何时', '什么时候', '怎样', '怎么样',
'是否', '有没有', '是不是', '能不能', '可不可以',
}
jieba.setLogLevel(jieba.logging.INFO)
def preprocess_text(self, text: str) -> str:
if not text:
return ''
text = text.strip()
text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9""''()【】《》\s]', '', text)
text = re.sub(r'\s+', ' ', text)
for colloquial, standard in self._colloquial_map.items():
text = text.replace(colloquial, standard)
text = re.sub(r'[,。?!、;:""''()【】《》]', ' ', text)
text = re.sub(r'\s+', ' ', text)
return text.strip()
def extract_keywords(self, text: str, top_k: int = 10) -> List[str]:
if not text:
return []
text = self.preprocess_text(text)
words = jieba.lcut(text)
keywords = []
for word in words:
word = word.strip()
if len(word) < 2:
continue
if word in self._stop_words:
continue
if word.isdigit():
continue
keywords.append(word)
question_keywords = []
for word in words:
if word in self._question_words:
question_keywords.append(word)
keywords = list(set(keywords))
word_freq = {}
for word in words:
if word in keywords:
word_freq[word] = word_freq.get(word, 0) + 1
keywords.sort(key=lambda x: word_freq.get(x, 0), reverse=True)
return keywords[:top_k]
def calculate_similarity(self, q1: str, q2: str) -> float:
if not q1 or not q2:
return 0.0
n1 = self.preprocess_text(q1)
n2 = self.preprocess_text(q2)
if n1 == n2:
return 1.0
kw1 = set(self.extract_keywords(q1))
kw2 = set(self.extract_keywords(q2))
logger.info(f'[MatchDebug] q1={q1} | n1={n1} | kw1={kw1}')
logger.info(f'[MatchDebug] q2={q2} | n2={n2} | kw2={kw2}')
if not kw1 or not kw2:
return 0.0
intersection = len(kw1 & kw2)
union = len(kw1 | kw2)
jaccard_sim = intersection / union if union > 0 else 0
len_sim = 1 - abs(len(n1) - len(n2)) / max(len(n1), len(n2))
len_sim = max(0, len_sim)
similarity = 0.7 * jaccard_sim + 0.3 * len_sim
return similarity
def find_best_match(self, query: str, candidates: List[Tuple[str, any]], threshold: float = 0.6) -> Tuple[Optional[any], float]:
if not query or not candidates:
return None, 0.0
best_match = None
best_similarity = 0.0
for candidate_text, candidate_data in candidates:
similarity = self.calculate_similarity(query, candidate_text)
logger.info(f'[MatchDebug] compare | query="{query}" | candidate="{candidate_text}" | sim={similarity:.3f}')
if similarity > best_similarity:
best_similarity = similarity
best_match = candidate_data
logger.info(f'[MatchDebug] best | query="{query}" | best_sim={best_similarity:.3f} | threshold={threshold}')
if best_similarity >= threshold:
return best_match, best_similarity
return None, best_similarity
def decompose_question(self, question: str) -> List[str]:
if not question:
return []
question = self.preprocess_text(question)
separators = ['', '?', '', '.', '', ';', '', ',', '', '以及', '还有', '另外']
sub_questions = [question]
for sep in separators:
new_sub_questions = []
for sq in sub_questions:
parts = sq.split(sep)
new_sub_questions.extend(parts)
sub_questions = new_sub_questions
sub_questions = [sq.strip() for sq in sub_questions if sq.strip()]
unique_sub_questions = []
seen = set()
for sq in sub_questions:
if sq not in seen:
seen.add(sq)
unique_sub_questions.append(sq)
return unique_sub_questions
def calculate_keyword_coverage(self, query: str, candidate: str) -> float:
if not query or not candidate:
return 0.0
kw1 = set(self.extract_keywords(query))
kw2 = set(self.extract_keywords(candidate))
if not kw1:
return 0.0
if not kw2:
return 0.0
coverage = len(kw1 & kw2) / len(kw1)
return coverage
_match_service_instance = None
def get_match_service() -> MatchService:
global _match_service_instance
if _match_service_instance is None:
_match_service_instance = MatchService()
return _match_service_instance