使用Sphinx 2.3.2-beta 实现全文搜索(rt 方案)

版权

本文为原创, 遵循 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>” => “&lt;XYZ&gt;”)
  • 再仅将"&lt;XYZ&gt;" 替换为 “<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 后, 搜"目的" 将只能匹配"目" 字, 所以此列表内容少.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值