# Manticore Buddy: pluggable design v2

各位朋友。关于Manticore Buddy的激动人心的消息：我们已经完成了向插件化设计的迁移！这意味着你可以构建自己的Manticore Search SQL/JSON查询作为插件，在[packagist.org](http://packagist.org/)上发布，并使用`CREATE PLUGIN`SQL命令进行安装。让我们深入探讨插件系统的架构以及一个简单的教程，帮助你入门。

## 架构

### 简介

如果你不熟悉Buddy，我们建议阅读以下博客文章：

- [介绍Buddy：Manticore Search的PHP伴侣](./manticoresearch-buddy-pluggable-design-v2/../manticoresearch-buddy-intro/)
- 和[Manticore Buddy：挑战与解决方案](./manticoresearch-buddy-pluggable-design-v2/../manticoresearch-buddy-challenges-and-solutions/)

我们之前发布的文章。

最初，Buddy是用PHP开发的，所有扩展和额外命令处理器都在同一个代码库中。在[Manticore Search 6](https://manticoresearch.com/blog/manticore-search-6-0-0/)发布后，我们收到了关于添加自定义功能的问题，并意识到当前系统缺乏灵活性。这就是为什么我们要迁移到插件化架构的原因。

我们认为软件开发中最重要的一条原则是不要重新发明轮子，保持简单。因此，我们没有花费太多时间进行权衡，而是选择了PHP世界中最知名和流行的包管理器——[Packagist](https://packagist.org/)来管理插件系统。

### 插件类型

有三种类型的插件：

- `core` - 默认包含在Buddy中，并且当你安装Manticore时会收到该插件包的一部分。
- `local` - 用于开发和调试，然后发布。
- `external` - 使用`CREATE PLUGIN`命令安装。

本教程将解释如何首先创建一个`local`插件，然后将其转换为`external`插件，使任何Manticore用户都可以通过使用你的插件来增强他们的设置。

### 请求 -> 响应流程

为了使其工作，我们将Buddy和所有插件中常用组件提取到一个新的包中——[Buddy Core](https://github.com/manticoresoftware/buddy-core)。在开发插件时，它应该被包含进来，以简化IDE中的操作并支持提示和自动完成。

使Buddy能够作为守护进程运行并执行内部任务（包括连接核心和插件源代码）的代码称为Buddy Base。这是一个ReactPHP应用程序，也使用[Buddy Core](https://github.com/manticoresoftware/buddy-core)，具有基本逻辑以使一切正常运行并在命令下找到正确的插件启动。

为了更清楚地理解过程，这里有一个示意图说明请求 → 响应流程：

![Manticore Buddy 请求到响应流程](./manticoresearch-buddy-pluggable-design-v2/manticoresearch-buddy-request-response-flow.png)

## 教程

为了让事情变得更容易，并深入开发自己的插件，请查看我们如何构建一个支持`SHOW HOSTNAME`命令并返回当前主机名的简单插件。这将帮助你了解创建自己插件时应遵循的过程，而且这也是一个非常简单的开始示例。如果我们先研究一下Manticore Search中`SHOW HOSTNAME`的功能，然后再实现其逻辑，我们可以期望得到类似的结果：

```sql
mysql> SHOW HOSTNAME;
ERROR 1064 (42000): sphinxql: syntax error, unexpected identifier, expecting VARIABLES near 'HOSTNAME'
```

### 准备工作

首先，我们应该准备我们的开发环境。你只需要这样做一次，之后可以从[ManticoreSearch-Buddy的GIT仓库](https://github.com/manticoresoftware/manticoresearch-buddy)更新Buddy以接收最新更新。让我们打开终端并克隆Buddy到我们的机器上的一个名为`manticoresearch-buddy`的文件夹中，然后导航到克隆的文件夹。

```bash
git clone --depth 1 https://github.com/manticoresoftware/manticoresearch-buddy.git
cd manticoresearch-buddy
```

现在你有了Buddy的整个源代码，但还没有安装任何包。Buddy使用Composer来管理其依赖项，我们需要先安装它们才能运行应用程序。但在那之前，我们将运行一个专门为开发准备的特殊Docker容器。Manticore Executor Kit镜像包括了Manticore Buddy开发所需的全部工具，无论是创建新插件还是贡献Buddy本身。

```bash
docker pull ghcr.io/manticoresoftware/manticoresearch:test-kit-latest
docker create --privileged --entrypoint bash \
  -v $(pwd):/workdir -w /workdir --name manticore-buddy \
  --network host -it ghcr.io/manticoresoftware/manticoresearch:test-kit-latest
docker start manticore-buddy
```

恭喜！你现在启动了一个包含**Manticore Search**和**Manticore Executor**的Docker容器来开发Buddy和插件。你当前所在的目录映射到了容器内的`/workdir`。你的下一步是安装Composer依赖项。请记住，所有后续命令都必须在`manticore-buddy`容器内执行。

```bash
docker exec -it manticore-buddy bash
composer install
```

你的Buddy现在可以使用、开发、调试和测试了。接下来，你需要完成两件事：

1. 首先，在容器内编辑`/etc/manticoresearch/manticore.conf`，并在此处添加`buddy_path`，指向我们的源代码路径。这将让Manticore Search知道我们需要从源代码而不是已安装模块的路径运行Buddy。在`searchd`部分添加此内容：
   ```text
   buddy_path = manticore-executor /workdir/src/main.php --debug
   ```
2. 第二步，尝试运行`searchd`（Manticore Search服务器），确保它能正常工作。
   ```bash
   # Launch daemon inside the container and keep it foreground
   searchd --nodetach
   ```
   你应该看到几行以`[BUDDY]`开头的内容。

此时，一切都已准备好，可以为Buddy准备开发环境并开始实现新的插件。你可以继续实验并尝试执行一些查询以确保一切正常运行。例如，在另一个终端窗口中运行以下命令：

```bash
$ docker exec -it manticore-buddy mysql -h0 -P9306
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 6.0.5 3bcbd00fa@230320 dev (columnar 2.0.5 8171c1a@230320) (secondary 2.0.5 8171c1a@230320) git branch HEAD (no branch)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]> show queries;
+------+--------------+----------+-----------------+
| id   | query        | protocol | host            |
+------+--------------+----------+-----------------+
|   10 | select       | http     | 127.0.0.1:19148 |
|    9 | show queries | mysql    | 127.0.0.1:54484 |
+------+--------------+----------+-----------------+
2 rows in set (0.008 sec)

MySQL [(none)]>
```

你刚刚向Manticore Search发送了`SHOW QUERIES`命令，该命令随后被路由到Buddy并成功执行。你可以在第一个终端标签页中的搜索日志中看到Buddy接收到的内容及其响应。所以，我们可以确认Buddy正在正确运行并且一切设置正确。

当你想停止searchd进程时，只需在第一个终端窗口/标签页中按**Ctrl + C**即可。

### 插件模板

我们创建了一个[特殊的GitHub模板仓库](https://github.com/manticoresoftware/buddy-plugin-template)，强烈建议您使用它来创建任何Manticore Buddy插件。所有插件名称都应以`buddy-plugin-[your-name]`开头，其中`[your-name]`是您的插件名称。例如，由于我们正在开发一个主机名选择插件，我们将它命名为`buddy-plugin-show-hostname`。所以：

1. 打开[https://github.com/manticoresoftware/buddy-plugin-template](https://github.com/manticoresoftware/buddy-plugin-template)。
2. 点击`Use this template`，然后点击`Create a new repository`。
3. 在表单中填写您的仓库名称并创建它。确保您的仓库名称以`buddy-plugin-`开头，这是强制要求的。
4. 将您的新仓库`git clone`到之前部署的Manticore Buddy仓库的`plugins`目录中。请注意，最好在容器外部执行此操作，特别是如果您正在克隆私有仓库。

在[https://github.com/manticoresoftware/buddy-plugin-show-hostname](https://github.com/manticoresoftware/buddy-plugin-show-hostname.git)，您可以找到整个任务的实现。如果您不想深入细节，可以直接`git clone`该仓库，而不是您分叉的仓库。但是，如果您确实想掌握创建插件的技能，让我们继续。

### 添加真实代码

现在我们需要按照以下步骤更新我们的模板，以添加与新插件相关的内容。

1. 首先，在您克隆的插件目录中打开`composer.json`，并更新插件的名称、描述和命名空间。最终，您的更改应如下所示（请确保使用您插件的仓库名称）：

```diff
diff --git a/composer.json b/composer.json
index 23c252b..f36cb6e 100644
--- a/composer.json
+++ b/composer.json
@@ -1,11 +1,11 @@
 {
- "name": "manticoresoftware/buddy-plugin-template",
- "description": "The Buddy template handler plugin",
+ "name": "manticoresoftware/buddy-plugin-show-hostname",
+ "description": "The Buddy SHOW hostname handler plugin",
  "type": "library",
  "license": "GPL-2.0-or-later",
  "autoload": {
    "psr-4": {
-     "Manticoresearch\\Buddy\\Plugin\\Template\\": "src/"
+     "Manticoresearch\\Buddy\\Plugin\\ShowHostname\\": "src/"
    }
  },
  "authors": [
```

2. 我们还需要更新插件中两个类的命名空间——`Payload`和`Handler`。差异应如下所示：

```diff
diff --git a/src/Handler.php b/src/Handler.php
index 3756dc3..07fcfd0 100644
--- a/src/Handler.php
+++ b/src/Handler.php
@@ -8,7 +8,7 @@
   version. You should have received a copy of the GPL license along with this
   program; if you did not, you can find it at http://www.gnu.org/
 */
-namespace Manticoresearch\Buddy\Plugin\Template;
+namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Plugin\BaseHandler;
 use Manticoresearch\Buddy\Core\Task\Task;
diff --git a/src/Payload.php b/src/Payload.php
index b170340..a35f201 100644
--- a/src/Payload.php
+++ b/src/Payload.php
@@ -8,7 +8,7 @@
   version. You should have received a copy of the GPL license along with this
   program; if you did not, you can find it at http://www.gnu.org/
 */
-namespace Manticoresearch\Buddy\Plugin\Template;
+namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Network\Request;
 use Manticoresearch\Buddy\Core\Plugin\BasePayload;
```

3. 作为最后一步，请不要忘记通过在容器内运行`composer install`来安装新依赖项（在`/workdir`中）。

### 从Manticore Search到您的插件及返回的请求流程

我们现在准备实现我们的逻辑。但首先，让我们了解一些关于其内部处理方式的更多信息。

![Manticore Buddy请求到插件流程](./manticoresearch-buddy-pluggable-design-v2/manticoresearch-buddy-request-plugin-flow.png)

假设我们使用MySQL客户端向启动的`searchd`进程发送`SHOW HOSTNAME`查询。Manticore Search无法处理它，会将其发送给Buddy以等待响应。Buddy可以返回确切的响应，该响应将简单地代理回MySQL客户端。

一旦Buddy从Manticore Search接收到查询，它会解析它，并在所有插件上运行验证：首先核心插件，然后外部插件。为了运行此验证，它会执行`Payload::hasMatch($request)`方法，将请求传递给它，其中包含查询和一些元数据，例如请求的端点、类型（JSON或SQL）等。

`Payload`表示包含处理命令所需所有数据的结构。如果插件的`Payload`在`hasMatch`方法中返回true，则表示我们将使用此插件来处理请求。它是从`Request`创建的，此时我们需要解析查询，提取有价值的信息，并将其设置到我们的payload数据中，这些数据将传递给`Handler`。

`Handler`是实际执行实现命令的类。它通过使用并行扩展在多线程环境中执行此操作，而不会阻塞主服务器循环。有一个简单的`run`方法，它创建一个闭包，必须执行任务并将响应返回给Manticore Search，然后该响应将被代理给用户。

在我们的情况下，我们只需要添加对支持`SHOW HOSTNAME`查询语法的检查，然后编写`Handler`逻辑以调用`gethostname` PHP函数并将其作为响应返回。就是这样！让我们从`Payload`开始。

### 实现Payload

当您打开`src/Payload.php`时，您会看到只有两个方法：`fromRequest`和`hasMatch`。我们用`TODO`注释标记了需要编辑的行，因此很容易找到需要执行的操作。在我们的情况下，查询非常简单且完全不灵活，因此我们不需要在`fromRequest`方法中进行任何更改。我们可以直接删除`TODO`注释。我们需要做的是更新静态函数`hasMatch()`。在那里，我们需要实现一个检查，以确保查询严格匹配字符串`SHOW HOSTNAME`（不区分大小写）。一旦匹配成功，我们应该返回true。否则，返回false。这就是基础系统理解在接收到显示主机名的查询时需要使用此插件的方式。

我们对`Payload`所做的最终差异应如下所示：

```diff
diff --git a/src/Payload.php b/src/Payload.php
index a35f201..bd0795e 100644
--- a/src/Payload.php
+++ b/src/Payload.php
@@ -26,7 +26,6 @@ final class Payload extends BasePayload {
   */
  public static function fromRequest(Request $request): static {
    $self = new static();
-   // TODO: add logic of parsing request into payload here
    // We just need to do something, but actually its' just for PHPstan
    $self->path = $request->path;
    return $self;
@@ -37,7 +36,6 @@ final class Payload extends BasePayload {
   * @return bool
   */
  public static function hasMatch(Request $request): bool {
-   // TODO: validate $request->payload and return true, if your plugin should handle it
-   return $request->payload === 'template';
+   return stripos($request->payload, 'show hostname') !== false;
  }
 }
```

我们完成了`Payload`的更改，可以继续到`Handler`中实现真正的逻辑。

### 实现Handler

现在，让我们打开`src/Handler.php`并查看`TODO`标记，以及简要描述正在发生的事情。我们需要导航到`run`方法并更新它。

我们可以看到一个返回`TaskResult`的闭包，这是一种特殊的结果类型，用于包装标准的JSON响应。

我们将包含一个`gethostname()`调用，并通过将其包装在将返回给客户端的`TaskResult`中来准备响应。最终差异将如下所示：

```diff
diff --git a/src/Handler.php b/src/Handler.php
index 07fcfd0..944899b 100644
--- a/src/Handler.php
+++ b/src/Handler.php
@@ -11,6 +11,7 @@
 namespace Manticoresearch\Buddy\Plugin\ShowHostname;

 use Manticoresearch\Buddy\Core\Plugin\BaseHandler;
+use Manticoresearch\Buddy\Core\Task\Column;
 use Manticoresearch\Buddy\Core\Task\Task;
 use Manticoresearch\Buddy\Core\Task\TaskResult;
 use RuntimeException;
@@ -33,9 +34,11 @@ final class Handler extends BaseHandler {
   * @throws RuntimeException
   */
  public function run(): Task {
-   // TODO: your logic goes into closure and should return TaskResult as response
    $taskFn = static function (): TaskResult {
-     return TaskResult::none();
+     $hostname = gethostname();
+     return TaskResult::withRow([
+       'hostname' => $hostname,
+     ])->column('hostname', Column::String);
    };

    return Task::create(
```

### 调试和开发

现在您已经完成了实现，是时候测试和调试（如果需要）您的插件了。有一种专门用于此目的的插件类型称为`local`。由于您的插件已经在`plugins`目录中，它会自动成为`local`插件。要使用它，只需：

1. 在Buddy的根目录中，运行命令`composer require [your-plugin-name]:dev-main`以包含插件。`your-plugin-name`是您之前编辑的插件的`composer.json`中的名称。
2. 重新启动`searchd`以确保代码已更新。

此方法允许您在不将插件推送到Git仓库并在[packagist.org](http://packagist.org/)上发布的情况下，实时开发、调试、编辑和测试您的插件。

### 发布和安装

当你完成调试并准备好发布插件时，只需将更改提交到Git仓库，并在[packagist.org](https://packagist.org/)上发布你的包。

之后，通过运行`CREATE PLUGIN [your-plugin-name] TYPE 'buddy' VERSION 'dev-main'`命令检查它是否按预期工作，这将尝试使用Composer下载插件并将其安装到`plugin_dir`。

像这样：

```bash
$ docker exec -it manticore-buddy mysql -h0 -P9306
MySQL [(none)]> CREATE PLUGIN manticoresoftware/buddy-plugin-show-hostname type 'buddy' VERSION 'dev-main';

Query OK, 0 rows affected (1 min 12.213 sec)

MySQL [(none)]> show hostname;
+----------+
| hostname |
+----------+
| dev      |
+----------+
1 row in set (0.011 sec)

MySQL [(none)]>
```

恭喜！您的插件运行良好，并且可以通过`SHOW hostname`查询来获取主机名。

## 结论

希望您喜欢这篇文章，并加深了对Manticore 6.0.4版本以来新增的Buddy可插拔架构的理解。您可以按照我们提供的说明立即开始开发自己的插件。虽然这是对可插拔系统的初步介绍，但我们计划创建并发布一篇关于开发复杂插件的更高级文章。不要犹豫在我们的[Slack](https://slack.manticoresearch.com/)或[Telegram](https://t.me/manticoresearch_en)聊天中向我们提问，并随时建议复杂插件教程的主题。

请记住，查看Buddy的[built-in plugin code](https://github.com/manticoresoftware/manticoresearch-buddy/tree/main/src/Plugin)是一个好主意，以更深入地了解其在更复杂场景中的工作原理。

此致，  
Manticore 团队。
