ElasticSearch

Mysql存在的问题

  1. 性能低下
  2. 没有相关性排名-刚需
  3. 无法全文搜索
  4. 搜索不准确,没有分词

全文搜索

生活中数据分为两种类型:结构化数据非结构化数据

  • 结构化数据:指固定的或有限长度的数据,如数据库,元数据
  • 非结构化数据:指不定长度或无固定格式的数据:邮件、文档

ElasticSearch简介

ElasticSearch是一个分布式可扩展的实时搜索引擎,一个建立在全文搜索引擎(Apache Lucence)基础上的搜索引擎,ElasticSearch包括如下功能:

  • 分布式文件存储,每将一个字段编入索引,都可以被搜索
  • 实时分析的分布式搜索引擎
  • 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据

通过docker安装elasticsearch

# 新建es的config配置文件
mkdir -p /data/elasticsearch/config
# 新建es的data目录
mkdir -p /data/elasticsearch/data
# 给目录设置权限
chmod 777 -R /data/elasticsearch
# 写入配置到elasticsearch.yaml中
echo "http.host: 0.0.0.0" >> /data/elasticsearch/config/elasticsearch.yml

 sudo docker run --name elasticsearch -p 9200:9200  -p 9300:9300 \
 -e "discovery.type=single-node" \
 -e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
 -v /data/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
 -v /data/elasticsearch/data:/usr/share/elasticsearch/data \
 -v /data/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
 -d elasticsearch:7.10.1
############################################################################
 -p 端口映射
 -e discovery.type=single-node 单点模式启动
 -e ES_JAVA_OPTS="-Xms84m -Xmx512m":设置启动占用的内存范围
 -v 目录挂载
 -d 后台运行

启动过程中es报错,查看日志信息:

image-20210921154118354

百度查看问题,原来是echo "http.host:0.0.0.0" >> /data/elasticsearch/config/elasticsearch.yml,之间没有存在空格,正常情况下yaml文件都是有空格的。

docker 安装kibana

docker run -d --name kibana -e ELASTICSEARCH_HOSTS="http://101.34.18.9:9200" -p 5601:5601 kibana:7.10.1

es中的type、index、mapping和dsl

image-20210921155722094

使用Kibana操作es

创建数据

查看索引

image-20210921160623984

通过put+id新建数据

image-20210921161012966

put操作必须带id,如果数据不存在就新建数据,存在就更新数据,同时version+1

Post创建信息

image-20210921161352262

Post如果带id的话和put操作会一致,put是必须要带id

# 如果使用post+id或put+id去更新字段的话,那么如果字段之前不存在会直接覆盖
	#需要使用_update去更新,更新内容放到需要更新的结构里面:
	POST account/_update/1
    {
      "doc":{
        "sex":"famale"
      }
    }
    //如果值相等的话,那么是没有执行操作的

post+_create

image-20210921161632285

获取数据

image-20210921162006780

如果只想获取信息,那么可以直接 GET account/_source/1

条件查询

通过url查询,请求参数位于_search之后,参数之间使用&分割

image-20210921162508550

使用query body进行查询

image-20210921162800478

删除数据

# 删除索引
DELETE /user
#删除数据
DELETE /user/_doc/1

批量操作lbluk

POST _bulk
{"index":{"_index":"user", "_id":"1"}}
{"name":"gewei1"}
{"index":{"_index":"user", "_id":"2"}}
{"name":"gewei2"}

# es中批量插入语句之间互不影响,第一条插入失败后也不影响后面的语句执行

批量获取

GET /_mget
{
	"docs":[
		{
			"_index":"user",
			"_id":"1"
		},
		{
			"_index":"account",
			"_id":"1"
		}
	]
}

复杂查询

一个复杂的查询应该如何书写那?

# 普通的使用url查询只适合简单的查询操作,功能性较差
GET url/_search
{
	"query":{
		"match_all"{
		
		}
	}
	"from":4,
	"size":4
}

