音乐搜索的极致
唐福林
tangfulin@gmail.com
http://blog.fulin.org
目录
项目简介
需求描述
搜索实现
查询示例
持续改进
项目简介 (1/3)
中国移动
12530
咪咕
Miniportal
搜索
Out source : edadao
项目简介 (2/3)
时间: 2009年 9月 12日到 10月 22日
地点:成都,郫县,犀浦,移动音乐基
地
参与人员:
需求提供: wangquanli@12530,
zhengchangsong@12530
开发人员: mike,tangfulin,xww,wanghui
特别贡献: dave
项目简介 (3/3)
部署情况:
位置:移动音乐基地,西区枢纽机房
机器:
建索引: 227.98
搜索: 227.221
DualCore AMD Opteron(tm) Processor 8218 2.6G * 8
8G mem
Red Hat Enterprise Linux Server release 5.3 (Tikanga)
Linux 2.6.18128.el5PAE #1 SMP i686 athlon GNU/Linux
索引大小: 3个索引目录共 1.2G
流量:
机器负载情况:
需求描述
搜索字段:歌手,歌曲,专辑,歌词
搜索方式:
精确匹配
前缀匹配
分词匹配
模糊匹配
拼音全量匹配
拼音首字母匹配
拼音同音匹配
拼音纠错匹配
关键词提示:
• 搜索框下拉提示
• 纠错提示
需求-精确匹配
规则:精确匹配或过滤所有特殊字符后精
确匹配
单个字段:
歌手:阿唬 , 80前后
歌曲:爱情 BT大讲堂
专辑: Alive!
多个字段联合:
歌手名+歌曲名:刘德华 今天
歌手名+专辑名:许茹芸 爱 .旅行 .一公里
需求-前缀匹配
规则:过滤所有特殊字符后前缀匹配
单个字段:
歌手:刘德,张学
歌曲:
专辑:
需求-分词匹配
规则:过滤所有特殊字符后分词匹配
单个字段:
歌手:德华,杰伦,学友
歌曲:
专辑:
歌词:
多个字段联合:先Must,再 Should
歌手名+歌曲名+专辑名:
歌手名+专辑名:
需求-模糊匹配
规则:一定模糊度的词匹配(注:很
慢)
单个字段:
歌手:刘大华
歌曲: beautful, califonia
专辑:
需求-拼音全量匹配
规则:用户输入拼音匹配
单个字段:
歌手: liudehua, zhangxueyou
歌曲:
专辑:
需求-拼音首字母匹配
规则:用户输入拼音首字母匹配
单个字段:
歌手: ldh, zxy
歌曲:
专辑:
需求-拼音同音匹配
规则:用户拼音输入法,输入错误的同
音字匹配
单个字段:
歌手:柳的话,两用器
歌曲:
专辑:
需求-拼音纠错匹配
规则:用户拼音输入法输入错误的字,
或直接输入错误的拼音
nl,hf,zzh,cch,ssh,anang,eneng,ining
单个字段:
歌手:牛德华, niudehua
歌曲:
专辑:
搜索实现
建索引策略
冗余字段
中文将拼音,首字母也建进索引里
搜索 Query策略
弃用多次查询的策略
采用多个 Query拼装成一个
BooleanQuery,设置不同的权值,
一次查询的策略
搜索实现:建索引策略 (1/2)
歌手: singer_name
singer_name_save:保存字段, trim后,原封不动
singer_name_filtered: 过滤字段,过滤所有的特殊字符,转小写
singer_name_analyzed: 分词字段
singer_name_notanalyzed: 不分词字段,前缀匹配使用
singer_name_full: 拼音全量字段
singer_name_first: 拼音首字母字段
歌曲: song_name
专辑: album_name
搜索实现:建索引策略 (2/2)
关键词: keyword
所有的歌手名,歌曲名,专辑名,
歌手+歌曲,歌手+专辑,都视为
关键词
单独一个索引文件
供下拉提示和搜索无结果时的纠错
提示使用
也提供拼音全量,拼音首字母,拼
音纠错等功能
搜索实现:搜索策略 (1/6)
策略列表:
1. 精确匹配:歌手,歌曲,专辑,不分词字段,去掉前后多余空格,精确匹配
2. 过滤后的精确匹配:歌手,歌曲,专辑,过滤字段,去掉所有特殊字符,英文转成小
写,精确匹配
3. 拼音全量匹配:歌手,歌曲,专辑,拼音全量字段,去掉所有非英文字符,英文转成小
写,精确匹配
4. 同音纠错匹配:歌手,歌曲,专辑,拼音全量字段,只对含中文的搜索词使用,中文转
拼音,英文转小写,去掉所有特殊字符,精确匹配
5. 拼音首字母匹配:歌手,拼音首字母字段,中文转拼音首字母,英文转小写,去掉所有
特殊字符,精确匹配
6. 前缀匹配:歌手,歌曲,专辑,不分词字段,去掉前后多余空格,英文转小写,前缀匹
配
7. 分词Must匹配:歌手,歌曲,专辑,(歌词),分词字段,分词,词之间使用Must连
接,分词匹配
搜索实现:搜索策略 (2/6)
策略列表(续):
1. 分词 Should 匹配:歌手,歌曲,专辑,(歌词),分词字段,分词,词之间使用
Should连接,分词匹配
2. 合并分词 (must)匹配:歌手+歌曲+专辑 分词字段,分词,(当前使用 must 连
接),分词匹配
3. 合并分词 (should)匹配:歌手+歌曲+专辑 分词字段,分词,(当前使用 Should 连
接),分词匹配
4. 拼音纠错匹配查询(忽略掉鼻音等) :歌手,歌曲,专辑,分词字段,去掉前后多余空
格,英文转小写 .
5. 中文模糊匹配 ,中文时模糊度: 0.65:歌手,歌曲,专辑,分词字段,去掉前后多余空
格,英文转小写 .
6. 英文模糊匹配,英文模糊度: 0.85:歌手,歌曲,专辑,分词字段,去掉前后多余空
格,英文转小写 .
搜索实现:搜索策略 (3/6)
精度选择
只搜索精确匹配结果
精确匹配,过滤后精确匹配,前缀匹配
拼音全量,首字母
分词全部命中
只搜索模糊匹配结果
分词部分命中
同音纠错,拼音纠错
模糊匹配
去掉精确匹配的结果
搜索全部结果
搜索实现:搜索策略 (4/6)
字段选择
只搜索歌手
只搜索歌曲
只搜索专辑
只搜索歌词
搜索关键词字段
搜索全部字段(暂时不包括关键词
和歌词)
搜索实现:搜索策略 (5/6)
设置权值
将所有的策略置入一个有序列表中
列表中相邻的两个策略之间权值相差常数倍(当前
设置为 10)。过大可能会导致 lucene评分溢出,过
小可能会导致不同策略命中的结果集重叠
调整列表中策略的先后次序以调整结果集中各种命
中的出现顺序
二级权值:在搜索全部的时候,歌手 >歌曲 >专
辑,所以需要在同一个策略内部再设置字段权值
中文分词命中的权值设置: Lucene 默认打分策略
中,并没有考虑命中的词的长度。为了优先显示长
的词命中的结果,对分词 Query中每个词根据长度
设置不同的权值
搜索实现:搜索策略 (6/6)
索引文件划分
搜索歌曲索引
搜索专辑索引
搜索关键词索引
排序策略
编辑置顶
Lucene 评分
业务量(点击,订阅,播放等)
关键词词频
搜索示例
歌曲索引,全部字段,精确搜索
搜 刘德华,结果条数: 329
QUERY:(((singer_name_notanslysis:刘德华 ^9.0
song_name_notanslysis:刘德华 ^4.0 album_name_notanslysis:刘德
华 ))^10000.0) (((singer_name_filtered:刘德华 ^9.0 song_name_filtered:
刘德华 ^4.0 album_name_filtered:刘德华 ))^1000.0)
(((singer_name_filtered:刘德华 *^9.0 song_name_filtered:刘德华 *^4.0
album_name_filtered:刘德华 *))^100.0) ((((((+singer_name_anslysis:刘
德华 +singer_name_anslysis:德华 )^9.0) ((+song_name_anslysis:刘德
华 +song_name_anslysis:德华 )^4.0) (+album_name_anslysis:刘德华
+album_name_anslysis:德华 ))))^10.0) ((((+singer_song_album:刘德华
+singer_song_album:德华 ))))
搜索示例
歌曲索引,全部字段,精确搜索
搜 ldh,结果条数: 401(命中刘德华,刘大浩等)
QUERY:(((singer_name_notanslysis:ldh^9.0
song_name_notanslysis:ldh^4.0
album_name_notanslysis:ldh))^1000.00006)
(((singer_name_filtered:ldh^9.0 song_name_filtered:ldh^4.0
album_name_filtered:ldh))^100.00001) (((singer_name_full:ldh^9.0
song_name_full:ldh^4.0 album_name_full:ldh))^10.000001)
(((singer_name_first:ldh))^1.0000001) (((singer_name_filtered:ldh*^9.0
song_name_filtered:ldh*^4.0 album_name_filtered:ldh*))^0.10000001)
((((((+singer_name_anslysis:ldh)^9.0) ((+song_name_anslysis:ldh)^4.0)
(+album_name_anslysis:ldh))))^0.010000001)
(((((+singer_song_album:ldh))))^0.0010)
搜索示例
歌曲索引,全部字段,精确搜索
搜 liudehua,结果条数: 324(歌名《我不是刘德华》无法命中)
QUERY:(((singer_name_notanslysis:liudehua^9.0
song_name_notanslysis:liudehua^4.0
album_name_notanslysis:liudehua))^1000.00006)
(((singer_name_filtered:liudehua^9.0 song_name_filtered:liudehua^4.0
album_name_filtered:liudehua))^100.00001)
(((singer_name_full:liudehua^9.0 song_name_full:liudehua^4.0
album_name_full:liudehua))^10.000001)
(((singer_name_filtered:liudehua*^9.0 song_name_filtered:liudehua*^4.0
album_name_filtered:liudehua*))^1.0000001)
((((((+singer_name_anslysis:liudehua)^9.0)
((+song_name_anslysis:liudehua)^4.0)
(+album_name_anslysis:liudehua))))^0.10000001)
(((((+singer_song_album:liudehua))))^0.010000001)
(((singer_name_first:liudehua))^0.0010)
搜索示例
歌曲索引,全部字段,模糊搜索
搜 刘德华,结果条数: 44(命中爱德华,杨德华等)
QUERY:((((singer_name_notanslysis:刘德华 ^9.0 song_name_notanslysis:刘德华 ^4.0
album_name_notanslysis:刘德华 ))^10000.0) (((singer_name_filtered:刘德华 ^9.0
song_name_filtered:刘德华 ^4.0 album_name_filtered:刘德华 ))^1000.0)
(((singer_name_filtered:刘德华 *^9.0 song_name_filtered:刘德华 *^4.0
album_name_filtered:刘德华 *))^100.0) ((((((+singer_name_anslysis:刘德华
+singer_name_anslysis:德华 )^9.0) ((+song_name_anslysis:刘德华 +song_name_anslysis:
德华 )^4.0) (+album_name_anslysis:刘德华 +album_name_anslysis:德华 ))))^10.0)
((((+singer_song_album:刘德华 +singer_song_album:德华 ))))) +(((((singer_song_album:
刘德华 ^9.0 singer_song_album: 德华 ^4.0)))^10000.0) (((((singer_name_anslysis:刘德华
^9.0 singer_name_anslysis:德华 ^4.0)^9.0) ((song_name_anslysis:刘德华 ^9.0
song_name_anslysis:德华 ^4.0)^4.0) (album_name_anslysis:刘德华 ^9.0
album_name_anslysis:德华 ^4.0)))^1000.0) (((((singer_name_full:liudehua)^9.0)
((song_name_full:liudehua)^4.0) (album_name_full:liudehua)))^100.0) ((())^10.0)
((singer_name_anslysis:刘德华 ~0.65^9.0 song_name_anslysis:刘德华 ~0.65^4.0
album_name_anslysis:刘德华 ~0.65)))
搜索示例
歌曲索引,全部字段,模糊搜索
搜 牛德华,结果条数: 840(命中刘德华,爱德华,杨德华等)
QUERY:((((singer_name_notanslysis:牛德华 ^9.0 song_name_notanslysis:牛德华 ^4.0
album_name_notanslysis:牛德华 ))^10000.0) (((singer_name_filtered:牛德华 ^9.0
song_name_filtered:牛德华 ^4.0 album_name_filtered:牛德华 ))^1000.0)
(((singer_name_filtered:牛德华 *^9.0 song_name_filtered:牛德华 *^4.0
album_name_filtered:牛德华 *))^100.0) ((((((+singer_name_anslysis:牛
+singer_name_anslysis:德华 )^9.0) ((+song_name_anslysis:牛 +song_name_anslysis:德
华 )^4.0) (+album_name_anslysis: 牛 +album_name_anslysis: 德华 ))))^10.0)
((((+singer_song_album:牛 +singer_song_album:德华 ))))) +(((((singer_song_album:牛
singer_song_album: 德华 ^4.0)))^10000.0) (((((singer_name_anslysis:牛
singer_name_anslysis:德华 ^4.0)^9.0) ((song_name_anslysis:牛 song_name_anslysis:德华
^4.0)^4.0) (album_name_anslysis:牛 album_name_anslysis:德华 ^4.0)))^1000.0)
(((((singer_name_full:niudehua)^9.0) ((song_name_full:niudehua)^4.0)
(album_name_full:niudehua)))^100.0) ((())^10.0) ((singer_name_anslysis:牛德华 ~0.65^9.0
song_name_anslysis:牛德华 ~0.65^4.0 album_name_anslysis: 牛德华 ~0.65)))
搜索示例
歌曲索引,全部字段,模糊搜索
搜 niudehua,结果条数: 324
QUERY:((((singer_name_notanslysis:niudehua^9.0 song_name_notanslysis:niudehua^4.0
album_name_notanslysis:niudehua))^1000.00006) (((singer_name_filtered:niudehua^9.0
song_name_filtered:niudehua^4.0 album_name_filtered:niudehua))^100.00001)
(((singer_name_full:niudehua^9.0 song_name_full:niudehua^4.0
album_name_full:niudehua))^10.000001) (((singer_name_filtered:niudehua*^9.0
song_name_filtered:niudehua*^4.0 album_name_filtered:niudehua*))^1.0000001)
((((((+singer_name_anslysis:niudehua)^9.0) ((+song_name_anslysis:niudehua)^4.0)
(+album_name_anslysis:niudehua))))^0.10000001)
(((((+singer_song_album:niudehua))))^0.010000001)
(((singer_name_first:niudehua))^0.0010)) +(((((singer_song_album:niudehua^64.0)))^1000.0)
(((((singer_name_anslysis:niudehua^64.0)^9.0) ((song_name_anslysis:niudehua^64.0)^4.0)
(album_name_anslysis:niudehua^64.0)))^100.0) (((((singer_name_full:niudefua
singer_name_full:liudehua)^9.0) ((song_name_full:niudefua song_name_full:liudehua)^4.0)
(album_name_full:niudefua album_name_full:liudehua)))^10.0)
((singer_name_anslysis:niudehua~0.85^9.0 song_name_anslysis:niudehua~0.85^4.0
album_name_anslysis:niudehua~0.85)))
搜索示例
歌曲索引,歌手字段,精确搜索
搜 杰伦,结果条数: 270
QUERY:(((singer_name_notanslysis:杰伦 ))^1000.0) (((singer_name_filtered:杰伦 ))^100.0)
(((singer_name_filtered:杰伦 *))^10.0) ((((+singer_name_anslysis:杰伦 ))))
搜索示例
歌曲索引,歌曲字段,模糊搜索
搜 li,结果条数: 117 (命中 你 )
QUERY:((((song_name_notanslysis:li))^10000.0) (((song_name_filtered:li))^1000.0)
(((song_name_full:li))^100.0) (((song_name_filtered:li*))^10.0)
((((+song_name_anslysis:li))))) +(((((song_name_anslysis:li^4.0)))^100.0)
((((song_name_full:ni)))^10.0) ((song_name_anslysis:li~0.85)))
搜索示例
关键词索引,搜索歌曲
搜 liu,结果:
流浪 流星 留恋 六月雪 浏阳河 流浪狗 流浪者之歌 留不住你的温柔
QUERY:+keyword_type:2 keyword_word_notanslysis:liu +
(((keyword_word_filtered:liu*)^100.0) ((keyword_word_full:liu*)^100.0)
((keyword_word_first:liu*)^100.0))
搜索示例
关键词索引,搜索歌手
搜 liu,结果:
刘德华 刘冠群 刘亦敏 刘韵 刘若英 刘基俊 刘益中 刘芳 刘庆
QUERY:+keyword_type:1 keyword_word_notanslysis:liu +
(((keyword_word_filtered:liu*)^100.0) ((keyword_word_full:liu*)^100.0)
((keyword_word_first:liu*)^100.0))
搜索示例
关键词索引,搜索全部字段
搜 zhoujie,结果:(有歌手+歌曲的命中)
周杰伦 周杰磊 周杰伦我求求你了 周杰伦传递祝福 周杰伦春节祝福 周杰伦情人节祝福
周杰伦 蒲公英的约定
周杰伦 最长的电影
周杰伦 阳光宅男
周杰伦 甜甜的
QUERY:+(keyword_type:1 keyword_type:2 keyword_type:3)
keyword_word_notanslysis:zhoujie +(((keyword_word_filtered:zhoujie*)^100.0)
((keyword_word_full:zhoujie*)^100.0) ((keyword_word_first:zhoujie*)^100.0))
搜索示例
关键词索引,搜索全部字段
搜 柳的话,结果:(拼音同音命中)
刘德华 留得华
刘德华 冰雨
刘德华 中国人
刘德华 幸福这么远那么甜
刘德华 百分百好戏
刘德华 笑着哭
刘德华 谢谢你的爱
刘德华 爱你一万年
刘德华 情义俩心坚
QUERY:+(keyword_type:1 keyword_type:2 keyword_type:3) keyword_word_notanslysis:
柳的话 +(((keyword_word_filtered:柳的话 *)^100.0)
(((keyword_word_full:liudehua*)^100.0) ((keyword_word_full:liudihua*)^100.0)))
持续改进
性能调优: resin,内存, cache
汉字转拼音:多音字,特殊符号
拼音纠错:另一种思路,
化 vs排列组合
关键词: Trie树,按词频排序,加入歌词数据
业务要求以频繁更新的业务量作为排序依据
标签搜索:新需求
搜索关键词,保持先后顺序
自定义打分算法的尝试
Lucene 升级到 2.9.1, bug 1974, explain 显示 0或者 NaN
显示时最佳片段截取, html实体截断问题
标红,按策略的山寨标红与 Lucene自带标红的优劣比较及取舍
性能调优
• 目标:单台机器,百万数量级的索引, 1000个并发
下, 99% 0.5秒内返回
• 优势:
• 索引更新不是很频繁,可以忽略不计
• 服务器性能不错, 8cpu, 8G内存
• 劣势:
• 并发大,返回时间 0.5秒要求太苛刻
• 一期代码有很多不合理的地方
• 项目时间紧张
• 使用了 resin作为容器,有太多不可控因素
汉字转拼音
• Pinyin4j 的词库
• 自己整理的多音字表
• 当前将所有多音字的组合都建在索引里
• 莫文蔚: mowenwei, mowenyu
• 优点:保证能查到
• 缺点:输入 mowenyu 能查到莫文蔚,而且一个歌
名中如果有好几个多音字,排列组合的数量比较
可观
• 拼音首字母,而不是拼音声母
• 张学友: zxy,不是 zhxy
拼音纠错
• 规则:
• nl,hf,zzh,cch,ssh,anang,eneng,ining (onong)
• 当前实现:
• 将用户输入的搜索词中的每个出现,依次替换成对应的纠
错,联合成一个 Should Query
• 如: liudehua: niudehua, liudefua
• 另一种思路:标准化
• 规定规则中的替换只能单向: n>l,z>zh等
• 在索引中增加一个标准化拼音字段,如曾经最美,该字段
存储的值为 chengjingzhuimei
• 用户输入关键词,也经过同样的标准化后,在该字段进行
查询
关键词
• 关键词当前使用 Lucene 的索引前缀查询的方式实现
• 也包括拼音,首字母,纠错等 Query
• 本来还有模糊查询的 Query,但后来发现太影响查询
速度了,于是就暂时去掉了
• 当前没有把歌词数据建到关键词索引中去。如果把歌
词建进去,这个索引就太大了,必须要进行分拆
• 考虑使用 Trie树:
• 多棵树,拼音,首字母,中文需要各自建树
• 模糊查询的问题
排序 vs更新
• 业务需求希望能以歌曲的业务量(播放,下载等量)
作为排序的一个依据
• 意味着需要频繁的更新索引,而且为了更新这样一个
数字字段,需要将整个文档删除重新添加,不划算
• 打算重载 Lucene 的 Collection 类,自己实现排序字
段值的加载,不从索引里面读取
• 问题: 2.4 与 2.9 在这个地方的实现上有很大的不
同,没法无缝切换
标签搜索
• 固定维度的标签,编辑填写,非用户产生内容
• 如:奥运,免费,铃声,开心,悲伤等
• 关键是产品
,非技术实现
• 参考: google 泡泡挑歌
保持搜索关键词的顺序
• 延后实现的一个需求
• 只命中跟用户输入的多个关键词之间的顺序一致的结果
• 如:
• 用户输入 “谢谢 爱”命中“谢谢你的爱”,但输入
“爱 谢谢”不命中
• 用户输入 “眼睛 背叛 心”命中“你的眼睛背叛了
你的心”,但输入“心 背叛 眼睛”不命中
• 实现:
• 分词命中,无法保留顺序信息
• 模糊查询,效率太差
• ???
打分算法
需求:
• 产品人员对Lucene打分算法的不理解,要求单纯的以某一个
依据来进行排序,如命中的词的个数
• 拼多个Query查询的副作用:多个Query的评分累加得到的
最后得分,会导致各个Query的命中结果重叠(想法:把
累加改成取最大值?)
• 一个想法:同样的分词命中,命中较长的词的结果排前面
教训:
• 没有金刚钻,别揽瓷器活!不要轻易的去改Lucene的评分算法
Lucene版本的选择
• 首选 2.9.0
• Bug 1974:
https://issues.apache.org/jira/browse/LUCENE1974
• 换成了 2.4.1
• 为了性能及长远打算,还是希望换回 2.9.1
• Explain 函数调用返回评分 0 或 NaN
摘要截取
• Miniportal 空间有限,歌曲,专辑,甚至歌手名都可
能需要截断
• 截断时需要考虑标红问题
• 截断时需要考虑 html 实体的问题
标红
• Lucene 标红的优点与不足
• 优点:正统,可升级
• 缺点:不能满足需求,前缀标红,拼音标红等
• 山寨标红
• 按照每种 Query进行相应的标红,最后合并
•
http://blog.fulin.org
更多讨论
页 1
页 2
页 3
页 4
页 5
页 6
页 7
页 8
页 9
页 10
页 11
页 12
页 13
页 14
页 15
页 16
页 17
页 18
页 19
页 20
页 21
页 22
页 23
页 24
页 25
页 26
页 27
页 28
页 29
页 30
页 31
页 32
页 33
页 34
页 35
页 36
页 37
页 38
页 39
页 40
页 41
页 42
页 43
页 44
页 45
页 46
页 47
页 48