Skip to content

整合 Elasticsearch ​

从连接示例到业务检索 ​

TioAdminElasticsearchConfig 不会自动把数据库业务表同步到 Elasticsearch。业务服务需要明确索引字段、写入时机、数据可见性与失败恢复策略:

  • 只索引允许公开搜索的字段,避免同步联系方式、密文或私有文件键。
  • 在最外层数据库事务提交后同步,避免事务回滚后留下可检索文档;嵌套 Service 调用不应提前写索引。
  • 搜索命中后仍由数据库检查审核、下架、删除、过期及作者状态,再进行业务过滤和分页。不能直接相信可能滞后的索引状态。
  • 索引故障不应回滚已经提交的业务写入;使用持久化同步任务,或在首版数据规模下使用分批周期校对来修复漏同步。
  • 搜索引擎不可用时可回退到数据库查询,但需要明确两种检索的匹配语义差异,并在接口联调信息中标明实际执行路径。
  • 不要只取搜索引擎前一页再做权限过滤,否则总数和分页可能错误。应设计完整的候选集过滤或能够正确组合权限条件的索引查询。

验证时使用独立测试索引,覆盖发布、编辑后重审、下架、删除、用户封禁、资源过期、索引失败与恢复。多实例环境下还应验证并发同步的最终一致性。

TioAdminElasticsearchConfig 读取连接配置,创建 RestHighLevelClient,注册到 java-db 的 Elastic 工具类,并在应用关闭时释放客户端。业务代码直接调用 Elastic,不必重复创建客户端或连接池。

1. 添加客户端依赖 ​

在已有 tio-boot-admin 工程的 pom.xml 中加入客户端依赖;已经声明时不重复添加:

xml
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.10.0</version>
</dependency>

这里使用框架配置类依赖的 High Level REST Client,不是新的 Java API Client。客户端需要与服务端兼容;不要直接替换成另一套客户端依赖后继续使用原有类型。上游 provided 依赖不会自动进入应用运行时,因此应检查最终应用的依赖和打包结果。

运行时依赖与按需初始化 ​

TioAdminElasticsearchConfig 使用的客户端依赖在上游声明为 provided,不会自动传递到业务应用。使用该配置类时,应用应显式添加上面的客户端依赖,并使用默认的 compile 作用域,让客户端及其传递依赖进入运行时和可执行 JAR。

例如,org/apache/http/auth/Credentials 来自 Apache HttpClient 的 httpclient 构件。若启动时在创建 Elasticsearch 配置类的位置出现该类的 NoClassDefFoundError,先检查应用是否引入了完整的 Elasticsearch 客户端依赖链。只补一个 httpclient 可能仍缺少其他客户端类型;httpclient5 的包名也不能替代这里使用的 org.apache.http 类型。

可在应用目录检查实际解析结果:

shell
mvn dependency:tree "-Dincludes=org.elasticsearch.client:*,org.apache.httpcomponents:*"

使用 Elasticsearch 时,结果应包含 High Level REST Client、REST Client 及 HTTP 客户端相关依赖。修改依赖后重新加载 IDE 的 Maven 工程,并重新打包,确认启动使用的是更新后的类路径或 JAR。

不使用 Elasticsearch 时,省略 TioAdminElasticsearchConfig 的导入和初始化调用即可。仅删除连接地址不足以代替这一步:JVM 可能在进入 config() 方法的地址检查之前,就需要解析配置类引用的客户端类型。因此,“地址为空时返回”以配置类能够正常加载为前提。

2. 配置连接地址与认证 ​

在当前环境的配置文件,例如 app-dev.properties 中添加:

properties
elasticsearch.rest.urls=http://127.0.0.1:9200

开启用户名和密码认证时,通过本地秘密配置补充:

properties
elasticsearch.rest.username=replace-with-username
elasticsearch.rest.password=replace-with-password

没有启用认证时不配置用户名、密码,不在源码和文档中保存真实凭据。

配置项内置配置的处理方式
elasticsearch.rest.urls地址为空或未设置时跳过初始化;填写包含协议、主机和端口的完整地址
elasticsearch.rest.username非空时设置 Basic Authentication
elasticsearch.rest.password与用户名一起使用的密码

虽然属性名为 urls,当前配置调用 HttpHost.create(urls) 并只创建一个 HttpHost,不能填写逗号分隔的多个地址。只配置密码、不配置用户名时不会启用认证。

当前内置配置没有读取多节点列表、API Key、超时或自定义证书参数。HTTPS 使用客户端默认的证书校验;如果需要自签名 CA、多节点或其他认证方式,应单独扩展客户端配置,不能用不存在的属性假定这些能力已经开启。

3. 在启动配置中初始化 ​

在已有 AdminAppConfig 顶部导入:

java
import nexus.io.tio.boot.admin.config.TioAdminElasticsearchConfig;

在其 config() 方法中调用一次,并放在业务使用 Elasticsearch 之前:

java
new TioAdminElasticsearchConfig().config();

如果已有该调用,不要再重复创建配置或客户端。内置配置依次完成:

  1. 读取地址和认证参数;没有有效地址时返回。
  2. 创建 RestHighLevelClient,并通过 Elastic.setClient(client) 注册。
  3. 注册关闭钩子,应用退出时关闭客户端。
  4. 使用 client.ping(RequestOptions.DEFAULT) 尝试检查连接。

启动过程会记录 ping 结果,IOException 会被捕获。因此应用启动成功不代表 Elasticsearch 一定可用,更不代表当前账号拥有索引读写权限。应继续验证实际文档操作。