# 对于es来说,from和size在数据量比较小的情况下可行,数据量大的情况下使用scoll

match

我们用第一个示例来解释使用 match 查询搜索全文字段中的单个词:

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "title": "QUICK!"
        }
    }
}

# 大小写不敏感

match:模糊匹配,需要指定字段名,输入会进行分词,比如"hello world"会被拆分为hello 和world,如果字段中包含这两个都会被查询出来,匹配条件较为宽松。

match_phrase
GET /my_index/my_type/_search
{
    "query": {
        "match_phrase": {
            "title": "hello world"
        }
    }
}

match_phrase:同样也会对查询进行分词,但要求结果中也要包含所有的分词,而且要求顺序一致,以"hello world"为例,要求结果中必须含有hello world,而且顺序也是固定的hello that world不满足条件。

multi_match

multi_match查询提供了一个简便的方法对多个字段执行相同的查询。

GET resume/_search
{
	"query":{
		"multi_match":{
			"query":"go",
			"fileds":["title","desc"]
		}
	}
}
# 对title和desc字段进行查询go

可以对其字段的权重进行改变

GET resume/_search
{
	"query":{
		"multi_match":{
			"query":"go",
			"fileds":["title^2","desc"]
		}
	}
}
# 将title的权重*2
query_string

query_string和match类似,但是match需要指定字段名,query_string是在所有的字段中进行搜索,搜索范围更广。

GET /my_index/my_type/_search
{
    "query": {
        "query_string": {
            "query": "hello world"
        }
    }
}
# 可以使用OR 和AND 进行修饰,查询更为灵活
# 使用default_field可以对指定的字段进行查询
match_all

查询所有数据

GET /my_index/my_type/_search
{
    "query": {
        "match_all": {
            
        }
    }
}

term级别查询

term

使用term进行查询时,不会去分词

GET /_search
{
    "query": {
        "term": {
            "user.id":{
            	"value":"kimi",
            	"boost":1.0
            }
        }
    }
}

# es支持嵌套查询,user.id表示user下的id属性
# boost为权重
# 使用term中可能会遇到一些坑

- 查询大写字母的单词查询不到,这是因为倒排索引是将单词小写后进行的插入。
range
GET /_search
{
    "query": {
        "range": {
            "age":{
            	"gte":10,
            	"lte":20,
            	"boost":2.0
            }
        }
    }
}
exists

查询某个字段存在的字段

GET /_search
{
    "query": {
        "exists": {
           "field":"age",
        }
    }
}
fuzzy

模糊查询

GET /_search
{
    "query": {
        "fuzzy": {
           "address":"streat",
        }
    }
}

当然使用match也可以开启模糊查询

GET /my_index/my_type/_search
{
    "query": {
        "match": {
            "address":{
            	"query":"midison streaet",
            	"fuzziness":1
            }
        }
    }
}
# 使用match开启模糊查询需要开启
bool

bool用于es的复合查询

{
"query":{
	"must":[],
	"should":[],
	"must_not":[],
	"filter":[]
}
}

# 使用should查询是让符合条件的得分更高
# must和filter条件差不多,只不过filter和must_not不影响分数
# bool 查询采用了一种匹配越多越好的方法
GET /my_index/my_type/_search
{
    "query": {
        "bool": {
            "must":[
                {
                    "term": {
                    	"state":"tn"
                    }
                },
                {
                	"range":{
                		"age":{
                			"gte":10,
                			"lte":20,
                		}
                	}
                }
                
        	]
        	"must_not":[
        		"term":{
        			"gender":"m"
        		}
        	]
        	"should":[
        		"match":{
        			"firstname":"Decker"
        		}
        	]
        }
    }
}
trems query
GET _search
{
	"query":{
		"ids":{
			"values":["1", "2", "3"]
		}
	}
}
GET _search
{
	"query":{
		"terms":{
			"user.id":["1", "2", "3"],
			"boost":1.0
		}
	}
}

