为什么要使用全文检索
面对复杂的搜索业务和数据量,使用传统数据库搜索就显得力不从心,一般我们都会使用全文检索技术。
常见的全文检索技术有 Lucene、solr 、elasticsearch 等。
理解索引结构
索引结构包括逻辑结构和物理结构
逻辑结构部分是一个倒排索引表:
1、将要搜索的文档内容分词,所有不重复的词组成分词列表。
2、将搜索的文档最终以Document方式存储起来。
3、每个词和docment都有关联。
如下:
Elasticsearch简介
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的
全文搜索引擎,基于RESTful web接口(可以通过postman操作索引库)。
优点:
(1)可以作为一个大型分布式集群(数百台服务器)技术,处理PB级数据,服务大公
司;也可以运行在单机上
(2)将全文检索、数据分析以及分布式技术,合并在了一起,才形成了独一无二的ES;
(3)开箱即用的,部署简单(无需安装,解压安装包后即可使用)
(4)全文检索,同义词处理,相关度排名,复杂数据分析,海量数据的近实时处理
下表是Elasticsearch与MySQL数据库逻辑结构概念的对比
Elasticsearch 关系型数据库Mysql
索引(index) 数据库(databases)
类型(type) 表(table)
文档(document) 行(row)
使用Postman操作索引库
以post方式提交 http://127.0.0.1:9200/testindex/doc
body:
{
“name”:“测试商品”,
“price”:123
}
查询某索引某类型的全部数据,以get方式请求
http://127.0.0.1:9200/testindex/doc/_search
映射与数据类型
映射(Mapping)相当于数据表的表结构。ElasticSearch中的映射(Mapping)用来定义一个文档,可以定义所包含的字段以及字段的类型
分词器及属性等等。
映射可以分为动态映射和静态映射。
动态映射 :在关系数据库中,需要事先创建数据库,然后在该数据库实例下创建数据表,然后才能在该数据表中插入数据。而ElasticSearch中不需要事先定义映射(Mapping),文档写入ElasticSearch时,会根据文档字段自动识别类型,这种机制称之为动态映射。
静态映射 :在ElasticSearch中也可以事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为静态映射。
静态映射相对于动态映射来说,能对索引字段定义很多属性,比如分词的属性,而动态映射是不具备这些属性的
常用类型如下:
字符串类型
text: 设置text类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分析器分成一个一个词项。
text类型的字段不用于排序,很少用于聚合。
keyword:keyword类型的字段不会进行分词,只能通过精确值搜索到。
如果字段需要进行过滤(比如查找已发布博客中status属性为published的文章)、排序、聚合, 则需要使用keyword类型
整数类型
byte -128~127
short -32768~32767
integer
long
浮点类型
doule 64位双精度浮点类型
float 32位单精度浮点类型
half_float 16位半精度浮点类型
scaled_float 缩放类型的的浮点数 12.34会存为1234
date类型
日期类型表示格式可以是以下几种:
(1)日期格式的字符串,比如 “2018-01-13” 或 “2018-01-13 12:10:30”
(2)long类型的毫秒数( milliseconds-since-the-epoch,epoch就是指UNIX诞生的UTC时间1970年1月1日0时0分0秒)
(3)integer的秒数(seconds-since-the-epoch)
boolean类型
逻辑类型(布尔类型)可以接受true/false
binary类型
二进制字段是指用base64来表示索引中存储的二进制数据,可用来存储二进制形式
的数据,例如图像。默认情况下,该类型的字段只存储不索引。二进制类型只支持
index_name属性。
array类型
在ElasticSearch中,没有专门的数组(Array)数据类型,但是,在默认情况下,任
意一个字段都可以包含0或多个值,这意味着每个字段默认都是数组类型,只不过,
数组类型的各个元素值的数据类型必须相同。
常用的数组类型是:
(1)字符数组: [ “one”, “two” ]
(2)整数数组: productid:[ 1, 2 ]
(3)对象(文档)数组: “user”:[ { “name”: “Mary”, “age”: 12 }, { “name”: “John”, “age”:10 }],ElasticSearch内部把对象数组展开为 {“user.name”: [“Mary”, “John”], “user.age”:[12,10]}
object类型
JSON天生具有层级关系,文档会包含嵌套的对象
IK分词器
默认的中文分词是将每个字看成一个词,这显然是不符合要求的,所以我们需要安装中文分词器来解决这个问题。
IK分词器安装
(1)先将其解压,将解压后的elasticsearch文件夹重命名文件夹为ik
(2)将ik文件夹拷贝到elasticsearch/plugins 目录下。
(3)重新启动,即可加载IK分词器
IK提供了两个分词算法ik_smart 和 ik_max_word
其中 ik_smart 为最少切分,ik_max_word为最细粒度划分
{“analyzer”: “ik_smart”, “text”: “黑马程序员” } 粗粒度
{“analyzer”: “ik_max_word”, “text”: “黑马程序员” } 细粒度
Kibana简介
Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 合作。
如果Kibana远程连接ElasticSearch ,可以修改config\kibana.yml,默认连接的是本地的ElasticSearch,localost:9200
执行bin\kibana.bat启动kibana
打开浏览器,键入http://localhost:5601 访问Kibana(默认端口为5601)
我们这里使用Kibana进行索引操作,Kibana与Postman相比省略了服务地址,并且有语法提示,非常便捷。
索引操作
创建索引与映射字段
PUT /索引库名
{
“mappings”: {
“类型名称”:{
“properties”: {
“字段名”: {
“type”: “类型”,
“index”: true,
“store”: true,
“analyzer”: “分词器”
}
}
}
}
类型名称:就是前面将的type的概念,类似于数据库中的不同表
字段的属性:
type:类型,可以是text、long、short、date、integer、object等
index:是否索引,默认为true, 如果想要被搜索,则需要设置为true
store:是否单独存储,默认为false ,一般内容比较多的字段设置成true(比如一个文档),可提升查询性能
analyzer:分词器
index和store都有默认值,所以可以不设置, 一般情况下,如果想要分词,则设置type和analyzer, 如果不想分词, 只需要设置type属性即可
文档增加与修改
增加文档自动生成ID
语法:
POST 索引库名/类型名
{
“key”:“value”
}
通过以下命令查询sku索引的数据
GET sku/_search
新增文档指定ID
语法
PUT /索引库名/类型/id值
{
…
}
索引查询
基本语法
GET /索引库名/_search
{
“query”:{
“查询类型”:{
“查询条件”:“查询条件值”
}
}
}
1.查询所有数据(match_all)
GET /sku/_search
{
“query”:{
“match_all”: {}
}
}
2.匹配查询(match)
示例:查询名称包含手机的记录
GET /sku/doc/_search
{
“query”: {
“match”:{“name”:“手机”}
}
}
3.多字段查询(multi_match)
GET /sku/_search
{
“query”:{
“multi_match”: {
“query”: “小米”,
“fields”: [ “name”, “brandName”,“categoryName”]
}
}
}
在name、brandName 、categoryName字段中查询 小米 这个词(3个字段中都要包含"小米"这个词)
4.词条匹配(term) 就是精确查询
GET /sku/_search
{
“query”:{
“term”:{
“price”:1000
}
}
}
5.多词条匹配(terms)
GET /sku/_search
{
“query”:{
“terms”:{
“price”:[1000,2000,3000]
}
}
}
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件
6.布尔组合(bool)
bool 把各种其它查询通过 must (与)、 must_not (非)、 should (或)的方式进行组合
must表示条件之间是并且的关系,should表示条件之间是或者的关系
示例:查询名称包含手机的,并且品牌为小米的。
GET /sku/_search
{
“query”:{
“bool”:{
“must”: [
{ “match”: { “name”: “手机” }} ,
{ “term”: {“brandName”: “小米”}}
]
}
}
}
示例:查询名称包含手机的,或者品牌为小米的。
GET /sku/_search
{
“query”:{
“bool”:{
“should”: [
{ “match”: { “name”: “手机” }} ,
{ “term”: {“brandName”: “小米”}}
]
}
}
}
7.过滤查询
过滤是针对搜索的结果进行过滤,过滤器主要判断的是文档是否匹配,不去计算和
判断文档的匹配度得分,所以过滤器性能比查询要高,且方便缓存,推荐尽量使用过滤
器去实现查询或者过滤器和查询共同使用。
示例:过滤品牌为小米的记录
GET /sku/_search
{
“query”:{
“bool”:{
“filter”: [
{“match”:{“brandName”: “小米”}}
]
}
}
}
8.分组查询
示例:按分组名称聚合查询,统计每个分组的数量
GET /sku/_search
{
“size” : 0,
“aggs” : {
“sku_category” : {
“terms” : {
“field” : “categoryName”
}
}
}
}
size为0 不会将数据查询出来,目的是让查询更快。
sku_category:分组的名称
查询结果:
{
“took”: 6,
“timed_out”: false,
“_shards”: {
“total”: 5,
“successful”: 5,
“skipped”: 0,
“failed”: 0
},
“hits”: {
“total”: 5,
“max_score”: 0,
“hits”: [] 因为size为0,所以hits没有数据
},
“aggregations”: {
“sku_category”: {
“doc_count_error_upper_bound”: 0,
“sum_other_doc_count”: 0,
“buckets”: [
{
“key”: “手机”,
“doc_count”: 3 分类名称为手机的一共有3条数据
},
{
“key”: “电视”,
“doc_count”: 2 分类名称为电视的一共有2条数据
}
]
}
}
}
我们也可以一次查询两种分组统计结果:
GET sku/_search
{
“size”:0,
“aggs”: {
“sku_category”: {
“terms”: {
“field”: “categoryName” 按分类名称进行分组
}
},
“sku_brand”: {
“terms”: {
“field”: “brandName” 按品牌名称进行分组
}
}
}
}
JavaRest 高级客户端入门
elasticsearch 存在三种Java客户端。
elasticsearch 存在三种Java客户端。
新增和修改数据
插入单条数据:
HttpHost : url地址封装
RestClientBuilder: rest客户端构建器
RestHighLevelClient: rest高级客户端
IndexRequest: 新增或修改请求
IndexResponse:新增或修改的响应结果
//1.连接rest接口
HttpHost http=new HttpHost(“127.0.0.1”,9200,“http”); ES也是数据源,和数据库一样,想操作它,首先得先建立连接
RestClientBuilder builder= RestClient.builder(http);//rest构建器
RestHighLevelClient restHighLevelClient=newRestHighLevelClient(builder);//高级客户端对象 这两行可以合并成一行
//2.封装请求对象 如果ID不存在则新增,如果ID存在则修改。
IndexRequest indexRequest=new IndexRequest(“sku”,“doc”,“3”); 参数分别为索引库名称,索引库类型和该条数据的id
Map skuMap =new HashMap();
skuMap.put(“name”,“华为p30pro”);
skuMap.put(“brandName”,“华为”);
skuMap.put(“categoryName”,“手机”);
indexRequest.source(skuMap); 将数据封装到indexRequest中
//3.获取响应结果
IndexResponse response = restHighLevelClient.index(indexRequest,RequestOptions.DEFAULT); index相当于插入数据库的insert
int status = response.status().getStatus();
System.out.println(status);
restHighLevelClient.close();
批处理请求:(批量插入,不用频繁的连接ES,提高插入效率)
BulkRequest: 批量请求(用于增删改操作)
BulkResponse:批量请求(用于增删改操作)
//1.连接rest接口
HttpHost http=new HttpHost(“127.0.0.1”,9200,“http”);
RestClientBuilder builder= RestClient.builder(http);//rest构建器
RestHighLevelClient restHighLevelClient=newRestHighLevelClient(builder);//高级客户端对象
//2.封装请求对象
BulkRequest bulkRequest=new BulkRequest();
从数据库将需要导入ES的数据都查询出来,然后循环
IndexRequest indexRequest=new IndexRequest(“sku”,“doc”,“4”);
Map skuMap =new HashMap();
skuMap.put(“name”,“华为p30pro 火爆上市”);
skuMap.put(“brandName”,“华为”);
skuMap.put(“categoryName”,“手机”);
indexRequest.source(skuMap); 每一条数据放到一个indexRequest中
bulkRequest.add(indexRequest);//可以多次添加 再将每一个indexRequest放到bulkRequest中
//3.获取响应结果
BulkResponse response =restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
int status = response.status().getStatus();
System.out.println(status);
String message = response.buildFailureMessage();
System.out.println(message);
restHighLevelClient.close();
匹配查询
SearchRequest: 查询请求对象
SearchResponse:查询响应对象
SearchSourceBuilder:查询源构建器
MatchQueryBuilder:匹配查询构建器
示例:查询商品名称包含手机的记录。
使用kibana操作:
GET /sku/doc/_search
{
“query”: {
“match”:{“name”:“手机”}
}
}
使用高级客户端操作:
//1.连接rest接口 相当于上面的GET请求
…
//2.封装查询请求
SearchRequest searchRequest=new SearchRequest(“sku”);
searchRequest.types(“doc”); //设置查询的类型
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder(); 相当于上面的query
MatchQueryBuilder matchQueryBuilder= QueryBuilders.matchQuery(“name”,“手机”); 相当于上面的match
searchSourceBuilder.query(matchQueryBuilder); 相当于上面的_search
searchRequest.source(searchSourceBuilder); 相当于最外面的大括号
//3.获取查询结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
SearchHits searchHits = searchResponse.getHits(); 相当于kibana结果里面最外面的Hits
long totalHits = searchHits.getTotalHits(); 总记录数
SearchHit[] hits = searchHits.getHits(); 得到的数据 相当于kibana里面一层的hits
for(SearchHit hit:hits){
String source = hit.getSourceAsString();
System.out.println(source);
}
restHighLevelClient.close();
布尔组合查询(这里组合的是匹配和精确查询)
示例:查询名称包含手机的,并且品牌为小米的记录
//1.连接rest接口 同上…
//2.封装查询请求
SearchRequest searchRequest=new SearchRequest(“sku”);
searchRequest.types(“doc”); //设置查询的类型
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//布尔查询构建器
MatchQueryBuilder matchQueryBuilder= QueryBuilders.matchQuery(“name”,“手机”);
boolQueryBuilder.must(matchQueryBuilder); //组合匹配查询
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(“brandName”,“小米”);
boolQueryBuilder.must(termQueryBuilder); //组合精确查询
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
//3.获取查询结果 同上…
过滤查询 相比匹配和精确查询,效率更高,不需要计算匹配度得分
示例:筛选品牌为小米的记录
//1.连接rest接口 同上…
//2.封装查询请求
SearchRequest searchRequest=new SearchRequest(“sku”);
searchRequest.types(“doc”); //设置查询的类型
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();//布尔查询构建器
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(“brandName”,“小米”);
boolQueryBuilder.filter(termQueryBuilder);
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
//3.获取查询结果 同上…
分组(聚合)查询
示例:按商品分类分组查询,求出每个分类的文档数
//1.连接rest接口 同上…
//2.封装查询请求
SearchRequest searchRequest=new SearchRequest(“sku”);
searchRequest.types(“doc”); //设置查询的类型
SearchSourceBuilder searchSourceBuilder=new SearchSourceBuilder();
//第一个参数为分组名称,第二个为需要聚合的字段名称
TermsAggregationBuilder termsAggregationBuilder =AggregationBuilders.terms(“sku_category”).field(“categoryName”);
searchSourceBuilder.aggregation(termsAggregationBuilder);
searchSourceBuilder.size(0);//不需要获取查询的文档,只需要聚合后的数据
searchRequest.source(searchSourceBuilder);
//3.获取查询结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest,RequestOptions.DEFAULT);
Aggregations aggregations = searchResponse.getAggregations();
Map
Terms terms = (Terms) asMap.get(“sku_category”);
List extends Terms.Bucket> buckets = terms.getBuckets();
for(Terms.Bucket bucket:buckets){
System.out.println(bucket.getKeyAsString()+":"+ bucket.getDocCount()
);
}
restHighLevelClient.close();
搜索分页
需求:每页显示30条记录,查询第3页内容。分页语法如下:
GET sku/_search
{
“from”: 60, 代表从第几条数据开始查, 而不是代表从第几页开始查
“size”: 30
}
分页代码
Integer pageNo = Integer.parseInt(searchMap.get(“pageNo”));//页码 Map的泛型为
Integer pageSize = 30; //每页记录数
Integer fromIndex = (pageNo-1)*pageSize; //开始查询的索引 代表从第几条数据开始查
//from,size和query是同级的
searchSourceBuilder.from(fromIndex);//开始索引设置
searchSourceBuilder.size(pageSize);//每页记录数设置
搜索排序
需求:按价格升序排序,语法如下:
GET sku/_search
{
“sort”: [
{
“price”: {
“order”: “asc” 如果是降序,则指定order为desc
}
}
]
}
代码如下:
//排序
String sortField= searchMap.get(“sortField”);//排序字段 和前端商量好,如果升序就传ASC,如果降序就传DESC
String sortOrder = searchMap.get(“sortOrder”);//排序规则
if(!"".equals(sortField)){
//searchSourceBuilder.sort(sort,SortOrder.DESC);//这种实际中不实用,将排序规则写死了
searchSourceBuilder.sort(sortField, SortOrder.valueOf(sortOrder)); //从前台获取排序规则
}
搜索高亮
所谓高亮,就是使用特别的样式修饰某字段中包含的搜索关键字。
需求:实现搜索高亮,商品名称使用红色显示搜索关键字。
使用默认高亮:
GET /sku/doc/_search
{
“query”: {
“match”: {
“name”:“手机”
}
},
“highlight”:{
“fields”: {
“name”: {} 默认的高亮
}
},
“size”: 2
}
返回的高亮部分:
“highlight” : {
“name” : [
“Apple 苹果 iPhone XR 手机 全网通4G手机 黑色 64GB”
]
}
高亮字段name对应的值为数组是为了数据类型的兼容,因为有可能name字段对应的值本来就是一个数组
自定义高亮 执行查询:
GET /sku/doc/_search
{
“query”: {
“match”: {
“name”:“手机”
}
},
“highlight”:{
“fields”: {
“name”: {
“pre_tags”:"", 前置标签 设置高亮的颜色
“post_tags”:"" 后置标签
}
}
},
“size”: 2
}
返回结果(高亮部分)
“highlight” : {
“name” : [
“Apple 苹果 iPhone XR 手机 全网通
4G手机 黑色 128GB”
]
}
高亮代码
//设置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field(“name”).preTags("").postTags(""); field为高亮字段
searchSourceBuilder.highlighter(highlightBuilder);
获取高亮结果:
for(SearchHit hit:hits){
//提取高亮内容
Map
HighlightField highlightFieldName = highlightFields.get(“name”);
Text[] fragments = highlightFieldName.fragments(); 搜素出来的高亮字段对应的是一个数组
String name = fragments[0].toString();
System.out.println(name);
}
代码实现
(1)修改SkuSearchServiceImpl的search方法,在第一个代码块中添加高亮显示处理代码
//设置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field(“name”).preTags("").postTags("");
searchSourceBuilder.highlighter(highlightBuilder);
修改商品列表部分代码
//2.1 商品列表
List
(3)修改模板中商品名称部分
因为返回给前端的数据中包含html代码,用text标签则会原样展示,需要使用utext标签来展示副文本
整体架构:
将不同类型的构建器放入搜索源构建器中,再将搜索源构建器放入SearchRequest中
比如:
MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery(“name”, searchMap.get(“keywords”));
boolQueryBuilder.must(matchQueryBuilder);
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(“categoryName”, searchMap.get(“category”));
boolQueryBuilder.filter(termQueryBuilder);
…可以组合不同类型的查询
searchSourceBuilder.query(boolQueryBuilder);
分页
searchSourceBuilder.from(fromIndex);//开始索引设置 from,size和query是同级的
searchSourceBuilder.size(pageSize);//每页记录数设置
排序
searchSourceBuilder.sort(sortField, SortOrder.valueOf(sortOrder));
高亮
searchSourceBuilder.highlighter(highlightBuilder);
聚合
searchSourceBuilder.aggregation(termsAggregationBuilder);
最后
searchRequest.source(searchSourceBuilder);
//3.获取查询结果
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHits searchHits = searchResponse.getHits();
… 对结果进行处理
Aggregations aggregations = searchResponse.getAggregations();//对分组查询结果的封装
Map