UDF хранятся во внешних динамических библиотеках (.so файлы в UNIX и .dll в системах Windows). Файлы библиотек должны быть размещены в доверенной папке, указанной в директиве plugin_dir по соображениям безопасности: легче защитить одну папку, чем позволить кому угодно устанавливать произвольный код в searchd. Вы можете динамически загружать и выгружать UDF в searchd с помощью SQL-запросов CREATE FUNCTION и DROP FUNCTION соответственно. Дополнительно можно бесшовно перезагружать UDF (и другие плагины) с помощью команды RELOAD PLUGINS. Manticore отслеживает текущие загруженные функции; каждый раз, когда вы создаёте или удаляете UDF, searchd обновляет своё состояние в файле sphinxql_state в виде простого SQL-скрипта.
UDF являются локальными. Чтобы использовать их в кластере, вы должны разместить одинаковую библиотеку на всех узлах и выполнить CREATE-запросы на каждом узле. В будущих версиях этот процесс может измениться.
После успешной загрузки UDF, вы можете использовать её в своих запросах SELECT или других запросах так же, как и любую встроенную функцию:
SELECT id, MYCUSTOMFUNC (groupid, authorname), ... FROM myindex
Несколько UDF (и других плагинов) могут находиться в одной библиотеке. Библиотека загрузится только один раз и автоматически выгрузится, когда все UDF и плагины в ней будут удалены.
В теории, вы можете написать UDF на любом языке, если его компилятор может импортировать стандартные заголовки C и создавать стандартные динамические библиотеки с правильно экспортированными функциями. Однако писать на C++ или простом C — самый простой путь. Мы предоставляем пример библиотеки UDF, написанной на простом C, которая реализует несколько функций (демонстрируя различные техники) вместе с нашим исходным кодом, находящимся по адресу 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;
}
Эта предосторожность защищает вас от случайной загрузки библиотеки с несовпадающей версией интерфейса UDF в более новый или более старый searchd. Во-вторых, вы должны также реализовать саму функцию.
sphinx_int64_t testfunc ( SPH_UDF_INIT * init, SPH_UDF_ARGS * args, char * error_flag )
{
return 123;
}
Имена функций UDF в SQL нечувствительны к регистру. Однако соответствующие C-функции должны быть в нижнем регистре, иначе UDF не загрузится. Более того, крайне важно, чтобы:
- соглашение о вызове было C (aka __cdecl),
- список аргументов точно соответствовал ожиданиям системы плагинов, и
- возвращаемый тип совпадал с указанным в
CREATE FUNCTION.
К сожалению, (легко) проверить эти ошибки при загрузке функции нельзя, и они могут привести к падению сервера и/или непредсказуемым результатам. И последнее, все реализованные вами функции на C должны быть потокобезопасными.
Первый аргумент — указатель на структуру SPH_UDF_INIT — фактически указатель на состояние нашей функции. Он опционален. В приведённом выше примере функция не хранит состояние, так как просто всегда возвращает 123. Поэтому мы не должны определять функцию инициализации, и можем просто игнорировать этот аргумент.
Этот аргумент служит и ещё одной цели. Поскольку один и тот же запрос может выполняться в нескольких потоках (см. pseudo-sharding), демон пытается определить, является ли UDF сохраняющей состояние или нет, проверяя этот аргумент. Если этот аргумент инициализирован, параллельное выполнение будет отключено. Так что если ваша UDF сохраняет состояние, но не использует этот аргумент, она будет вызвана из нескольких потоков, и ваш код должен это учитывать.
Второй аргумент — указатель на SPH_UDF_ARGS — самый важный. Все фактические аргументы вызова передаются в вашу UDF через эту структуру; она содержит количество аргументов, имена, типы и так далее. Значит, будь вызов типа SELECT id, testfunc(1) или SELECT id, testfunc('abc', 1000*id+gid, WEIGHT()) или любой другой, UDF получит одинаковую структуру 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. Однако имена и типы аргументов уже известны и будут переданы. Вы можете проверить их в функции инициализации и вернуть ошибку, если они имеют неподдерживаемый тип.
UDF могут получать аргументы практически любого допустимого внутреннего типа Manticore. Полный список смотрите в перечислении sphinx_udf_argtype в файле sphinxudf.h. Большинство типов напрямую соответствуют соответствующим типам C.
Наиболее примечательным типом является тип аргумента SPH_UDF_TYPE_FACTORS. Вы получаете этот тип, вызывая вашу UDF с аргументом [PACKEDFACTOR()](../../Functions/Searching_and_ranking_functions#PACKEDFACTORS()). Его данные представляют собой бинарный блок в определённом внутреннем формате, и для извлечения отдельных сигналов ранжирования из этого блока вам необходимо использовать одну из двух семейств функций: sphinx_factors_XXX() или sphinx_get_YYY_factor().
Это семейство состоит из 3 функций.
sphinx_factors_init()инициализирует распакованную структуруSPH_UDF_FACTORSsphinx_factors_unpack()распаковывает бинарный блок в структуруSPH_UDF_FACTORSsphinx_factors_deinit()очищает и освобождает память структурыSPH_UDF_FACTORS.
Сначала вам нужно вызвать init() и unpack(), затем вы можете использовать поля SPH_UDF_FACTORS, и, наконец, необходимо выполнить очистку с помощью deinit().
Этот подход прост, но может приводить к множеству выделений памяти для каждого обрабатываемого документа, что может быть медленным.
Другой интерфейс, состоящий из набора функций sphinx_get_YYY_factor(), использовать немного более многословно, но он обращается к данным блока напрямую и гарантирует отсутствие выделений памяти. Для максимальной производительности UDF ранжирования вам следует использовать этот подход.
Что касается возвращаемых типов, UDF в настоящее время могут возвращать единственное значение типа INTEGER, BIGINT, FLOAT или STRING. Соответствующий тип возвращаемого значения C-функции должен быть sphinx_int64_t, sphinx_int64_t, double или char*. В последнем случае вы обязаны использовать функцию args->fn_malloc для выделения памяти под возвращаемые строковые значения. Внутри вашей UDF вы можете использовать что угодно, поэтому пример testfunc_init() выше является корректным кодом, даже несмотря на прямое использование malloc(): вы управляете этим указателем самостоятельно, он освобождается с помощью соответствующего вызова free(), и всё в порядке. Однако возвращаемые строковые значения управляются Manticore, и у нас есть собственный аллокатор, поэтому именно для возвращаемых значений вам также необходимо использовать его.
В зависимости от того, как ваши UDF используются в запросе, основной вызов функции (testfunc() в нашем примере) может вызываться в разном объёме и порядке. А именно:
- UDF, упомянутые в предложениях WHERE, ORDER BY или GROUP BY, должны и будут вычисляться для каждого подходящего документа. Они будут вызываться в естественном порядке соответствия.
- без подзапросов, UDF, которые могут быть вычислены на самом последнем этапе над окончательным набором результатов, будут вычислены именно так, но до применения предложения
LIMIT. Они будут вызываться в порядке набора результатов. - с подзапросами, такие UDF также будут вычислены после применения внутреннего предложения
LIMIT.
Однако последовательность вызова других функций фиксирована. А именно:
testfunc_init()вызывается один раз при инициализации запроса. Она может вернуть ненулевой код для указания на ошибку; в этом случае запрос будет прерван, и будет возвращено сообщение об ошибке из буфераerror_message.testfunc()вызывается для каждой подходящей строки (см. выше), когда Manticore нуждается в вычислении значения UDF. Она также может указать на (внутреннюю) ошибку, записав ненулевое байтовое значение вerror_flag. В этом случае гарантируется, что функция не будет вызываться для последующих строк, и будет подставлено возвращаемое значение по умолчанию, равное 0. Manticore может как прервать такие запросы досрочно, так и не делать этого; ни одно из этих поведений в настоящее время не гарантируется.testfunc_deinit()вызывается один раз при завершении обработки запроса (в данном шарде таблицы).