在宝塔环境中实现 Elasticsearch 与数据库的实时同步，主要有以下几种方案：一、同步方案概览方案实时性复杂度数据一致性适用场景应用层双写最高中等最终一致新建项目，可修改代码Logstash JDBC分钟级低延迟一致已有项目，增量同步Canal秒级高最终一致MySQL 数据库，要求高实时性数据库触发器实时高强一致数据库层面同步二、方案一：应用层双写（推荐）实现原理在业务代码中同时写入数据库和 Elasticsearch。PHP 实现示例<?php /** * 应用层双写方案 * 在写入数据库的同时写入 Elasticsearch */ require '/www/wwwroot/your-site/vendor/autoload.php'; use Elasticsearch\ClientBuilder; class DualWriteService { private $esClient; private $db; public function __construct() { // 初始化 ES 客户端 $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); // 初始化数据库连接 $this->initDatabase(); } private function initDatabase() { $host = 'localhost'; $dbname = 'your_database'; $username = 'your_username'; $password = 'your_password'; try { $this->db = new PDO( "mysql:host={$host};dbname={$dbname};charset=utf8mb4", $username, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC ] ); } catch (PDOException $e) { throw new Exception("数据库连接失败: " . $e->getMessage()); } } /** * 添加文章（双写） */ public function addArticle($articleData) { $dbResult = $this->writeToDatabase($articleData); if ($dbResult['success']) { $esResult = $this->writeToElasticsearch($articleData, $dbResult['id']); if (!$esResult['success']) { // ES 写入失败，记录日志或加入重试队列 $this->logSyncFailure('add', $dbResult['id'], $articleData); } return $dbResult; } return $dbResult; } /** * 更新文章（双写） */ public function updateArticle($id, $updateData) { $dbResult = $this->updateDatabase($id, $updateData); if ($dbResult['success']) { $esResult = $this->updateElasticsearch($id, $updateData); if (!$esResult['success']) { $this->logSyncFailure('update', $id, $updateData); } } return $dbResult; } /** * 删除文章（双写） */ public function deleteArticle($id) { $dbResult = $this->deleteFromDatabase($id); if ($dbResult['success']) { $esResult = $this->deleteFromElasticsearch($id); if (!$esResult['success']) { $this->logSyncFailure('delete', $id); } } return $dbResult; } /** * 写入数据库 */ private function writeToDatabase($data) { try { $sql = "INSERT INTO articles (title, content, author, category, tags, status, created_at, updated_at) VALUES (:title, :content, :author, :category, :tags, :status, NOW(), NOW())"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':title' => $data['title'], ':content' => $data['content'], ':author' => $data['author'], ':category' => $data['category'], ':tags' => is_array($data['tags']) ? implode(',', $data['tags']) : $data['tags'], ':status' => $data['status'] ?? 1 ]); $id = $this->db->lastInsertId(); return [ 'success' => true, 'id' => $id ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 写入 Elasticsearch */ private function writeToElasticsearch($data, $id) { try { $esData = [ 'id' => (int)$id, 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']), 'status' => $data['status'] ?? 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s') ]; $params = [ 'index' => 'articles', 'id' => $id, 'body' => $esData ]; $response = $this->esClient->index($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 更新数据库 */ private function updateDatabase($id, $data) { try { $sql = "UPDATE articles SET title = :title, content = :content, author = :author, category = :category, tags = :tags, status = :status, updated_at = NOW() WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':title' => $data['title'], ':content' => $data['content'], ':author' => $data['author'], ':category' => $data['category'], ':tags' => is_array($data['tags']) ? implode(',', $data['tags']) : $data['tags'], ':status' => $data['status'] ?? 1, ':id' => $id ]); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 更新 Elasticsearch */ private function updateElasticsearch($id, $data) { try { $updateData = [ 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => is_array($data['tags']) ? $data['tags'] : explode(',', $data['tags']), 'status' => $data['status'] ?? 1, 'updated_at' => date('Y-m-d H:i:s') ]; $params = [ 'index' => 'articles', 'id' => $id, 'body' => [ 'doc' => $updateData ] ]; $response = $this->esClient->update($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 从数据库删除 */ private function deleteFromDatabase($id) { try { $sql = "DELETE FROM articles WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $id]); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 从 Elasticsearch 删除 */ private function deleteFromElasticsearch($id) { try { $params = [ 'index' => 'articles', 'id' => $id ]; $response = $this->esClient->delete($params); return ['success' => true]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 记录同步失败 */ private function logSyncFailure($operation, $id, $data = null) { $logData = [ 'timestamp' => date('Y-m-d H:i:s'), 'operation' => $operation, 'id' => $id, 'data' => $data ]; file_put_contents( '/www/wwwlogs/es_sync_failures.log', json_encode($logData) . "\n", FILE_APPEND ); } /** * 重试失败的同步 */ public function retryFailedSyncs() { $logFile = '/www/wwwlogs/es_sync_failures.log'; if (!file_exists($logFile)) { return ['success' => true, 'message' => '无失败记录']; } $logs = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $successCount = 0; $failCount = 0; foreach ($logs as $log) { $data = json_decode($log, true); try { switch ($data['operation']) { case 'add': $this->writeToElasticsearch($data['data'], $data['id']); break; case 'update': $this->updateElasticsearch($data['id'], $data['data']); break; case 'delete': $this->deleteFromElasticsearch($data['id']); break; } $successCount++; } catch (Exception $e) { $failCount++; } } // 清空日志文件 file_put_contents($logFile, ''); return [ 'success' => true, 'retried' => $successCount, 'failed' => $failCount ]; } } // 使用示例 $syncService = new DualWriteService(); // 添加文章 $article = [ 'title' => '测试文章标题', 'content' => '这是文章内容...', 'author' => '张三', 'category' => '技术', 'tags' => ['PHP', 'Elasticsearch', '搜索'], 'status' => 1 ]; $result = $syncService->addArticle($article); if ($result['success']) { echo "文章添加成功，ID: " . $result['id']; } else { echo "添加失败: " . $result['error']; } ?>三、方案二：Logstash JDBC 输入插件安装和配置 Logstash1. 在宝塔中安装 Logstash# 进入宝塔终端 cd /www/server # 下载 Logstash（版本需要与 ES 对应） wget https://artifacts.elastic.co/downloads/logstash/logstash-7.17.0-linux-x86_64.tar.gz # 解压 tar -zxvf logstash-7.17.0-linux-x86_64.tar.gz mv logstash-7.17.0 logstash # 下载 MySQL JDBC 驱动 cd logstash wget https://cdn.mysql.com/archives/mysql-connector-java-8.0/mysql-connector-java-8.0.28.tar.gz tar -zxvf mysql-connector-java-8.0.28.tar.gz2. 创建 Logstash 配置文件创建 /www/server/logstash/config/mysql-sync.conf：input { jdbc { # MySQL 连接配置 jdbc_connection_string => "jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&useSSL=false" jdbc_user => "your_username" jdbc_password => "your_password" # JDBC 驱动路径 jdbc_driver_library => "/www/server/logstash/mysql-connector-java-8.0.28/mysql-connector-java-8.0.28.jar" jdbc_driver_class => "com.mysql.cj.jdbc.Driver" # 启用分页 jdbc_paging_enabled => true jdbc_page_size => 50000 # 调度配置（每分钟执行一次） schedule => "* * * * *" # SQL 查询（增量同步） statement => "SELECT * FROM articles WHERE updated_at > :sql_last_value OR created_at > :sql_last_value" # 记录最后一次同步时间 use_column_value => true tracking_column_type => "timestamp" tracking_column => "updated_at" # 记录文件位置 last_run_metadata_path => "/www/server/logstash/last_run_metadata.txt" # 时区设置 jdbc_default_timezone => "Asia/Shanghai" } } filter { # 添加 @timestamp 字段 date { match => [ "updated_at", "yyyy-MM-dd HH:mm:ss" ] target => "@timestamp" } # 处理 tags 字段 if [tags] { mutate { gsub => [ "tags", ",", "|" ] split => { "tags" => "|" } } } # 移除不需要的字段 mutate { remove_field => ["@version", "@timestamp"] } } output { # 输出到 Elasticsearch elasticsearch { hosts => ["localhost:9200"] index => "articles" document_id => "%{id}" # 定义映射模板 template => "/www/server/logstash/config/articles-template.json" template_name => "articles" template_overwrite => true } # 可选：输出到文件用于调试 file { path => "/www/server/logstash/logs/mysql-sync.log" } }3. 创建 Elasticsearch 映射模板创建 /www/server/logstash/config/articles-template.json：{ "index_patterns": ["articles*"], "settings": { "number_of_shards": 1, "number_of_replicas": 0, "analysis": { "analyzer": { "ik_smart_analyzer": { "type": "custom", "tokenizer": "ik_smart" }, "ik_max_analyzer": { "type": "custom", "tokenizer": "ik_max_word" } } } }, "mappings": { "properties": { "id": {"type": "integer"}, "title": { "type": "text", "analyzer": "ik_smart_analyzer", "search_analyzer": "ik_smart_analyzer" }, "content": { "type": "text", "analyzer": "ik_max_analyzer", "search_analyzer": "ik_smart_analyzer" }, "author": {"type": "keyword"}, "category": {"type": "keyword"}, "tags": {"type": "keyword"}, "status": {"type": "integer"}, "created_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"}, "updated_at": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss"} } } }4. 启动 Logstash# 创建日志目录 mkdir -p /www/server/logstash/logs # 启动 Logstash cd /www/server/logstash ./bin/logstash -f config/mysql-sync.conf # 或作为守护进程运行 nohup ./bin/logstash -f config/mysql-sync.conf > logs/logstash.log 2>&1 &5. 创建宝塔计划任务在宝塔面板中添加计划任务：# 命令 cd /www/server/logstash && ./bin/logstash -f config/mysql-sync.conf # 执行周期：每分钟四、方案三：Canal（MySQL Binlog 同步）安装和配置 Canal1. 安装 Canal# 下载 Canal cd /www/server wget https://github.com/alibaba/canal/releases/download/canal-1.1.6/canal.deployer-1.1.6.tar.gz tar -zxvf canal.deployer-1.1.6.tar.gz mv canal.deployer-1.1.6 canal2. 配置 MySQL 开启 Binlog-- 检查是否开启 binlog SHOW VARIABLES LIKE 'log_bin'; -- 如果没有开启，在 MySQL 配置文件中添加 -- /etc/my.cnf [mysqld] log-bin=mysql-bin binlog-format=ROW server_id=13. 创建 Canal 用户CREATE USER 'canal'@'%' IDENTIFIED BY 'canal_password'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;4. 配置 Canal编辑 /www/server/canal/conf/example/instance.properties：# 数据库配置 canal.instance.master.address=127.0.0.1:3306 canal.instance.dbUsername=canal canal.instance.dbPassword=canal_password canal.instance.connectionCharset=UTF-8 # 要监听的数据库和表 canal.instance.filter.regex=.*\\..* canal.instance.filter.black.regex= # 表映射 canal.instance.filter.regex=your_database.articles5. 创建 PHP Canal 客户端<?php /** * Canal PHP 客户端 * 监听 MySQL Binlog 并同步到 Elasticsearch */ class CanalClient { private $esClient; public function __construct() { $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } public function startSync() { // 连接 Canal 服务器 $client = new \Canal\Client(); $client->connect("127.0.0.1", 11111); $client->subscribe("example", "your_database.articles", ""); while (true) { $message = $client->get(100); if ($message->getEntries()) { foreach ($message->getEntries() as $entry) { if ($entry->getEntryType() == \Canal\Protocol\EntryType::ROWDATA) { $this->processRowChange($entry); } } } sleep(1); // 避免 CPU 过高 } } private function processRowChange($entry) { $rowChange = \Canal\Protocol\RowChange::parseFromString($entry->getStoreValue()); foreach ($rowChange->getRowDatasList() as $rowData) { switch ($rowChange->getEventType()) { case \Canal\Protocol\EventType::INSERT: $this->handleInsert($rowData); break; case \Canal\Protocol\EventType::UPDATE: $this->handleUpdate($rowData); break; case \Canal\Protocol\EventType::DELETE: $this->handleDelete($rowData); break; } } } private function handleInsert($rowData) { $afterColumns = $rowData->getAfterColumnsList(); $data = []; foreach ($afterColumns as $column) { $data[$column->getName()] = $column->getValue(); } $this->syncToElasticsearch('index', $data); } private function handleUpdate($rowData) { $afterColumns = $rowData->getAfterColumnsList(); $data = []; $id = null; foreach ($afterColumns as $column) { $data[$column->getName()] = $column->getValue(); if ($column->getName() == 'id') { $id = $column->getValue(); } } $this->syncToElasticsearch('update', $data, $id); } private function handleDelete($rowData) { $beforeColumns = $rowData->getBeforeColumnsList(); $id = null; foreach ($beforeColumns as $column) { if ($column->getName() == 'id') { $id = $column->getValue(); break; } } $this->syncToElasticsearch('delete', null, $id); } private function syncToElasticsearch($operation, $data = null, $id = null) { try { switch ($operation) { case 'index': $params = [ 'index' => 'articles', 'id' => $data['id'], 'body' => $this->transformData($data) ]; $this->esClient->index($params); break; case 'update': $params = [ 'index' => 'articles', 'id' => $id, 'body' => [ 'doc' => $this->transformData($data) ] ]; $this->esClient->update($params); break; case 'delete': $params = [ 'index' => 'articles', 'id' => $id ]; $this->esClient->delete($params); break; } echo "同步成功: {$operation} ID: {$id}\n"; } catch (Exception $e) { echo "同步失败: {$e->getMessage()}\n"; $this->logFailure($operation, $id, $data); } } private function transformData($data) { return [ 'id' => (int)$data['id'], 'title' => $data['title'], 'content' => $data['content'], 'author' => $data['author'], 'category' => $data['category'], 'tags' => !empty($data['tags']) ? explode(',', $data['tags']) : [], 'status' => (int)$data['status'], 'created_at' => $data['created_at'], 'updated_at' => $data['updated_at'] ]; } private function logFailure($operation, $id, $data) { // 记录同步失败日志 file_put_contents( '/www/wwwlogs/canal_sync_failures.log', json_encode([ 'timestamp' => date('Y-m-d H:i:s'), 'operation' => $operation, 'id' => $id, 'data' => $data ]) . "\n", FILE_APPEND ); } } // 启动 Canal 客户端 $canalClient = new CanalClient(); $canalClient->startSync(); ?>五、方案四：数据库触发器 + 消息队列1. 创建消息表CREATE TABLE es_sync_queue ( id BIGINT AUTO_INCREMENT PRIMARY KEY, table_name VARCHAR(100) NOT NULL, record_id BIGINT NOT NULL, operation ENUM('INSERT', 'UPDATE', 'DELETE') NOT NULL, sync_status TINYINT DEFAULT 0 COMMENT '0:未同步, 1:已同步, 2:同步失败', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status (sync_status), INDEX idx_created (created_at) );2. 创建数据库触发器-- INSERT 触发器 DELIMITER $$ CREATE TRIGGER after_article_insert AFTER INSERT ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', NEW.id, 'INSERT', 0); END$$ DELIMITER ; -- UPDATE 触发器 DELIMITER $$ CREATE TRIGGER after_article_update AFTER UPDATE ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', NEW.id, 'UPDATE', 0); END$$ DELIMITER ; -- DELETE 触发器 DELIMITER $$ CREATE TRIGGER after_article_delete AFTER DELETE ON articles FOR EACH ROW BEGIN INSERT INTO es_sync_queue (table_name, record_id, operation, sync_status) VALUES ('articles', OLD.id, 'DELETE', 0); END$$ DELIMITER ;3. PHP 消息队列处理器<?php /** * 消息队列同步处理器 */ class QueueSyncService { private $db; private $esClient; public function __construct() { $this->initDatabase(); $this->esClient = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } public function processQueue($batchSize = 100) { // 获取待同步的记录 $sql = "SELECT * FROM es_sync_queue WHERE sync_status = 0 ORDER BY id ASC LIMIT :limit FOR UPDATE"; $stmt = $this->db->prepare($sql); $stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(); foreach ($records as $record) { $this->processRecord($record); } return count($records); } private function processRecord($record) { try { switch ($record['operation']) { case 'INSERT': case 'UPDATE': $this->syncUpsert($record); break; case 'DELETE': $this->syncDelete($record); break; } // 标记为已同步 $this->markAsSynced($record['id']); } catch (Exception $e) { // 标记为同步失败 $this->markAsFailed($record['id'], $e->getMessage()); } } private function syncUpsert($record) { // 从数据库获取最新数据 $data = $this->getRecordData($record['table_name'], $record['record_id']); if ($data) { $params = [ 'index' => $record['table_name'], 'id' => $record['record_id'], 'body' => $this->transformData($data) ]; $this->esClient->index($params); } } private function syncDelete($record) { $params = [ 'index' => $record['table_name'], 'id' => $record['record_id'] ]; $this->esClient->delete($params); } private function getRecordData($tableName, $recordId) { $sql = "SELECT * FROM {$tableName} WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $recordId]); return $stmt->fetch(); } private function transformData($data) { // 根据表结构转换数据 if (isset($data['tags']) && !empty($data['tags'])) { $data['tags'] = explode(',', $data['tags']); } return $data; } private function markAsSynced($queueId) { $sql = "UPDATE es_sync_queue SET sync_status = 1 WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([':id' => $queueId]); } private function markAsFailed($queueId, $error) { $sql = "UPDATE es_sync_queue SET sync_status = 2, error_message = :error WHERE id = :id"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':id' => $queueId, ':error' => $error ]); } public function retryFailed($batchSize = 50) { $sql = "SELECT * FROM es_sync_queue WHERE sync_status = 2 ORDER BY id ASC LIMIT :limit"; $stmt = $this->db->prepare($sql); $stmt->bindValue(':limit', $batchSize, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(); $successCount = 0; foreach ($records as $record) { try { $this->processRecord($record); $successCount++; } catch (Exception $e) { // 记录重试失败 } } return $successCount; } } // 创建宝塔计划任务执行同步 $syncService = new QueueSyncService(); // 每次处理 100 条记录 $processed = $syncService->processQueue(100); echo "已处理 {$processed} 条同步记录"; // 重试失败的记录 $retried = $syncService->retryFailed(50); echo "重试成功 {$retried} 条记录"; ?>六、方案对比和选择建议方案选择指南场景推荐方案理由新建项目应用层双写代码可控，实时性最好已有项目，可修改代码应用层双写 + 全量初始化渐进式改造MySQL 数据库，要求高实时性Canal基于 Binlog，对业务无侵入简单同步，可接受分钟级延迟Logstash JDBC配置简单，稳定可靠数据库层面同步触发器 + 消息队列强一致性，对应用透明宝塔环境推荐配置对于大多数宝塔用户，我推荐：首选：应用层双写方案备选：Logstash JDBC 方案高级需求：Canal 方案性能优化建议// 批量操作优化 public function bulkSync($records) { $params = ['body' => []]; foreach ($records as $record) { $params['body'][] = [ 'index' => [ '_index' => 'articles', '_id' => $record['id'] ] ]; $params['body'][] = $this->transformData($record); } return $this->esClient->bulk($params); }选择最适合你项目需求和团队技术能力的方案。对于宝塔环境，应用层双写和 Logstash 方案相对更容易实施和维护。
IK 分词器安装指南是的，IK 分词器需要单独安装。 Elasticsearch 默认只提供标准分词器，对于中文分词效果不佳，IK 分词器是专门针对中文优化的分词插件。一、IK 分词器安装方法方法 1：手动安装（推荐）步骤 1：下载对应版本的 IK 分词器# 进入 Elasticsearch 插件目录 cd /www/server/elasticsearch/plugins # 创建 ik 目录 mkdir ik cd ik # 下载对应版本的 IK 分词器 # 注意：版本号必须与 Elasticsearch 版本一致！ # 查看 ES 版本：curl -X GET "localhost:9200/" wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip # 解压 unzip elasticsearch-analysis-ik-7.17.0.zip # 删除 zip 文件 rm -f elasticsearch-analysis-ik-7.17.0.zip # 返回插件目录上级，确保目录结构正确 cd .. ls -la ik/步骤 2：重启 Elasticsearch# 通过宝塔面板重启 # 或者使用命令 systemctl restart elasticsearch步骤 3：验证安装# 验证 IK 分词器是否安装成功 curl -X GET "localhost:9200/_cat/plugins?v" # 测试分词效果 curl -X POST "localhost:9200/_analyze" -H 'Content-Type: application/json' -d' { "analyzer": "ik_smart", "text": "中华人民共和国" }'正常响应：{ "tokens" : [ { "token" : "中华人民共和国", "start_offset" : 0, "end_offset" : 7, "type" : "CN_WORD", "position" : 0 } ] }方法 2：使用 Elasticsearch 插件命令安装# 进入 Elasticsearch 安装目录 cd /www/server/elasticsearch # 使用插件管理器安装 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip # 重启 Elasticsearch systemctl restart elasticsearch二、宝塔面板特有的安装方法通过宝塔终端操作打开宝塔面板 → 终端执行安装命令：# 切换到 elasticsearch 用户（宝塔环境可能需要） sudo -u elasticsearch /bin/bash # 进入 ES 插件目录 cd /www/server/elasticsearch/plugins # 下载并安装 IK 分词器 wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip unzip elasticsearch-analysis-ik-7.17.0.zip -d ik rm -f elasticsearch-analysis-ik-7.17.0.zip # 退出 elasticsearch 用户 exit # 重启 Elasticsearch 服务 systemctl restart elasticsearch宝塔文件管理器操作下载 IK 分词器：访问 IK 分词器 Releases下载对应版本的 ZIP 文件通过宝塔文件管理器上传：进入 /www/server/elasticsearch/plugins/创建 ik 文件夹上传 ZIP 文件到 ik 文件夹解压并删除 ZIP 文件重启服务：宝塔面板 → 软件商店 → Elasticsearch → 重启三、IK 分词器版本对应关系Elasticsearch 版本IK 分词器版本7.17.x7.17.07.16.x7.16.07.15.x7.15.07.14.x7.14.07.13.x7.13.07.12.x7.12.07.11.x7.11.07.10.x7.10.07.9.x7.9.0重要：必须确保版本一致，否则 Elasticsearch 无法启动！四、验证安装的完整脚本<?php /** * IK 分词器验证脚本 * 保存为 test_ik.php 并在浏览器访问 */ require '/www/wwwroot/your-site/vendor/autoload.php'; use Elasticsearch\ClientBuilder; class IKValidator { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 检查 IK 分词器是否安装 */ public function checkIKInstallation() { echo " IK 分词器安装验证 "; // 1. 检查插件列表 echo " 1. 已安装插件列表 "; try { $plugins = $this->client->cat()->plugins(['v' => true]); echo " "; foreach ($plugins as $plugin) { echo $plugin . "\n"; if (strpos($plugin, 'analysis-ik') !== false) { echo "✅ IK 分词器已安装\n"; } } echo " "; } catch (Exception $e) { echo "❌ 获取插件列表失败: " . $e->getMessage() . "\n"; } // 2. 测试 IK 分词器功能 echo " 2. IK 分词器功能测试 "; $testTexts = [ "中华人民共和国", "北京大学生活动中心", " Elasticsearch中文分词器", "我喜欢吃苹果" ]; foreach ($testTexts as $text) { $this->testAnalyzer($text); } } /** * 测试分词器 */ private function testAnalyzer($text) { echo " 测试文本: \"{$text}\" "; // 测试 ik_smart（智能分词） echo "ik_smart 分词: "; $this->analyzeText($text, 'ik_smart'); // 测试 ik_max_word（细粒度分词） echo "ik_max_word 分词: "; $this->analyzeText($text, 'ik_max_word'); // 测试 standard（标准分词器对比） echo "standard 分词: "; $this->analyzeText($text, 'standard'); echo " "; } private function analyzeText($text, $analyzer) { try { $params = [ 'body' => [ 'analyzer' => $analyzer, 'text' => $text ] ]; $response = $this->client->indices()->analyze($params); echo "分析器: {$analyzer} "; echo "分词结果: "; foreach ($response['tokens'] as $token) { echo "" . $token['token'] . " "; } echo " "; } catch (Exception $e) { echo "❌ 分析器 {$analyzer} 不可用: " . $e->getMessage() . " "; } } /** * 创建测试索引验证 IK 分词器 */ public function createTestIndex() { $indexName = 'test_ik_index'; // 删除已存在的测试索引 if ($this->client->indices()->exists(['index' => $indexName])) { $this->client->indices()->delete(['index' => $indexName]); } // 创建使用 IK 分词器的索引 $params = [ 'index' => $indexName, 'body' => [ 'settings' => [ 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ], 'ik_max_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_max_word' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer', 'search_analyzer' => 'ik_smart_analyzer' ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer', 'search_analyzer' => 'ik_smart_analyzer' ] ] ] ] ]; try { $response = $this->client->indices()->create($params); echo " 3. 测试索引创建 "; echo "✅ 测试索引创建成功，IK 分词器配置正常 "; // 测试索引文档 $doc = [ 'title' => 'Elasticsearch中文分词器测试', 'content' => '这是一个关于IK分词器的功能测试文档，用于验证中文分词效果。' ]; $this->client->index([ 'index' => $indexName, 'body' => $doc ]); echo "✅ 测试文档索引成功 "; } catch (Exception $e) { echo " 3. 测试索引创建 "; echo "❌ 创建测试索引失败: " . $e->getMessage() . " "; } } } // 执行验证 $validator = new IKValidator(); $validator->checkIKInstallation(); $validator->createTestIndex(); echo " 验证完成 "; echo "如果看到 IK 分词器正常工作，说明安装成功！"; ?>五、IK 分词器使用示例PHP 中使用 IK 分词器<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class IKSearchExample { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 创建使用 IK 分词器的索引 */ public createNewsIndex() { $params = [ 'index' => 'news', 'body' => [ 'settings' => [ 'number_of_shards' => 1, 'number_of_replicas' => 0, 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ], 'ik_max_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_max_word' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer', // 索引时使用智能分词 'search_analyzer' => 'ik_smart_analyzer' // 搜索时使用智能分词 ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer', // 索引时使用最大分词 'search_analyzer' => 'ik_smart_analyzer' // 搜索时使用智能分词 ], 'tags' => [ 'type' => 'text', 'analyzer' => 'ik_max_analyzer' ], 'author' => [ 'type' => 'keyword' // 关键字类型，不分词 ] ] ] ] ]; return $this->client->indices()->create($params); } /** * 使用 IK 分词器进行搜索 */ public function searchWithIK($keyword) { $params = [ 'index' => 'news', 'body' => [ 'query' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^3', 'content^2', 'tags^2'], 'type' => 'best_fields' ] ], 'highlight' => [ 'fields' => [ 'title' => [ 'pre_tags' => [''], 'post_tags' => [''] ], 'content' => [ 'pre_tags' => [''], 'post_tags' => [''], 'fragment_size' => 150, 'number_of_fragments' => 3 ] ] ] ] ]; return $this->client->search($params); } } // 使用示例 $search = new IKSearchExample(); // 创建索引 $search->createNewsIndex(); // 搜索示例 $result = $search->searchWithIK('北京大学生'); print_r($result['hits']['hits']); ?>六、常见问题解决问题 1：版本不匹配症状：Elasticsearch 启动失败解决：# 检查错误日志 tail -f /www/server/elasticsearch/logs/elasticsearch.log # 如果看到版本不匹配错误，下载正确版本 cd /www/server/elasticsearch/plugins/ik rm -rf * wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v[正确版本]/elasticsearch-analysis-ik-[正确版本].zip unzip elasticsearch-analysis-ik-[正确版本].zip问题 2：权限问题症状：无法创建插件目录或文件解决：# 确保 elasticsearch 用户有权限 chown -R elasticsearch:elasticsearch /www/server/elasticsearch/plugins/ik chmod -R 755 /www/server/elasticsearch/plugins/ik问题 3：IK 分词器不生效症状：安装成功但分词效果不对解决：# 重启 Elasticsearch systemctl restart elasticsearch # 验证插件是否加载 curl -X GET "localhost:9200/_cat/plugins?v" # 检查索引配置是否正确使用了 IK 分词器七、IK 分词器扩展词典（可选）如果需要自定义词典，可以编辑 IK 配置文件：# 进入 IK 配置目录 cd /www/server/elasticsearch/plugins/ik/config # 编辑主词典 vi IKAnalyzer.cfg.xml # 添加自定义词典 # 在 <entry key="ext_dict">custom.dic</entry> # 创建自定义词典文件 vi custom.dic # 添加自定义词汇，每行一个词 区块链 人工智能 大数据记得安装完成后一定要重启 Elasticsearch 服务！这样就完成了 IK 分词器的安装和配置，现在可以在 PHP 代码中使用更优秀的中文搜索功能了。
【Elasticsearch】Elasticsearch 安装步骤与常见问题解决
1. 环境准备# 更新系统 sudo apt update && sudo apt upgrade -y # 安装 Java (Elasticsearch 需要 Java 环境) sudo apt install openjdk-11-jdk -y # 验证 Java 安装 java -version2. 安装 Elasticsearch方法一：使用 APT 仓库安装（推荐）# 导入 Elasticsearch GPG 密钥 wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add - # 添加 Elasticsearch 仓库 echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list # 更新并安装 sudo apt update sudo apt install elasticsearch方法二：手动下载安装# 下载 Elasticsearch wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.0-amd64.deb # 安装 sudo dpkg -i elasticsearch-7.17.0-amd64.deb3. 配置 Elasticsearch# 编辑配置文件 sudo vi /etc/elasticsearch/elasticsearch.yml主要配置项：# 集群名称 cluster.name: my-elasticsearch-cluster # 节点名称 node.name: node-1 # 数据存储路径 path.data: /var/lib/elasticsearch # 日志存储路径 path.logs: /var/log/elasticsearch # 网络绑定地址 network.host: 0.0.0.0 # HTTP 端口 http.port: 9200 # 集群初始主节点 cluster.initial_master_nodes: ["node-1"] # 允许跨域访问（用于开发） http.cors.enabled: true http.cors.allow-origin: "*"4. 启动和管理 Elasticsearch# 启动 Elasticsearch 服务 sudo systemctl start elasticsearch # 设置开机自启 sudo systemctl enable elasticsearch # 查看服务状态 sudo systemctl status elasticsearch # 重启服务 sudo systemctl restart elasticsearch # 停止服务 sudo systemctl stop elasticsearch5. 验证安装# 检查 Elasticsearch 是否运行 curl -X GET "localhost:9200/" # 查看集群健康状态 curl -X GET "localhost:9200/_cluster/health" # 查看节点信息 curl -X GET "localhost:9200/_nodes"正常响应示例：{ "name" : "node-1", "cluster_name" : "my-elasticsearch-cluster", "cluster_uuid" : "abcd1234", "version" : { "number" : "7.17.0", "build_flavor" : "default", "build_type" : "deb", "build_hash" : "abcdef123456", "build_date" : "2022-01-01T00:00:00.000Z", "build_snapshot" : false, "lucene_version" : "8.11.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" }6. 安全配置（可选）# 安装 Elasticsearch 安全插件 sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install repository-s3 # 配置防火墙 sudo ufw allow 9200 sudo ufw allow 9300二、常见问题解决1. 内存不足问题# 编辑 JVM 配置 sudo vi /etc/elasticsearch/jvm.options # 调整内存设置（根据服务器内存调整） -Xms1g -Xmx1g2. 无法连接问题# 检查 Elasticsearch 是否运行 sudo systemctl status elasticsearch # 检查端口是否监听 netstat -tulpn | grep 9200 # 检查防火墙设置 sudo ufw status sudo ufw allow 92003. 索引性能优化// 在 PHP 中使用批量操作提高性能 $es->bulkInsert('large_index', $largeDataArray); // 使用滚动查询处理大量数据 $params = [ 'index' => 'large_index', 'scroll' => '1m', 'size' => 1000, 'body' => [ 'query' => [ 'match_all' => new \stdClass() ] ] ];
1. 使用 Composer 安装# 安装 Composer（如果未安装） curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer # 创建项目目录 mkdir elasticsearch-php-demo cd elasticsearch-php-demo # 初始化 Composer composer init # 安装 Elasticsearch PHP 客户端 composer require elasticsearch/elasticsearch2. 手动安装（不使用 Composer）<?php // 手动引入 Elasticsearch 客户端 require_once 'path/to/elasticsearch-php/autoload.php';示例 1：基础连接和索引操作<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class ElasticsearchDemo { private $client; public function __construct() { // 创建 Elasticsearch 客户端 $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) // ES 服务器地址 ->setRetries(2) // 重试次数 ->build(); // 检查连接 if (!$this->checkConnection()) { throw new Exception("无法连接到 Elasticsearch"); } } /** * 检查 Elasticsearch 连接 */ private function checkConnection() { try { return $this->client->ping(); } catch (Exception $e) { echo "连接失败: " . $e->getMessage() . "\n"; return false; } } /** * 创建索引 */ public function createIndex($indexName) { $params = [ 'index' => $indexName, 'body' => [ 'settings' => [ 'number_of_shards' => 2, 'number_of_replicas' => 1 ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'standard' ], 'content' => [ 'type' => 'text', 'analyzer' => 'standard' ], 'author' => [ 'type' => 'keyword' ], 'publish_date' => [ 'type' => 'date' ], 'views' => [ 'type' => 'integer' ] ] ] ] ]; try { $response = $this->client->indices()->create($params); echo "索引 {$indexName} 创建成功\n"; return $response; } catch (Exception $e) { echo "创建索引失败: " . $e->getMessage() . "\n"; return false; } } /** * 删除索引 */ public function deleteIndex($indexName) { $params = ['index' => $indexName]; try { // 检查索引是否存在 if ($this->client->indices()->exists($params)) { $response = $this->client->indices()->delete($params); echo "索引 {$indexName} 删除成功\n"; return $response; } else { echo "索引 {$indexName} 不存在\n"; return false; } } catch (Exception $e) { echo "删除索引失败: " . $e->getMessage() . "\n"; return false; } } /** * 添加文档 */ public function addDocument($indexName, $document, $id = null) { $params = [ 'index' => $indexName, 'body' => $document ]; // 如果指定了 ID if ($id !== null) { $params['id'] = $id; } try { $response = $this->client->index($params); echo "文档添加成功，ID: " . $response['_id'] . "\n"; return $response; } catch (Exception $e) { echo "添加文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 获取文档 */ public function getDocument($indexName, $id) { $params = [ 'index' => $indexName, 'id' => $id ]; try { $response = $this->client->get($params); return $response; } catch (Exception $e) { echo "获取文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 更新文档 */ public function updateDocument($indexName, $id, $updateData) { $params = [ 'index' => $indexName, 'id' => $id, 'body' => [ 'doc' => $updateData ] ]; try { $response = $this->client->update($params); echo "文档更新成功\n"; return $response; } catch (Exception $e) { echo "更新文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 删除文档 */ public function deleteDocument($indexName, $id) { $params = [ 'index' => $indexName, 'id' => $id ]; try { $response = $this->client->delete($params); echo "文档删除成功\n"; return $response; } catch (Exception $e) { echo "删除文档失败: " . $e->getMessage() . "\n"; return false; } } /** * 搜索文档 */ public function search($indexName, $query) { $params = [ 'index' => $indexName, 'body' => [ 'query' => $query ] ]; try { $response = $this->client->search($params); return $response; } catch (Exception $e) { echo "搜索失败: " . $e->getMessage() . "\n"; return false; } } /** * 批量插入文档 */ public function bulkInsert($indexName, $documents) { $params = ['body' => []]; foreach ($documents as $document) { $params['body'][] = [ 'index' => [ '_index' => $indexName ] ]; $params['body'][] = $document; } try { $response = $this->client->bulk($params); echo "批量插入完成，处理了 " . count($documents) . " 个文档\n"; return $response; } catch (Exception $e) { echo "批量插入失败: " . $e->getMessage() . "\n"; return false; } } } // 使用示例 try { $es = new ElasticsearchDemo(); // 创建索引 $es->createIndex('blog'); // 添加单个文档 $document = [ 'title' => 'Elasticsearch PHP 客户端使用指南', 'content' => '这是一篇关于如何使用 PHP 操作 Elasticsearch 的详细教程。', 'author' => '张三', 'publish_date' => '2023-10-01', 'views' => 100 ]; $es->addDocument('blog', $document, '1'); // 批量插入文档 $documents = [ [ 'title' => 'PHP 开发技巧', 'content' => '分享一些 PHP 开发中的实用技巧和最佳实践。', 'author' => '李四', 'publish_date' => '2023-10-02', 'views' => 150 ], [ 'title' => 'Linux 系统管理', 'content' => 'Linux 系统管理的基本命令和操作指南。', 'author' => '王五', 'publish_date' => '2023-10-03', 'views' => 200 ] ]; $es->bulkInsert('blog', $documents); // 搜索文档 $query = [ 'match' => [ 'title' => 'PHP' ] ]; $searchResult = $es->search('blog', $query); echo "搜索到 " . $searchResult['hits']['total']['value'] . " 个结果:\n"; foreach ($searchResult['hits']['hits'] as $hit) { echo "- " . $hit['_source']['title'] . " (作者: " . $hit['_source']['author'] . ")\n"; } } catch (Exception $e) { echo "错误: " . $e->getMessage() . "\n"; } ?>示例 2：高级搜索功能<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class AdvancedSearchDemo { private $client; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); } /** * 多字段搜索 */ public function multiFieldSearch($indexName, $keyword) { $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^2', 'content', 'author'], // title 字段权重为 2 'type' => 'best_fields' ] ], 'highlight' => [ 'fields' => [ 'title' => new \stdClass(), 'content' => new \stdClass() ] ] ] ]; return $this->client->search($params); } /** * 布尔搜索 */ public function boolSearch($indexName, $must = [], $should = [], $must_not = []) { $boolQuery = []; if (!empty($must)) { $boolQuery['must'] = $must; } if (!empty($should)) { $boolQuery['should'] = $should; } if (!empty($must_not)) { $boolQuery['must_not'] = $must_not; } $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'bool' => $boolQuery ] ] ]; return $this->client->search($params); } /** * 范围查询 */ public function rangeSearch($indexName, $field, $gte = null, $lte = null) { $range = []; if ($gte !== null) $range['gte'] = $gte; if ($lte !== null) $range['lte'] = $lte; $params = [ 'index' => $indexName, 'body' => [ 'query' => [ 'range' => [ $field => $range ] ] ] ]; return $this->client->search($params); } /** * 聚合查询 */ public function aggregationSearch($indexName) { $params = [ 'index' => $indexName, 'body' => [ 'size' => 0, // 不需要返回具体文档 'aggs' => [ 'authors' => [ 'terms' => [ 'field' => 'author.keyword', 'size' => 10 ] ], 'total_views' => [ 'sum' => [ 'field' => 'views' ] ], 'avg_views' => [ 'avg' => [ 'field' => 'views' ] ] ] ] ]; return $this->client->search($params); } } // 使用示例 $searchDemo = new AdvancedSearchDemo(); // 多字段搜索 $result = $searchDemo->multiFieldSearch('blog', 'PHP'); print_r($result['hits']); // 布尔搜索 $boolResult = $searchDemo->boolSearch('blog', [ // must 条件 ['match' => ['author' => '张三']], ['range' => ['views' => ['gte' => 50]]] ], [ // should 条件 ['match' => ['title' => '教程']], ['match' => ['content' => '指南']] ] ); // 聚合查询 $aggResult = $searchDemo->aggregationSearch('blog'); echo "总浏览量: " . $aggResult['aggregations']['total_views']['value'] . "\n"; echo "平均浏览量: " . $aggResult['aggregations']['avg_views']['value'] . "\n"; ?>示例 3：完整的博客搜索应用<?php require 'vendor/autoload.php'; use Elasticsearch\ClientBuilder; class BlogSearch { private $client; private $indexName = 'blog_posts'; public function __construct() { $this->client = ClientBuilder::create() ->setHosts(['localhost:9200']) ->build(); $this->createBlogIndex(); } private function createBlogIndex() { // 检查索引是否存在，不存在则创建 if (!$this->client->indices()->exists(['index' => $this->indexName])) { $params = [ 'index' => $this->indexName, 'body' => [ 'settings' => [ 'number_of_shards' => 1, 'number_of_replicas' => 1, 'analysis' => [ 'analyzer' => [ 'ik_smart_analyzer' => [ 'type' => 'custom', 'tokenizer' => 'ik_smart' ] ] ] ], 'mappings' => [ 'properties' => [ 'title' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer' ], 'content' => [ 'type' => 'text', 'analyzer' => 'ik_smart_analyzer' ], 'author' => [ 'type' => 'keyword' ], 'tags' => [ 'type' => 'keyword' ], 'category' => [ 'type' => 'keyword' ], 'publish_date' => [ 'type' => 'date' ], 'views' => [ 'type' => 'integer' ], 'status' => [ 'type' => 'keyword' ] ] ] ] ]; $this->client->indices()->create($params); } } public function addBlogPost($post) { $params = [ 'index' => $this->indexName, 'body' => $post ]; return $this->client->index($params); } public function searchPosts($keyword, $filters = [], $page = 1, $pageSize = 10) { $from = ($page - 1) * $pageSize; $query = [ 'bool' => [ 'must' => [], 'filter' => [] ] ]; // 关键词搜索 if (!empty($keyword)) { $query['bool']['must'][] = [ 'multi_match' => [ 'query' => $keyword, 'fields' => ['title^3', 'content^2', 'tags^2'], 'type' => 'best_fields' ] ]; } // 过滤器 foreach ($filters as $field => $value) { if (!empty($value)) { $query['bool']['filter'][] = [ 'term' => [$field => $value] ]; } } $params = [ 'index' => $this->indexName, 'body' => [ 'from' => $from, 'size' => $pageSize, 'query' => $query, 'sort' => [ ['publish_date' => ['order' => 'desc']], ['views' => ['order' => 'desc']] ], 'highlight' => [ 'fields' => [ 'title' => [ 'number_of_fragments' => 0 ], 'content' => [ 'fragment_size' => 150, 'number_of_fragments' => 3 ] ] ] ] ]; $response = $this->client->search($params); return [ 'total' => $response['hits']['total']['value'], 'posts' => $response['hits']['hits'], 'took' => $response['took'] ]; } public function getRelatedPosts($postId, $limit = 5) { // 先获取当前文章 $currentPost = $this->client->get([ 'index' => $this->indexName, 'id' => $postId ]); $tags = $currentPost['_source']['tags'] ?? []; if (empty($tags)) { return []; } $params = [ 'index' => $this->indexName, 'body' => [ 'size' => $limit, 'query' => [ 'bool' => [ 'must' => [ 'terms' => [ 'tags' => $tags ] ], 'must_not' => [ 'term' => [ '_id' => $postId ] ] ] ], 'sort' => [ ['views' => ['order' => 'desc']], ['publish_date' => ['order' => 'desc']] ] ] ]; $response = $this->client->search($params); return $response['hits']['hits']; } } // 使用示例 $blogSearch = new BlogSearch(); // 添加示例文章 $samplePosts = [ [ 'title' => 'PHP 8 新特性详解', 'content' => 'PHP 8 引入了许多令人兴奋的新特性，如命名参数、联合类型、属性等...', 'author' => '技术达人', 'tags' => ['PHP', '编程', '后端'], 'category' => '编程语言', 'publish_date' => '2023-09-15', 'views' => 245, 'status' => 'published' ], [ 'title' => 'Elasticsearch 入门教程', 'content' => '学习如何使用 Elasticsearch 进行全文搜索和数据聚合...', 'author' => '搜索专家', 'tags' => ['Elasticsearch', '搜索', '数据库'], 'category' => '数据库', 'publish_date' => '2023-09-20', 'views' => 189, 'status' => 'published' ] ]; foreach ($samplePosts as $post) { $blogSearch->addBlogPost($post); } // 搜索文章 $result = $blogSearch->searchPosts('PHP 教程', ['category' => '编程语言'], 1, 10); echo "找到 {$result['total']} 篇文章，耗时 {$result['took']} 毫秒\n"; foreach ($result['posts'] as $post) { echo "标题: " . $post['_source']['title'] . "\n"; if (isset($post['highlight'])) { echo "高亮: " . implode('...', $post['highlight']['content'] ?? []) . "\n"; } echo "---\n"; } ?>