如果应用编译提示找不到 TioAdminElasticsearchConfig,确认应用依赖的框架构建产物已经包含该类;本地修改框架源码后,应先安装框架构建产物,再编译应用。

4. 最小 Handler:写入、读取、删除 ​

下面的示例直接使用 Elastic.index、Elastic.get、Elastic.delete,不单独创建 Service。固定使用示例索引和文档 ID,避免开放任意索引操作。

java
import com.jfinal.kit.Kv;
import nexus.io.es.client.Elastic;
import nexus.io.model.body.RespBodyVo;
import nexus.io.model.exception.BusinessException;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.validator.ParameterValidator;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.client.RequestOptions;

/**
 * Minimal document operations using the client initialized by the admin
 * framework.
 */
public class ElasticsearchHandler extends BaseHandler {
  private static final String INDEX = "example_admin_demo";

  public HttpResponse set(HttpRequest request) {
    Kv parameters = request.getBodyObject(Kv.class);
    String message = ParameterValidator.text(parameters.get("message"), "message", 4096);
    String documentId = String.valueOf(currentUserId());

    Kv document = Kv.create().set("message", message);
    IndexRequest indexRequest = new IndexRequest(INDEX).id(documentId).source(document);
    IndexResponse response = Elastic.index(indexRequest, RequestOptions.DEFAULT);
    Kv result = Kv.create().set("id", response.getId()).set("result", response.getResult().getLowercase());
    return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
  }

  public HttpResponse get(HttpRequest request) {
    String documentId = String.valueOf(currentUserId());

    GetRequest getRequest = new GetRequest(INDEX, documentId);
    GetResponse response = Elastic.get(getRequest, RequestOptions.DEFAULT);
    Kv result = Kv.create().set("id", documentId).set("exists", response.isExists());
    if (response.isExists()) {
      Kv document = Kv.create().set(response.getSourceAsMap());
      result.set("document", document);
    }
    return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
  }

  public HttpResponse delete(HttpRequest request) {
    String documentId = String.valueOf(currentUserId());
    DeleteRequest deleteRequest = new DeleteRequest(INDEX, documentId);
    DeleteResponse response = Elastic.delete(deleteRequest, RequestOptions.DEFAULT);
    Kv result = Kv.create().set("id", response.getId()).set("result", response.getResult().getLowercase());
    return TioRequestContext.getResponse().respond(RespBodyVo.ok(result));
  }
}

同一个 ID 再次写入会覆盖整份文档,不是局部字段更新。GET 默认实时读取文档,不需要为了读回刚写入的数据强制刷新索引;全文检索属于另一条接口链路,受到索引刷新时机影响。

首次写入需要具备索引权限。若集群禁止自动创建索引,由管理员提前手动执行以下示例,不在应用启动时自动建索引:

http
PUT /example_admin_demo
Content-Type: application/json

{
  "mappings": {
    "properties": {
      "message": { "type": "text" }
    }
  }
}

索引存在但文档不存在时,读取返回 exists=false,重复删除返回 not_found。索引本身不存在、认证失败或权限不足时,客户端可能抛出异常,由应用统一异常处理器处理;示例不把这些异常伪装成操作成功。

5. 注册方法路由 ​

在已有路由配置方法中直接创建 Handler:

java
HttpRequestRouter router = TioBootServer.me().getRequestRouter();
ElasticsearchDemoHandler handler = new ElasticsearchDemoHandler();
router.add(HttpMethod.POST, "/api/elasticsearch-demo/set", handler::set);
router.add(HttpMethod.GET, "/api/elasticsearch-demo/get", handler::get);
router.add(HttpMethod.POST, "/api/elasticsearch-demo/delete", handler::delete);

对应类型是 nexus.io.tio.http.server.router.HttpRequestRouter、nexus.io.tio.boot.server.TioBootServer、nexus.io.tio.http.common.HttpMethod 和上面的示例 Handler。Handler 交给路由器持有,无需 Aop。

保留后台 Token 拦截器保护,不把示例写接口加入匿名放行列表。自定义业务鉴权时,仍需按项目的路由权限策略注册。真实业务还需要文档归属校验与合适的业务分层。

6. HTTP 验证步骤 ​

假设服务端口为 8100,没有额外 context-path,使用后台登录返回的 token:

http
POST /api/elasticsearch-demo/set HTTP/1.1
Host: 127.0.0.1:8100
Authorization: Bearer <token>

依次执行:

请求预期结果
POST /api/elasticsearch-demo/set首次写入 result=created,重复写入 result=updated
GET /api/elasticsearch-demo/getexists=true,document.message 为 Hello Elasticsearch
POST /api/elasticsearch-demo/deleteresult=deleted
再次 GETexists=false
再次 POST deleteresult=not_found

如果配置了 context-path,需要在上述路径前补上前缀。删除接口只删除示例文档,不删除整个索引。

7. 排查连接问题 ​

  • 客户端类缺失:检查运行时依赖及最终 JAR。
  • 连接被拒绝或超时:检查地址、端口、网络及 Elasticsearch 是否启动。
  • 认证或授权失败:检查用户名、密码和索引读写权限。
  • TLS 握手失败:确认协议、证书信任及主机名匹配,不关闭证书校验来规避问题。
  • ping 成功但写入失败:检查索引权限、自动创建索引策略以及 mapping。

相关章节:配置职责、整合数据库、整合 Redis、后端开发规范。

MIT Licensed