1、在es里面先添加好规则
{ "mappings":{ "properties":{ "id":{ "type":"keyword" }, "title":{ "type":"text", "analyzer":"ik_max_word", "copy_to":"all" }, "content":{ "type":"text", "analyzer":"ik_max_word", "copy_to":"all" }, "framework_id":{ "type":"keyword" }, "framework_name":{ "type":"text", "analyzer":"ik_max_word", "copy_to":"all" }, "published": { "type": "boolean" }, "user_id": { "type": "keyword" }, "created": { "type": "date" }, "modified": { "type": "date" }, "all":{ "type":"text", "analyzer":"ik_max_word" } } } }
2、安装ES插件,在项目根目录下执行
composer require cakephp/elastic-search
3、加载插件,在src/Application.php文件的bootstrap()方法的parent::bootstrap();下面添加
$this->addPlugin('Cake/ElasticSearch');
4、配置ES帐号密码config/app_local.php的Datasources下,添加下面这段话,帐号密码端口自行替换
'elastic' => [ 'className' => 'Cake\ElasticSearch\Datasource\Connection', 'driver' => 'Cake\ElasticSearch\Datasource\Connection', 'hosts' => ['http://elastic:A3MZa=H5sjtyIyE+7aSr@localhost:9201'], ],
5、在对应的模型层里面,添加afterSave和afterDelete代码,对应字段自行替换
public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options){ try { // 连接ElasticSearch $notesIndex = $this->getElasticsearchIndex(); // 定义数据 $data = [ 'id' => (string)$entity ->get('id'), 'title' => $entity ->get('title'), 'content' => $entity ->get('content'), 'framework_id' => $entity ->get('framework_id'), 'published' => $entity ->get('published'), 'user_id' => $entity ->get('user_id'), 'created' => $entity ->get('created'), 'modified' => $entity ->get('modified'), ]; if (!$entity->has('framework') && $entity->framework_id) { // 通过 loadInto 方法加载关联 $entity = $this->loadInto($entity, ['Frameworks']); } if ($entity->has('framework') && !empty($entity->framework)) { $data['framework_name'] = $entity->framework->name; } else if ($entity->framework_id) { // 如果关联加载失败,可以直接查询数据库获取框架名 $framework = $this->Frameworks->find() ->select(['name']) ->where(['id' => $entity->framework_id]) ->first(); if ($framework) { $data['framework_name'] = $framework->name; } } // 保存数据 $document = $notesIndex->newEntity($data); if ($notesIndex->save($document)) { Log::info("Note ID {$entity->id} 同步到 Elasticsearch 成功"); } else { Log::error("Note ID {$entity->id} 同步到 Elasticsearch 失败"); } } catch (Exception $e) { Log::error("Elasticsearch 同步异常,Note ID: {$entity->id}, 错误: " . $e->getMessage()); } } /** * 在删除笔记后触发,用于从 Elasticsearch 删除文档 * * @param \Cake\Event\EventInterface $event * @param \Cake\Datasource\EntityInterface $entity * @param \ArrayObject $options * @return void */ public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options) { try { // 连接ElasticSearch $notesIndex = $this->getElasticsearchIndex(); try { $document = $notesIndex->get((string)$entity->id); if ($notesIndex->delete($document)) { Log::info("ID {$entity->id} 删除成功"); } else { Log::warning("⚠️ 删除操作返回 false,Note ID: {$entity->id}"); } } catch (\Cake\Datasource\Exception\RecordNotFoundException $e) { // 文档不存在,无需删除 Log::debug("Note ID {$entity->id} 在 Elasticsearch 索引中不存在,无需删除"); } } catch (MissingDocumentException $e) { Log::debug("Note ID {$entity->id} 在 Elasticsearch 索引中不存在,无需删除"); } catch (Exception $e) { // 捕获其他所有可能的异常(如连接失败、语法错误等) Log::error("🔥 从 Elasticsearch 删除文档时发生异常,Note ID: {$entity->id}"); Log::error("错误信息: " . $e->getMessage()); } } private function getElasticsearchIndex() { // 单例模式,避免重复创建连接 static $notesIndex = null; if ($notesIndex === null) { $connection = ConnectionManager::get('elastic'); $notesIndex = new Index([ 'connection' => $connection, 'name' => 'notes' ]); } return $notesIndex; }
6、关于搜索,目前cake官方插件where条件只存在and不能用or。所以自己写了一下。还没优化,先凑活用。
public function search() { $this->request->allowMethod(['get']); $keyword = $this->request->getQuery('q'); $framework_id = $this->request->getQuery('framework_id'); $page = $this->request->getQuery('page', 1); $limit = $this->request->getQuery('limit', 10); // 检查Datasources配置 $requestUrl = ConnectionManager::get('elastic')->config()['hosts'][0] . '/notes/_search'; $from = ($page - 1) * $limit; $query = [ 'query' => [ 'bool' => [ 'must' => [ ['term' => ['published' => true]] ] ] ], 'from' => $from, 'size' => $limit, 'sort' => [ [ 'created' => [ 'order' => 'desc' ] ] ] ]; // 添加搜索条件 if (!empty($framework_id)) { $query['query']['bool']['must'][] = ['term' => ['framework_id' => $framework_id]]; } if (!empty($keyword)) { $query['query']['bool']['must'][] = ['match' => ['all' => $keyword]]; } // echo "请求体: " . json_encode($query, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "<br>"; // 解析URL获取认证信息 $parsedUrl = parse_url($requestUrl); // 重新构建不带认证信息的URL $cleanUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'] . ':' . $parsedUrl['port'] . $parsedUrl['path']; // 发送POST请求(CakePHP HttpClient的GET请求不支持请求体) $http = new Client(); $options = [ 'type' => 'json', 'headers' => ['Content-Type' => 'application/json'] ]; // 添加认证头部 if (isset($parsedUrl['user']) && isset($parsedUrl['pass'])) { $auth = base64_encode($parsedUrl['user'] . ':' . $parsedUrl['pass']); $options['headers']['Authorization'] = 'Basic ' . $auth; } // 发送POST请求 $response = $http->post($cleanUrl, json_encode($query), $options); if ($response->isOk()) { $data = json_decode($response->getStringBody(),true); // pr($data); // 处理响应数据 $notes = []; $total = $data['hits']['total']['value'] ?? 0; if (isset($data['hits']['hits'])) { foreach ($data['hits']['hits'] as $hit) { $note = $hit['_source']; $note['id'] = $hit['_id']; $notes[] = $note; } } // 创建分页数据 $paging = [ 'count' => $total, 'current' => count($notes), 'perPage' => $limit, 'page' => $page, 'pages' => $total > 0 ? ceil($total / $limit) : 1, 'prevPage' => $page > 1, 'nextPage' => ($page * $limit) < $total, 'start' => $from + 1, 'end' => min($from + $limit, $total) ]; // 7. 设置视图变量 $this->set(compact('notes', 'paging','page','limit','total')); $this->viewBuilder()->setOption('serialize', ['notes', 'paging']); } }
cakephp5 连ElasticSearch
| Title | cakephp5 连ElasticSearch |
|---|---|
| Framework | CakePHP5 |
| User | wy8817399@vip.qq.com |
| Id | 20 |
| Created | 1/13/26, 8:34 AM |
| Modified | 1/22/26, 10:42 AM |
| Published | Yes |
Content