扩展 MySQL 9.0  /  ...  /  编写全文解析器插件

4.4.4 编写全文解析器插件

MySQL 使用 MyISAMInnoDB 支持服务器端全文解析器插件。有关全文解析器插件的介绍性信息,请参阅 全文解析器插件.

可以使用全文解析器插件来替换或修改内置的全文解析器。本节介绍如何编写名为 simple_parser 的全文解析器插件。此插件执行基于比 MySQL 内置全文解析器使用的规则更简单的规则的解析:单词是非空的白字符运行。

说明使用 MySQL 源代码发行版中 plugin/fulltext 目录中的源代码,因此将位置更改为该目录。以下步骤描述了如何创建插件库

  1. 要编写全文解析器插件,请在插件源文件中包含以下头文件。根据插件的功能和要求,可能还需要其他 MySQL 或通用头文件。

    #include <mysql/plugin.h>

    plugin.h 定义了 MYSQL_FTPARSER_PLUGIN 服务器插件类型以及声明插件所需的数据结构。

  2. 为插件库文件设置库描述符。

    此描述符包含服务器插件的通用插件描述符。对于全文解析器插件,类型必须为 MYSQL_FTPARSER_PLUGIN。这是标识插件在创建 FULLTEXT 索引时在 WITH PARSER 子句中合法使用的值。(此子句中没有其他插件类型是合法的。)

    例如,包含名为 simple_parser 的单个全文解析器插件的库的库描述符如下所示

    mysql_declare_plugin(ftexample)
    {
      MYSQL_FTPARSER_PLUGIN,      /* type                            */
      &simple_parser_descriptor,  /* descriptor                      */
      "simple_parser",            /* name                            */
      "Oracle Corporation",       /* author                          */
      "Simple Full-Text Parser",  /* description                     */
      PLUGIN_LICENSE_GPL,         /* plugin license                  */
      simple_parser_plugin_init,  /* init function (when loaded)     */
      simple_parser_plugin_deinit,/* deinit function (when unloaded) */
      0x0001,                     /* version                         */
      simple_status,              /* status variables                */
      simple_system_variables,    /* system variables                */
      NULL,
      0
    }
    mysql_declare_plugin_end;

    name 成员 (simple_parser) 指示在诸如 INSTALL PLUGINUNINSTALL PLUGIN 的语句中用于引用插件的名称。这也是 SHOW PLUGINSINFORMATION_SCHEMA.PLUGINS 显示的名称。

    有关更多信息,请参阅 第 4.4.2.1 节,“服务器插件库和插件描述符”.

  3. 设置类型特定的插件描述符。

    库描述符中的每个通用插件描述符都指向一个类型特定的描述符。对于全文解析器插件,类型特定的描述符是 plugin.h 文件中 st_mysql_ftparser 结构的实例

    struct st_mysql_ftparser
    {
      int interface_version;
      int (*parse)(MYSQL_FTPARSER_PARAM *param);
      int (*init)(MYSQL_FTPARSER_PARAM *param);
      int (*deinit)(MYSQL_FTPARSER_PARAM *param);
    };

    如结构定义所示,描述符具有接口版本号并包含指向三个函数的指针。

    接口版本号使用符号指定,符号的形式为:MYSQL_xxx_INTERFACE_VERSION。对于全文解析器插件,符号是 MYSQL_FTPARSER_INTERFACE_VERSION。在源代码中,您将在 include/mysql/plugin_ftparser.h 中找到为全文解析器插件定义的实际接口版本号。当前接口版本号是 0x0101

    initdeinit 成员应该指向一个函数,或者如果不需要该函数,则设置为 0。 parse 成员必须指向执行解析的函数。

    simple_parser 声明中,该描述符由 &simple_parser_descriptor 指示。该描述符指定全文插件接口的版本号(如 MYSQL_FTPARSER_INTERFACE_VERSION 所示),以及插件的解析、初始化和反初始化函数

    static struct st_mysql_ftparser simple_parser_descriptor=
    {
      MYSQL_FTPARSER_INTERFACE_VERSION, /* interface version      */
      simple_parser_parse,              /* parsing function       */
      simple_parser_init,               /* parser init function   */
      simple_parser_deinit              /* parser deinit function */
    };

    全文解析器插件在两种不同的上下文中使用:索引和搜索。在这两种上下文中,服务器在处理导致调用插件的每个 SQL 语句的开始和结束时调用初始化和反初始化函数。但是,在语句处理期间,服务器以特定于上下文的的方式调用主解析函数

    • 对于索引,服务器为要索引的每个列值调用解析器。

    • 对于搜索,服务器调用解析器来解析搜索字符串。解析器还可能被调用以处理语句处理的行。在自然语言模式下,服务器无需调用解析器。对于布尔模式短语搜索或具有查询扩展的自然语言搜索,解析器用于解析列值以获取索引中没有的信息。此外,如果对没有 FULLTEXT 索引的列进行布尔模式搜索,则将调用内置解析器。(插件与特定索引相关联。如果没有索引,则不会使用任何插件。)

    通用插件描述符中的插件声明具有指向初始化和反初始化函数的 initdeinit 成员,类型特定的插件描述符也指向它。但是,这两对函数具有不同的用途,并且由于不同的原因而被调用

    • 对于通用插件描述符中的插件声明,初始化和反初始化函数在加载和卸载插件时被调用。

    • 对于类型特定的插件描述符,初始化和反初始化函数被调用以用于插件的每个 SQL 语句。

    插件描述符中命名的每个接口函数都应该返回零表示成功,或者返回非零表示失败,并且它们都接收一个指向包含解析上下文的 MYSQL_FTPARSER_PARAM 结构的 arguments。该结构具有以下定义

    typedef struct st_mysql_ftparser_param
    {
      int (*mysql_parse)(struct st_mysql_ftparser_param *,
                         char *doc, int doc_len);
      int (*mysql_add_word)(struct st_mysql_ftparser_param *,
                            char *word, int word_len,
                            MYSQL_FTPARSER_BOOLEAN_INFO *boolean_info);
      void *ftparser_state;
      void *mysql_ftparam;
      struct charset_info_st *cs;
      char *doc;
      int length;
      int flags;
      enum enum_ftparser_mode mode;
    } MYSQL_FTPARSER_PARAM;

    结构成员的用途如下

    • mysql_parse: 指向调用服务器内置解析器的回调函数的指针。当插件充当内置解析器的前端时,请使用此回调。也就是说,当调用插件解析函数时,它应该处理输入以提取文本并将文本传递给 mysql_parse 回调。

      此回调函数的第一个参数应该是 param 值本身

      param->mysql_parse(param, ...);

      前端插件可以提取文本并将其一次性传递给内置解析器,也可以逐段提取和传递文本给内置解析器。但是,在这种情况下,内置解析器会将文本片段视为它们之间存在隐式词语分隔符。

    • mysql_add_word: 指向向全文索引或搜索词列表添加单词的回调函数的指针。当解析器插件替换内置解析器时,请使用此回调。也就是说,当调用插件解析函数时,它应该将输入解析为单词,并为每个单词调用 mysql_add_word 回调。

      此回调函数的第一个参数应该是 param 值本身

      param->mysql_add_word(param, ...);
    • ftparser_state: 这是一个通用指针。插件可以将其设置为指向要用于其自身目的的内部信息。

    • mysql_ftparam: 由服务器设置。它作为第一个参数传递给 mysql_parsemysql_add_word 回调。

    • cs: 指向有关文本字符集的信息的指针,如果信息不可用,则为 0。

    • doc: 指向要解析的文本的指针。

    • length: 要解析的文本的长度,以字节为单位。

    • flags: 解析器标志。如果没有特殊标志,则为零。唯一非零标志是 MYSQL_FTFLAGS_NEED_COPY,这意味着 mysql_add_word() 必须保存单词的副本(也就是说,它不能使用指向单词的指针,因为单词位于将被覆盖的缓冲区中。)

      此标志可能在调用解析器插件之前、由解析器插件本身或由 mysql_parse() 函数设置或重置。

    • mode: 解析模式。此值将是以下常量之一

      • MYSQL_FTPARSER_SIMPLE_MODE: 以快速简单模式解析,用于索引和自然语言查询。解析器应该只将应该被索引的单词传递给服务器。如果解析器使用长度限制或停用词列表来确定要忽略的单词,则它不应该将这些单词传递给服务器。

      • MYSQL_FTPARSER_WITH_STOPWORDS: 以停用词模式解析。这用于布尔搜索中的短语匹配。解析器应该将所有单词传递给服务器,即使是停用词或超出任何正常长度限制的单词。

      • MYSQL_FTPARSER_FULL_BOOLEAN_INFO: 以布尔模式解析。这用于解析布尔查询字符串。解析器应该不仅识别单词,还应该识别布尔模式运算符,并使用 mysql_add_word 回调将它们作为标记传递给服务器。为了告诉服务器正在传递什么类型的标记,插件需要填充 MYSQL_FTPARSER_BOOLEAN_INFO 结构并将指向它的指针传递给它。

    注意

    对于 MyISAM,停用词列表和 ft_min_word_len 以及 ft_max_word_len 在分词器内部进行检查。对于 InnoDB,停用词列表和等效的词长变量设置(innodb_ft_min_token_sizeinnodb_ft_max_token_size)在分词器外部进行检查。因此,InnoDB 插件解析器不需要检查停用词列表、innodb_ft_min_token_sizeinnodb_ft_max_token_size。相反,建议将所有单词返回到 InnoDB。但是,如果希望在插件解析器中检查停用词,请使用 MYSQL_FTPARSER_SIMPLE_MODE,它适用于全文搜索索引和自然语言搜索。对于 MYSQL_FTPARSER_WITH_STOPWORDSMYSQL_FTPARSER_FULL_BOOLEAN_INFO 模式,建议将所有单词(包括停用词)返回到 InnoDB,以防出现短语搜索。

    如果解析器在布尔模式下调用,则 param->mode 值将为 MYSQL_FTPARSER_FULL_BOOLEAN_INFO。解析器用于将标记信息传递到服务器的 MYSQL_FTPARSER_BOOLEAN_INFO 结构如下所示

    typedef struct st_mysql_ftparser_boolean_info
    {
      enum enum_ft_token_type type;
      int yesno;
      int weight_adjust;
      char wasign;
      char trunc;
      int position;
      /* These are parser state and must be removed. */
      char prev;
      char *quot;
    } MYSQL_FTPARSER_BOOLEAN_INFO;

    解析器应按如下方式填写结构成员

    • type:标记类型。下表显示了允许的类型。

      表 4.3 全文解析器标记类型

      标记值 含义
      FT_TOKEN_EOF 数据结束
      FT_TOKEN_WORD 普通单词
      FT_TOKEN_LEFT_PAREN 组或子表达式的开始
      FT_TOKEN_RIGHT_PAREN 组或子表达式的结束
      FT_TOKEN_STOPWORD 停用词

    • yesno:单词是否必须存在才能匹配。0 表示单词是可选的,但如果存在会提高匹配的相关性。大于 0 的值表示单词必须存在。小于 0 的值表示单词必须不存在。

    • weight_adjust:一个权重因子,决定单词匹配的权重。它可以用来提高或降低单词在相关性计算中的重要性。值为零表示没有权重调整。大于或小于零的值分别表示更高或更低的权重。布尔全文搜索 中使用 <> 运算符的示例说明了权重的作用。

    • wasign:权重因子的符号。负值的作用类似于 ~ 布尔搜索运算符,它会导致单词对相关性的贡献为负。

    • trunc:是否应该像布尔模式 * 截断运算符一样进行匹配。

    • position:单词在文档中的起始位置(以字节为单位)。由 InnoDB 全文搜索使用。对于在布尔模式下调用的现有插件,必须为 position 成员添加支持。

    插件不应使用 MYSQL_FTPARSER_BOOLEAN_INFO 结构的 prevquot 成员。

    注意

    插件解析器框架不支持

    • @distance 布尔运算符。

    • 一个前导加号 (+) 或减号 (-) 布尔运算符,后跟一个空格,然后是一个单词 ('+ apple''- apple')。前导加号或减号必须直接与单词相邻,例如:'+apple''-apple'

    有关布尔全文搜索运算符的信息,请参见 布尔全文搜索

  4. 设置插件接口函数。

    库描述符中的通用插件描述符指定了服务器在加载和卸载插件时应调用的初始化和反初始化函数。对于 simple_parser,这些函数不执行任何操作,只是返回零以指示它们成功

    static int simple_parser_plugin_init(void *arg __attribute__((unused)))
    {
      return(0);
    }
    
    static int simple_parser_plugin_deinit(void *arg __attribute__((unused)))
    {
      return(0);
    }

    由于这些函数实际上没有执行任何操作,因此可以省略它们,并在插件声明中为它们分别指定 0。

    用于 simple_parser 的类型特定插件描述符指定了服务器在使用插件时调用的初始化、反初始化和解析函数。对于 simple_parser,初始化和反初始化函数不执行任何操作

    static int simple_parser_init(MYSQL_FTPARSER_PARAM *param
                                  __attribute__((unused)))
    {
      return(0);
    }
    
    static int simple_parser_deinit(MYSQL_FTPARSER_PARAM *param
                                    __attribute__((unused)))
    {
      return(0);
    }

    同样,由于这些函数不执行任何操作,因此可以省略它们,并在插件描述符中为它们分别指定 0。

    主解析函数 simple_parser_parse() 充当内置全文解析器的替代,因此它需要将文本分割成单词并将每个单词传递到服务器。解析函数的第一个参数是指向包含解析上下文的结构的指针。此结构有一个 doc 成员,它指向要解析的文本,以及一个 length 成员,它指示文本的长度。插件执行的简单解析将非空空白字符运行视为单词,因此它通过以下方式识别单词

    static int simple_parser_parse(MYSQL_FTPARSER_PARAM *param)
    {
      char *end, *start, *docend= param->doc + param->length;
    
      for (end= start= param->doc;; end++)
      {
        if (end == docend)
        {
          if (end > start)
            add_word(param, start, end - start);
          break;
        }
        else if (isspace(*end))
        {
          if (end > start)
            add_word(param, start, end - start);
          start= end + 1;
        }
      }
      return(0);
    }

    当解析器找到每个单词时,它将调用一个函数 add_word() 将单词传递到服务器。 add_word() 只是一个辅助函数;它不是插件接口的一部分。解析器将解析上下文指针传递给 add_word(),以及指向单词的指针和长度值

    static void add_word(MYSQL_FTPARSER_PARAM *param, char *word, size_t len)
    {
      MYSQL_FTPARSER_BOOLEAN_INFO bool_info=
        { FT_TOKEN_WORD, 0, 0, 0, 0, 0, ' ', 0 };
    
      param->mysql_add_word(param, word, len, &bool_info);
    }

    对于布尔模式解析, add_word()bool_info 结构的成员填写,如前面关于 st_mysql_ftparser_boolean_info 结构的讨论中所述。

  5. 设置状态变量。对于 simple_parser 插件,以下状态变量数组设置了一个状态变量,其值为静态文本,另一个状态变量的值存储在一个长整型变量中

    long number_of_calls= 0;
    
    struct st_mysql_show_var simple_status[]=
    {
      {"simple_parser_static", (char *)"just a static text", SHOW_CHAR},
      {"simple_parser_called", (char *)&number_of_calls,     SHOW_LONG},
      {0,0,0}
    };

    通过使用以插件名称开头的状态变量名称,可以使用 SHOW STATUS 轻松显示插件的变量。

    mysql> SHOW STATUS LIKE 'simple_parser%';
    +----------------------+--------------------+
    | Variable_name        | Value              |
    +----------------------+--------------------+
    | simple_parser_static | just a static text |
    | simple_parser_called | 0                  |
    +----------------------+--------------------+
  6. 要编译和安装插件库文件,请使用 第 4.4.3 节“编译和安装插件库” 中的说明。要使库文件可供使用,请将其安装到插件目录(由 plugin_dir 系统变量命名的目录)中。对于 simple_parser 插件,它在从源代码构建 MySQL 时进行编译和安装。它也包含在二进制发行版中。构建过程会生成一个名为 mypluglib.so 的共享对象库(.so 后缀可能因平台而异)。

  7. 要使用插件,请将其注册到服务器。例如,要在运行时注册插件,请使用以下语句,根据需要调整您的平台的 .so 后缀

    INSTALL PLUGIN simple_parser SONAME 'mypluglib.so';

    有关插件加载的更多信息,请参见 安装和卸载插件

  8. 要验证插件安装,请检查 INFORMATION_SCHEMA.PLUGINS 表或使用 SHOW PLUGINS 语句。请参见 获取服务器插件信息

  9. 测试插件以验证它是否正常工作。

    创建一个包含字符串列的表,并将解析器插件与该列上的 FULLTEXT 索引关联起来

    mysql> CREATE TABLE t (c VARCHAR(255),
        ->   FULLTEXT (c) WITH PARSER simple_parser
        -> ) ENGINE=MyISAM;
    Query OK, 0 rows affected (0.01 sec)

    在表中插入一些文本并尝试一些搜索。这应该验证解析器插件是否将所有非空格字符视为单词字符

    mysql> INSERT INTO t VALUES
        ->   ('utf8mb4_0900_as_cs is a case-sensitive collation'),
        ->   ('I\'d like a case of oranges'),
        ->   ('this is sensitive information'),
        ->   ('another row'),
        ->   ('yet another row');
    Query OK, 5 rows affected (0.02 sec)
    Records: 5  Duplicates: 0  Warnings: 0
    
    mysql> SELECT c FROM t;
    +--------------------------------------------------+
    | c                                                |
    +--------------------------------------------------+
    | utf8mb4_0900_as_cs is a case-sensitive collation |
    | I'd like a case of oranges                       |
    | this is sensitive information                    |
    | another row                                      |
    | yet another row                                  |
    +--------------------------------------------------+
    5 rows in set (0.00 sec)
    
    mysql> SELECT MATCH(c) AGAINST('case') FROM t;
    +--------------------------+
    | MATCH(c) AGAINST('case') |
    +--------------------------+
    |                        0 |
    |          1.2968142032623 |
    |                        0 |
    |                        0 |
    |                        0 |
    +--------------------------+
    5 rows in set (0.00 sec)
    
    mysql> SELECT MATCH(c) AGAINST('sensitive') FROM t;
    +-------------------------------+
    | MATCH(c) AGAINST('sensitive') |
    +-------------------------------+
    |                             0 |
    |                             0 |
    |               1.3253291845322 |
    |                             0 |
    |                             0 |
    +-------------------------------+
    5 rows in set (0.01 sec)
    
    mysql> SELECT MATCH(c) AGAINST('case-sensitive') FROM t;
    +------------------------------------+
    | MATCH(c) AGAINST('case-sensitive') |
    +------------------------------------+
    |                    1.3109166622162 |
    |                                  0 |
    |                                  0 |
    |                                  0 |
    |                                  0 |
    +------------------------------------+
    5 rows in set (0.01 sec)
    
    mysql> SELECT MATCH(c) AGAINST('I\'d') FROM t;
    +--------------------------+
    | MATCH(c) AGAINST('I\'d') |
    +--------------------------+
    |                        0 |
    |          1.2968142032623 |
    |                        0 |
    |                        0 |
    |                        0 |
    +--------------------------+
    5 rows in set (0.01 sec)

    对于内置解析器,caseinsensitive 都无法像匹配 case-insensitive 一样匹配。