Reber's Blog

只会一点点编程、只会一点点渗透


通过 Sphinx 快速查询数据

0x00 Sphinx

Sphinx 是一款基于 SQL 的高性能全文检索引擎,Sphinx 的性能在众多全文检索引擎中也是数一数二的,利用 Sphinx我们可以完成比数据库本身更专业的搜索功能,而且可以有很多针对性的性能优化。

  • 快速创建索引:3 分钟左右即可创建近 100 万条记录的索引,并且采用了增量索引的方式,重建索引非常迅速。
  • 闪电般的检索速度:尽管是 1 千万条的大数据量,查询数据的速度也在毫秒级以上,2-4G 的文本量中平均查询速度不到 0.1 秒。
  • 为很多脚本语言设计了检索 API,如 PHP,Python,Perl,Ruby 等,因此你可以在大部分编程应用中很方便地调用 Sphinx 的相关接口。
  • 为 MySQL 设计了一个存储引擎插件,因此如果你在 MySQL 上使用 Sphinx,那简直就方便到家了。
  • 支持分布式搜索,可以横向扩展系统性能。

0x01 安装

  • windows 可以去 这里 下载对应版本,然后添加环境变量
  • ubuntu 可以 sudo apt-get install sphinxsearch (sphinxapi.py 要使用对应版本)
  • macbook 可以 brew install sphinx (sphinxapi.py 要使用对应版本)

0x02 使用 Sphinx 查询的流程

  • 通过 Sphinx 的 indexer 生成索引(需要先配置文件 sphinx.conf)
    部分索引:indexer -c ./sphinx.conf <index_name>
    全部索引:indexer -c ./sphinx.conf --all
    若 searchd 已启动则需要加 --rotate 参数:indexer -c ./sphinx.conf --all --rotate

  • Sphinx 启动一个 searchd 进行监听(调接口)
    searchd -c ./sphinx.conf –console

  • 查询时把关键字给 Sphinx 的接口,searchd 返回 id
    searchd 返回的 id 是该关键字在数据库中对应的 id

  • 在数据库中根据 id 查数据

0x03 Sphinx 配置文件编写

下面索引了两个库中的两个表

一个是 37wan_com 库的 cdb_members、cdb_uc_members 表,索引的 username,email 这两列

一个是 3pk_com 库的 member 表,索引的 uname,email 这两列

source base_source {
    type = mysql

    sql_host = 127.0.0.1
    sql_user = root
    sql_pass = root
    sql_port = 3306
    # sql_db = tttttmp # 需要建索引的数据库名字

    sql_query_pre = SET NAMES utf8 # 定义查询时的编码
    # sql_query = select id,content from test # 设置要做索引的字段,包含至少一个唯一主键,这里会索引 id、content 这个两个字段
}
index base_index {
    min_word_len = 1 # 最小索引词长度,小于这个长度的词不会被索引

    min_prefix_len = 3 # 最小索引前缀(搜索关键字长度至少为3) 12345* 得到 12345xxxxxx
    # min_infix_len = 3 # 最小索引中缀(搜索关键字长度至少为3) *aaa* 得到 xxxxxaaa、xxxxxaaaxxxxx、aaaxxxxx

    # 不按照词典,而是按照字长来分词,主要针对非英文体系的语言,如果指定为1,搜 丰 得到 张丰、邓丰至、丰年
    ngram_len = 2
    # 需要分词的字符,如果要搜索中文,需要配置 ngram_chars
    # U+3000..U+2FA1F是汉字。为了能搜到完整邮箱:添加 U+0040,U+002D,U+002E 代表 @,-,.
    ngram_chars = U+3000..U+2FA1F,U+0040,U+002D,U+002E

    html_strip = 1 # html标记清理,是否从输出全文数据中去除HTML标记

    # source = base_source # 这里与上面的source对应
    # path = ./sphinx_data/index/base_source # 索引文件存放路径及索引的文件名
}

source 37wan_com_cdb_members:base_source {
    sql_db = 37wan_com
    sql_query = select id,username,email from cdb_members
}
index 37wan_com_cdb_members:base_index {
    source = 37wan_com_cdb_members
    path = /opt/sgk/sphinx_data/index/37wan_com_cdb_members # 索引文件存放路径及索引的文件名
}

source 37wan_com_cdb_uc_members:base_source {
    sql_db = 37wan_com
    sql_query = select id,username,email from cdb_uc_members
}
index 37wan_com_cdb_uc_members:base_index
{
    source = 37wan_com_cdb_uc_members
    path = /opt/sgk/sphinx_data/index/37wan_com_cdb_uc_members
}

source 3pk_com_member:base_source {
    sql_db = 3pk_com
    sql_query = select id,email,uname as username from member
}
index 3pk_com_member:base_index {
    source = 3pk_com_member
    path = /opt/sgk/sphinx_data/index/3pk_com_member
}


