ElasticSearch
使用elasticsearch
启动 Elasticsearch:
cd elasticsearch-<version>
./bin/elasticsearch
Apache Lucene
全文检索(Full-text Search)
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
- 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
- 非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
当然有的地方还会提到第三种,半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。
非结构化数据又一种叫法叫全文数据。
按照数据的分类,搜索也分为两种:
- 对结构化数据的搜索:如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows搜索对文件名,类型,修改时间进行搜索等。
- 对非结构化数据的搜索:如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。
-
顺序扫描法(Serial Scanning):所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。
-
全文检索大体分两个过程,索引创建(Indexing)和搜索索引(Search)。
- 索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
- 搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。
基本概念
- 文档(document):索引和搜索时使用的主要数据载体,包含一个或多个存有数据的字段。
- 字段(field):文档的一部分,包含名称和值两部分。
- 词(term):一个搜索单元,表示文本中的一个词。
- 标记(token):表示在字段文本中出现的词,由这个词的文本、开始和结束偏移量以及类
型组成。
倒排索引
倒排索引建立索引中词和文档之间的映射,数据是面向词而不是面向文档。
ElasticSearch
- 一个分布式的实时文档存储,每个字段可以被索引与搜索
- 一个分布式实时分析搜索引擎
- 能胜任上百个服务节点的扩展,并支持PB级别的结构化或者非结构化数据
MySql与ES概念对比
ElasticSearch | MySQL |
---|---|
index(索引,名词) | database |
doc type(文档类型) | table |
document(文档) | row |
field(字段) | column |
mapping(映射) | schema |
query DSL(查询语言) | SQL |
基本概念
1)Cluster:集群
ES可以作为一个独立的单个搜索服务器。不过,为了处理大型数据集,实现容错和高可用性,ES可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。
2)Node:节点
形成集群的每个服务器称为节点。
3)Shard:分片
当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。
当你查询的索引分布在多个分片上时,ES会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的。
4)Replia:副本
为提高查询吞吐量或实现高可用性,可以使用分片副本。
副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。
当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。
创建
Elasticsearch数据类型
Elasticsearch自带的数据类型数Lucene索引的依据,也是我们做手动映射调整到依据。
映射中主要就是针对字段设置类型以及类型相关参数。
JSON基础类型如下:
- 字符串:string
- 数字:byte、short、integer、long、float、double、
- 时间:date
- 布尔值: true、false
- 数组: array
- 对象: object
Elasticsearch独有的类型:
- 多重: multi
- 经纬度: geo_point
- 网络地址: ip
- 堆叠对象: nested object
- 二进制: binary
- 附件: attachment
注意点:
Elasticsearch映射虽然有idnex和type两层关系,但是实际索引时是以index为基础的。如果同一个index下不同type的字段出现mapping不一致的情况,虽然数据依然可以成功写入并生成并生成各自的mapping,但实际上fielddata中的索引结果却依然是以index内第一个mapping类型来生成的。
精确索引:
字段都有几个基本的映射选项,类型(type)和索引方式(index)。以字符串类型为例,index有三个选项:
-
analyzed:默认选项,以标准的全文索引方式,分析字符串,完成索引。
-
not_analyzed:精确索引,不对字符串做分析,直接索引字段数据的精确内容。
-
no:不索引该字段。
mapping
mapping是类似于数据库中的表结构定义,主要作用如下:
- 定义index下的字段名
- 定义字段类型,比如数值型、浮点型、布尔型等
- 定义倒排索引相关的设置,比如是否索引、记录position等
Text vs Keyword
数据类型被用来索引长文本,比如说电子邮件的主体部分或者一款产品的介绍。这些文本会被分析,在建立索引前会将这些文本进行分词,转化为词的组合,建立索引。允许 ES来检索这些词语。text 数据类型不能用来排序和聚合。
Keyword 数据类型用来建立电子邮箱地址、姓名、邮政编码和标签等数据,不需要进行分词。可以被用来检索过滤、排序和聚合。keyword 类型字段只能用本身来进行检索。
查询
Apache Lucene评分机制
默认评分机制
TF/IDF(词频/逆文档频率)算法
基本原则
- 匹配到的关键词越稀有,文档的得分就越高。
- 文档的域越小(包含比较少的Term),文档的得分就越高。
- 设置的权重(索引和搜索时设置的都可以)越大,文档得分越高。
分页和结果集大小
-
from:该属性指定我们希望在结果中返回的起始文档。它的默认值是0,表示想要得到从
第一个文档开始的结果。 -
size:该属性指定了一次查询中返回的最大文档数,默认值为10。如果只对切面结果感兴趣,并不关心文档本身,可以把这个参数设置成0。
基本查询
过滤 is not null
"exists": {
"field": "gmtCreate"
}
返回版本值
"version": true
query_string和排序
GET /course_audit_aliased/course_audit_aliased/_search
{
"query": {
"query_string": {
"default_field": "courseName",
"query": "1"
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"lastSubmitReviewTime": {
"order": "asc"
}
}
]
}
结构化查询 Query DSL (Domain Specific Language)
term vs match
- term是精确查询,搜索前不会再对搜索词进行分词
- match是模糊查询
terms类似mysql的in
"terms" : {
"price" : [20, 30]
}
multi-match
{
"dis_max": {
"queries": [
{
"match": {
"title": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
{
"match": {
"body": {
"query": "Quick brown fox",
"minimum_should_match": "30%"
}
}
},
],
"tie_breaker": 0.3
}
}
{
"multi_match": {
"query": "Quick brown fox",
"type": "best_fields",
"fields": [ "title", "body" ],
"tie_breaker": 0.3,
"minimum_should_match": "30%"
}
}
bool
Bool查询包括四种子句,must,filter,should, must_not。
filter快在两个方面:
- 对结果进行缓存
- 避免计算分值
{
"bool" : {
"must" : {
"term" : { "user" : "kimchy" }
},
"filter": {
"term" : { "tag" : "tech" }
},
"must_not" : {
"range" : {
"age" : { "from" : 10, "to" : 20 }
}
},
"should" : [
{
"term" : { "tag" : "wow" }
},
{
"term" : { "tag" : "elasticsearch" }
}
]
}
}
range
- gt 大于
- gte 大于等于
- lt 小于
- lte 小于等于
"query": {
"range": {
"status": {
"gte": 3,
"lte": 20
}
}
}
"query": {
"bool": {
"must": [
{
"term": {
"courseName": {
"value": "1"
}
}
},
{
"range": {
"status": {
"lte": 1
}
}
}
]
}
}
高亮
高亮器
public Page<CourseAuditESDoc> pageByStatus(Integer status, Integer liveFlag, String courseName, String ownerName, PaginationBaseQuery paginationBaseQuery, Sort sort) {
int pageIndex = paginationBaseQuery.getPageIndex();
int pageSize = paginationBaseQuery.getPageSize();
if (pageIndex <= 0) {
pageIndex = 1;
}
if (pageSize <= 0) {
pageSize = 20;
}
// 高亮
String preTag = "<font color='#dd4b39'>";
String postTag = "</font>";
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (Objects.nonNull(status)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("status", status));
}
if (Objects.nonNull(liveFlag)) {
boolQueryBuilder.filter(QueryBuilders.termQuery("liveFlag", liveFlag));
}
nativeSearchQueryBuilder.withFilter(boolQueryBuilder);
int count = 0;
boolean hasCourseName = StringUtils.isNotBlank(courseName);
boolean hasOwnerName = StringUtils.isNotBlank(ownerName);
if (hasCourseName) {
count++;
}
if (hasOwnerName) {
count++;
}
if (count > 0) {
sort.and(new Sort(Sort.Direction.DESC, "_score"));
HighlightBuilder.Field[] fields = new HighlightBuilder.Field[count];
BoolQueryBuilder queryBuilders = QueryBuilders.boolQuery();
int index = 0;
if (hasCourseName) {
queryBuilders.must(QueryBuilders.matchQuery("courseName", courseName));
fields[index] = new HighlightBuilder.Field("courseName").preTags(preTag).postTags(postTag).highlighterType("unified");
index++;
}
if (hasOwnerName) {
queryBuilders.must(QueryBuilders.matchQuery("ownerName", ownerName));
fields[index] = new HighlightBuilder.Field("ownerName").preTags(preTag).postTags(postTag).highlighterType("unified");
index++;
}
nativeSearchQueryBuilder.withQuery(queryBuilders);
nativeSearchQueryBuilder.withHighlightFields(fields);
}
// 设置分页。Spring data的pageIndex和我们内部的差1
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageIndex - 1, pageSize, sort));
Page<CourseAuditESDoc> result = elasticsearchOperations.queryForPage(nativeSearchQueryBuilder.build(),
CourseAuditESDoc.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
List<CourseAuditESDoc> docList = new ArrayList<>();
for (SearchHit searchHit : response.getHits()) {
Map<String, Object> result = searchHit.getSource();
Gson gson = new Gson();
JsonElement jsonElement = gson.toJsonTree(result);
CourseAuditESDoc doc = gson.fromJson(jsonElement, CourseAuditESDoc.class);
doc.setId(searchHit.getId());
Map<String, HighlightField> map = searchHit.getHighlightFields();
if (MapUtils.isNotEmpty(map)) {
HighlightField courseNameFiled = map.get("courseName");
if (Objects.nonNull(courseNameFiled)) {
doc.setCourseName(courseNameFiled.fragments()[0].toString());
}
HighlightField ownerNameFiled = map.get("ownerName");
if (Objects.nonNull(ownerNameFiled)) {
doc.setOwnerName(ownerNameFiled.fragments()[0].toString());
}
}
docList.add(doc);
}
if (docList.size() > 0) {
return new AggregatedPageImpl<>((List<T>) docList, pageable, response.getHits().totalHits);
}
return null;
}
});
return result;
}
{
"query": {
"match": {
"courseName": "1"
}
},
"highlight": {
"pre_tags": [
"<b>"
],
"post_tags": [
"</b>"
],
"fields": {
"courseName": {}
}
}
}
删除文档
POST /course_audit_aliased/course_audit_aliased/_delete_by_query
{
"query": {
"match_all": {}
}
}
分词
ik分词器
基于_version进行乐观锁并发控制
准实时
要把数据写到磁盘,需要调用 fsync,但是fsync十分耗资源,无法频繁的调用,在这种情况下,Elasticsearch 利用了filesystem cache,新文档先写到in-memory buffer,然后写入到 filesystem cache,过一段时间后,再将segment写到磁盘。在这个过程中,只要文档写到filesystem cache,就可以被搜索到了。