美文网首页PHP经验分享程序员
Sphinx实时搜索设计探讨

Sphinx实时搜索设计探讨

作者: JobinLi | 来源:发表于2019-12-20 19:23 被阅读0次

    背景

    Sphinx是一个全文搜索引擎,虽然官方没对中文分词检索做直接支持,但是配合coreseek还是能很好地实现中文全文检索的。至于分词结果,不本文讨论范围内,本文主要针对Sphinx的实时搜索能力进行探讨。
    用过Sphinx的应该都知道,虽然提供了更新属性的接口(php中是 UpdateAttributes 函数),但是却无法对文本类型字段进行更新。本文主要以PHP来进行实际操作示范。

    题外话:其实ElectricSearch这款全文检索工具做实时索引支持更好,但是因为本人工作中使用的是Sphinx,且ElectricSearch的使用成本相对来Sphinx来说较重,所以本人暂时没有迁移过去,但还是十分推荐有条件的直接使用ElasticSearch,真香!

    解决思路

    总结就是: 全量索引(更新周期1天) + 增量索引(更新周期1分钟) + 实时索引

    1. 全量索引,main,对目标数据源在一个较长周期中进行全量更新
    2. 增量索引, inc,对目标数据源在一个相对较短的周期中进行增量更新,也就是把上次全量索引后新增的以及修改过的数据进行索引
    3. 实时索引, rtdata,sphinx对实时所以有较好的更新支持,而且是基于内存的(未超过设定的最大值时),速度较快,弥补UpdateAttributes 函数不能更新文本字段的缺陷,且为增量索引更新间隙中生成的新内容进行索引。

    通过这三者的结合,就可以基于Sphinx实现一个无限接近于实时,且占用资源相对可观的全文搜索。

    疑点与要点

    1. 全量索引与增量索引之间的覆盖问题
      增量索引中会含有全量索引的部分数据,如全量索引中有个 id1 的文档,keywrods 字段为 黄金,加入全量索引后,该字段被更新为了 铂金,这时候,无论是搜索 黄金 还是 铂金 都能检索出 id = 1 的这个文档。这种情况可以通过在增量索引源中配置 sql_query_killlist 参数来避免增量索引更新后,全量索引内容还能被检索的问题。注意 较新 的索引数据在搜索时候要放在 较旧 的索引数据后。放在此处就是,搜索时候应该是 $sphinx->Query('xxx', 'main;inc')
    2. 实时索引与全量索引及增量索引之间的覆盖问题
      和上面说到的情况一样,如果在增量索引更新间隔中,旧数据被更新的时候,也会检索出滞后的数据。而且受限于前面说到的 不能即时更新文本类型字段 的问题,我们可以在 非实时索引 中添加过滤字段,如 is_del,并通过 UpdateAttributes 函数将其更新后,再在 实时索引 中添加/修改该条记录,搜索时候加上 is_del = 0 的filter,就可以避免这种覆盖的情况。

    实际操作

    一、数据准备

    1. 数据源
    CREATE TABLE `article` (
        `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
        `title` VARCHAR(100) NOT NULL DEFAULT '0',
        `keywrods` VARCHAR(100) NOT NULL DEFAULT '0',
        `is_del` TINYINT(1) NOT NULL DEFAULT 0,
        `create_at` INT(11) NOT NULL DEFAULT 0,
        `update_at` INT(11) NOT NULL DEFAULT 0,
        PRIMARY KEY (`id`)
    )
    COLLATE='utf8_general_ci';
    
    1. 更新记录表
    CREATE TABLE `up_record` (
        `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
        `max_id` INT(11) UNSIGNED NOT NULL DEFAULT '0',
        `update_at` INT(11) NOT NULL DEFAULT 0,
        PRIMARY KEY (`id`)
    )
    COLLATE='utf8_general_ci';
    
    1. 全量索引sphinx配置
    source main
    {
            type            = mysql
            sql_host        = 127.0.0.1
            sql_user        = www
            sql_pass        = 123456
            sql_db          = test
            sql_port        = 3306  # optional, default is 3306
            sql_query_pre   = SET NAMES UTF8
            sql_query_range = SELECT MIN(id), MAX(id) FROM artile # 配合step对数据源进行分段导入
            sql_range_step  = 1000 # 每次导入1000条
            sql_query       = SELECT id, title, keywrods, is_del FROM article WHERE id >= $start AND id <= $end
            # 执行完毕后更新记录表,记录当前最大id与索引时间
            sql_query_post_index = REPLACE INTO up_record SELECT 1, MAX(id), UNIX_TIMESTAMP() FROM article
            sql_attr_uint   = is_del
    }
    index main
    {
            source = main
            path = /usr/local/sphinx/var/data/main
            # 文件存储模式(默认为extern)
            docinfo = extern
            # 缓存数据内存锁定
            mlock = 0
            # 马氏形态学(对中文无效)
            morphology = none
            # 索引词最小长度
            min_word_len = 1
            # 数据编码(设置成utf8才能索引中文)
            charset_type = utf-8
            # 最小索引前缀长度
            min_prefix_len = 0
            # 最小索引中缀长度
            min_infix_len = 1
            # 对于非字母型数据的长度切割(for CJK indexing)
            ngram_len = 1
            # 对否对去除用户输入查询内容的html标签
            html_strip = 0
           # propen = 1
            charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
            ngram_chars = U+3000..U+2FA1F
    }
    
    
    1. 增量索引sphinx配置
    source inc
    {
            type            = mysql
            sql_host        = 127.0.0.1
            sql_user        = www
            sql_pass        = 123456
            sql_db           = test
            sql_port         = 3306  # optional, default is 3306
            sql_query_pre   = SET NAMES UTF8
            sql_query_range = SELECT MIN(id), MAX(id) FROM artile # 配合step对数据源进行分段导入
            sql_range_step  = 1000 # 每次导入1000条
            sql_query       = SELECT id, title, keywrods, is_del FROM article \ 
                WHERE id >= $start AND id <= $end \ 
                AND (\ 
                  id > (SELECT max_id FROM up_record WHERE id = 1) \ 
                OR update_at > (SELECT update_at FROM pm_sphinx WHERE id = 1) \ 
            )
            sql_attr_uint       = is_del
    }
    index inc
    {
            source = inc
            path = /usr/local/sphinx/var/data/inc
            # 文件存储模式(默认为extern)
            docinfo = extern
            # 缓存数据内存锁定
            mlock = 0
            # 马氏形态学(对中文无效)
            morphology = none
            # 索引词最小长度
            min_word_len = 1
            # 数据编码(设置成utf8才能索引中文)
            charset_type = utf-8
            # 最小索引前缀长度
            min_prefix_len = 0
            # 最小索引中缀长度
            min_infix_len = 1
            # 对于非字母型数据的长度切割(for CJK indexing)
            ngram_len = 1
            # 对否对去除用户输入查询内容的html标签
            html_strip = 0
           # propen = 1
            charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
            ngram_chars = U+3000..U+2FA1F
    }
    
    
    1. 实时索引sphinx配置
    index rtdata
    {
            type            = rt
            rt_mem_limit    = 64M # 最大内存,视情况而定,超出此值会存入下面的path降低效率
            path            = /usr/local/sphinx/var/data/rtdata
            # 中文分词词典
            chinese_dictionary = /var/lib/sphinx/xdict
            # 最小索引前缀长度
            min_prefix_len = 0
            # 最小索引中缀长度
            min_infix_len = 1
            # 对于非字母型数据的长度切割(for CJK indexing)
            ngram_len = 1
            # 对否对去除用户输入查询内容的html标签
            html_strip      = 0
            #charset_type    = utf-8
            charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
            ngram_chars = U+3000..U+2FA1F
    
            rt_attr_uint        = id
            rt_field              = title
            rt_field              = keywords
            rt_attr_uint        = is_del
    }
    

    注意:上面的sphinx配置为同一个sphinx.conf内容节选

    二、更新脚本与任务

    1. 全量更新脚本
    # reload_all.sh
    
    # 实时索引需要手动清理
    mysql -P9306 -h127.0.0.1 -e "truncate rtindex rtdata;"
    /usr/local/sphinx/bin/indexer -c /usr/local/sphinx/conf/sphinx.conf --all --rotate
    
    1. 增量更新脚本
    # reload_inc.sh
    
    # 实时索引需要手动清理
    mysql -P9306 -h127.0.0.1 -e "truncate rtindex rtdata;"
    /usr/local/sphinx/bin/indexer -c /usr/local/sphinx/conf/sphinx.conf inc --rotate
    
    1. 定时任务设置
    -> crontab -l
    
    # 每分钟更新一次增量脚本
    * * * * * /path/to/reload_inc.sh
    # 每天凌晨3点更新全量脚本
    0 3 * * * /path/to/reload_all.sh
    

    三、相关php伪代码

    增量索引已经解决了很大部分的索引更新问题,我们主要关注在新增与修改时候更新实时索引即可。此处以thinkphp5.0代码为例子。

    1. article模型类伪代码
    class Article extends Model
    {
        public function update($id, $data)
        {
            // 数据校验部分忽略
            $this->where('id', $id)->update($data);
            $this->updateSphinxRt($id, $data);
        }
        
        public function create($data)
        {
            // 数据校验部分忽略
            $id = $this->insertGetId($data);
            $this->updateSphinxRt($id, $data);
        }
    
        public function del($id)
        {
            $this->where('id', $id)->update(['is_del', 1]);
            $this->updateSphinxRt($id, ['is_del' => 1]);
        }
    
        protected fuction updateSphinxRt($id, $data)
        {
            $field = ['id', 'title', 'keywords', 'is_del'];
            $article = $this->where('id', $id)->field($field)->find()->toArray();
            # 过滤data多余数据,并避免缺少所需字段
            $data = array_merge($article , array_intersect_key($data, array_flip($field)));
    
            # 更新实时索引,config('sphinx.rt')为tp5适配sphinx的Query配置,后面会提到
            $rtDb = db('rtdata', config('sphinx.rt'));
            if ($rtDb->where('id', $id)->find()) {
                $rtDb->where('id', $id)->update($data);
            } else {
                $rtDb->insert($data);
            }
    
            # 更新全量与增量索引(重点)
            $sphinx = SphinxClient::getInstance();// 对SphinxClient进行了单例封装
            $sphinx->UpdateAttributes('main;inc', ['is_del'], [$id => [1]]);
        }
    }
    
    1. tp5适配sphinx的配置
    # config.php
    # .......忽略部分
      'sphinx' => [
            'type' => 'mysql',
            'hostname' => '127.0.0.1',
            'hostport' => Env::get('sphinx.rt_port', 9306),
            'charset' => 'utf8',
            'debug' => true,
            'query' => 'app\common\lib\sphinx\RtQuery',// 适配sphinx后的Query类
      ]
    # .......忽略部分
    
    1. tp5适配sphinx的Query类
    <?php
    
    namespace app\common\lib\sphinx;
    
    use think\db\Query;
    
    /**
     * 解决Sphinx的rt索引操作时候读取表字段错误的问题,
     * 直接在sphinx/rt_field中将需要用到的rt表字段定义好,
     * 具体结构可以查看运行相关命令后产生的缓存,
     * 指的注意的一点是id不可以设置为主键,否则无法写入
     */
    class RtQuery extends Query
    {
        // 重写父类该方法
        public function getTableInfo($tableName = '', $fetch = '')
        {
            # ......省略部分代码
            if (!isset(self::$info[$db . '.' . $guid])) {
                $schema = $guid;
                // 强制读预先定义好的结构,主要重点!!
                if (is_file(ROOT_PATH . 'sphinx/rt_field/' . $schema . '.php')) {
                    $info = include ROOT_PATH . 'sphinx/rt_field/' . $schema . '.php';
                } else {
                    throw new \RuntimeException('rt field cache no exists');
                }
                # ......省略部分代码
            }
            return $fetch ? self::$info[$db . '.' . $guid][$fetch] : self::$info[$db . '.' . $guid];
        }
    }
    
    

    最后搜索时候应该三个索引都进行搜索,且按照 全量->增量->实时 顺序,避免旧数据不更新问题
    $sphinx->Query('xxxx', 'main;inc;rtdata');

    总结

    通过全量+增量+实时索引,我们可以愉快地进行近实时的全文检索了。总的来说,我们在常规的全量索引上,增加了增量索引,来避免建立全量索引时候耗时过长的问题;再增加实时索引来进一步规避在增量索引更新间隔中新增/修改数据无法正确检索的问题。同时,由于每次更新增量索引的时候会清除实时索引,所以实时索引占用的内存不会很高。进一步的优化点,可以增加一条规则,比如每N个小时,来进行一次增量索引与全量索引的合并(注意更新相关的记录表),来减少增量所以每次建立的时间(其实一般不是十分海量的情况,建立速度还是秒级的,可以查下相关测试数据)。如果面对海量数据,还可以假设分布式的结构。当然,Elasticsearch才是真的香啊!!!

    声明

    本人技术有限,如有不当的地方还望指正。
    同时,受限于本人接触的数据量大小问题,该方案还没经历过TB级别的验证。
    欢迎大家一起探讨更好的解决方案。

    相关文章

      网友评论

        本文标题:Sphinx实时搜索设计探讨

        本文链接:https://www.haomeiwen.com/subject/lnminctx.html