SphinxSE 是一个可以编译进 MySQL/MariaDB 服务器的 MySQL 存储引擎,利用它们的插件架构。
尽管名为 SphinxSE,但它实际上并不存储任何数据。相反,它充当内置客户端,使 MySQL 服务器能够与 searchd 通信,执行搜索查询并检索搜索结果。所有的索引和搜索都在 MySQL 之外进行。
一些常见的 SphinxSE 应用包括:
- 简化将 MySQL 全文搜索(FTS)应用程序迁移到 Manticore;
- 使 Manticore 能够与尚未提供原生 API 的编程语言一起使用;
- 当需要在 MySQL 侧进行额外的 Manticore 结果集处理(例如,与原始文档表的 JOIN 或额外的 MySQL 侧过滤)时提供优化。
您需要获取 MySQL 源代码,准备这些源代码,然后重新编译 MySQL 二进制文件。可以从 http://dev.mysql.com 网站获取 MySQL 源代码(mysql-5.x.yy.tar.gz)。
- 将
sphinx.5.0.yy.diff差异文件复制到 MySQL 源代码目录,并运行$ patch -p1 < sphinx.5.0.yy.diff如果不存在与所需具体版本完全匹配的 .diff 文件: 构建,尝试使用最近的版本号应用 .diff。 重要的是,该补丁应能够应用且没有任何拒绝。
- 在 MySQL 源代码目录中,运行
$ sh BUILD/autorun.sh - 在 MySQL 源代码目录中,创建
sql/sphinx目录,并将 Manticore 源代码中的mysqlse目录中的所有文件复制到该目录。例如:$ cp -R /root/builds/sphinx-0.9.7/mysqlse /root/builds/mysql-5.0.24/sql/sphinx - 配置 MySQL 并启用新引擎:
$ ./configure --with-sphinx-storage-engine - 编译并安装 MySQL:
$ make $ make install
- 在 MySQL 源代码目录中,创建一个
storage/sphinx目录,并将 Manticore 源代码中的mysqlse目录中的所有文件复制到此新位置。例如:$ cp -R /root/builds/sphinx-0.9.7/mysqlse /root/builds/mysql-5.1.14/storage/sphinx - 在 MySQL 源目录中,运行:
$ sh BUILD/autorun.sh - 配置 MySQL 并启用 Manticore 引擎:
$ ./configure --with-plugins=sphinx - 编译并安装 MySQL:
$ make $ make install
要验证是否已成功将 SphinxSE 编译进 MySQL,请启动新构建的服务器,运行 MySQL 客户端并执行 SHOW ENGINES 查询。您应该会看到所有可用引擎的列表。Manticore 应该在其中,并且 "Support" 列应显示 "YES":
- sql
mysql> show engines;+------------+----------+-------------------------------------------------------------+
| Engine | Support | Comment |
+------------+----------+-------------------------------------------------------------+
| MyISAM | DEFAULT | Default engine as of MySQL 3.23 with great performance |
...
| SPHINX | YES | Manticore storage engine |
...
+------------+----------+-------------------------------------------------------------+
13 rows in set (0.00 sec)要使用 SphinxSE 进行搜索,您需要创建一个特殊的 ENGINE=SPHINX "搜索表",然后使用带有全文查询的 SELECT 语句在查询列中进行查询。
以下是一个创建语句和搜索查询的示例:
CREATE TABLE t1
(
id INTEGER UNSIGNED NOT NULL,
weight INTEGER NOT NULL,
query VARCHAR(3072) NOT NULL,
group_id INTEGER,
INDEX(query)
) ENGINE=SPHINX CONNECTION="sphinx://localhost:9312/test";
SELECT * FROM t1 WHERE query='test it;mode=any';
在搜索表中,前三个列 必须 具有以下类型:第 1 列(文档 ID)为 INTEGER UNSIGNED 或 BIGINT,第 2 列(匹配权重)为 INTEGER 或 BIGINT,第 3 列(您的查询)为 VARCHAR 或 TEXT。这种映射是固定的;您不能省略这三个必需的列,也不能改变它们的顺序或类型。此外,查询列必须索引,而其他列应保持未索引。列名是忽略的,因此您可以使用任意名称。
额外的列必须是 INTEGER、TIMESTAMP、BIGINT、VARCHAR 或 FLOAT。它们将绑定到 Manticore 结果集中的属性,因此它们的名称必须与 sphinx.conf 中指定的属性名称匹配。如果 Manticore 搜索结果中没有匹配的属性名称,则该列将具有 NULL 值。
还可以将特殊“虚拟”属性名称绑定到 SphinxSE 列。为此,请使用 _sph_ 而不是 @。例如,要获取 @groupby、@count 或 @distinct 虚拟属性的值,请使用 _sph_groupby、_sph_count 或 _sph_distinct 列名,分别。
CONNECTION 字符串参数用于指定 Manticore 的主机、端口和表。如果在 CREATE TABLE 中未指定连接字符串,则假定表名为 *(即,搜索所有表)和 localhost:9312。连接字符串的语法如下:
CONNECTION="sphinx://HOST:PORT/TABLENAME"
稍后可以更改默认连接字符串:
mysql> ALTER TABLE t1 CONNECTION="sphinx://NEWHOST:NEWPORT/NEWTABLENAME";
您也可以在每次查询时覆盖这些参数。
如示例所示,查询文本和搜索选项应放置在搜索查询列(即第 3 列)的 WHERE 子句中。选项之间用分号分隔,名称与值之间用等号分隔。可以指定任意数量的选项。可用的选项有:
- query - 查询文本;
- mode - 匹配模式。必须是 "all"、"any"、"phrase"、"boolean" 或 "extended" 之一。默认为 "all";
- sort - 匹配排序模式。必须是 "relevance"、"attr_desc"、"attr_asc"、"time_segments" 或 "extended" 之一。在除 "relevance" 之外的所有模式中,冒号后还需要属性名(或 "extended" 的排序子句):
... WHERE query='test;sort=attr_asc:group_id'; ... WHERE query='test;sort=extended:@weight desc, group_id asc'; - offset - 结果集中的偏移量;默认为 0;
- limit - 从结果集中检索的匹配项数量;默认为 20;
- index - 要搜索的表名:
... WHERE query='test;index=test1;'; ... WHERE query='test;index=test1,test2,test3;'; - minid, maxid - 要匹配的最小和最大文档 ID;
- weights - 分配给 Manticore 全文字段的权重逗号分隔列表:
... WHERE query='test;weights=1,2,3;'; - filter, !filter - 逗号分隔的属性名和要匹配的值集合:
# only include groups 1, 5 and 19 ... WHERE query='test;filter=group_id,1,5,19;'; # exclude groups 3 and 11 ... WHERE query='test;!filter=group_id,3,11;'; - range, !range - 逗号分隔的(整数或大整数)Manticore 属性名,以及要匹配的最小和最大值:
# include groups from 3 to 7, inclusive ... WHERE query='test;range=group_id,3,7;'; # exclude groups from 5 to 25 ... WHERE query='test;!range=group_id,5,25;'; - floatrange, !floatrange - 逗号分隔的(浮点数)Manticore 属性名,以及要匹配的最小和最大值:
# filter by a float size ... WHERE query='test;floatrange=size,2,3;'; # pick all results within 1000 meter from geoanchor ... WHERE query='test;floatrange=@geodist,0,1000;'; - maxmatches - maxmatches - 每个查询的最大匹配值,如 max_matches 搜索选项 所述:
... WHERE query='test;maxmatches=2000;'; - cutoff - 最大允许匹配数,如 cutoff 搜索选项 所述:
... WHERE query='test;cutoff=10000;'; - maxquerytime - 最大允许查询时间(毫秒),如 max_query_time 搜索选项 所述:
... WHERE query='test;maxquerytime=1000;'; - groupby - 分组函数和属性。关于分组搜索结果,请阅读 此处:
... WHERE query='test;groupby=day:published_ts;'; ... WHERE query='test;groupby=attr:group_id;'; - groupsort - 分组排序子句:
... WHERE query='test;groupsort=@count desc;'; - distinct - 进行分组时用于计算 COUNT(DISTINCT) 的属性:
... WHERE query='test;groupby=attr:country_id;distinct=site_id'; - indexweights - 逗号分隔的表名和权重列表,用于在多个表中搜索时使用:
... WHERE query='test;indexweights=tbl_exact,2,tbl_stemmed,1;'; - fieldweights - 逗号分隔的每字段权重列表,可供排序器使用:
... WHERE query='test;fieldweights=title,10,abstract,3,content,1;'; - comment - 用于在查询日志中标记此查询的字符串,如 comment 搜索选项 所述:
... WHERE query='test;comment=marker001;'; - select - 包含要计算的表达式的字符串:
... WHERE query='test;select=2*a+3*** as myexpr;'; - host, port - 远程
searchd主机名和 TCP 端口,分别对应:... WHERE query='test;host=sphinx-test.loc;port=7312;'; - ranker - 与 "extended" 匹配模式一起使用的排序函数,如 ranker 所述。已知值有 "proximity_bm25"、"bm25"、"none"、"wordcount"、"proximity"、"matchany"、"fieldmask"、"sph04"、"expr:EXPRESSION" 语法支持基于表达式的排序器(其中 EXPRESSION 应替换为您的特定排序公式),以及 "export:EXPRESSION":
... WHERE query='test;mode=extended;ranker=bm25;'; ... WHERE query='test;mode=extended;ranker=expr:sum(lcs);';"export" 排序器的功能类似于 ranker=expr,但它会保留每个文档的因子值,而 ranker=expr 在计算最终的
WEIGHT()值后会丢弃它们。请注意,ranker=export 旨在偶尔使用,例如训练机器学习(ML)函数或手动定义您自己的排序函数,不应在实际生产中使用。使用此排序器时,您可能需要检查RANKFACTORS()函数的输出,该函数会生成一个包含每个文档所有字段级因子的字符串。
- sql
SELECT *, WEIGHT(), RANKFACTORS()
FROM myindex
WHERE MATCH('dog')
OPTION ranker=export('100*bm25');*************************** 1\. row ***************************
id: 555617
published: 1110067331
channel_id: 1059819
title: 7
content: 428
weight(): 69900
rankfactors(): bm25=699, bm25a=0.666478, field_mask=2,
doc_word_count=1, field1=(lcs=1, hit_count=4, word_count=1,
tf_idf=1.038127, min_idf=0.259532, max_idf=0.259532, sum_idf=0.259532,
min_hit_pos=120, min_best_span_pos=120, exact_hit=0,
max_window_hits=1), word1=(tf=4, idf=0.259532)
*************************** 2\. row ***************************
id: 555313
published: 1108438365
channel_id: 1058561
title: 8
content: 249
weight(): 68500
rankfactors(): bm25=685, bm25a=0.675213, field_mask=3,
doc_word_count=1, field0=(lcs=1, hit_count=1, word_count=1,
tf_idf=0.259532, min_idf=0.259532, max_idf=0.259532, sum_idf=0.259532,
min_hit_pos=8, min_best_span_pos=8, exact_hit=0, max_window_hits=1),
field1=(lcs=1, hit_count=2, word_count=1, tf_idf=0.519063,
min_idf=0.259532, max_idf=0.259532, sum_idf=0.259532, min_hit_pos=36,
min_best_span_pos=36, exact_hit=0, max_window_hits=1), word1=(tf=3,
idf=0.259532)- geoanchor - 地理距离锚点。在 本节 中了解更多关于地理搜索的信息。接受 4 个参数,分别是纬度和经度属性名,以及锚点坐标:
... WHERE query='test;geoanchor=latattr,lonattr,0.123,0.456';
一个 非常重要 的注意事项是,让 Manticore 处理结果集的排序、过滤和分片,比增加最大匹配数并在 MySQL 端使用 WHERE、ORDER BY 和 LIMIT 子句 要高效得多。这有两个原因。首先,Manticore 采用了多种优化措施,并且比 MySQL 更好地执行这些任务。其次,searchd 需要打包、传输以及 SphinxSE 需要解包的数据量会更少。
自 5.0.0 版本起,Manticore 默认存储所有字段。当 Manticore 通过 SphinxSE 与 MySQL 或 MariaDB 一起使用时,存储所有字段通常没有意义,因为原始数据已经存储在 MySQL/MariaDB 中。在此类设置中,建议通过以下设置显式禁用相关 Manticore 表的存储字段:
stored_fields =
请参阅设置参考:stored_fields。
如果您保持默认设置(存储所有字段),然后通过 SphinxSE 一次性选择大量文档,可能会超出引擎的内部限制,并收到类似以下的错误:
"bad searchd response length"
设置 stored_fields = 可以避免将大型存储负载发送回 MySQL/MariaDB,并在典型的 SphinxSE 集成中防止此错误。
您可以使用 SHOW ENGINE SPHINX STATUS 语句获取与查询结果相关的附加信息:
- sql
mysql> SHOW ENGINE SPHINX STATUS;+--------+-------+-------------------------------------------------+
| Type | Name | Status |
+--------+-------+-------------------------------------------------+
| SPHINX | stats | total: 25, total found: 25, time: 126, words: 2 |
| SPHINX | words | sphinx:591:1256 soft:11076:15945 |
+--------+-------+-------------------------------------------------+
2 rows in set (0.00 sec)您也可以通过状态变量访问此信息。请注意,使用此方法不需要超级用户权限。
- sql
mysql> SHOW STATUS LIKE 'sphinx_%';+--------------------+----------------------------------+
| Variable_name | Value |
+--------------------+----------------------------------+
| sphinx_total | 25 |
| sphinx_total_found | 25 |
| sphinx_time | 126 |
| sphinx_word_count | 2 |
| sphinx_words | sphinx:591:1256 soft:11076:15945 |
+--------------------+----------------------------------+
5 rows in set (0.00 sec)SphinxSE 搜索表可以与其他引擎的表进行连接。以下是使用 example.sql 中的 "documents" 表的示例:
- sql
mysql> SELECT content, date_added FROM test.documents docs
-> JOIN t1 ON (docs.id=t1.id)
-> WHERE query="one document;mode=any";
mysql> SHOW ENGINE SPHINX STATUS;+-------------------------------------+---------------------+
| content | docdate |
+-------------------------------------+---------------------+
| this is my test document number two | 2006-06-17 14:04:28 |
| this is my test document number one | 2006-06-17 14:04:28 |
+-------------------------------------+---------------------+
2 rows in set (0.00 sec)
+--------+-------+---------------------------------------------+
| Type | Name | Status |
+--------+-------+---------------------------------------------+
| SPHINX | stats | total: 2, total found: 2, time: 0, words: 2 |
| SPHINX | words | one:1:2 document:2:2 |
+--------+-------+---------------------------------------------+
2 rows in set (0.00 sec)SphinxSE 还具有一个 UDF 函数,允许您使用 MySQL 创建摘要。此功能类似于 HIGHLIGHT(),但可以通过 MySQL+SphinxSE 访问。
提供 UDF 的二进制文件名为 sphinx.so,应当与 SphinxSE 一起自动构建并安装到相应位置。如果由于某种原因没有自动安装,请在构建目录中找到 sphinx.so,并将其复制到你的 MySQL 实例的插件目录。完成后,使用以下语句注册 UDF:
CREATE FUNCTION sphinx_snippets RETURNS STRING SONAME 'sphinx.so';
函数名 必须 是 sphinx_snippets;不能使用任意名称。函数参数如下:
原型: function sphinx_snippets ( document, table, words [, options] );
document 和 words 参数可以是字符串或表列。options 必须像这样指定:'value' AS option_name。有关支持的选项列表,请参阅高亮部分。唯一特定于 UDF 的附加选项称为 sphinx,允许你指定 searchd 的位置(主机和端口)。
使用示例:
SELECT sphinx_snippets('hello world doc', 'main', 'world',
'sphinx://192.168.1.1/' AS sphinx, true AS exact_phrase,
'[**]' AS before_match, '[/**]' AS after_match)
FROM documents;
SELECT title, sphinx_snippets(text, 'table', 'mysql php') AS text
FROM sphinx, documents
WHERE query='mysql php' AND sphinx.id=documents.id;
使用MySQL的FEDERATED引擎,您可以从MySQL/MariaDB连接到本地或远程的Manticore实例,并执行搜索查询。
实际的Manticore查询不能直接与FEDERATED引擎一起使用,而必须通过“代理”(作为字符串在列中发送)发送,因为FEDERATED引擎的限制以及Manticore实现自定义语法如MATCH子句。
要通过FEDERATED进行搜索,您首先需要创建一个FEDERATED引擎表。Manticore查询将包含在SELECT语句中执行的FEDERATED表的query列中。
创建一个FEDERATED兼容的MySQL表:
- SQL
CREATE TABLE t1
(
id INTEGER UNSIGNED NOT NULL,
year INTEGER NOT NULL,
rating FLOAT,
query VARCHAR(1024) NOT NULL,
INDEX(query)
) ENGINE=FEDERATED
DEFAULT CHARSET=utf8
CONNECTION='mysql://FEDERATED@127.0.0.1:9306/DB/movies';Query OK, 0 rows affected (0.00 sec)查询FEDERATED兼容表:
- SQL
SELECT * FROM t1 WHERE query='SELECT * FROM movies WHERE MATCH (\'pie\')';+----+------+--------+------------------------------------------+
| id | year | rating | query |
+----+------+--------+------------------------------------------+
| 1 | 2019 | 5 | SELECT * FROM movies WHERE MATCH ('pie') |
+----+------+--------+------------------------------------------+
1 row in set (0.04 sec)唯一的固定映射是query列。它是必需的,并且必须是唯一的一个与表关联的列。
通过FEDERATED链接的Manticore表必须是一个物理表(普通或实时)。
FEDERATED表的列名应与远程Manticore表属性相同,因为它们将绑定到Manticore结果集中的属性名称。然而,它可能只映射一些属性,而不是所有属性。
Manticore服务器通过用户名“FEDERATED”识别来自FEDERATED客户端的查询。CONNECTION字符串参数用于指定通过连接发送的查询的Manticore主机、SQL端口和表。CONNECTION字符串的语法如下:
CONNECTION="mysql://FEDERATED@HOST:PORT/DB/TABLENAME"
由于Manticore没有数据库的概念,DB字符串可以是随机的,因为它将被Manticore忽略,但MySQL要求CONNECTION字符串定义中有一个值。如示例所示,完整的SELECTSQL查询应放置在query列的WHERE子句中。
仅支持SELECT语句,不支持INSERT、REPLACE、UPDATE或DELETE。
一个非常重要的注意事项是,允许Manticore执行排序、过滤和切片结果集比在MySQL侧增加最大匹配数并使用WHERE、ORDER BY和LIMIT子句要高效得多。这是有两个原因。首先,Manticore实现了许多优化并在此类任务中表现优于MySQL。其次,Manticore需要传输的数据量更少,因此在Manticore和MySQL之间传输的数据量更少。
可以在FEDERATED表和其他MySQL表之间执行JOIN操作。这可以用于检索未存储在Manticore表中的信息。
- SQL
+----+------+--------------+
| id | year | comment |
+----+------+--------------+
| 1 | 2019 | was not good |
+----+------+--------------+
1 row in set (0.00 sec)Manticore可以通过用户定义函数(简称UDF)进行扩展,如下所示:
SELECT id, attr1, myudf (attr2, attr3+attr4) ...
您可以在不重启服务器的情况下动态加载和卸载UDF到searchd中,并在搜索、排名等操作时使用它们。UDF的一些特点如下:
- UDF可以接受整数(32位和64位)、浮点数、字符串、MVA或
PACKEDFACTORS()参数。 - UDF可以返回整数、浮点数或字符串值。
- UDF可以在查询设置阶段检查参数数量、类型和名称,并在必要时抛出错误。
我们尚未支持聚合函数。换句话说,您的UDF将仅针对单个文档调用,并且期望为该文档返回某些值。编写一个可以计算整个具有相同GROUP BY键的文档组的聚合值(如AVG())的函数目前是不可能的。但是,您可以在内置聚合函数中使用UDF:也就是说,即使MYCUSTOMAVG()尚未支持,AVG(MYCUSTOMFUNC())也应该可以正常工作!
UDF提供了广泛的应用场景,例如:
- 集成自定义数学或字符串函数;
- 从Manticore访问数据库或文件;
- 创建复杂的排名函数。
插件提供了扩展搜索功能的额外机会。它们当前可用于计算自定义排名和分词文档和查询。
以下是插件类型的完整列表:
- UDF插件(本质上是UDF,但由于它们是插件,因此也被称为“UDF插件”)
- 排名插件
- 索引时分词过滤器插件
- 查询时分词过滤器插件
本节涵盖了编写和管理插件的一般过程;有关创建不同类型的插件的具体细节将在各自的子节中讨论。
那么,如何编写和使用插件?这里有一个快速的四步指南:
- 创建一个动态库(可能是.so或.dll),最有可能使用C或C++;
- 使用CREATE PLUGIN将插件加载到searchd中;
- 使用插件特定的调用(通常通过特定的OPTIONS)使用插件;
- 使用DROP PLUGIN卸载或使用RELOAD PLUGINS重新加载插件。
请注意,虽然UDF是一等插件,但它们使用单独的CREATE FUNCTION语句安装。这允许简洁地指定返回类型,而不牺牲向后兼容性或更改语法。
动态插件在多线程和thread_pool工作线程中受支持。一个库文件中可以包含多个插件(和/或UDF)。您可以选择将所有项目特定的插件放在一个大型库中,或者为每个UDF和插件创建单独的库;这取决于您。
与UDF一样,您应该包含src/sphinxudf.h头文件。至少,您需要SPH_UDF_VERSION常量来实现适当版本的函数。根据特定的插件类型,您可能或可能不需要将插件链接到src/sphinxudf.c。但是,sphinxudf.c中实现的所有函数都与PACKEDFACTORS()的解包有关,而没有任何插件类型可以访问这些数据。因此,目前仅链接头文件就足够了。(事实上,如果您复制了UDF版本号,您甚至不需要头文件来实现某些插件类型。)
形式上,插件是一组遵循特定命名模式的C函数。通常,您需要定义一个关键函数来执行主要任务,但也可以定义其他函数。例如,要实现一个名为“myrank”的排名器,您必须定义一个myrank_finalize()函数,该函数返回排名值。但是,您也可以定义myrank_init()、myrank_update()和myrank_deinit()函数。特定的已知后缀集和调用参数根据插件类型而异,但_init()和_deinit()是通用的,每个插件都有它们。提示:要快速参考已知的后缀及其参数类型,请参阅sphinxplugin.h,其中在文件开头定义了调用原型。
尽管公共接口是纯C定义的,但我们的插件实际上遵循一种面向对象模型。确实,每个_init()函数接收一个void ** userdata输出参数,然后将存储在(*userdata)中的指针值作为所有其他插件函数的第一个参数传递。因此,您可以将插件视为每次需要处理请求的对象的类实例化:userdata指针充当this指针;函数作为方法,而_init()和_deinit()函数分别作为构造函数和析构函数。
由于插件运行在多线程环境中,且某些插件需要维护状态,因此这种面向对象的C语言微小复杂性是因为您不能在插件中使用全局变量来存储状态,所以我们传递userdata参数,自然导致面向对象模型。如果您的插件简单且无状态,接口允许您省略_init()、_deinit()和其他任何函数。
总之,这里是一个最简单的完整排名器插件,仅用三行C代码实现:
// gcc -fPIC -shared -o myrank.so myrank.c
#include "sphinxudf.h"
int myrank_ver() { return SPH_UDF_VERSION; }
int myrank_finalize(void *u, int w) { return 123; }
这是如何使用简单的排名器插件:
mysql> CREATE PLUGIN myrank TYPE 'ranker' SONAME 'myrank.dll';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT id, weight() FROM test1 WHERE MATCH('test') OPTION ranker=myrank('');
+------+----------+
| id | weight() |
+------+----------+
| 1 | 123 |
| 2 | 123 |
+------+----------+
2 rows in set (0.01 sec)
UDF 存储在外部动态库中(UNIX 系统上为 .so 文件,Windows 系统上为 .dll 文件)。出于安全考虑,库文件必须放置在由 plugin_dir 指令指定的受信任文件夹中:保护单个文件夹比允许任何人向 searchd 安装任意代码要容易得多。您可以使用 CREATE FUNCTION 和 DROP FUNCTION SQL 语句动态加载和卸载 searchd 中的 UDF。此外,您还可以使用 RELOAD PLUGINS 语句无缝重新加载 UDF(及其他插件)。Manticore 会跟踪当前加载的函数;每次创建或删除 UDF 时,searchd 会将其状态更新到 sphinxql_state 文件中,作为纯 SQL 脚本。
UDF 是本地的。要在集群中使用它们,必须在所有节点上放置相同的库,并在每个节点上运行 CREATE 语句。此流程在未来版本中可能会有所变化。
一旦成功加载 UDF,您可以像使用任何内置函数一样在 SELECT 或其他语句中使用它:
SELECT id, MYCUSTOMFUNC (groupid, authorname), ... FROM myindex
多个 UDF(及其他插件)可以共存在同一个库中。该库只会被加载一次,并在其内所有 UDF 和插件都被删除后自动卸载。
理论上,您可以使用任何语言编写 UDF,只要其编译器能够导入标准 C 头文件并生成带有正确导出函数的标准动态库。然而,使用 C++ 或纯 C 是最少阻力的路径。我们提供了一个用纯 C 编写的示例 UDF 库,实现在多个函数中展示各种技术,源代码位于 src/udfexample.c。该示例包含头文件 src/sphinxudf.h,其中包含若干 UDF 相关结构和类型的定义。对于大多数 UDF 和插件,只需像示例中那样使用 #include "sphinxudf.h" 即可。然而,如果您编写排名函数且需要在 UDF 内访问排名信号(因素)数据,则还需要编译并链接 src/sphinxudf.c(可在我们的源码中获得),因为允许您从 UDF 内访问信号数据的函数实现位于该文件。
sphinxudf.h 头文件和 sphinxudf.c 是独立的,因此您可以单独复制这些文件;它们不依赖 Manticore 源码的其他部分。
在您的 UDF 中,您必须只实现并导出几个函数。首先,为了 UDF 接口版本控制,您必须定义一个函数 int LIBRARYNAME_ver(),其中 LIBRARYNAME 是您库文件的名字,并且此函数必须返回 SPH_UDF_VERSION(定义在 sphinxudf.h 中)。示例如下。
#include <sphinxudf.h>
// our library will be called udfexample.so, thus, so it must define
// a version function named udfexample_ver()
int udfexample_ver()
{
return SPH_UDF_VERSION;
}
此预防措施可避免您意外加载与 searchd 的 UDF 接口版本不匹配的库。不论是较新还是较旧版本。其次,您必须实现实际的函数。
sphinx_int64_t testfunc ( SPH_UDF_INIT * init, SPH_UDF_ARGS * args, char * error_flag )
{
return 123;
}
SQL 中的 UDF 函数名不区分大小写。但对应的 C 函数名区分大小写;它们必须全部为小写,否则 UDF 将无法加载。更重要的是,以下几点至关重要:
- 调用约定必须是 C(即 __cdecl),
- 参数列表必须完全符合插件系统的期望,
- 返回类型必须与您在
CREATE FUNCTION中指定的类型匹配。
不幸的是,我们无法(轻松地)在加载函数时检查这些错误,它们可能会导致服务器崩溃和/或产生意外结果。最后但同样重要的是,您实现的所有 C 函数都必须是线程安全的。
第一个参数,是指向 SPH_UDF_INIT 结构的指针,本质上是指向我们的函数状态的指针。它是可选的。就如上面示例中,该函数无状态,每次调用都返回 123。因此我们不需要定义初始化函数,可以忽略该参数。
此参数还有另一个目的。由于单个查询可以在多个线程上执行(参见 pseudo-sharding),守护进程会通过检查此参数来判断一个 UDF 是有状态还是无状态。如果该参数被初始化,平行执行将被禁用。因此,如果您的 UDF 是有状态的但未使用此参数,则它将由多个线程调用,您的代码需要对此有所意识。
第二个参数,是指向 SPH_UDF_ARGS 结构的指针,是最重要的参数。所有实际调用参数都通过此结构传递;它包含调用参数的数量、名称、类型等信息。所以无论函数是以 SELECT id, testfunc(1) 还是 SELECT id, testfunc('abc', 1000*id+gid, WEIGHT()) 或其他语句调用,它接收到的都是同一个 SPH_UDF_ARGS 结构。但 args 结构中传递的数据会不同。在第一个例子中,args->arg_count 被设置为 1;在第二个例子中被设置为 3,args->arg_types 数组会包含不同的类型数据,依此类推。
最后,第三个参数是错误标志。UDF 可设置此标志以表明发生某种内部错误,UDF 无法继续执行,查询应提前终止。您不应该用此标志进行参数类型检查或用于任何可能在正常使用中发生的错误报告。该标志设计用于报告突然的严重运行时错误,例如内存耗尽。
如果我们想为函数分配临时存储区或预先检查参数是否为支持的类型,则需要添加两个额外函数,分别用于 UDF 初始化和反初始化。
int testfunc_init ( SPH_UDF_INIT * init, SPH_UDF_ARGS * args,
char * error_message )
{
// allocate and initialize a little bit of temporary storage
init->func_data = malloc ( sizeof(int) );
*(int*)init->func_data = 123;
// return a success code
return 0;
}
void testfunc_deinit ( SPH_UDF_INIT * init )
{
// free up our temporary storage
free ( init->func_data );
}
注意 testfunc_init() 也接收调用参数结构。在调用时,它不会接收任何实际值,因此 args->arg_values 将为 NULL。但是,参数名称和类型是已知的,并会被传递。你可以在初始化函数中检查它们,如果它们是不支持的类型,则返回错误。
UDFs 可以接收几乎任何有效的内部 Manticore 类型的参数。参见 sphinx_udf_argtype 枚举在 sphinxudf.h 中的完整列表。大多数类型都直接映射到相应的 C 类型。
最值得注意的类型是 SPH_UDF_TYPE_FACTORS 参数类型。当你用 [PACKEDFACTOR()](../../Functions/Searching_and_ranking_functions#PACKEDFACTORS()) 参数调用 UDF 时,你会得到这种类型。它的数据是以某种内部格式的二进制数据块,要从中提取单独的排名信号,你需要使用 sphinx_factors_XXX() 或 sphinx_get_YYY_factor() 家族中的任何一个函数。
这个家族由 3 个函数组成。
sphinx_factors_init()初始化未打包的SPH_UDF_FACTORS结构sphinx_factors_unpack()将二进制数据块解包到SPH_UDF_FACTORS结构sphinx_factors_deinit()清理并释放SPH_UDF_FACTORS。
首先,你需要调用 init() 和 unpack(),然后你可以使用 SPH_UDF_FACTORS 字段,最后,你需要调用 deinit() 进行清理。
这种方法简单但可能会导致每个处理文档都产生大量内存分配,这可能会很慢。
另一种接口,由多个 sphinx_get_YYY_factor() 函数组成,使用起来稍微繁琐一些,但它直接访问数据块,并保证不会进行分配。为了获得顶级排名 UDF 的最佳性能,你将希望使用这种方法。
至于返回类型,UDFs 目前可以返回单个 INTEGER、BIGINT、FLOAT 或 STRING 值。C 函数返回类型应分别为 sphinx_int64_t、sphinx_int64_t、double 或 char*。在最后一种情况下,你 必须 使用 args->fn_malloc 函数为返回的字符串值分配空间。在你的 UDF 内部,你可以使用任何你想要的,因此上面的 testfunc_init() 示例是正确的代码,即使它直接使用了 malloc():你自行管理那个指针,它会在匹配的 free() 调用中被释放,一切都很顺利。但是,返回的字符串值由 Manticore 管理,我们有自己的分配器,因此对于返回值,你需要使用它。
根据你的 UDF 在查询中的使用方式,主要函数调用(例如我们示例中的 testfunc())可能会以不同的数量和顺序被调用。具体来说,
- 在 WHERE、ORDER BY 或 GROUP BY 子句中引用的 UDF 必须并且会被为每个匹配的文档评估。它们将以自然匹配顺序被调用。
- 没有子查询时,可以在最终结果集的最后一个阶段评估 UDF,但在应用
LIMIT子句之前。它们将以结果集顺序被调用。 - 有子查询时,这样的 UDF 也会在应用内部的
LIMIT子句之后进行评估。
其他函数的调用顺序是固定的。具体来说,
testfunc_init()在初始化查询时被调用一次。它可以返回非零代码以指示失败;在这种情况下,查询将终止,并返回error_message缓冲区中的错误消息。testfunc()为每个符合条件的行(见上文)被调用,每当 Manticore 需要计算 UDF 值时。它也可以通过将非零字节值写入error_flag来指示(内部)失败错误。在这种情况下,它将不会被调用后续行,并将使用默认返回值 0。Manticore 可能或可能不会选择提前终止此类查询;目前没有保证这种行为。testfunc_deinit()在查询处理(在给定表分片中)结束时被调用一次。