目录
版权
本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/122456273.
rt 方案
本文介绍在一个实际项目中如何使用Sphinx 2.3.2-beta (Sphinx 2.3.2-beta 笔记) 实现全文搜索. 小型项目可以参考本文的rt 方案, 更大型项目未必合适.
Sphinx 文档主要推荐main+delta 索引方案(文档"3.12. Delta index updates"): main 是全量, 几乎不变, 较长时间才重建一次; delta 是增量(新增的doc), 频繁地重建. main+delta 既能高效地索引(indexing), 也能较实时地查询.
但是本文采用一种rt 方案: 仅使用单一rt 索引(上线时重建); 系统运行期间实时更新索引(rt 索引本身允许实时更新); 每天自动optimize 一次. 好处是:
- 实时更新. 业务数据一更新马上就能搜索到(“4.3. RT index internals” 第一段: 仅在RAM块溢出要写disk块时才会延迟几秒, 但2.1.1-beta 采用双缓冲避免了该延迟). 这是主要优势.
- 自动管理. 采用main+delta 需要考虑的问题rt 似乎(几乎)都已经内置解决了: 修改doc、删除doc、delta 是否要并入main - 因为始终就是在更新同一个索引, 不用考虑合并的细节. 至于rt 索引碎片问题, 可以每天optimize 一次.
- 使用简单(见上一条).
主要需考虑的细节是重建索引期间发生的更新怎么处理? 目前可以简化(对小型项目): 停服重建. 例如每个月1号深夜执行一次.
另需注意: 由于不能参与db 事务, 全文索引数据不能保证与db 数据完全一致(除非使用db 本身提供的全文索引功能). 一般中小型项目应该适度容许索引数据的不准确, 例如在点击搜索结果明细时再校验.
document
项目需要搜索论坛帖子, 包括每楼发言的内容/附件名称、帖子标题. 索引doc 设计如下(有适当简化):
- id = 发言id
- content 内容, field
- content_txt 内容, attr string
- thread_id 帖子id, attr uint
- is_p_del 发言删否, attr uint
帖子表一对多关联发言表. 索引主键 = 发言表id.
content_txt 与content 内容相同, 用于snippet, 见后.
thread_id 不变. 发言逻辑删除或恢复时更新is_p_del.
snippet
snippet 函数(文档"8.1. SELECT syntax") 用于构造高亮结果. 例如对文本"cat dog" 搜索关键字"cat", 高亮结果是"<b>cat</b> dog".
(文档"12.1.26. sql_field_string") sql 数据源支持一种既被索引也存储文本内容的字段/属性, 即sql_field_string, 它可作为snippet 函数的输入文本. 但是rt 索引类型不支持这种类型: “12.2.50. rt_field” rt 字段被索引后丢弃了原始文本, 不能用于snippet 函数; “12.2.58. rt_attr_string” 可用于snippet 函数, 但是attr 不能被全文搜索(可用于条件过滤, 例如where is_p_del=0, 但不能用于例如match(‘xx’) ).
所以document 有content 也有content_txt: 一个是字段可以被搜索, 一个是属性用于snippet, 两者内容实际相同.
sql_query
重建索引时查询全量数据, 就是发言表 join 帖子表.
需要能搜索标题, 但标题在帖子表, 所以sql 里将标题合并到一楼发言content 里. 即标题匹配只针对一楼.
业务增删改
在业务发帖/发言/删除发言/恢复发言/物理删除发言等时, 同时更新rt. 例如:
- replace into rt (id,content,content_txt, thread_id,is_p_del) values (…) 用于新增发言或修改了发言内容时
- update rt set is_p_del=xx where id=yy 用于删除/恢复发言时
- delete from rt where id in (xx,yy,…) 用于物理删除一批发言时
关于事务
由于不能参与db 事务, 且Sphinx 本身很快, 可以在db 事务commit 成功后直接(同步)更新rt.
由于Sphinx 事务功能偏弱, 简化: 直接更新rt 而不使用事务; 同时捕获异常记录日志 - 因为我们的前提是小型项目适度容许索引数据不准确(会定期重建).
搜索
http://sphinxsearch.com/blog/2014/03/28/basics-of-paginating-results/
第一步-搜索:
select id,thread_id, snippet(content_txt, "q")
from rt
where
match("q")
and is_p_del = 0
limit 0,10 -- 第一页
option
max_matches = 50 -- 最多5页
其中match 使用content 字段, snippet 使用content_txt 属性.
一般全文搜索不需要返回太多页数据, 使用max_matches 限制.
第二步-total:
show meta like 'total'
紧接着执行此查询, total 是不超过max_matches 的全部匹配数. 注意total 不同于total_found: 后者可能远大于前者, 这就是前一步查询使用max_matches 限制的原因 - 为了节约资源.
文档"8.45. Multi-statement queries", 这2步可以一次查询.
第三步-查到的该页数据, 根据发言id 再去db 查询展示所需相关数据如发言人/发言时间等.
q
(文档"5.3. Extended query syntax") Sphinx 默认使用一种查询语法, 支持OR/NOT/限制字段/位置/相似度等. 例如查询"cat dog" 按该语法是既含"cat" 也含"dog", 但对一般用户期望是含"cat" 或者"dog".
(文档"8.15. CALL KEYWORDS syntax") 可使用keywords 函数将用户输入文本分拆为一个个的keyword, 此过程也会按索引设置进行normalization 例如复数转单数/去掉stopwords. 将分拆的结果OR 后查询就是用户期望的结果.
例如用户输入"cats dogs" => 分拆为"cat" 和"dog" => 构造查询"cat | dog".
高亮、escape 与js 注入
前面doc.content 我们设计为发言内容的纯文本而非富文本(即innerText 而非innerHTML), 因为展示搜索结果时应该去掉原发言的各种格式(例如p/ul/img/a).
snippet 函数默认使用"<b></b>" 标注高亮片段, 但是用户在富文本编辑器输入例如"a<b>c" 取到的innerText 也是(未escape的)“a<b>c”, 这就会与前者相同, 导致不匹配的文字也会被处理成高亮.
snippet 函数支持动态指定高亮的标注, 可以选择一个较不容易冲突的标签例如:
select snippet(content_txt, "q", 'before_match=<XYZ>', 'after_match=</XYZ>')
另外, 原则上用户输入的内容应视为不可信任的 - 攻击者可能替换提交的内容例如包含js 脚本"<script>xxx</script>" - 展示搜索结果时可:
- 首先html_escape content_txt (含snippet 已标注高亮的片段: “<XYZ>” => “<XYZ>”)
- 再仅将"<XYZ>" 替换为 “<b>”
- 直接(原样)输出到页面
其他
attach index
rt 索引加载大量数据时据说可能较慢, 重建时可先重建plain 索引, 再attach 到rt 索引上(文档"8.26. ATTACH INDEX syntax"):
attach index main to rtindex rt
optimize rt
文档"8.36. OPTIMIZE INDEX syntax":
optimize index rt
golang 程序/配置
Sphinx连接串(github.com/go-sql-driver/mysql):
root:xx@tcp(192.168.0.1:9306)/none?collation=utf8_general_ci&interpolateParams=true
程序设置:
ft.SetMaxOpenConns(30) // sphinx.conf [searchd] max_children = 30
ft.SetMaxIdleConns(10) // 空闲连接最多10个
ft.SetConnMaxIdleTime(3 * time.Minute) // 连接最长空闲3min
read_timeout (文档"12.4.5") 在sphinx.conf 配置或使用默认值.
实际资源占用
目前项目content_txt 字符总计141K(中文为主), rt 索引磁盘文件:
- .sps 376K
- .spd 156K
- .spi 44K
- .spa 14K
其他文件都较小(文档"12.2.3. path").
top 命令显示searchd (与db 在同一台机器; 闲时) 每隔7~8 秒top 1 一次: %CPU 0.3 %MEM 2.2.
已知问题
服务重启后stopword 规则丢失
发现attach main 到rt 后, 重启searchd 服务后rt 的stopword 规则丢失, 例如"at" 是stopword 但仍能搜到.
具体试验结果是:
- attach 后新增/修改数据时rt 已经会索引"at", 但因为此时call keywords 还能过滤"at", 所以表面看不出问题
- 重启服务后call keywords 也不能过滤"at", 此时问题暴露
http://sphinxsearch.com/bugs/view.php?id=1300#c3178 说类似问题已在2.0.6 修复, 但看来不准确.
附录
sphinx.conf
source srcMain
{
type = mysql
sql_host = localhost
sql_port = 3306
sql_user = root
sql_pass = xx
sql_db = dbxx
sql_query_pre = SET NAMES utf8
sql_query_range = select min(id), max(id) from post
sql_query = \
select ... \
from post p \
join thread t on p.thread_id = t.id \
where \
p.id >= $start \
and p.id <= $end
sql_attr_string = content_txt
sql_attr_uint = thread_id
sql_attr_uint = is_p_del
# sql_ranged_throttle = 1000
}
index main
{
source = srcMain
path = /var/lib/sphinx/main
morphology = stem_en
stopwords = /etc/sphinx/stopwords-innodb.txt /etc/sphinx/stopwords-myisam.txt /etc/sphinx/stopwords-zh_cn.txt
ngram_len = 1
ngram_chars = U+3000..U+2FA1F
preopen = 1
hitless_words = all
}
index rt
{
type = rt
path = /var/lib/sphinx/rt
morphology = stem_en
stopwords = /etc/sphinx/stopwords-innodb.txt /etc/sphinx/stopwords-myisam.txt /etc/sphinx/stopwords-zh_cn.txt
ngram_len = 1
ngram_chars = U+3000..U+2FA1F
preopen = 1
hitless_words = all
rt_mem_limit = 128M
rt_field = content
rt_attr_string = content_txt
rt_attr_uint = thread_id
rt_attr_uint = is_p_del
}
indexer
{
mem_limit = 128M
}
searchd
{
listen = 9306:mysql41
log = /var/log/sphinx/searchd.log
query_log = /var/log/sphinx/query.log
max_children = 30 # ( 对应ft.SetMaxOpenConns(30) )
pid_file = /var/run/sphinx/searchd.pid
seamless_rotate = 1
max_packet_size = 8M
workers = threads # for RT to work (?)
binlog_path = /var/lib/sphinx/
binlog_max_log_size = 16M
collation_server = utf8_general_ci
rt_flush_period = 3600
rt_merge_iops = 40
rt_merge_maxiosize = 1M
shutdown_timeout = 3
query_log_min_msec = 10
}
stopwords-zh_cn.txt
, 。 ! ? : ;
的 了 在 不 是 很 也 们 此 一
供参考. 使用indexer(文档"7.1. indexer command reference") “--buildstops” 生成.
由于ngram 是单字匹配, 例如"的" 设为stopword 后, 搜"目的" 将只能匹配"目" 字, 所以此列表内容少.