倒排索引

image-20210921171040023

mapping

什么是Mapping?

用于定义索引中的字段名称和定义字段的数据类型,比如字符串,数字,布尔,倒排索引相关类型。

# text和keyword的区别是什么?

比如我们添加一个字段
POST user/_doc
{
	"address":"beijing"
}
此时es会自动添加一个字段
{
	"address":{
		"value":"beijing",
		"keyword":"beijing"
	}
}

上面添加的类型为text类型,下面的是keyword类型。keyword类型不会进行分词

所以我们对详细信息可以这样查询

GET user/_search
{
	"query":{
		"match":{
			"address.keyword":"671 Bristol Street"
		}
	}
}

创建mapping

image-20210921183508408

添加一条数据

image-20210921183736208

此时使用match查询yonyou 1988 可以查询到数据,这是为什么那?match不是会分词吗?

ElasticSearch Analyzer

Character Filters

Character Filters字符过滤器接受原始文本text的字符流,可以对原始文本增加、删除字段或者对原始字段进行转换。

# 一个Analyzer分析器可以有0-n个按顺序执行的`字符过滤器`

Tokenizer

Tokenizer 接收Character Filters输出的字符流,将字符流分解成单词,并且输出单词流,例如空格分词器将文本进行分解,"Quick brown fox!"转换成[Quick, brown, fox!],分词器也记录每个单词的位置和该单词在原始文本中的位置和结束偏移量offsets

# 一个Analyer只有一个tokenzer

TokenFilter

接收单词流,将单词进行添加、删除和转换,例如将[Quick, brown, fox!]全部转换为小写,并且删除 "!"。synonym token filter会往单词流中添加单词的同义词。

# 一个Analyzer分析器可以有0-n个按顺序执行的`单词过滤器`
image-20210921185115977

使用_analyze可以查看分词信息

GET _analyze
{
	"analyzer":"simple",
	"text":"The 2 QUICK Brown-Foxes jumped over"
}

指定分词器

GET user/_search
{
	"query":{
		"match":{
			"desc":{
				"query":"671 Bristol Street",
				"analyzer":"keyword"
			}
		}
	}
}

怎么决定使用哪个Analyzer那?

  • 如果查询条件指明了使用哪个Analyzer,则优先使用
  • search_analyzer同样可以指定
  • 使用默认的或者一开始配置好的Analyzer进行分词

分词

分词在进行查询的过程中非常重要,而对于中文分词较为困难,使用默认的simple分词器会出现很多问题:

image-20210921190651637

ik分词器

安装:

1. 从github上下载ik分词器,版本号要对应,对其解压
2. 拷贝文件到es的plugins文件夹中
3.设置权限
chmod 777 -R ik
4 重启容器
docker restartd 20070996292

可以看到,我们做了文件的映射,将ik分词器拷贝到本地,镜像中也会有ik文件。

image-20210921192632956

进入容器中,可以看到ik分词器已经被安装了。

默认分词

image-20210921232556211

使用ik分词器进行分词

image-20210921232719297

扩大词库

自己配置词库,在ik分词器的config目录下进行进行修改,将“中华牙膏”,“慕课网”加入到词库中,同时将 的、了、排除分词

修改IKAnalyzer.cfg.xml即可

image-20210921235453240

重启es之后生效

image-20210921235653512

可以看到慕课网被成功的加入到了词库中。

指定分词器

put newCn
{
	"mappings":{
		"properties":{
			"name":{
				"type":"text",
				"analyzer":"ik_max_word",
				"search_analyzer":"standard"
			}
		}
	}
}

Go语言操作ES

# Go语言操作es的苦为github.com/olivere/elastic/v7
	# 但是这个库的文档介绍的不是很详细。不过我们可以通过提供的代码找到我们需要的api

首先我们观察下查询出来的数据结构:

