Sphinx 应用到项目实践踩坑记

之前写的关于sphinx环境及使用的文章只是简单的做个小demo的测试,真正应用到项目中会遇到各种问题袭来,没有leader指导,只能自己摸索,应用到项目上线也是花了好几天的时间,这次记录只要是指出各个坑点,避免日后再踩坑。

环境搭建

docker 如何安装在这里就不详细说了,有了docker环境后使用 docker search sunfjun/coreseek ,笔者使用的docker源是阿里源 vi /etc/docker/daemon.json

{
  "registry-mirrors": ["https://78zjyej0.mirror.aliyuncs.com"]
}

这个镜像也是笔者找了很久才找到的,本来想下载软件环境重新安装走一遍的,可是www.coreseek.cn网站已经停止运营了,无法下载到coreseek,很多csdn下载coreseek都要积分,于是就找到了这个现成的容器,不过这个支持中文搜索的coreseek容器似乎环境版本有点低,只能下载sphinx-2.0.10-release.tar.gz,亲试过以上版本的会提示版本问题,下载地址{:target=_blank},sunfjun/coreseek作者项目说明地址
拉取镜像 docker pull sunfjun/coreseek

启动容器

启动容器必须要有一个 sphinx.conf,笔者刚开始在这里折腾了好久,该文件要映射到 /usr/local/etc/sphinx 目录,要不然会报 csrf.conf 找不到的错误,还有一点需要注意的是 sphinx.conf 必须是可使用的配置,因为启动容器会运行脚本 entrypoint.sh ,

#!/bin/bash

/usr/local/bin/indexer --all > /var/sphinx/log/indexer.log

echo "Starting Sphinx"
/usr/local/bin/searchd --nodetach

新建目录 mkdir -p /data/sphinx/conf /data/sphinx/data /data/sphinx/log ,刚开始不了解配置,最简单的方法是 copy sphinx 包里的 sphinx-min.conf.in 文件到 conf 目录 cp sphinx-min.conf.in /data/sphinx/conf/sphinx.conf ,将包里的 example.sql 导入到已有数据库,把配置里的数据库连接改为自己的数据库连接即可,把配置文件里的 data 目录和 log 目录改为 /var/sphinx/data /var/sphinx/log。
docker run -id --name sphinx -v /data/sphinx/conf:/usr/local/etc/sphinx -v /data/sphinx/data:/var/sphinx/data -v /data/sphinx/log:/var/sphinx/log -p 9312:9312 sunfjun/coreseek,配置如果没有什么大问题应该可以正常启动,可以使用 docker ps -a 查看容器有没有启动,使用 docker logs sphinx 查看失败原因

配置解析

#
# Minimal Sphinx configuration sample (clean, simple, functional)
#

source src
{
    type            = mysql
    sql_host        = xxx
    sql_user        = xxx
    sql_pass        = xxx
    sql_db          = xxx
    sql_port        = 3306
    #sql_query_pre      = SET NAMES utf8

}

source release_src:src
{
    sql_query_pre       = replace into release_counter select 1, max(id) from sre_sql_release;
    sql_query_pre       = SET NAMES utf8
    sql_query       = select id,applicant,operation,reason,dbname,scrum_key,bug_key,content,is_deleted,status,CRC32(env) as env,UNIX_TIMESTAMP(createTime) as create_time,group_team_id,time_to_sec(createTime) as time_gap from release where id <= (select max_id from release_counter where id=1)
    sql_attr_uint       = is_deleted
    sql_attr_uint       = status
    sql_attr_uint       = env
    sql_attr_timestamp  = create_time
    sql_attr_uint       = group_team_id
    sql_attr_uint       = time_gap
    sql_attr_multi  = uint exec_time from query; SELECT release_id,exec_time from sql_exec_time
}

source delta_release:release_src
{
    sql_query_pre           = SET NAMES utf8
    sql_query               = select id,applicant,operation,reason,dbname,scrum_key,bug_key,content,is_deleted,status, CRC32(env) as env,UNIX_TIMESTAMP(createTime) as create_time,group_team_id,time_to_sec(createTime) as time_gap\
                                from release where id > (select max_id from release_counter where id=1)
}

