ES(文档,DSL)
文档操作#
有了索引库,接下来就可以向索引库中添加数据了。
Elasticsearch中的数据其实就是JSON风格的文档。操作文档自然保护增
、删
、改
、查
等几种常见操作,我们分别来学习。
1.新增语法#
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
}
java中新增文档
/**
* 新增文档
*
* @throws IOException
*/
@Test
void testConnect() throws IOException {
// 从服务中获取Item对象
Item item = iItemService.getById("317578");
item.setName("靠你娘");
// 创建索引请求,指定索引名称和文档ID
IndexRequest request = new IndexRequest("items").id(item.getId().toString());
// 创建ItemDTO2对象,并拷贝Item对象的属性
ItemDTO2 itemDTO2 = new ItemDTO2();
BeanUtils.copyProperties(item, itemDTO2); // 直接拷贝属性到 itemDTO2
// 将 itemDTO2 转换为 JSON 字符串
// String jsonString = JSONObject.toJSONString(itemDTO2);
String jsonString = JSONObject.toJSONString(itemDTO2);
// 设置请求的JSON内容
request.source(jsonString, XContentType.JSON);
// 执行索引请求,添加文档到索引中
IndexResponse index = client.index(request, RequestOptions.DEFAULT);
// 你可以根据需要打印或验证索引响应结果
System.out.println("Index Response: " + index.getId());
}
2.查询文档#
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
GET /{索引库名称}/_doc/{id}
/**
* 查询文档
*
* @throws IOException
*/
@Test
public void get() throws IOException {
GetRequest getRequest = new GetRequest("items", "317578");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
String sourceAsString = response.getSourceAsString();
log.info("{}", sourceAsString);
}
3.删除文档#
DELETE /{索引库名}/_doc/id值
4.修改文档#
修改有两种方式:
-
全量修改:直接覆盖原来的文档
-
局部修改:修改文档中的部分字段
1.全量修改#
全量修改是覆盖原来的文档,其本质是两步操作:
-
根据指定的id删除文档
-
新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
2.局部修改#
局部修改是只修改指定id匹配的文档中的部分字段。
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
java中代码
/**
* 局部修改
*
* @throws IOException
*/
@Test
public void update() throws IOException {
UpdateRequest request = new UpdateRequest("items", "317578");
request.doc("name", "密码箱", "price", 38900);
client.update(request, RequestOptions.DEFAULT);
}
5.批量导入#
java中批量导入代码
/**
* 批量导入
*/
@Test
public void addList() throws IOException {
int pageNo = 1;
int pageSize = 1000;
while (true) {
Page<Item> page = iItemService.lambdaQuery()
.eq(Item::getStatus, 1).page(new Page<>(pageNo, pageSize));
List<Item> items = page.getRecords();
if (CollUtils.isEmpty(
items)) {
log.info("没有更多记录可处理,退出");
break; // 更清晰的退出条件
}
log.info("第{}次处理,当前页记录数: {}", pageNo, items.size());
BulkRequest request = new BulkRequest("items");
// 批量操作
for (Item item : items) {
ItemDTO2 itemDTO2 = new ItemDTO2();
BeanUtils.copyProperties(item, itemDTO2);
request.add(
new IndexRequest().id(item.getId().toString())
.source(JSONObject.toJSONString(itemDTO2), XContentType.JSON)
);
}
// 执行批量操作并处理结果
BulkResponse bulkResponse = client.bulk(request, RequestOptions.DEFAULT);
if (bulkResponse.hasFailures()) {
log.error("批量索引时发生错误: {}", bulkResponse.buildFailureMessage());
} else {
log.info("第{}次批量索引成功,共处理记录数: {}", pageNo, items.size());
}
// 下一页
pageNo++;
}
}
DSL查询#
Elasticsearch的查询可以分为两大类:
-
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
-
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
这里列举一些常见的,例如:
-
全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
-
match
: -
multi_match
-
-
精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
-
ids
-
term
-
range
-
-
地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
-
geo_bounding_box
:按矩形搜索 -
geo_distance
:按点和半径搜索
-
1.叶子查询#
1.全文检索查询#
GET /{索引库名}/_search
{
"query": {
"match": {
"字段名": "搜索条件"
}
}
}
java代码
@Test
void testMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
与match类似的还有multi_match,区别在于可以同时对多个字段搜索,而且多个字段都要满足
GET /{索引库名}/_search
{
"query": {
"multi_match": {
"query": "搜索条件",
"fields": ["字段1", "字段2"]
}
}
}
java代码
@Test
void testMultiMatch() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
2.精确查询#
精确查询,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword、数值、日期、boolean类型的字段。
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}
java中代码
@Test
void testTerm() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.termQuery("brand", "华为"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
range
是范围查询,对于范围筛选的关键字有:
-
gte
:大于等于 -
gt
:大于 -
lte
:小于等于 -
lt
:小于
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}
java中代码
@Test
void testRange() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
2.复合查询#
1.算分函数查询#
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
从elasticsearch5.1开始,采用的相关性打分算法是BM25算法,公式如下:
基本语法:
function score 查询中包含四部分内容:
-
原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
-
过滤条件:filter部分,符合该条件的文档才会重新算分
-
算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
-
weight:函数结果是常量
-
field_value_factor:以文档中的某个字段值作为函数结果
-
random_score:以随机数作为函数结果
-
script_score:自定义算分函数算法
-
-
运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
-
multiply:相乘
-
replace:用function score替换query score
-
其它,例如:sum、avg、max、min
-
function score的运行流程如下:
-
1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
-
2)根据过滤条件,过滤文档
-
3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
-
4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。
因此,其中的关键点是:
-
过滤条件:决定哪些文档的算分被修改
-
算分函数:决定函数算分的算法
-
运算模式:决定最终算分结果
示例:给IPhone这个品牌的手机算分提高十倍,分析如下:
-
过滤条件:品牌必须为IPhone
-
算分函数:常量weight,值为10
-
算分模式:相乘multiply
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数
{
"filter": { // 满足的条件,品牌必须是Iphone
"term": {
"brand": "Iphone"
}
},
"weight": 10 // 算分权重为2
}
],
"boost_mode": "multipy" // 加权模式,求乘积
}
}
}
2.bool查询#
bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:
-
must:必须匹配每个子查询,类似“与”
-
should:选择性匹配子查询,类似“或”
-
must_not:必须不匹配,不参与算分,类似“非”
-
filter:必须匹配,不参与算分
GET /items/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"should": [
{"term": {"brand": { "value": "vivo" }}},
{"term": {"brand": { "value": "小米" }}}
],
"must_not": [
{"range": {"price": {"gte": 2500}}}
],
"filter": [
{"range": {"price": {"lte": 1000}}}
]
}
}
}
java中代码
@Test
void testBool() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.准备bool查询
BoolQueryBuilder bool = QueryBuilders.boolQuery();
// 2.2.关键字搜索
bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.3.品牌过滤
bool.filter(QueryBuilders.termQuery("brand", "德亚"));
// 2.4.价格过滤
bool.filter(QueryBuilders.rangeQuery("price").lte(30000));
request.source().query(bool);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
3.排序#
排序
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}
4.分页#
1.基础分页#
elasticsearch中通过修改
from
、size
参数来控制要返回的分页结果:
from
:从第几个文档开始size
:总共查询几个文档
GET /items/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10, // 每页文档数量,默认10
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
2.深度分页#
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。现在每页查询10条,查询第99页。
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}
java中代码
@Test
void testPageAndSort() throws IOException {
int pageNo = 1, pageSize = 5;
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.搜索条件参数
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.排序参数
request.source().sort("price", SortOrder.ASC);
// 2.3.分页参数
request.source().from((pageNo - 1) * pageSize).size(pageSize);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
5.高亮#
1.高亮原理#
css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有
elasticsearch
做分词搜索,是知道哪些词条需要高亮的。因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。
因此实现高亮的思路就是:
-
用户输入搜索关键字搜索数据
-
服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加
html
标签 -
前端提前给约定好的
html
标签添加CSS
样式
2.实现高亮#
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
注意:
-
搜索必须有查询条件,而且是全文检索类型的查询条件,例如
match
-
参与高亮的字段必须是
text
类型的字段 -
默认情况下参与高亮的字段要与搜索字段一致,除非添加:
required_field_match=false
java代码实现高亮
高亮查询与前面的查询有两点不同:
-
条件同样是在
request.source()
中指定,只不过高亮条件要基于HighlightBuilder
来构造 -
高亮响应结果与搜索的文档结果不在一起,需要单独解析
@Test
void testHighlight() throws IOException {
// 1.创建Request
SearchRequest request = new SearchRequest("items");
// 2.组织请求参数
// 2.1.query条件
request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
// 2.2.高亮条件
request.source().highlighter(
SearchSourceBuilder.highlight()
.field("name")
.preTags("<em>")
.postTags("</em>")
);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析响应
handleResponse(response);
}
代码解读:
-
第
3、4
步:从结果中获取_source
。hit.getSourceAsString()
,这部分是非高亮结果,json字符串。还需要反序列为ItemDoc
对象 -
第
5
步:获取高亮结果。hit.getHighlightFields()
,返回值是一个Map
,key是高亮字段名称,值是HighlightField
对象,代表高亮值 -
第
5.1
步:从Map
中根据高亮字段名称,获取高亮字段值对象HighlightField
-
第
5.2
步:从HighlightField
中获取Fragments
,并且转为字符串。这部分就是真正的高亮字符串了 -
最后:用高亮的结果替换
ItemDoc
中的非高亮结果
完整代码
private void handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
// 1.获取总条数
long total = searchHits.getTotalHits().value;
System.out.println("共搜索到" + total + "条数据");
// 2.遍历结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// 3.得到_source,也就是原始json文档
String source = hit.getSourceAsString();
// 4.反序列化
ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);
// 5.获取高亮结果
Map<String, HighlightField> hfs = hit.getHighlightFields();
if (CollUtils.isNotEmpty(hfs)) {
// 5.1.有高亮结果,获取name的高亮结果
HighlightField hf = hfs.get("name");
if (hf != null) {
// 5.2.获取第一个高亮结果片段,就是商品名称的高亮值
String hfName = hf.getFragments()[0].string();
item.setName(hfName);
}
}
System.out.println(item);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了