虽然全文搜索领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库。
但是,Lucene只是一个库。想要使用它,你必须使用Java来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene的配置及使用非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的。
实际项目中,我们建立一个网站或应用程序,并要添加搜索功能,令我们受打击的是:搜索工作是很难的。我们希望我们的搜索解决方案要快,我们希望有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用JSON/XML通过HTTP的索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并在需要扩容时方便地扩展到数百,我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。
ES即为了解决原生Lucene使用的不足,优化Lucene的调用方式,并实现了高可用的分布式集群的搜索方案,其第一个版本于2010年2月出现在GitHub上并迅速成为最受欢迎的项目之一。
首先,ES的索引库管理支持依然是基于Apache Lucene™的开源搜索引擎。
ES也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能,但是它的目的是通过简单的 RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
不过,ES的核心不在于Lucene,其特点更多的体现为:
ES服务只依赖于JDK,推荐使用JDK1.7+。
(1)下载ES安装包:ES官方下载地址
(2)运行ES (双击bin目录下的elasticsearch.bat)
(3)验证是否运行成功,访问:http://localhost:9200/
如果看到如下信息,则说明ES集群已经启动并且正常运行。
(1)基于RESTful API
ES和所有客户端的交互都是使用JSON格式的数据.
其他所有程序语言都可以使用RESTful API,通过9200端口的与ES进行通信,在开发测试阶段,你可以使用你喜欢的WEB客户端, curl命令以及火狐的POSTER插件方式和ES通信。
Curl命令方式:
默认windows下不支持curl命令,在资料中有curl的工具及简单使用说明。
火狐的POSTER插件界面:
类似于Firebug,在火狐的“扩展”中搜索“POSTER”,并安装改扩展工具。
使用POSTER模拟请求的效果
(2)Java API
ES为Java用户提供了两种内置客户端:
节点客户端(node client):
节点客户端以无数据节点(none data node)身份加入集群,换言之,它自己不存储任何数据,但是它知道数据在集群中的具体位置,并且能够直接转发请求到对应的节点上。
传输客户端(Transport client):
这个更轻量的传输客户端能够发送请求到远程集群。它自己不加入集群,只是简单转发请求给集群中的节点。
两个Java客户端都通过9300端口与集群交互,使用ES传输协议(ES Transport Protocol)。集群中的节点
之间也通过9300端口进行通信。如果此端口未开放,你的节点将不能组成集群。
注意
Java客户端所在的ES版本必须与集群中其他节点一致,否则,它们可能互相无法识别。
(1)Kibana5.2.2下载地址:Kibana官方下载地址
(2)解压并编辑config/kibana.yml,设置elasticsearch.url的值为已启动的ES
(3)启动Kibana5 (在bin目录下双击kibana.bat)
(4)验证是否成功,默认访问地址:http://localhost:5601
(1)进入head文件中,输入cmd,打开控制台,输入npm install进行安装。
(2)安装完成,输入命令npm run start启动服务
(3)配置允许跨域访问,在elasticsearch/config/elasticsearch.yml文件末尾加上
http.cors.enabled: true
http.cors.allow-origin: “*”
(4)重启elasticsearch服务,访问http://localhost:9100
#新增
PUT crm/department/1
{
"name":"财务部",
"sn":"cwb"
}
#查询
GET crm/department/1
#修改
POST crm/department/1
{
"name":"人事部",
"sn":"rsb"
}
#删除
DELETE crm/department/1
#如果没有指定id,则自动生成id
POST crm/department
{
"name":"开发部",
"sn":"kfb"
}
# AW8jt-oSqTn8hjKcCo2i
GET crm/department/AW8jt-oSqTn8hjKcCo2i
# 查询全部
GET _search
# 查询所有
GET _search
#打印出漂亮格式
GET crm/department/AW8jt-oSqTn8hjKcCo2i?pretty
#指定返回的列
GET crm/department/AW8jt-oSqTn8hjKcCo2i?_source=name,sn
#不要元数据 只返回具体数据
GET crm/department/AW8jt-oSqTn8hjKcCo2i/_source
#修改 --覆盖以前json
POST crm/department/AW8jt-oSqTn8hjKcCo2i
{
"name":"市场部"
}
#局部更新
POST crm/department/AW8jt-oSqTn8hjKcCo2i/_update
{
"doc":{
"name":"管理部"
}
}
POST _bulk
{ "delete": { "_index": "xlj", "_type": "department", "_id": "123" }}
{ "create": { "_index": "xlj", "_type": "book", "_id": "123" }}
{ "title": "我发行的第一本书" }
{ "index": { "_index": "itsource", "_type": "book" }}
{ "title": "我发行的第二本书" }
# 普通查询:
GET crm/department/id
# 批量查询:
GET xlj/book/_mget
{
"ids" : [ "123", "AH8ht-oSqTn8hjKcHo2i" ]
}
# 从第0条开始查询3条student信息
GET crm/student/_search?size=3
# 从第2条开始查询2条student信息
GET crm/student/_search?from=2&size=2
# 表示查询age=15的人
GET crm/student/_search?q=age:15
# 查询3条student的信息,他们的age范围到10到20
GET crm/student/_search?size=3&q=age[10 TO 20]
由ES提供丰富且灵活的查询语言叫做DSL查询(Query DSL),它允许你构建更加复杂、强大的查询。
DSL(Domain Specific Language特定领域语言)以JSON请求体的形式出现。
DSL分成两部分:DSL查询 、DSL过滤。
(1)过滤结果可以缓存并应用到后续请求。
(2)查询语句同时匹配文档,计算相关性,所以更耗时,且不缓存。
(3)过滤语句可有效地配合查询语句完成文档过滤。
总之在原则上,使用DSL查询做全文本搜索或其他需要进行相关性评分的场景,其它全用DSL过滤。
{
"query" : {
"match_all" : {}
}
}
如果需要使用过滤条件:
{
"query": {
"bool": {
"must": [
{
"match_all": {}
}
],
"filter": {
"term": {
"name": "zs1"
}
}
}
}
}
match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。
如果你使用match查询一个全文本字段,它会在真正查询之前用分析器先分析查询字符:
{
"query": {
"match": {
"fullName": "Steven King"
}
}
}
上面的搜索会对Steven King分词,并找到包含Steven或King的文档,然后给出排序分值。如果用match下指定了一个确切值,在遇到数字,日期,布尔值或者not_analyzed的字符串时,它将会为你搜索你给定的值,如:
{ "match": { "age": 26 }}
{ "match": { "date": "2014-09-01" }}
{ "match": { "public": true }}
{ "match": { "tag": "full_text" }}
multi_match查询允许你做match查询的基础上同时搜索多个字段:
{
"query":{
"multi_match": {
"query": "Steven King",
"fields": [ "fullName", "title" ]
}
}
}
fullName = ‘steven King’ or tile = ‘steven King’
上面的搜索同时在fullName和title字段中匹配。
提示:match一般只用于全文字段的匹配与查询,一般不用于过滤。
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"term": {
"tags": "elasticsearch"
}
}
}
}
}
Terms搜索与过滤:
{
"query": {
"terms": {
"tags": ["jvm", "hadoop", "lucene"],
"minimum_match": 2
}
}
}
minimum_match:至少匹配个数,默认为1
组合搜索bool可以组合多个查询条件为一个查询对象,查询条件包括must、should和must_not。
例如:查询爱好有篮球,同时也有喜欢游戏或者运动的,且出生于1996-09-02及之后的人。
{
"query": {
"bool": {
"must": [{"term": {"hobby": "篮球"}}],
"should": [{"term": {"hobby": "游戏"}},
{"term": {"hobby": "运动"}}
],
"must_not": [
{"range" :{"birth_date":{"lt": "1996-09-02"}}}
],
"filter": [...],
"minimum_should_match": 1
}
}
}
注意:如果bool查询下没有must子句,那至少应该有一个should自居。但是如果有must子句,那么没有should子句也可以进行查询。
range过滤允许我们按照指定范围查找一批数据:
{
"query":{
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
}
上例中查询年龄大于等于20并且小于30的人
gt:> gte:>= lt:< lte:<=
{
"query": {
"bool": {
"must": [{
"match_all": {}
}],
"filter": {
"exists": { "field": "gps" }
}
}
}
}
提示:exists和missing只能用于过滤结果。
和term查询相似,前匹配搜索不是精确匹配,而是类似于SQL中的like ‘key%’
{
"query": {
"prefix": {
"fullName": "王"
}
}
}
上例即查询所有姓王的人。
使用*代表0~N个,使用?代表一个。
{
"query": {
"wildcard": {
"fullName": "王*华"
}
}
}
表示查询所有姓名以王开头华结尾的人。
在全文检索理论中,文档的查询是通过关键字查询文档索引来进行匹配,因此将文本拆分为有意义的单词,对于搜索结果的准确性至关重要,因此,在建立索引的过程中和分析搜索语句的过程中都需要对文本串分词。
ES中分词需要对具体字段指定分词器等细节,因此需要在文档的映射中明确指出。
ES默认对英文文本的分词器支持较好,但和lucene一样,如果需要对中文进行全文检索,那么需要使用中文分词器,同lucene一样,在使用中文全文检索前,需要集成IK分词器。
POST _analyze
{
"analyzer":"ik_smart",
"text":"中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
}
注意:IK分词器有两种类型,分别是ik_smart分词器和ik_max_word分词器。
ES的文档映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型。
(1)基本字段类型
{user:{“key”:value}}
{hobbys:[xxx,xx]}
(2)复杂数据类型
查看索引类型的映射配置:GET {indexName}/_mapping/{typeName}
ES在没有配置Mapping的情况下新增文档,ES会尝试对字段类型进行猜测,并动态生成字段和类型的映射关系。
JSON type | Field type |
---|---|
Boolean: true or false | “boolean” |
Whole number: 123 | “long” |
Floating point: 123.45 | “double” |
String, valid date:“2014-09-15” | “date” |
String: “foo bar” | “string” |
字段映射的常用属性配置列表:
type | 类型:基本数据类型,integer,long,date,boolean,keyword,text… |
enable | 是否启用:默认为true。 false:不能索引、不能搜索过滤,仅在_source中存储 |
boost | 权重提升倍数:用于查询时加权计算最终的得分 |
format | 格式:一般用于指定日期格式,如 yyyy-MM-dd HH:mm:ss.SSS |
ignore_above | 长度限制:长度大于该值的字符串将不会被索引和存储 |
ignore_malformed | 转换错误忽略:true代表当格式转换错误时,忽略该值,被忽略后不会被存储和索引 |
include_in_all | 转换错误忽略:true代表当格式转换错误时,忽略该值,被忽略后不会被存储和索引 |
null_value | 默认控制替换值。如空字符串替换为”NULL”,空数字替换为-1 |
store | 是否存储:默认为false。true意义不大,因为_source中已有数据 |
index | 索引模式:analyzed (索引并分词,text默认模式), not_analyzed (索引不分词,keyword默认模式),no(不索引) |
analyzer | 索引分词器:索引创建时使用的分词器,如ik_smart,ik_max_word,standard |
search_analyzer | 搜索分词器:搜索该字段的值时,传入的查询内容的分词器 |
fields | 多字段索引:当对该字段需要使用多种索引模式时使用。有些类型 有时候需要分词 有时候不需要分词 |
如:城市搜索New York 下面字段city既可以分词有可以不分词
"city": {
"type": "text",
"analyzer": "ik_smart",
"fields": {
"raw": {
"type": "keyword"
}
}
}
city分词
city.raw 不分词
那么以后搜索过滤和排序就可以使用city.raw字段名
(1)针对单个类型的映射配置方式
{
"goods": {
"properties": {
"price": {
"type": "integer"
},
"name": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
}
}
}
}
d.加入数据
put shop/goods/1
{
"price":6888,
"name": "iphone8"
}
注意:你可以在第一次创建索引的时候指定映射的类型。此外,你也可以晚些时候为新类型添加映射(或者为已有的类型更新映射)。
你可以向已有映射中增加字段,但你不能修改它。如果一个字段在映射中已经存在,这可能意味着那个字段的数据已经被索引。如果你改变了字段映射,那已经被索引的数据将错误并且不能被正确的搜索到。
(2)同时对多个类型的映射配置方式
PUT {indexName}
{
"mappings": {
"user": {
"properties": {
"id": {
"type": "integer"
},
"info": {
"type": "text",
"analyzer": "ik_smart",
"search_analyzer"
}
}
},
"dept": {
"properties": {
"id": {
"type": "integer"
},
....更多字段映射配置
}
}
}
}
(1)对象的映射与索引
{
“id” : 1,
“star” : {
“name” : “刘德华”,
“age” : 45
}
}
对应的mapping配置:
{
"properties": {
"id": {"type": "long"},
"girl": {
"properties":{
"name": {"type": "keyword"},
"age": {"type": "integer"}
}
}
}
}
(2)数组与对象数组
a.数组的映射
注意:数组中元素的类型必须一致
{
“id” : 1,
“hobby” : [“吃东西”,“听音乐”]
}
对应的mapping配置是:
{
"properties": {
"id": {"type": "long"},
"hobby": {"type": "keyword"}
}
}
b.对象数组的映射
{
"id" : 1,
"star":[{"name":"刘德华","age":50},{"name":"黎明","age":51}]
}
对应的映射配置为:
"properties": {
"id": {
"type": "long"
},
"star": {
"properties": {
"age": { "type": "long" },
"name": { "type": "text" }
}
}
}
-----------put---------
[{
Age:50
Name:刘德华
},{
Age:51
Name:黎明
}]
注意:同内联对象一样,对象数组也会被扁平化索引
{
"user1.star.age": [50, 51],
"user2.star.name": ["刘德华", "黎明"]
}
全局映射可以通过动态模板和默认设置两种方式实现。
PUT {indexName}
{
"mappings": {
"_default_": {
"_all": {
"enabled": false 关闭默认映射配置
}
},
"user": {
//指定自己的自定义配置
},
"dept": {
"_all": {
"enabled": true //启动默认配置
}
},
....
}
}
上例中:默认的enabled=false 表示关闭模式的配置,如果你想用,在自己的配置里面开启配置.
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
在实际应用场景中,一个对象的属性中,需要全文检索的字段较少,大部分字符串不需要分词,因此,需要利用全局模板覆盖自带的默认模板:
PUT _template/global_template //创建名为global_template的模板
{
"template": "*", //匹配所有索引库
"settings": { "number_of_shards": 1 }, //匹配到的索引库只创建1个主分片
"mappings": {
"_default_": {
"_all": {
"enabled": false //关闭所有类型的_all字段
},
"dynamic_templates": [
{
"string_as_text": {
"match_mapping_type": "string",//匹配类型string
"match": "*_text", //匹配字段名字以_text结尾 a_text
"mapping": {
"type": "text",//将类型为string的字段映射为text类型
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}}
上面的意思:如果索引库里面字段是以_text结尾就需要进行分词,如果不是,就不分词。
真实环境:
NodeName | Web端口,客户端端口 |
---|---|
node-1 | 172.168.1.1:9200 172.168.1.1:9300 |
node-2 | 172.168.1.2:9200 172.168.1.2:9300 |
node-3 | 172.168.1.3:9200 172.168.1.3:9300 |
模拟环境:
NodeName | Web端口,客户端端口 |
---|---|
node-1 | 127.0.0.1:9201 127.0.0.1:9301 |
node-2 | 127.0.0.1:9202 127.0.0.1:9302 |
node-3 | 127.0.0.1:9203 127.0.0.1:9303 |
步骤:
(1)拷贝三份ES服务
(2)修改每个内存配置lg
(3)修改三个节点配置
配置参数详解:
名称 | 解释 |
---|---|
cluster.name | 集群名,自定义集群名,默认为elasticsearch,建议修改,因为低版本多播模式下同一网段下相同集群名会自动加入同一集群,如生产环境这样易造成数据运维紊乱。 |
node.name | 节点名,同一集群下要求每个节点的节点名不一致,起到区分节点和辨认节点作用 |
node.master | 是否为主节点,选项为true或false,当为true时在集群启动时该节点为主节点,在宕机或任务挂掉之后会选举新的主节点,恢复后该节点依然为主节点 |
node.data | 是否处理数据,选项为true或false。负责数据的相关操作 |
path.logs | 默认日志路径 |
bootstrap.mlockall | 内存锁,选项为true或false,用来确保用户在es-jvm中设置的ES_HEAP_SIZE参数内存可以使用一半以上而又不溢出 |
network.host | 对外暴露的host,0.0.0.0时暴露给外网 |
http.port | 对外访问的端口号,默认为9200,所以外界访问该节点一般为http://ip:9200/ |
transport.tcp.port | 集群间通信的端口号,默认为9300 |
discovery.zen.ping.unicast.hosts | 集群的ip集合,可指定端口,默认为9300,如 [“192.168.1.101”,“192.168.1.102”] |
discovery.zen.minimum_master_nodes | 最少的主节点个数,为了防止脑裂,最好设置为(总结点数/2 + 1)个 |
discovery.zen.ping_timeout | 主节点选举超时时间设置 |
gateway.recover_after_nodes | 值为n,网关控制在n个节点启动之后才恢复整个集群 |
node.max_local_storage_nodes | 值为n,一个系统中最多启用节点个数为n |
action.destructive_requires_name | 选项为true或false,删除indices是否需要现实名字 |
配置信息:
# 统一的集群名
cluster.name: my-ealsticsearch
# 当前节点名
node.name: node-1
# 对外暴露端口使外网访问
network.host: 127.0.0.1
# 对外暴露端口
http.port: 9201
#集群间通讯端口号
transport.tcp.port: 9301
#集群的ip集合,可指定端口,默认为9300
discovery.zen.ping.unicast.hosts: [“127.0.0.1:9301”,”127.0.0.1:9302”,”127.0.0.1:9303”]
# 统一的集群名
cluster.name: my-ealsticsearch
# 当前节点名
node.name: node-2
# 对外暴露端口使外网访问
network.host: 127.0.0.1
# 对外暴露端口
http.port: 9202
#集群间通讯端口号
transport.tcp.port: 9302
#集群的ip集合,可指定端口,默认为9300
discovery.zen.ping.unicast.hosts: [“127.0.0.1:9301”,”127.0.0.1:9302”,”127.0.0.1:9303”]
# 统一的集群名
cluster.name: my-ealsticsearch
# 当前节点名
node.name: node-3
# 对外暴露端口使外网访问
network.host: 127.0.0.1
# 对外暴露端口
http.port: 9203
#集群间通讯端口号
transport.tcp.port: 9303
#集群的ip集合,可指定端口,默认为9300
discovery.zen.ping.unicast.hosts: [“127.0.0.1:9301”,”127.0.0.1:9302”,”127.0.0.1:9303”]
分别启动创建索引,创建类型,插入文档。
新增数据:
查询数据:
ES对Java提供一套操作索引库的工具包,即Java API。所有的ES操作都使用Client对象执行。
ES的Maven引入:
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>transportartifactId>
<version>5.2.2version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-apiartifactId>
<version>2.7version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.7version>
dependency>
// on startup
TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host1"), 9300))
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host2"), 9300));
// on shutdown
client.close();
Settings settings = Settings.builder()
.put("cluster.name", "myClusterName").build();
TransportClient client = new PreBuiltTransportClient(settings);
//添加地址到client中
/**
* 连接服务方法(嗅探方式)
* @return
* @throws Exception
*/
public TransportClient getClient() throws Exception{
Settings settings = Settings.builder()
.put("client.transport.sniff", true).build();
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
return client;
}
/**
* 新增数据
* @throws Exception
*/
@Test
public void test() throws Exception{
//得到client
TransportClient client = getClient();
IndexRequestBuilder builder = client.prepareIndex("xihua", "students", "1");
Map mp = new HashMap();
//存值
mp.put("name","xiaoming");
mp.put("age",18);
builder.setSource(mp);
IndexResponse indexResponse = builder.get();
System.out.println(indexResponse);
//关闭服务
client.close();
}
/**
* 查询数据
* @throws Exception
*/
@Test
public void testSearch() throws Exception{
//得到client
TransportClient client = getClient();
System.out.println(client.prepareGet("xihua", "students", "1").get().getSource());
client.close();
}
/**
* 修改数据
* @throws Exception
*/
@Test
public void testUpdate() throws Exception{
TransportClient client = getClient();
IndexRequest indexRequest = new IndexRequest("xihua", "students", "1");
Map mp = new HashMap();
mp.put("name","xiaohong");
mp.put("age",28);
//id不存在就新增,如果存在就更新
UpdateRequest updateRequest = new UpdateRequest("xihua", "students", "1").doc(mp).upsert(indexRequest);
//执行
client.update(updateRequest).get();
client.close();
}
/**
* 删除数据
* @throws Exception
*/
@Test
public void testDel() throws Exception{
TransportClient client = getClient();
client.prepareDelete("xihua","students","1").get();
client.close();
}
/**
* 批量操作
* @throws Exception
*/
@Test
public void testBulk() throws Exception{
TransportClient client = getClient();
BulkRequestBuilder req = client.prepareBulk();
for(int i=0;i<50;i++){
Map mp = new HashMap();
mp.put("name","xm"+i);
mp.put("age",18+i);
req.add(client.prepareIndex("shoppings","goods",""+i).setSource(mp));
}
BulkResponse responses = req.get();
if(responses.hasFailures()){
System.out.println("出错了!");
}
}
/**
* DSL查询 分页、排序、过滤
* @throws Exception
*/
@Test
public void testDSL() throws Exception{
//得到client对象
TransportClient client = getClient();
//得到builder
SearchRequestBuilder builder = client.prepareSearch("shoppings").setTypes("goods");
//得到bool对象
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
//得到must
List<QueryBuilder> must = boolQuery.must();
must.add(QueryBuilders.matchAllQuery());
//添加filter过滤器
boolQuery.filter(QueryBuilders.rangeQuery("age").gte("18").lte("58"));
builder.setQuery(boolQuery);
//添加分页
builder.setFrom(0);
builder.setSize(10);
//设置排序
builder.addSort("age", SortOrder.DESC);
//设置查询字段
builder.setFetchSource(new String[]{"name","age"},null);
//取值
SearchResponse response = builder.get();
//得到查询内容
SearchHits hits = response.getHits();
//得到命中数据 返回数组
SearchHit[] hitsHits = hits.getHits();
//循环数组 打印获取值
for (SearchHit hitsHit : hitsHits) {
System.out.println("得到结果"+hitsHit.getSource());
}
}