index release
{
    source                  = release_src
        path                    = /var/sphinx/data/release
        docinfo                 = extern
        charset_type            = zh_cn.utf-8
        charset_dictpath        = /usr/local/etc
        min_word_len            =1
        min_prefix_len          = 1 
}

index release_delta:release
{
    source          = delta_release
    path            = /var/sphinx/data/release_delta
}

indexer
{
    mem_limit       = 32M
}

searchd
{
    listen          = 9312
    log         = /var/sphinx/log/searchd.log
    query_log       = /var/sphinx/log/query.log
    read_timeout        = 5
    max_children        = 30
    pid_file        = /var/sphinx/log/searchd.pid
    max_matches     = 1000
    seamless_rotate     = 1
    preopen_indexes     = 1
    unlink_old      = 1
    workers         = threads # for RT to work
    binlog_path     = /var/sphinx/data
    compat_sphinxql_magics  = 0
}

笔记在这里就不详细讲解配置,只讲解使用过程中遇到的坑点,参考资料 https://www.cnblogs.com/yjf512/p/3598332.h... https://blog.csdn.net/beyond_boy/article/d...
笔者应用的是增量索引,所以新建一个 release_counter 表来存储最大 id。

  • 中文搜索

刚开始测试一直没有不能索引到中文,排查了好久才找到原因,不仅要配置 sql_query_pre = SET NAMES utf8, 还要配置中文字典路径

charset_type            = zh_cn.utf-8
charset_dictpath        = /usr/local/etc

我在测试时字典文件移到了 /usr/local/etc/data 目录,并且指向该目录,导致在部署环境生成另一个新的 sphinx 容器时出现找不到索引后缀为 .sph 的文件,容器启动失败,因为错误原因不明确也是排查了很长时间。索引中文必须配置以上三项!!!

  • 继承

无论是源还是索引都使用的继承模式,这里有一个坑点,就是继承和增量索引配置影响中文搜索,也是花了一些时间去排查的。当子源配置了 sql_query_pre = replace into release_counter select 1, max(id) from sre_sql_release; 这个时,父源的 sql_query_pre = SET NAMES utf8 会被覆盖,导致不能正确索引到中文,所以我在子源多加了这个配置 sql_query_pre = SET NAMES utf8

  • 属性、字段配置

sql_field_string 为设置全文索引字段,sql_attr_uint 、 sql_attr_timestamp 设置过滤字段,实测在 select 里面的字段中,在 sqlattr* 配置的都是过滤字段,其它都是索引字段,如果两者都配置,属性优先,索引无效,在使用api的返回结果中有个字段是 field 指明索引字段,attr 指明过滤字段(api后文说),我刚开始没注意到这两个字段,能不能索引,能不能过滤也折腾了好久。这里有一点需要注意的是 sphinx 不能过滤字符串,设置的 sql_attr_string 并没有过滤的效果,刚开始不知道也折腾了好久,既然提供了配置为什么会没有效果呢?最后上网找资料,参考 https://blog.csdn.net/longxingzhiwen/artic... ,该文中提到过滤字符串的方法,实测方法一不可行,方法二使用 crc32转化为数字 非常完美的解决了我的字符串过滤问题

  • 时间段过滤

我项目里的需求是筛选出每天某个时间段里的数据,字符串过滤、timestamp、crc32都无法解决这个问题,也是找了好久才找到 time_to_sec 解决方法,time_to_sec 可以将时间转化为秒数,目前 sphinx 的 api 只可以过滤整形和范围过滤,范围是一个区间,需要边界值,如果是大小或者小于,只能自己加一个合理的边界值凑成两个边界参数了

  • 跨表多值过滤

刚开始使用的 sql_joined_field 配置,后来才发现这个是索引配置,而过滤配置是sql_attr_multi , 参考文章 https://blog.csdn.net/websites/article/det... ,这里联表的返回的字段也要求是 uint 或 timestamp 所以如果碰到字符串也要通过转化的方式解决了。

  • 索引长度问题

