Dense Embedding 是一种将文本的含义转换为数字向量的方法,从而“如果含义相似,则向量接近”,而 BM25 指数是一种根据文本中包含的单词本身出现的频率和稀有度进行评分的方法。前者可以找到“同义词或其他表达方式”,而后者可以找到“恰好包含该单词的文档”。
选择理由:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
embeddings = model.encode(
texts,
batch_size=64,
normalize_embeddings=True, # L2 정규화 → 코사인 유사도 = 내적
)
为什么 L2 正则化很关键:
@)scores = embeddings @ query_embedding.T # (9586,) 벡터, <100ms
这里,numpy是Python的数值计算库,可以快速计算大型数字数组。感谢 numpy,您可以使用上面的 @ 运算符一次计算 9,586 个向量的相似度。仅通过密集嵌入来准确的关键词匹配是很弱的。我搜索“甲木”,出现了语义相似的词“阳能”。 在准确的术语匹配很重要的领域,BM25 是一个重要的补充。
# 한국어: Kiwi 형태소 분석기 (명사/동사/형용사만 추출)
def tokenize_ko(text: str) -> list[str]:
kiwi = Kiwi()
tokens = []
for token in kiwi.tokenize(text):
if token.tag.startswith(("NN", "VV", "VA", "SL", "SH", "SN")):
min_len = 1 if token.tag.startswith("NN") else 2
if len(token.form) >= min_len:
tokens.append(token.form)
return tokens
# 중국어: jieba 분절 (불용어 제거)
def tokenize_zh(text: str) -> list[str]:
return [w.strip() for w in jieba.cut(text)
if w.strip() and w not in _ZH_STOP]
# 영어: regex + stopwords (100개)
def tokenize_en(text: str) -> list[str]:
words = re.findall(r"[a-zA-Z]+", text.lower())
return [w for w in words if w not in _ENGLISH_STOP and len(w) > 2]
允许韩语单字母名词的原因:
诸如五元素(周五/周三/周四/周二/周六)和天安(Gap/Eul/Bying/Jeong)之类的单字母名词是该域的关键术语。在一般的 NLP 中,单字母名词会被作为噪音去除,但这里在 min_len = 1 if NN 条件下只允许使用名词。
IT服务中同样的问题:
即使在 IT 领域,短缩写通常也是关键术语:CI、CD、DB、VM、LB、DR、QA 等。如果您在英语分词器中应用 len(w) > 2 过滤器,所有这些两个字母的缩写都将被删除。根据您的域,您可能需要调整分词器的最小长度限制或将重要的缩写添加到停用词例外列表中。
JSONL(JSON Lines)是一种每行存储一个 JSON 对象的格式,并且可以逐行流式传输,而无需将整个对象加载到内存中,使其适合处理大量块数据。常规 JSON 数组 ([{...}, {...}]) 需要解析整个文件,但 JSONL 可以自由地仅读取特定行或将它们附加到末尾。
data/
├── chunks.jsonl # 원본 청크 (chunk_id, source, page, lang, text)
├── chunks_meta.jsonl # 검색 결과 반환용 메타+텍스트
├── embeddings.npy # Dense 벡터 (9586 × 384, float32)
├── bm25_index.pkl # BM25Okapi 직렬화
├── tokenized_corpus.pkl # 사전 토큰화 코퍼스 (검색 시 재토큰화 방지)
└── .last_build_count # 마지막 빌드 시 청크 수 (변경 감지용)
总重建时间:9,586 个块大约需要 3-5 分钟(M1 Mac 本地)
쿼리: "갑목 인월 용신 조후"
│
┌──────────┼──────────┐
▼ │ ▼
┌──────────┐ │ ┌──────────┐
│ BM25 │ │ │ Dense │
│ top-K │ │ │ top-K │
└────┬─────┘ │ └────┬─────┘
│ │ │
└─────┬─────┘─────────┘
│
합집합 (K=max(20, top_k×4), 기본 top_k=5 시 최대 40개)
│
▼
┌─────────────────────┐
│ Stage 2: Reranker │
│ bge-reranker-v2-m3 │
│ (Cross-Encoder) │
└──────────┬──────────┘
│
top-K 결과 반환
BM25 和密集搜索是根本不同的方法,哪一种更有效将取决于查询的性质。**当以固定比例组合(例如 BM25 40% + Dense 60%)时,对于某些类型的查询,您将获得次优结果。
我们具体看一下问题:
| 查询类型 | BM25 擅长什么 | Dense 擅长什么 | 固定比率问题 |
|---|---|---|---|
"窮通寶鑑 甲木 寅月"(汉字关键字) |
查找包含这些汉字的文档 | 导入语义相似的韩语解释(噪音) | 如果密集的比例高,则错误的韩语解释将排名靠前。 |
"갑목 일간이 봄에 태어났을 때 보충해야 할 기운"(描述性) |
仅匹配单词“Gapmok”和“Spring”的碎片结果 | 理解整个句子的意思,找到答案,例如“喉气强时,以金制之”。 | 如果 BM25 的比例较高,则包含低相关性“黑色”项目的文档将排名较高。 |
"도화살"(简短行话) |
查找包含确切单词“do arrow”的文档 | “Dohwasal”嵌入也匹配“爱”和“吸引力”等一般含义。 | 如果你倾向于一侧,你就会失去准确性或多样性。 |
此项目的自适应权重:
def _adaptive_weights(query, lang):
hanja_ratio = (쿼리 내 한자 비율)
domain_term_count = (도메인 사전에 매칭된 용어 수)
if hanja_ratio > 0.3: # 한자 쿼리
return 0.6, 0.4 # BM25↑: 한자는 정확한 글자 매칭이 핵심
elif len(query) <= 8 and domain_term_count >= 1: # 짧은 전문용어
return 0.5, 0.5 # 균등: 양쪽 모두 필요
elif len(query) > 20 and domain_term_count == 0: # 긴 서술형
return 0.25, 0.75 # Dense↑: 의미 파악이 핵심
else:
return 0.4, 0.6 # 기본: Dense 약간 우세
生产影响:
自适应权重不仅是 Saju Myeongrihak 所必需的。例如:
GET /api/v2/users → BM25 增强功能,“如何获取用户列表”→ 密集增强功能通过在运行时分析查询的性质并自动调整权重,可以响应各种查询模式。
这个项目最核心的技术。允许韩语查询找到中文来源的设备:
# 143개 키(209쌍)의 한↔漢 매핑 테이블 (일부)
_KOREAN_TO_HANJA = {
"갑목": ["甲木"],
"용신": ["用神"],
"도화살": ["桃花煞", "桃花"],
"합충형파해": ["合沖刑破害", "合冲刑破害"], # 정체자+간체자 모두 포함
"인신충": ["寅申沖", "寅申冲"],
...
}
查询处理流程:
입력 쿼리: "갑목 인월 용신"
↓ Kiwi 토큰화
토큰: ["갑목", "인", "월", "용신"]
↓ 복합 도메인 용어 추출 (형태소 분석 우회)
추가: ["갑목", "용신"] # 이미 포함됨 확인
↓ 한→漢 확장
확장: ["갑목", "인", "월", "용신", "甲木", "寅", "用神"]
↓ BM25 검색 (확장된 토큰으로)
IT 服务中的相同模式:
即使在 IT 环境中,具有相同含义的术语在韩语/英语/缩写中也可以互换使用。如果没有查询扩展表,当您搜索“Kubernetes”时,您将找不到写为“Kubernetes”或“K8s”的文档:
_IT_TERM_EXPANSION = {
"쿠버네티스": ["Kubernetes", "K8s", "k8s"],
"도커": ["Docker", "docker"],
"로드밸런서": ["Load Balancer", "LB", "L4", "L7"],
"배포": ["deploy", "deployment", "릴리즈", "release"],
"장애": ["incident", "outage", "장애 대응", "postmortem"],
"인증": ["authentication", "auth", "OAuth", "SSO"],
"캐시": ["cache", "Redis", "Memcached", "CDN"],
"모니터링": ["monitoring", "Grafana", "Prometheus", "옵저버빌리티", "observability"],
...
}
通过此表,您还可以找到英文文档,其中韩语查询“deployment rollback method”被写为“deployment rollback procedure”。构建领域词典是一次性成本,但提高搜索召回率的效果是永久性的。
为什么需要复杂的领域术语提取:
新西兰人有时会将“Daeun”分成“Dae”+“Un”或错误地将“Gungtongbogam”分割成“Gungtongbogam”。 在词法分析之前先匹配并保存字典中注册的复合词:
def _extract_compound_terms(query: str) -> list[str]:
# 길이 내림차순 정렬 → 긴 용어 우선 매칭 (합충형파해 > 합충 > 합)
sorted_terms = sorted(_KOREAN_TO_HANJA.keys(), key=len, reverse=True)
found = []
for term in sorted_terms:
if len(term) >= 2 and term in query:
found.append(term)
return found
如果来自同一来源的多个块排名较高,则答案将会有偏差。从同一源的第二个块开始衰减 5%:
# 같은 소스 2번째부터 5%씩 감쇠 (최대 15%)
penalty = min(seen * 0.05, 0.15)
adjusted = original_score * (1.0 - penalty)
示例:即使 梁湘润_명리전승반필기.pdf 中的 5 个块进入前 10 名,多样性惩罚也允许其他源块进入并获得多个视角。
IT服务中同样的问题:
当您搜索“支付超时”时,如果支付团队 Confluence 空间中的前 10 个块占据了顶部,您就会错过基础设施或 QA 团队从其他角度编写的文档。通过应用源多样性惩罚,您可以获得平衡的结果,例如 3 个支付团队文档 + 2 个基础设施团队失败响应文档 + 1 个 QA 测试场景。