负一、 repository
可以通过继承Repository的方式去快速的实现查询操作
官方文档里面有写
https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.repositories
通过表格中字段的拼接去定义一个方法,就可以进行查询,实在是很方便,所以我这里就没写关于Repository的内容。
零 、ES搬砖总共分几步
模板类我使用的是:
@Autowired
ElasticsearchRestTemplate template;
核心使用的类有
-
Query
-
NativeQuery
-
QueryBuilder
-
QueryBuilders
大部分的操作基本就是 上边四个类,使用方法 分为三步
- template.search(query)
- query=new NativeQuery(queryBuilder)
- queryBulder=QueryBuilder.xxxxBuilder();
会了这三步,基本就都会了,全剧终,再见~
正文
我们假设我们es中有一些手机的商品数据,我现在想要进行一些查询,然后看看查询结果,对比一下api的使用的区别。
java侧有一个产品类EsProduct,我们要使用的字段是name、和subTitle,可以看到都是text类型,且经过ik分词了
es侧有个手机名称叫 【华为 HUAWEI P20】
@Data
@EqualsAndHashCode(callSuper = false)
@Document(indexName = "pms",type="product",shards = 1,replicas = 0)
public class EsProduct implements Serializable {
private static final long serialVersionUID = -1L;
@Id
private Long id;
@Field(type = FieldType.Keyword)
private String productSn;
private Long brandId;
@Field(type = FieldType.Keyword)
private String brandName;//品牌
private Long productCategoryId;
@Field(type = FieldType.Keyword)
private String productCategoryName;//产品类别名称
private String pic;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String name;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String subTitle;
@Field(analyzer = "ik_max_word",type = FieldType.Text)
private String keywords;
private BigDecimal price;
private Integer sale;
private Integer newStatus;
private Integer recommandStatus;
private Integer stock;
private Integer promotionType;
private Integer sort;
@Field(type =FieldType.Nested)
private List<EsProductAttributeValue> attrValueList;
}
一、match 与term
1、举个栗子一
首先写了一个get方法,传入相关的手机名称
@GetMapping("/search1/{name}")
public void search1(@PathVariable("name") String name) {
Query query = null;
QueryBuilder queryBuilder = null;
queryBuilder = QueryBuilders.matchQuery("name", name);
query = new NativeSearchQuery(queryBuilder);
SearchHits<EsProduct> search = template.search(query, EsProduct.class);
}
结果:
- 1.当输入华为、或华为哥哥时,就可以搜索到华为手机
- 2.如果输入华,就搜索不到,因为es中 ‘华为’ 分词后 并没有分解成‘华’ 和‘为’,却仍然是 ‘华为’,所以搜索不到
结论 - 1.matchQuery 是指 对name进行分词,成为数组 arr1[a,b…],然后分别将a,b…与es数据中的name属性的索引去对比,只要有一个对比上了,就算符合。
- 2.举例:如果name属性不是keyword类型时,会被分词为【华为, HUAWEI, P20,P,20】,当输入华为哥哥时,会分词为【华为,哥哥】,可以看到两个数组 的 【华为】匹配到了,所以最终可以查到 华为手机。
- 3.如果输入【华】、【为华】,就不能查到。原因如上
- 另外说一下,可在kibana中,自己分词查一下:例如:
2、举个栗子二
@GetMapping("/search2/{name}")
@ApiOperation("通过name查询名字中带有name的数据 term query(短语查询)")
public void search2(@PathVariable("name") String name) {
Query query = null;
QueryBuilder queryBuilder = null;
queryBuilder = QueryBuilders.termQuery("name", name);
query = new NativeSearchQuery(queryBuilder);
SearchHits<EsProduct> search = template.search(query, EsProduct.class);
}
结果:
- 输入 华为,可以搜索到
- 输入 华 ,失败
- 输入 华为哥哥,搜索不到
结论
- 1.term 查询与上述match查询的区别在于,match查询时会将 输入的name进行分词,只有分词数组中有一个匹配到了es中的索引,就算搜索成功。term 与之相反,不对 输入的name进行分词
二、match query与fuzzy query
1.举个栗子一
结合结论看栗子
@GetMapping("/search3/{name}")
@ApiOperation("通过name查询名字中带有name的数据 match query 与fuzzy query(模糊查询)")
public void search3(@PathVariable("name") String name) {
Query query = null;
QueryBuilder queryBuilder = null;
queryBuilder = QueryBuilders.fuzzyQuery("name", name).fuzziness(Fuzziness.ONE);
query = new NativeSearchQuery(queryBuilder);
}
总结
-
1.match query时携带模糊查询,首先 输入的name 会被分词,.fuzziness参数会指定编辑次数,fuzziness.zone 为绝对匹配,fuzziness.one为编辑一次
-
fuzziness.two为编辑两次。fuzziness.auto 为默认推荐的方式
这篇文章讲的有挺好(https://www.jianshu.com/p/06f43b537a29)
-
编辑一次指:
-
1.将一个字符替换成另一个字符
-
2.插入一个字符
-
3.删除一个字符
-
4.两个字符进行位置交换(莱文斯坦距离【Levenshtein distance】算法中 视之为两次编辑,而【Damerau–Levenshtein distance】视为一次编辑【默认是这种算法】)
- 一.结合上方栗子,在fuzzyQuery(“name”,name)后添加fuzziness参数 如下:
queryBuilder= QueryBuilders.matchQuery("name",name).fuzziness(Fuzziness.ONE);
- 1、输入华为哥,分词后变成【华为、哥】,但是因为是match查询,所有不需要模糊也能搜索到华为
- 但是上述分词中还有【哥】,所以可视之 为删除掉,变成了 空字符串,所以除了华为之外,还可以匹配到其他的数据
- 2、输入华为哥哥,分词后变成【华为、哥哥】,华为手机依然能搜索到。
- 但是【哥哥】这个分词无论加一个字符、删字符、变化位置,都不能与其他索引匹配,所以最终只能查到华为手机
- 二、结合上方栗子,继续添加.prefixLength(1)参数,表示在term级别上至少要匹配1个字符后才能进行上述的编辑,如下:
queryBuilder= QueryBuilders.matchQuery("name",name).fuzziness(Fuzziness.ONE).prefixLength(1);
- 1.输入华为哥,分词后为【华为、哥】,【华为】可以匹配到华为手机
- 但是这回哥字就不能删除了,必要要和其他索引匹配1个字符后才能开始编辑,所以最终只能匹配到华为手机
-
三、结合上方栗子,添加transpostion(true|false),true(默认)时为Damerau–Levenshtein distance,false为莱文斯坦-距离算法
queryBuilder= QueryBuilders.matchQuery("name",name).fuzziness(Fuzziness.ONE).fuzzyTranspositions(true);
- 1.输入: 为华,分词为【‘为’,‘华‘】,这样编辑一次,就可以删除掉,所有匹配的是所有的数据
- 2.使用fuzzyQuery进行查询,如下:
queryBuilder=QueryBuilders.fuzzyQuery("name",name).fuzziness(Fuzziness.ONE);;
- 输入: 为华,因为fuzzyQuery是 不会对需要检索的内容进行分词,所以term级别的短语是【为华】,所以编辑一次时,会将 这两个字进行调换,最终查到的是华为手机
三、query 和filter
query会计算相关度从而得到score,filter是从query的结果中进行筛选,不进行score的计算
/**
* 通过关键字-keyword去匹配字段【name,subTitle】
*/
@GetMapping("/search4")
@ApiOperation("在商品名称和描述中查询,品牌名称和种类进行筛选【根据商品价格进行排序】")
public voidsearch4(@RequestParam(required = false) String brandName,
@RequestParam(required = false) String productCategoryName,
@RequestParam(required = false) String keyword) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//keyword 匹配 name 或 subTitle
if (StringUtils.isEmpty(keyword)) {
//如果没有输入关键字,就匹配所有
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
} else {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.should().add(QueryBuilders.matchQuery("name", keyword));
boolQueryBuilder.should().add(QueryBuilders.matchQuery("subTitle", keyword));
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
}
//过滤 品牌和种类
BoolQueryBuilder filter = QueryBuilders.boolQuery();
if (!StringUtils.isEmpty(brandName)) {
filter.must().add(QueryBuilders.termQuery("brandName", brandName));
}
if (!StringUtils.isEmpty(productCategoryName)) {
filter.must().add(QueryBuilders.termQuery("productCategoryName", productCategoryName));
}
//page 分页
nativeSearchQueryBuilder.withPageable(PageRequest.of(1,2));
//sort 根据价格进行排序
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort("price"));
String result = "";
nativeSearchQueryBuilder.withFilter(filter);
NativeSearchQuery query = nativeSearchQueryBuilder.build();
}
说一下我对es的dsl的学习方法,一方面看官方文档外,其实我最喜欢的方式是通过spring-data-elasticsearch提供的template去一一尝试各个方法,debug代码,把DSL复制出来,然后放到kibana中运行,就可以自己创造样例去学习了,如下图这般
四、boost
QueryBuilder对象后接.boost(float),会将该query的score * boost所设置的值
@GetMapping("/search5")
@ApiOperation("name查询,name 优先显示,分数不够,乘法来凑")
public void search5(@RequestParam(required = false) String keyword) {
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
//keyword 匹配 name 或 subTitle
if (StringUtils.isEmpty(keyword)) {
nativeSearchQueryBuilder.withQuery(QueryBuilders.matchAllQuery());
} else {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.should().add(QueryBuilders.matchQuery("name", keyword).boost(2));
boolQueryBuilder.should().add(QueryBuilders.matchQuery("subTitle", keyword));
nativeSearchQueryBuilder.withQuery(boolQueryBuilder);
}
NativeSearchQuery query = nativeSearchQueryBuilder.build();
SearchHits<EsProduct> search = template.search(query, EsProduct.class);
结论
- 该方法的目的是在 手机的名称 或 描述中查找 【华为】(注意是或),如果手机的名称中就带有华为,那就把分数×2。目的是优先显示名字有关键字的数据。
五、function_score
我们创建一个查询,一般都是通过QueryBuilders,现在要说的是 其下的functionScoreQuery方法,这个方法就是用于处理和自定义分数有关的操作
- 首先看一下他的五种重构,实际上 就是两种
其一:ScoreFunctionBuilder function
(不妨直接理解字面意思:分数方法 的构建器)
- 该对象 通过 建造者 ScoreFunctionBuilders来创建 一个 分数方法的构建器
-
都可以创建什么种类的呢?
-
1.可以让他们按照我给的权重来判分 weightFactorFunction( 权重乘法 ) ,例:ScoreFunctionBuilders.weightFactorFunction(2); 将权重变成 2
-
2.可以让他们先按默认的算分,然后再乘上 我给的某个数值类型的字段fieldValueFactorFunction
- 例如:按销售量算分。
- 问:那如果销售量太大算出来的分有点太大怎么办?
- 答:可以乘以一个倍数啊,比如 *0.001 ,代码上的实现就是 .factor(float)
- 问:乘以倍数 也是线性的放大或缩小,我想不要线性的,我想要对数类型的曲线变化怎么办?
- 答:也可以作答啊,代码上实现就是.modifier(Modifier.log)
- 问:本来我的分数是0,log O=NAN啊,因为10的n次方中,n等于多少时 都不可能计算得到0的,那怎么办
- 答:可以使用modifiler.log1p 或者log2p啊
- 问:为什么刚才乘上字段的值,而不是加上字段的值,或者取平均数啊,可以做到么?
- 答:默认是做乘法,但也可以你想要的计算操作。不过不是在 FunctionScoreQueryBuilder(其父类也不能设置)中设置,而是再querybuilder中设置
- 例子:
FieldValueFactorFunctionBuilder price = ScoreFunctionBuilders.fieldValueFactorFunction("price").modifier(FieldValueFactorFunction.Modifier.LOG); FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(price); functionScoreQueryBuilder.boostMode(CombineFunction.SUM) ---看这句 ,不就实现了么
-
3.可以让他们按0-1随机给分吧 ,那就选用 randomFunction
-
4.还有其他方法可以自己看看吧,有一些与地址位置相关的。
其二: filterFunctionBuilder
-
A:(仔细看) 我们不忘初心,回到起点,最初我们只是 想要调动 QueryBuilders.functionScoreQuery() 进行一个查询啊,
-
B: 是啊,然后我们传入了 一个scoreFunctionBuilder啊,有什么问题么?
-
A: 你刚才设置了几次权重?
-
B: 设置了一次,设置为2 啊!
-
A: 嗯,你说的没问题,那我再问你,你刚才操作了几个字段?
-
B: 操作了一个字段
-
A: 那我现在想操作多个字段,设置不同的权重怎么办
-
B: 呃…这有什么意义
-
A: 比如我想做一个查询,我是大众点评软件,想查询 ‘水煮鱼’后,显示 销量高,评分高,然后是有wifi
- 显然销量和评分都很重要,但是我们软件呢 是良心软件,更注重评分,评分高的自然要排在推荐的最上边
- 不过也有可能有人恶意的去给低分,
- 那么如果销量很高时,我们也可以不太注重评分,从而将他排在前边。
- 最后是wifi了,有的话,当然好了,没有的话,也不太重要。
- 好了,以上情景给出来了。显然评分最重要,销量次之,最后是wifi,我们可以先看看 这几个参数的数值对比,
- 假设有两家店都直接叫‘水煮鱼’,和我的搜索完美契合,就假设这样算出的分是1分的话,评分最多是5分,
- 销量可能是成百上千,wifi是有或无,那么从最简单的来,我们就设置wifi的权重是1分吧。
FunctionScoreQueryBuilder.FilterFunctionBuilder wifi= new FunctionScoreQueryBuilder.FilterFunctionBuilder ( QueryBuilders.termQuery("wifi","有"),ScoreFunctionBuilders.weightFactorFunction(1);//查询wifi 为有,设置权重为1 );
- 评分呢…就算五分吧
FunctionScoreQueryBuilder.FilterFunctionBuilder grade= new FunctionScoreQueryBuilder.FilterFunctionBuilder ( ScoreFunctionBuilders.fieldValueFactorFunction("grade") );
-
B:等等!我记得这默认是乘法吧,听你刚才的意思,好像我们要用加法的!~~
-
A:是的!,别急嘛!follow me~~~
* 最后是销量了…,我打开了大众点评一看,销售都是按月售的,基本最多的是几千条吧,那我们就用log1p吧,如果10的3次是1千,4次方 是1万,
* 也就是说如果销量了 1千-1万之间的话,就能得3分,好像并不能有效区分啊,那我们还是除以1000吧,这样就相当于 1000的销量可以媲美 评分1分。FunctionScoreQueryBuilder.FilterFunctionBuilder sales=new FunctionScoreQueryBuilder.FilterFunctionBuilder( ScoreFunctionBuilders.fieldValueFactorFunction("sales").factor(0.001f) )
-
A: 好,我们最后都弄完了,最终得到了三个对象 wifi,grade,sales 都是 FunctionScoreQueryBuilder.FilterFunctionBuilder 类型的
* 接下来放到一个数组中
* FunctionScoreQueryBuilder.FilterFunctionBuilder[] arr=new FunctionScoreQueryBuilder.FilterFunctionBuilder[3];
* 不忘初心!我们写出我们最开始要干的事:最一个自定义的分数查询:
* QueryBuilders.functionScoreQuery()
* 将arr 作为参数参入进去 QueryBuilders.functionScoreQuery(arr); -
B:刚才说好的 三个得分要相加呢
-
A: 哦,差点忘了
queryBuilder= QueryBuilders.functionScoreQuery(arr).boostMode(CombineFunction.SUM)
简单练习
/** * 接下来简单练习一下: * 还有一种用法是: 我现在是大学招生办的人,现在开始筛选学生的志愿。 * 学生呢,有编号为1,2,3 的三个志愿可填写。可重复填写(三个志愿都写的我们学校),我们学校呢 想招生100个人。 * 对于第一志愿 就选我们学校的人,我们特别重视,第二志愿次之,第三志愿再次之,如果三个志愿都填写的我们学校,那这样的人我们最喜欢 * 除此之外呢,我们要学生的分数要超过670分才能被招生进来 * * 好,情景为以上信息。 * 1.先找到 分数大于670分的人 * 2.我们分别在三个志愿中查询,三个志愿的权重分别是 10,5,2 ,且最少有一个志愿填写了我们学校。 * 3.只要前100名
//分数要大于670 RangeQueryBuilder score = QueryBuilders.rangeQuery("score").gt(670); FunctionScoreQueryBuilder.FilterFunctionBuilder[] arr = new FunctionScoreQueryBuilder.FilterFunctionBuilder[3]; String schoolName = "哈哈大学"; FunctionScoreQueryBuilder.FilterFunctionBuilder voluntary1 = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("voluntary1", schoolName), ScoreFunctionBuilders.weightFactorFunction(10)); FunctionScoreQueryBuilder.FilterFunctionBuilder voluntary2 = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("voluntary2", schoolName), ScoreFunctionBuilders.weightFactorFunction(10)); FunctionScoreQueryBuilder.FilterFunctionBuilder voluntary3 = new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("voluntary3", schoolName), ScoreFunctionBuilders.weightFactorFunction(10)); arr[0] = voluntary1; arr[1] = voluntary2; arr[2] = voluntary3; FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(score, arr); Query query = new NativeSearchQueryBuilder().withQuery(functionScoreQueryBuilder).withPageable(PageRequest.of(0,100)).build(); template.search(query, Student.class);
回到 重载方法
上方两种方式 都额外还有一种重载方法,就是增加了一个queryBuilder的参数。那有什么区别呢。
你可以创建一个普通的queyBulder测试一下
@GetMapping("/search9")
public void search9(String name) {
FunctionScoreQueryBuilder.FilterFunctionBuilder[] arr = new FunctionScoreQueryBuilder.FilterFunctionBuilder[2];
FunctionScoreQueryBuilder.FilterFunctionBuilder item1 =
new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("name", name),
ScoreFunctionBuilders.weightFactorFunction(10));
FunctionScoreQueryBuilder.FilterFunctionBuilder item2 =
new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("subTitle", name),
ScoreFunctionBuilders.weightFactorFunction(5));
arr[0] = item1;
arr[1] = item2;
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.should().add(QueryBuilders.matchQuery("name", name));
boolQueryBuilder.should().add(QueryBuilders.matchQuery("subTitle", name));
//有查询,在查询结果后 对分数进行filter
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQueryBuilder, arr).scoreMode(FunctionScoreQuery.ScoreMode.SUM);
Query query = new NativeSearchQueryBuilder().withQuery(functionScoreQueryBuilder).build();
SearchHits<EsProduct> search = template.search(query, EsProduct.class);
print(search);
}
然后和上述一样传入方法中,然后debug看一下 DSL
可以发现,加入query查询的DSL 会先match查询,然后再用function_score去计算分数,而反之不加入query查询的,在query阶段会直接matchALL
加了query查询
{
"function_score" : {
"query" : {
"bool" : {
"should" : [ 加了query 的会进行查询
{
"match" : {
"name" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
},
{
"match" : {
"subTitle" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
}
],
"adjust_pure_negative" : true,
"boost" : 1.0
}
},
"functions" : [
{
"filter" : {
"match" : {
"name" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
},
"weight" : 10.0
},
{
"filter" : {
"match" : {
"subTitle" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
},
"weight" : 5.0
}
],
"score_mode" : "sum",
"max_boost" : 3.4028235E38,
"boost" : 1.0
}
}
不加query查询
{
"function_score" : {
"query" : {
"match_all" : { 不加query的会直接match_all
"boost" : 1.0
}
},
"functions" : [
{
"filter" : {
"match" : {
"name" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
},
"weight" : 10.0
},
{
"filter" : {
"match" : {
"subTitle" : {
"query" : "手机",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
},
"weight" : 5.0
}
],
"score_mode" : "sum",
"max_boost" : 3.4028235E38,
"min_score" : 5.0,
"boost" : 1.0
}
}