当设置字符是 utf-8 后,一两字母或者一个中文是索引不出来的,后来加了这个配置才生效,

min_word_len            =1
min_prefix_len          = 1 

这是设置索引长度的问题,但是如果是说解释跟字符编码内部的长度联系起来则不了解了。

  • sphinxapi.php 使用
public function listFromSphinx($params)
    {
        $page = $params['page'] ?$params['page']:1;
        $pageSize = $params['page_size'] ? $params['page_size'] : 20;
        $offset = ($page - 1) * $pageSize;
        $sphinxClient = new SphinxClient();
        $host = ENV_DATA['tool_sphinx']['host'];
        $port = ENV_DATA['tool_sphinx']['port'];
        $sphinxClient->SetServer($host, $port);//连接
        $sphinxClient->SetConnectTimeout(5);//超时时间
        $sphinxClient->SetMatchMode(SPH_MATCH_ALL);//匹配模式
        $sphinxClient->SetSortMode(SPH_SORT_EXTENDED, '[[[[[[[[[[[[[[[[[[[[[@](https://learnku.com/users/29000)](https://learnku.com/users/27102)](https://learnku.com/users/25661)](https://learnku.com/users/19319)](https://learnku.com/users/10240)](https://learnku.com/users/8907)](https://learnku.com/users/8278)](https://learnku.com/users/29000)](https://learnku.com/users/27102)](https://learnku.com/users/25661)](https://learnku.com/users/19319)](https://learnku.com/users/10240)](https://learnku.com/users/8907)](https://learnku.com/users/8278)](https://learnku.com/users/29000)](https://learnku.com/users/27102)](https://learnku.com/users/25661)](https://learnku.com/users/19319)](https://learnku.com/users/10240)](https://learnku.com/users/8907)](https://learnku.com/users/8278)id DESC');//设置排序
        $sphinxClient->SetLimits($offset, $pageSize);//分页
        //全文索引
        $queryKeys = [];
        $keys = ['applicant', 'operation', 'reason', 'dbname', 'scrum_key', 'bug_key', 'content'];
        foreach ($keys as $key) {
            if (!empty($params[$key])) {
                array_push($queryKeys, $params[$key]);
            }
        }
        //过滤
        $sphinxClient->SetFilter('is_deleted', [0]);
        isset($params['status']) && $params['status'] !== '' && $params['status'] >=0 && $sphinxClient->SetFilter('status', [intval($params['status'])]);
        $params['env'] && $sphinxClient->SetFilter('env', [crc32($params['env'])]);
        if (!empty($params['createTime'])) {
            //日期区间
            $arr = explode('~', $params['createTime']);
            $startDate = $arr[0];
            $endDate = date('Y-m-d', strtotime('+1 day', strtotime($arr[1])));
            $sphinxClient->SetFilterRange('create_time', strtotime($startDate), strtotime($endDate));
        }
        $params['group_team_id'] && $sphinxClient->SetFilter('group_team_id', [$params['group_team_id']]);
        //执行时间
        if ($params['execTime']) {
            $execTime = $params['execTime'] * 60;
            $maxTime = 3600 * 6;  //限制最大执行时间6个小时内的
            $sphinxClient->SetFilterRange('exec_time', $execTime, $maxTime);
        }
        //时间区间
        if ($params['interval']) {
            $time_interval = explode('-', $params['interval']);
            if (count($time_interval) == 2) {
                $min = strtotime(date('Y-m-d ').$time_interval[0]) - strtotime(date('Y-m-d'));
                $max = strtotime(date('Y-m-d').$time_interval[1]) - strtotime(date('Y-m-d'));
                $sphinxClient->SetFilterRange('time_gap', $min, $max);
            }
        }
        $result = $sphinxClient->Query(implode(' ', $queryKeys), 'sre_sql_release sre_sql_release_delta');
        if ($result === false) {
            return ['status' => false, 'msg' => $sphinxClient->GetLastError()?:'sphinx返回错误', 'data' => []];
        } else {
            $ids = count($result['matches']) > 0 ? array_keys($result['matches']):[];
            return ['status' => true, 'msg' => '', 'data' => $ids, 'total' => $result['total']];
        }
    }