image-20210922193851717

可以看到查询结果存放在hits里面,hits里面又包含了众多小hits,数据就放在hits下面的_source中。

func main() {
   //初始化一个连接
   host := "http://101.34.18.9:9200"
   //这里必须指定sniff为false,因为使用olivere/elastic链接es时,发现明明输入的是网址但是连接时
   //会自动转换为内网地址,或者是docker中的ip地址,导致链接不上
   client, err := elastic.NewClient(elastic.SetURL(host), elastic.SetSniff(false))
   if err != nil {
      panic(err)
   }
   q := elastic.NewMatchQuery("name", "gewei1")
   result, err := client.Search().Index("user").Query(q).Do(context.Background())
   if err != nil {
      panic(err)
   }
   total := result.Hits.TotalHits.Value
   fmt.Printf("搜索结果数量是:%d\n", total)
   for _, value := range result.Hits.Hits {
      if jsonDate, err := value.Source.MarshalJSON(); err != nil {
         panic(err)
      } else {
         fmt.Println(string(jsonDate))
      }
   }

}

将es数据保存到struct中

type Account struct{
	AccountNumber int32 `json:"account_number"`
	FirstName string `json:"firstname"`
}
  for _ , value := range result.Hits.Hits {
  		account := Account{}
  		_ = json.Unmarshal(value.Source,&account)
  }

插入数据

account := Account{AccountNumber: 9527, FirstName: "zhouzhou"}
put1, err := client.Index().Index("xingye").BodyJson(account).Do(context.Background())
if err != nil {
   panic(err)
}
fmt.Printf("Index xingye %s  to index %s, type: %s", put1.Id, put1.Index, put1.Type)

设置日志

//创建一个日志对象
var logger = log.New(os.Stdout, "biubiu_",log.LstdFlags)
//将日志放入es中
client, err := elastic.NewClient(elastic.SetURL(host), elastic.SetSniff(false), elastic.SetTraceLog(logger))

新建mapping

参考文档

const goodsMapping = `
{
	"settings":{
		"number_of_shards": 1,
		"number_of_replicas": 0
	},
	"mappings":{
	//index的名称
		"mygoods":{
			"properties":{
				"name":{
					"type":"text",
					"analyzer":"ik_max_word"
				},
				"id":{
					"type":"interger"
				}
			}
		}
	}
}`
"number_of_shards": 分片数量
"number_of_replicas": 副本数量 
# 这两个一般不需要修改
createIndex, err := client.CreateIndex("mygoods").BodyString(goodsMapping).Do(context.Background())
if err != nil {
   // Handle error
   panic(err)
}
if !createIndex.Acknowledged {
   // Not acknowledged
}

bool查询

func TestBoolQuery(t *testing.T) {
	q := NewBoolQuery()
	q = q.Must(NewTermQuery("tag", "wow"))
	q = q.MustNot(NewRangeQuery("age").From(10).To(20))
	q = q.Filter(NewTermQuery("account", "1"))
	q = q.Should(NewTermQuery("tag", "sometag"), NewTermQuery("tag", "sometagtag"))
	q = q.Boost(10)
	q = q.QueryName("Test")
	src, err := q.Source()
	if err != nil {
		t.Fatal(err)
	}
	data, err := json.Marshal(src)
	if err != nil {
		t.Fatalf("marshaling to JSON failed: %v", err)
	}
	got := string(data)
	expected := `{"bool":{"_name":"Test","boost":10,"filter":{"term":{"account":"1"}},"must":{"term":{"tag":"wow"}},"must_not":{"range":{"age":{"from":10,"include_lower":true,"include_upper":true,"to":20}}},"should":[{"term":{"tag":"sometag"}},{"term":{"tag":"sometagtag"}}]}}`
	if got != expected {
		t.Errorf("expected\n%s\n,got:\n%s", expected, got)
	}
}

Q.E.D.


勤俭节约,艰苦奋斗。