indexer
{
    mem_limit = 512M # 用来构建索引的索引器运行所占用的内存
}
searchd
{
    listen = 127.0.0.1:23333 # 查询服务监听的端口
    # listen = 127.0.0.1:3333:mysql41 # mysql -h localhost -P 3333; show tables 可查看生成的索引
    pid_file = ./sphinx_data/searchd.pid

    log = ./sphinx_data/logs/searchd.log # 监听日志
    query_log = ./sphinx_data/logs/query.log # 查询日志
    binlog_path = # 二进制日志路径,这里禁止掉

    read_timeout = 5 # 客户端读超时时间
    max_children = 30 # 并行执行搜索的数目
    seamless_rotate = 1 # 启动无缝轮转,防止 searchd 轮换在需要预取大量数据的索引时停止响应
    preopen_indexes = 0 # 索引预开启,是否强制重新打开所有索引文件,索引比较多时如果设置为 1 会出现 Too many open files 错误
    unlink_old = 1 # 索引轮换成功之后,是否删除以.old为扩展名的索引拷贝
    dist_threads = 12 # 并发查询线程数
}

0x04 Sphinx 索引

  • 生成索引
indexer -c ./sphinx.conf --all
Sphinx 3.3.1 (commit b72d67bc)
Copyright (c) 2001-2020, Andrew Aksyonoff
Copyright (c) 2008-2016, Sphinx Technologies Inc (http://sphinxsearch.com)

using config file './sphinx.conf'...
indexing index '37wan_com_cdb_members'...
collected 34219 docs, 0.8 MB
sorted 0.1 Mhits, 100.0% done
total 34219 docs, 835.3 Kb
total 0.4 sec, 1.915 Mb/sec, 78452 docs/sec
indexing index '37wan_com_cdb_uc_members'...
collected 34219 docs, 0.8 MB
sorted 0.1 Mhits, 100.0% done
total 34219 docs, 835.2 Kb
total 0.4 sec, 2.048 Mb/sec, 83899 docs/sec
indexing index '3pk_com_member'...
collected 10007 docs, 0.3 MB
sorted 0.0 Mhits, 100.0% done
total 10007 docs, 258.6 Kb
total 0.2 sec, 1.707 Mb/sec, 66041 docs/sec
  • 启动 searchd 守护进程
searchd -c ./sphinx.conf --console
Sphinx 3.3.1 (commit b72d67bc)
Copyright (c) 2001-2020, Andrew Aksyonoff
Copyright (c) 2008-2016, Sphinx Technologies Inc (http://sphinxsearch.com)

using config file './sphinx.conf'...
listening on all interfaces, port=23333
listening on all interfaces, port=3333
precaching index '37wan_com_cdb_members'
precaching index '37wan_com_cdb_uc_members'
precaching index '3pk_com_member'
precached 3 indexes using 3 threads in 0.0 sec
accepting connections

0x05 Sphinx 使用

import pymysql
from sphinxapi import *

limit = 200
sphinx = SphinxClient()
sphinx.SetServer("localhost", 23333) # 设置连接 sphinx 的 searched
sphinx.SetConnectTimeout(5.0)
sphinx.SetLimits(0, limit, max(limit,1000))

sgk_index_msg = [
    {
        "index": "37wan_com_cdb_members",
        "db_name": "37wan_com",
        "table_name": "cdb_members",
        "columns": "id,username,password,email"
    },
    {
        "index": "37wan_com_cdb_uc_members",
        "db_name": "37wan_com",
        "table_name": "cdb_uc_members",
        "columns": "id,username,password,email,salt"
    },
    {
        "index": "3pk_com_member",
        "db_name": "3pk_com",
        "table_name": "member",
        "columns": "id,email,uname as username,password"
    }
]

all_results = list()
try:
    mysql_uri = "mysql+pymysql://root:root@localhost:3306/mysql?charset=utf8mb4"
    sqlconn = MySQLX(mysql_uri)
    for index_dict in sgk_conf:
        index = index_dict.get("index")
        db_name = index_dict.get("db_name")
        table_name = index_dict.get("table_name")
        columns = index_dict.get("columns")

        res = sphinx.Query(keyword, index)
        # print(keyword, index, res)
        if res and res["total_found"]:
            ids = [str(x["id"]) for x in res["matches"]]
            ids = ",".join(ids)

            sql = "select {} from {}.{} where id in({})".format(columns, db_name, table_name, ids)
            results = sqlconn.query(sql)
            for result in results:
                _tmp = dict()
                _tmp["id"] = result.get("id", "")
                _tmp["from"] = db_name
                _tmp["uid"] = result.get("uid", "")
                _tmp["username"] = result.get("username", "")
                _tmp["password"] = result.get("password", "")
                _tmp["salt"] = result.get("salt", "")
                _tmp["mobile"] = result.get("mobile", "")
                _tmp["email"] = result.get("email", "")
                all_results.append(_tmp)
except Exception as e:
    print(e)