将 sphinx 包里 api 目录里的 sphinxclient.php copy 到加入到项目中,添加命名空间并使用 use 引用到文件中,可能是版本太低,所以要把文件里的 SphinxClient() 构造方法改成 __construct(),不然一直返回 false。

$host = ENV_DATA['tool_sphinx']['host'];
$port = ENV_DATA['tool_sphinx']['port'];

这是我的项目里获取配置的方法,无需深究。

$sphinxClient->SetMatchMode(SPH_MATCH_ALL);,设置索引模式,这里是索引所有条件符合才返回,可参考 https://blog.csdn.net/myweishanli/article/... ,索引是分词索引,也就是会把你的字符串拆分来索引匹配,而不是像 like 去包含某个字符串。

$sphinxClient->SetSortMode(SPH_SORT_EXTENDED, '@id DESC');//设置排序,设置排序,id 是 sphinx 里的内置变量,所以加上@ ,其它属性则不必,按我理解应该使用$sphinxClient->SetSortMode(SPH_SORT_ATTR_DESC, '@id'); 即可,可是没有生效,可参考 https://blog.csdn.net/slqgenius/article/de...

$result = $sphinxClient->Query(implode(' ', $queryKeys), 'release release_delta'); 将要索引的字段用空格分开,第二个参数使用空格分开引用多个索引

  • 部署上线

sphinx 的执行命令在 /usr/local/bin 目录下,命令 /usr/local/bin/indexer --all --rotate 重建所有索引,命令 /usr/local/bin/indexer release_delta --rotate 只重建增量索引。要求写到 crontab 里,重建所有索引在每天晚上 0 点,重建增量索引要求每隔 2 秒执行一次(因为新建 数据后直接跳到列表页,所以要求要快)。这里提示一下 sphinx 里的 crontab 有问题,设置不成功,运维重建了镜像才生效。

##增量索引,2钟执行一次
* * * * * sleep 2;/usr/local/bin/indexer release_delta --rotate >/dev/nul
###全量索引,每天晚上12点执行
59 23 * * * /usr/local/bin/indexer --all --rotate >/dev/nul

因为 crontab 不支持秒设置,度娘了一下有两种方案,使用了 crontab sleep 2 的方法来解决这个问题,另一个方法是写在一个 shell 脚本里一直 while true 在 sleep 2,再执行那个脚本。

但是这个 2 秒还不能满足我的需求,新建跳转到列表页不能及时展示我的数据,只能 model 里触发事件去执行一次索引了。我的项目里是调用 python 的接口,python 连接到服务器再执行容器的命令去重建索引,这样就达到我的目的了。还有一个问题是担心重构索引太频繁担心太吃服务器资源。

public function init()
    {
        parent::init(); // TODO: Change the autogenerated stub
        $this->on(self::EVENT_AFTER_INSERT, [RefreshIndexEvent::class, 'refreshReleaseDelta']);
        $this->on(self::EVENT_AFTER_UPDATE, [RefreshIndexEvent::class, 'refreshReleaseDelta']);
        $this->on(self::EVENT_AFTER_DELETE, [RefreshIndexEvent::class, 'refreshReleaseDelta']);
    }

public static function refreshReleaseDelta()
    {
        ...

        $command = "docker exec -i {$container} /usr/local/bin/indexer release_delta --rotate";

        ...
        ...

    }

总结

回想遇到的坑基本都在这里了,坑都是在实践的过程中才会发现的,仅仅是看文档的配置只是一个了解的层面而已。实践才是检验真理的惟一标准!!!

雪花飘
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 4

格式可能会有些问题,因为是先在有道云笔记的markdown写好再复制过来的-_-

5年前 评论

发现 laravel markdown 有一个 bug ,如果文章里 "@" 会莫名加一些 laravel 的链接,刚手动删除了

5年前 评论

@Winner @ 默认会当做通知消息 加个空格应该可以

5年前 评论

貌似也不行。。。

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!