fix(music): CJK 标题适配 Lucene 分词的逐字 OR 检索

- MusicBrainz 索引将连续 CJK 视为单一词元,短语检索对中文标题永远零命中,
  新增 _query_phrase 将 CJK 文本拆为逐字 OR 检索式(OR 组带括号避免 AND 优先级歧义),
  拉丁文本保持短语检索
- 逐字 OR 召回宽,候选挑选收紧:标题未命中的候选不能仅凭艺术家署名得分
- 汉字数字与阿拉伯数字写法差异归一比对(茹此精彩十三首/茹此精彩13首)
- 补充逐字 OR 检索式、数字归一、艺术家独占候选拒绝等测试案例
This commit is contained in:
jxxghp
2026-08-11 09:28:40 +08:00
parent 779e10c6ae
commit 62019d3d23
2 changed files with 130 additions and 25 deletions
+41 -7
View File
@@ -35,7 +35,8 @@ def test_build_query_strips_audio_quality_tokens():
)
)
assert query == 'recording:"永远是朋友" AND artist:"毛阿敏"'
# CJK 短语在 Lucene 索引中是单一词元,检索式拆为逐字 OR,OR 组带括号避免 AND 优先级歧义
assert query == 'recording:("" OR "" OR "" OR "" OR "") AND artist:"毛阿敏"'
def test_select_candidate_matches_traditional_chinese_title():
@@ -165,8 +166,8 @@ def test_search_music_interleaves_recordings_albums_and_artists(monkeypatch):
assert results[1].album == "叶惠美"
assert results[2].title == "周杰伦"
assert results[2].artists == []
assert requested[1][1]["query"] == 'releasegroup:"晴天" AND artist:"周杰伦"'
assert requested[2][1]["query"] == 'artist:""'
assert requested[1][1]["query"] == 'releasegroup:("" OR "") AND artist:"周杰伦"'
assert requested[2][1]["query"] == 'artist:("" OR "" OR "")'
def test_file_recognition_searches_recordings_only(monkeypatch):
@@ -608,11 +609,29 @@ def test_recording_queries_ladder_relaxes_to_bare_title_last():
MetaMusic(title="晴天 (电影版)", artists=["周杰伦"])
)
assert queries[0] == 'recording:"晴天 (电影版)" AND artist:"周杰伦"'
assert queries[1] == 'recording:"晴天 (电影版)"'
assert queries[2] == 'recording:"晴天" AND artist:"周杰伦"'
full = '("" OR "") OR ("" OR "" OR "")'
assert queries[0] == f'recording:({full}) AND artist:"周杰伦"'
assert queries[1] == f'recording:({full})'
assert queries[2] == 'recording:("" OR "") AND artist:"周杰伦"'
# 署名变体兜底放在最后,由候选挑选的艺术家要求收紧
assert queries[-1] == 'recording:"晴天"'
assert queries[-1] == 'recording:("" OR "")'
def test_query_phrase_latin_unchanged_and_cjk_char_or():
"""拉丁文本保持短语检索,CJK 文本拆为逐字 OR,混合文本按词元拆分。"""
assert MusicBrainzModule._query_phrase("Fearless") == '"Fearless"'
assert MusicBrainzModule._query_phrase("晴天") == '("" OR "")'
assert MusicBrainzModule._query_phrase("好歌茹芸 Vol. 3") == (
'(("" OR "" OR "" OR "") OR "Vol." OR "3")'
)
assert MusicBrainzModule._query_phrase("") is None
def test_same_text_normalizes_cjk_numerals():
"""汉字数字与阿拉伯数字写法差异不应阻断候选比对。"""
assert MusicBrainzModule._same_text("茹此精彩十三首", "茹此精彩13首")
assert MusicBrainzModule._same_text("二十周年演唱会", "20周年演唱会")
assert not MusicBrainzModule._same_text("茹此精彩十三首", "茹此精彩14首")
def test_select_candidate_rejects_wrong_artist_same_title():
@@ -629,6 +648,21 @@ def test_select_candidate_rejects_wrong_artist_same_title():
assert MusicBrainzModule._select_candidate(meta, [wrong_artist], source="musicbrainz") is None
def test_select_candidate_rejects_artist_only_match():
"""CJK 逐字 OR 检索召回宽,标题未命中的候选不能仅凭艺术家署名被采信。"""
meta = MetaMusic(title="茹此精彩十三首", artists=["许茹芸"])
same_artist_other_song = MusicInfo(
source="musicbrainz",
music_type="recording",
media_id="recording-wrong",
title="半首歌",
artists=["许茹芸"],
)
assert MusicBrainzModule._select_candidate(
meta, [same_artist_other_song], source="musicbrainz") is None
def test_select_album_candidate_requires_title_and_artist():
"""专辑候选需标题(含去括号弱匹配)与艺术家同时命中才采信。"""
meta = MetaMusic(title="我爱夜 (新歌+精选)", artists=["许茹芸"], year=2003)