Hello,World

社区网站更新日志

这里专门用来放社区网站更新的日志。
 
  • 2017.4.28 支持 SSL

Snip20170428_1.png

 
这里专门用来放社区网站更新的日志。
 
  • 2017.4.28 支持 SSL

Snip20170428_1.png

 

Sql on Elasticsearch

esql
Git地址
https://github.com/unimassystem/esql5 

elsh.png

 
create table my_index.my_table (
id keyword,
name text,
age long,
birthday date
);

select * from my_index.my_type;

select count(*) from my_index.my_table group by age;
#Create table

字段参数,ES中分词规则、索引类型、字段格式等高级参数的支持

create table my_table (
name text (analyzer = ik_max_word),
dd text (index=no),
age long (include_in_all=false)
);


对象、嵌套字段支持 as

create table my_index (
id long,
name text,
obj object as (
first_name text,
second_name text (analyzer=pinyin)
)
);


create table my_index (
id long,
name text,
obj nested as (
first_name text,
second_name text (analyzer=pinyin)
)
);


ES索引高级参数支持 with option

create table my_index (
id long,
name text
) with option (
index.number_of_shards=10,
index.number_of_replicas = 1
);
#Insert/Bulk

单条数据插入
insert into my_index.index (name,age) values ('zhangsan',24);

多条插入
bulk into my_index.index (name,age) values ('zhangsan',24),('lisi',24);


对象数据插入,list,{}Map

insert into my_index.index (ds) values (['zhejiang','hangzhou']);

insert into my_index.index (dd) values ({address='zhejiang',postCode='330010'});
#select/Aggregations

select * from my_table.my_index where name like 'john *' and age between 20 and 30 and (hotel = 'hanting' or flight = 'MH4510');

地理位置中心点查询
select * from hz_point where geo_distance({distance='1km',location='30.306378,120.247427'});

地理坐标区域查询
select * from hz_point where geo_bounding_box({location={top_left='31.306378,119.247427',bottom_right='29.285797,122.172329'}});

pipeline统计 move_avg
select count(*) as total, moving_avg({buckets_path=total}) from my_index group by date_histogram({field=timestamp,interval='1h'});
Getting Started

环境要求python >= 2.7

export PYTHONHOME=(%python_path)
export PATH=$PYTHONHOME/bin:$PATH


安装第三方依赖包
pip install -r esql5.egg-info/requires.txt
或python setup.py install

运行esql5服务
(standalone):
cd esql5
python -m App.app

(with uwsgi)
cd esql5
uwsgi --ini conf/uwsgi.ini


shell终端:
python -m elsh.Command
继续阅读 »
esql
Git地址
https://github.com/unimassystem/esql5 

elsh.png

 
create table my_index.my_table (
id keyword,
name text,
age long,
birthday date
);

select * from my_index.my_type;

select count(*) from my_index.my_table group by age;
#Create table

字段参数,ES中分词规则、索引类型、字段格式等高级参数的支持

create table my_table (
name text (analyzer = ik_max_word),
dd text (index=no),
age long (include_in_all=false)
);


对象、嵌套字段支持 as

create table my_index (
id long,
name text,
obj object as (
first_name text,
second_name text (analyzer=pinyin)
)
);


create table my_index (
id long,
name text,
obj nested as (
first_name text,
second_name text (analyzer=pinyin)
)
);


ES索引高级参数支持 with option

create table my_index (
id long,
name text
) with option (
index.number_of_shards=10,
index.number_of_replicas = 1
);
#Insert/Bulk

单条数据插入
insert into my_index.index (name,age) values ('zhangsan',24);

多条插入
bulk into my_index.index (name,age) values ('zhangsan',24),('lisi',24);


对象数据插入,list,{}Map

insert into my_index.index (ds) values (['zhejiang','hangzhou']);

insert into my_index.index (dd) values ({address='zhejiang',postCode='330010'});
#select/Aggregations

select * from my_table.my_index where name like 'john *' and age between 20 and 30 and (hotel = 'hanting' or flight = 'MH4510');

地理位置中心点查询
select * from hz_point where geo_distance({distance='1km',location='30.306378,120.247427'});

地理坐标区域查询
select * from hz_point where geo_bounding_box({location={top_left='31.306378,119.247427',bottom_right='29.285797,122.172329'}});

pipeline统计 move_avg
select count(*) as total, moving_avg({buckets_path=total}) from my_index group by date_histogram({field=timestamp,interval='1h'});
Getting Started

环境要求python >= 2.7

export PYTHONHOME=(%python_path)
export PATH=$PYTHONHOME/bin:$PATH


安装第三方依赖包
pip install -r esql5.egg-info/requires.txt
或python setup.py install

运行esql5服务
(standalone):
cd esql5
python -m App.app

(with uwsgi)
cd esql5
uwsgi --ini conf/uwsgi.ini


shell终端:
python -m elsh.Command
收起阅读 »

用logstash导入ES且自定义mapping时踩的坑

问题发生背景:

1.本来我是使用logstash的默认配置向ES导入日志的。然后很嗨皮,发现一切OK,后来我开始对日志进行聚合统计,发现terms聚合时的key很奇怪,后来查询这奇怪的key,发现这些关键字都是源字符串的一段,而且全部复现场景都是出现"xxxx-xxxxxx"时就会截断,感觉像是分词器搞的鬼。所以想自己定制mapping。下面是原来的logstash配置
output{
elasticsearch{
action => "index"
hosts => ["xxxxxx:9200"]
index => "xxxxx"
document_type => "haha"
}
}



说干就干:

开始四处查阅文档,发现可以定制mapping,很开心。
output{
elasticsearch{
action => "index"
hosts => ["xxx"]
index => "logstashlog"
template => "xx/http-logstash.json"
template_name => "http-log-logstash"
template_overwrite => true
}
stdout{
codec => rubydebug
}

}没有什么一帆风顺:
问题1:
但是我发现我已经上传了自定义的template,但是就是不能生效。
这时知道了,这个要设置order才能覆盖,默认的order是0,必须更大才行,参考
http://elasticsearch.cn/article/21
问题2:
我看到自己上传的template的order已经是1了,怎么还是不生效呢?
原来自己的索引名称不匹配自己的template的名称,所以不能使用,就又用了默认的template。
改成下面后OK,终于生效了。(注意index名称变化)output{
elasticsearch{
action => "index"
hosts => ["xxx"]
index => "http-log-logstash"
document_type => "haha"
template => "xxx/http-logstash.json"
template_name => "http-log-logstash"
template_overwrite => true
}
stdout{
codec => rubydebug
}

}问题3:
发现导入失败,原来自己的时间字符串不能用默认的date的format匹配,
如2017-04-11 00:07:25   不能用 { "type" : "date"} 的默认format匹配,
改成:"format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"}, 
这样就能解析了。
一切OK,谢谢社区,谢谢Google(你是我见过的除了书籍和老师之后最提升生产力的工具)

附上我的模板
{ 
"template" : "qmpsearchlog",
"order":1,
"settings" : { "index.refresh_interval" : "60s" },
"mappings" : {
"_default_" : {
"_all" : { "enabled" : false },
"dynamic_templates" : [{
"message_field" : {
"match" : "message",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "not_analyzed" }
}
}, {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "not_analyzed" }
}
}],
"properties" : {
"@timestamp" : { "type" : "date"},
"@version" : { "type" : "integer", "index" : "not_analyzed" },
"path" : { "type" : "string", "index" : "not_analyzed" },
"host" : { "type" : "string", "index" : "not_analyzed" },
"record_time":{"type":"date","format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"},
"method":{"type":"string","index" : "not_analyzed"},
"unionid":{"type":"string","index" : "not_analyzed"},
"user_name":{"type":"string","index" : "not_analyzed"},
"query":{"type":"string","index" : "not_analyzed"},
"ip":{ "type" : "ip"},
"webbrower":{"type":"string","index" : "not_analyzed"},
"os":{"type":"string","index" : "not_analyzed"},
"device":{"type":"string","index" : "not_analyzed"},
"ptype":{"type":"string","index" : "not_analyzed"},
"serarch_time":{"type":"date","format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"},
"have_ok":{"type":"string","index" : "not_analyzed"},
"legal":{"type":"string","index" : "not_analyzed"}
}
}
}
}



 
继续阅读 »
问题发生背景:

1.本来我是使用logstash的默认配置向ES导入日志的。然后很嗨皮,发现一切OK,后来我开始对日志进行聚合统计,发现terms聚合时的key很奇怪,后来查询这奇怪的key,发现这些关键字都是源字符串的一段,而且全部复现场景都是出现"xxxx-xxxxxx"时就会截断,感觉像是分词器搞的鬼。所以想自己定制mapping。下面是原来的logstash配置
output{
elasticsearch{
action => "index"
hosts => ["xxxxxx:9200"]
index => "xxxxx"
document_type => "haha"
}
}



说干就干:

开始四处查阅文档,发现可以定制mapping,很开心。
output{
elasticsearch{
action => "index"
hosts => ["xxx"]
index => "logstashlog"
template => "xx/http-logstash.json"
template_name => "http-log-logstash"
template_overwrite => true
}
stdout{
codec => rubydebug
}

}没有什么一帆风顺:
问题1:
但是我发现我已经上传了自定义的template,但是就是不能生效。
这时知道了,这个要设置order才能覆盖,默认的order是0,必须更大才行,参考
http://elasticsearch.cn/article/21
问题2:
我看到自己上传的template的order已经是1了,怎么还是不生效呢?
原来自己的索引名称不匹配自己的template的名称,所以不能使用,就又用了默认的template。
改成下面后OK,终于生效了。(注意index名称变化)output{
elasticsearch{
action => "index"
hosts => ["xxx"]
index => "http-log-logstash"
document_type => "haha"
template => "xxx/http-logstash.json"
template_name => "http-log-logstash"
template_overwrite => true
}
stdout{
codec => rubydebug
}

}问题3:
发现导入失败,原来自己的时间字符串不能用默认的date的format匹配,
如2017-04-11 00:07:25   不能用 { "type" : "date"} 的默认format匹配,
改成:"format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"}, 
这样就能解析了。
一切OK,谢谢社区,谢谢Google(你是我见过的除了书籍和老师之后最提升生产力的工具)

附上我的模板
{ 
"template" : "qmpsearchlog",
"order":1,
"settings" : { "index.refresh_interval" : "60s" },
"mappings" : {
"_default_" : {
"_all" : { "enabled" : false },
"dynamic_templates" : [{
"message_field" : {
"match" : "message",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "not_analyzed" }
}
}, {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : { "type" : "string", "index" : "not_analyzed" }
}
}],
"properties" : {
"@timestamp" : { "type" : "date"},
"@version" : { "type" : "integer", "index" : "not_analyzed" },
"path" : { "type" : "string", "index" : "not_analyzed" },
"host" : { "type" : "string", "index" : "not_analyzed" },
"record_time":{"type":"date","format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"},
"method":{"type":"string","index" : "not_analyzed"},
"unionid":{"type":"string","index" : "not_analyzed"},
"user_name":{"type":"string","index" : "not_analyzed"},
"query":{"type":"string","index" : "not_analyzed"},
"ip":{ "type" : "ip"},
"webbrower":{"type":"string","index" : "not_analyzed"},
"os":{"type":"string","index" : "not_analyzed"},
"device":{"type":"string","index" : "not_analyzed"},
"ptype":{"type":"string","index" : "not_analyzed"},
"serarch_time":{"type":"date","format": "yyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"},
"have_ok":{"type":"string","index" : "not_analyzed"},
"legal":{"type":"string","index" : "not_analyzed"}
}
}
}
}



  收起阅读 »

社区网站服务器迁移完毕

image001.png

 感谢 ConvertLab 为本站提供服务器,目前服务器已经迁移完毕,大家可以感受一下速度!

 
logoblack.png

同时感谢在此之前为本站提供网站空间的:谱时 
 
 
社区账号也支持 Github 绑定了。

感谢大家一路支持,社区有你更精彩。
继续阅读 »
image001.png

 感谢 ConvertLab 为本站提供服务器,目前服务器已经迁移完毕,大家可以感受一下速度!

 
logoblack.png

同时感谢在此之前为本站提供网站空间的:谱时 
 
 
社区账号也支持 Github 绑定了。

感谢大家一路支持,社区有你更精彩。 收起阅读 »

ELK学习资料整理

刚开始学习使用ELK,整理一个学习资料列表,当做备忘录。
 
1.第一个当然是官方文档
  • ElasticSearch参考手册,学习 DSL查询语法,包括查找(query)、过滤(filter)和聚合(aggs)等。
  • Logstash参考手册,学习数据导入,包括输入(input)、过滤(filter)和输出( output)等,主要是filter中如何对复杂文本 进行拆分和类型 转化。
  • Kibana参考手册,使用Kibana提供的前端界面对数据进行快速展示,主要是对Visulize 模块的使

2.中文文档

 
欢迎补充……
继续阅读 »
刚开始学习使用ELK,整理一个学习资料列表,当做备忘录。
 
1.第一个当然是官方文档
  • ElasticSearch参考手册,学习 DSL查询语法,包括查找(query)、过滤(filter)和聚合(aggs)等。
  • Logstash参考手册,学习数据导入,包括输入(input)、过滤(filter)和输出( output)等,主要是filter中如何对复杂文本 进行拆分和类型 转化。
  • Kibana参考手册,使用Kibana提供的前端界面对数据进行快速展示,主要是对Visulize 模块的使

2.中文文档

 
欢迎补充…… 收起阅读 »

logstash多时间(date)字段文档导入

我们都知道如果文档中有表示时间的字段时可以用如下方法将字段转化为date(timestamp)格式导入
date {
match => [ "submission_time", "yyyyMMdd" ]
}
但是如果一条记录中有多个字段需要使用date类型呢?有人遇到了 同样的问题How to parse multiple date fields?其实多次使用date{}语法就行了,例如:
date {
match => [ "submission_time", "UNIX_MS" ]
target => "@timestamp"
}
date {
match => [ "submission_time", "UNIX_MS" ]
target => "submission_time"
}
date {
match => [ "start_time", "UNIX_MS" ]
target => "start_time"
}
date {
match => [ "end_time", "UNIX_MS" ]
target => "end_time"
}
查看官方文档后, 发现一直使用的date是省去了参数target的,而target默认值为@timestamp。
继续阅读 »
我们都知道如果文档中有表示时间的字段时可以用如下方法将字段转化为date(timestamp)格式导入
date {
match => [ "submission_time", "yyyyMMdd" ]
}
但是如果一条记录中有多个字段需要使用date类型呢?有人遇到了 同样的问题How to parse multiple date fields?其实多次使用date{}语法就行了,例如:
date {
match => [ "submission_time", "UNIX_MS" ]
target => "@timestamp"
}
date {
match => [ "submission_time", "UNIX_MS" ]
target => "submission_time"
}
date {
match => [ "start_time", "UNIX_MS" ]
target => "start_time"
}
date {
match => [ "end_time", "UNIX_MS" ]
target => "end_time"
}
查看官方文档后, 发现一直使用的date是省去了参数target的,而target默认值为@timestamp。 收起阅读 »

ES节点memory lock重要性与实现方式

看一下Elastic官网对开启 bootstrap.memory_lock的介绍:
 
Swapping is very bad for performance and for node stability and should be avoided at all costs. It can cause garbage collections to last for minutes instead of milliseconds and can cause nodes to respond slowly or even to disconnect from the cluster.                                                                   ----截取自官网


意思是说发生系统swapping的时候ES节点的性能会非常差,也会影响节点的稳定性。所以要不惜一切代价来避免swapping。swapping会导致Java GC的周期延迟从毫秒级恶化到分钟,更严重的是会引起节点响应延迟甚至脱离集群。                                                                      ----如果不了解到底什么是swapping的,可以找点Linux IO章节文章看看


 
1. 先检查一下你的各个ES节点是否开启了Mem_lock


GET 请求 /_nodes?filter_path=**.mlockall



{
  "nodes": {
    "dCH5FCpATRO7D1azyPhsRQ": {
      "process": {
        "mlockall": false
      }
    },
    "GoNfwnNzSwmJy3y1QdfluA": {
      "process": {
        "mlockall": false
      }
    },
    "ijW61kA-SAqnnVHjpTSw2w": {
      "process": {
        "mlockall": false
      }
    },
    "yHl9GUGbS46o4hwKvHpwnQ": {
      "process": {
        "mlockall": false
      }
    }
  }
}


上述返回内容,可见都没有开启mem_lock,集全随时都可能发生故障(尤其是集群正常运行了一段时间,莫名其妙的故障)
 
2. root权限执行ulimit -l unlimited


告诉操作系统可以无限制分配内存给一个进程


3.重新启动ES


[2017-04-06T11:51:14,840][INFO ][o.e.b.BootstrapCheck     ] [Portal_ES_Node10_0_36_49] bound or publishing to a non-loopback or non-link-local address, enforcing bootstrap checks
ERROR: bootstrap checks failed
memory locking requested for elasticsearch process but memory is not locked


 
4. 如果你遇到上面的错误,说明你还需要配置/etc/security/limits.conf
    增加下面3行到文件末尾,其中XXX表示当前用户


# allow user 'XXX' mlockall
XXX soft memlock unlimited
XXX hard memlock unlimited


继续阅读 »
看一下Elastic官网对开启 bootstrap.memory_lock的介绍:
 
Swapping is very bad for performance and for node stability and should be avoided at all costs. It can cause garbage collections to last for minutes instead of milliseconds and can cause nodes to respond slowly or even to disconnect from the cluster.                                                                   ----截取自官网


意思是说发生系统swapping的时候ES节点的性能会非常差,也会影响节点的稳定性。所以要不惜一切代价来避免swapping。swapping会导致Java GC的周期延迟从毫秒级恶化到分钟,更严重的是会引起节点响应延迟甚至脱离集群。                                                                      ----如果不了解到底什么是swapping的,可以找点Linux IO章节文章看看


 
1. 先检查一下你的各个ES节点是否开启了Mem_lock


GET 请求 /_nodes?filter_path=**.mlockall



{
  "nodes": {
    "dCH5FCpATRO7D1azyPhsRQ": {
      "process": {
        "mlockall": false
      }
    },
    "GoNfwnNzSwmJy3y1QdfluA": {
      "process": {
        "mlockall": false
      }
    },
    "ijW61kA-SAqnnVHjpTSw2w": {
      "process": {
        "mlockall": false
      }
    },
    "yHl9GUGbS46o4hwKvHpwnQ": {
      "process": {
        "mlockall": false
      }
    }
  }
}


上述返回内容,可见都没有开启mem_lock,集全随时都可能发生故障(尤其是集群正常运行了一段时间,莫名其妙的故障)
 
2. root权限执行ulimit -l unlimited


告诉操作系统可以无限制分配内存给一个进程


3.重新启动ES


[2017-04-06T11:51:14,840][INFO ][o.e.b.BootstrapCheck     ] [Portal_ES_Node10_0_36_49] bound or publishing to a non-loopback or non-link-local address, enforcing bootstrap checks
ERROR: bootstrap checks failed
memory locking requested for elasticsearch process but memory is not locked


 
4. 如果你遇到上面的错误,说明你还需要配置/etc/security/limits.conf
    增加下面3行到文件末尾,其中XXX表示当前用户


# allow user 'XXX' mlockall
XXX soft memlock unlimited
XXX hard memlock unlimited


收起阅读 »

做为接入方,你的痛点是什么?

希望大家踊跃发言。
希望大家踊跃发言。

100种让ES宕机的方法,请详细描述过程,且可复现的。

大家好,最近这个客题需要大家的帮忙啦,后面会专门录个视频来汇总讲解这些 bad case.
 
OOM:
  方式1:
       版本: all
       深度分页和大数据量数据返回会导致OOM。  
  方式2:
       版本: es 1.x
       使用delete_by_query删除海量数据时,由于内部没有使用scroll模块,会由深度分页导致OOM
  方式3:
       版本: all
       使用scroll返回大量数据导致OOM
 
继续阅读 »
大家好,最近这个客题需要大家的帮忙啦,后面会专门录个视频来汇总讲解这些 bad case.
 
OOM:
  方式1:
       版本: all
       深度分页和大数据量数据返回会导致OOM。  
  方式2:
       版本: es 1.x
       使用delete_by_query删除海量数据时,由于内部没有使用scroll模块,会由深度分页导致OOM
  方式3:
       版本: all
       使用scroll返回大量数据导致OOM
  收起阅读 »

Elastic南京meetup分享征集中

南京meetup筹备中,有意提供分享的同学请将
1. 个人简介
2. 主题简介
3. 大概内容
发送到 kenny_ye@trendmicro.com.cn
 
征稿截止日期: 2017年5月7日
 
欢迎踊跃投稿!
继续阅读 »
南京meetup筹备中,有意提供分享的同学请将
1. 个人简介
2. 主题简介
3. 大概内容
发送到 kenny_ye@trendmicro.com.cn
 
征稿截止日期: 2017年5月7日
 
欢迎踊跃投稿! 收起阅读 »

elasticsearch-query-tookit一款基于SQL查询elasticsearch编程工具包,支持SQL解析生成DSL,支持JDBC驱动,支持和Spring、MyBatis集成

`elasticsearch-query-tookit`是一款基于SQL查询elasticsearch编程工具包,支持SQL解析生成DSL,支持JDBC驱动,支持和Spring、MyBatis集成,提供Java编程接口可基于此工具包二次开发
 
只是重新造了个轮子,有兴趣的同学可以相互交流,QQ: 465360798
 
项目地址:https://github.com/gitchennan/ ... olkit
 
一、SQL解析生成DSL使用示例
SQL语法帮助手册戳这里: https://github.com/gitchennan/ ... p-doc
 
String sql = "select * from index.order where status='SUCCESS' and price > 100 order by nvl(pride, 0) asc routing by 'JD' limit 0, 20";

ElasticSql2DslParser sql2DslParser = new ElasticSql2DslParser();
//解析SQL
ElasticSqlParseResult parseResult = sql2DslParser.parse(sql);
//生成DSL(可用于rest api调用)
String dsl = parseResult.toDsl();

//toRequest方法接收一个clinet对象参数
SearchRequestBuilder searchReq = parseResult.toRequest(esClient);
//执行查询
SearchResponse response = searchReq.execute().actionGet();
生成的DSL如下:
{
"from" : 0,
"size" : 20,
"query" : {
"bool" : {
"filter" : {
"bool" : {
"must" : [ {
"term" : {
"status" : "SUCCESS"
}
}, {
"range" : {
"price" : {
"from" : 100,
"to" : null,
"include_lower" : false,
"include_upper" : true
}
}
} ]
}
}
}
},
"sort" : [ {
"pride" : {
"order" : "asc",
"missing" : 0
}
} ]
}
二、集成MyBatis、Spring
 
首先在Spring配置文件中增加如下代码
1. 指定driverClassName:org.elasticsearch.jdbc.api.ElasticDriver
2. 指定连接ES的连接串:jdbc:elastic:192.168.0.109:9300/product_cluster
3. 创建一个SqlMapClient对象,并指定sqlMapConfig.xml路径
 
<bean id="elasticDataSource" class="org.elasticsearch.jdbc.api.ElasticSingleConnectionDataSource" destroy-method="destroy">
<property name="driverClassName" value="org.elasticsearch.jdbc.api.ElasticDriver" />
<property name="url" value="jdbc:elastic:192.168.0.109:9300/product_cluster" />
</bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="elasticDataSource" />
<property name="configLocation" value="classpath:sqlMapConfig.xml"/>
</bean>
sqlMapConfig.xml文件内容如下:
<sqlMapConfig>
<settings
cacheModelsEnabled="true"
lazyLoadingEnabled="true"
enhancementEnabled="true"
maxSessions="64"
maxTransactions="20"
maxRequests="128"
useStatementNamespaces="true"/>

<sqlMap resource="sqlmap/PRODUCT.xml"/>

</sqlMapConfig>
PRODUCT.xml文件中声明select sql语句
<sqlMap namespace="PRODUCT">
<select id="getProductByCodeAndMatchWord" parameterClass="java.util.Map" resultClass="java.lang.String">
SELECT *
FROM index.product
QUERY match(productName, #matchWord#) or prefix(productName, #prefixWord#, 'boost:2.0f')
WHERE productCode = #productCode#
AND advicePrice > #advicePrice#
AND $$buyers.buyerName IN ('china', 'usa')
ROUTING BY #routingVal#
</select>
</sqlMap>
编写对应DAO代码:
@Repository
public class ProductDao {
@Autowired
@Qualifier("sqlMapClient")
private SqlMapClient sqlMapClient;


public List<Product> getProductByCodeAndMatchWord(String matchWord, String productCode) throws SQLException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("productCode", productCode);
paramMap.put("advicePrice", 1000);
paramMap.put("routingVal", "A");
paramMap.put("matchWord", matchWord);
paramMap.put("prefixWord", matchWord);
String responseGson = (String) sqlMapClient.queryForObject("PRODUCT.getProductByCodeAndMatchWord", paramMap);

//反序列化查询结果
JdbcSearchResponseResolver responseResolver = new JdbcSearchResponseResolver(responseGson);
JdbcSearchResponse<Product> searchResponse = responseResolver.resolveSearchResponse(Product.class);

return searchResponse.getDocList();

}
}
编写测试方法
@Test
public void testProductQuery() throws Exception {
BeanFactory factory = new ClassPathXmlApplicationContext("application-context.xml");
ProductDao productDao = factory.getBean(ProductDao.class);

List<Product> productList = productDao.getProductByCodeAndMatchWord("iphone 6s", "IP_6S");
for (Product product : productList) {
System.out.println(product.getProductName());
}
}

 
 
继续阅读 »
`elasticsearch-query-tookit`是一款基于SQL查询elasticsearch编程工具包,支持SQL解析生成DSL,支持JDBC驱动,支持和Spring、MyBatis集成,提供Java编程接口可基于此工具包二次开发
 
只是重新造了个轮子,有兴趣的同学可以相互交流,QQ: 465360798
 
项目地址:https://github.com/gitchennan/ ... olkit
 
一、SQL解析生成DSL使用示例
SQL语法帮助手册戳这里: https://github.com/gitchennan/ ... p-doc
 
String sql = "select * from index.order where status='SUCCESS' and price > 100 order by nvl(pride, 0) asc routing by 'JD' limit 0, 20";

ElasticSql2DslParser sql2DslParser = new ElasticSql2DslParser();
//解析SQL
ElasticSqlParseResult parseResult = sql2DslParser.parse(sql);
//生成DSL(可用于rest api调用)
String dsl = parseResult.toDsl();

//toRequest方法接收一个clinet对象参数
SearchRequestBuilder searchReq = parseResult.toRequest(esClient);
//执行查询
SearchResponse response = searchReq.execute().actionGet();
生成的DSL如下:
{
"from" : 0,
"size" : 20,
"query" : {
"bool" : {
"filter" : {
"bool" : {
"must" : [ {
"term" : {
"status" : "SUCCESS"
}
}, {
"range" : {
"price" : {
"from" : 100,
"to" : null,
"include_lower" : false,
"include_upper" : true
}
}
} ]
}
}
}
},
"sort" : [ {
"pride" : {
"order" : "asc",
"missing" : 0
}
} ]
}
二、集成MyBatis、Spring
 
首先在Spring配置文件中增加如下代码
1. 指定driverClassName:org.elasticsearch.jdbc.api.ElasticDriver
2. 指定连接ES的连接串:jdbc:elastic:192.168.0.109:9300/product_cluster
3. 创建一个SqlMapClient对象,并指定sqlMapConfig.xml路径
 
<bean id="elasticDataSource" class="org.elasticsearch.jdbc.api.ElasticSingleConnectionDataSource" destroy-method="destroy">
<property name="driverClassName" value="org.elasticsearch.jdbc.api.ElasticDriver" />
<property name="url" value="jdbc:elastic:192.168.0.109:9300/product_cluster" />
</bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="elasticDataSource" />
<property name="configLocation" value="classpath:sqlMapConfig.xml"/>
</bean>
sqlMapConfig.xml文件内容如下:
<sqlMapConfig>
<settings
cacheModelsEnabled="true"
lazyLoadingEnabled="true"
enhancementEnabled="true"
maxSessions="64"
maxTransactions="20"
maxRequests="128"
useStatementNamespaces="true"/>

<sqlMap resource="sqlmap/PRODUCT.xml"/>

</sqlMapConfig>
PRODUCT.xml文件中声明select sql语句
<sqlMap namespace="PRODUCT">
<select id="getProductByCodeAndMatchWord" parameterClass="java.util.Map" resultClass="java.lang.String">
SELECT *
FROM index.product
QUERY match(productName, #matchWord#) or prefix(productName, #prefixWord#, 'boost:2.0f')
WHERE productCode = #productCode#
AND advicePrice > #advicePrice#
AND $$buyers.buyerName IN ('china', 'usa')
ROUTING BY #routingVal#
</select>
</sqlMap>
编写对应DAO代码:
@Repository
public class ProductDao {
@Autowired
@Qualifier("sqlMapClient")
private SqlMapClient sqlMapClient;


public List<Product> getProductByCodeAndMatchWord(String matchWord, String productCode) throws SQLException {
Map<String, Object> paramMap = Maps.newHashMap();
paramMap.put("productCode", productCode);
paramMap.put("advicePrice", 1000);
paramMap.put("routingVal", "A");
paramMap.put("matchWord", matchWord);
paramMap.put("prefixWord", matchWord);
String responseGson = (String) sqlMapClient.queryForObject("PRODUCT.getProductByCodeAndMatchWord", paramMap);

//反序列化查询结果
JdbcSearchResponseResolver responseResolver = new JdbcSearchResponseResolver(responseGson);
JdbcSearchResponse<Product> searchResponse = responseResolver.resolveSearchResponse(Product.class);

return searchResponse.getDocList();

}
}
编写测试方法
@Test
public void testProductQuery() throws Exception {
BeanFactory factory = new ClassPathXmlApplicationContext("application-context.xml");
ProductDao productDao = factory.getBean(ProductDao.class);

List<Product> productList = productDao.getProductByCodeAndMatchWord("iphone 6s", "IP_6S");
for (Product product : productList) {
System.out.println(product.getProductName());
}
}

 
  收起阅读 »

写了几篇ElasticSearch入门的文章分享一下

我把自己建立一个简单的中文搜索引擎的过程写成了几篇博客。在这里和大家分享一下:
 
轻轻松松做个强大的搜索引擎01 -- 数据库安装:
https://www.zhuxichi.com/2017/ ... al01/
 
轻轻松松做个强大的搜索引擎02 -- 数据录入:
https://www.zhuxichi.com/2017/ ... al02/
 
轻轻松松做个强大的搜索引擎03 -- 全文搜索:
https://www.zhuxichi.com/2017/ ... al03/
 
轻轻松松做个强大的搜索引擎04 -- 分词:
https://www.zhuxichi.com/2017/ ... al04/
 
轻轻松松做个强大的搜索引擎05 -- 关键词高亮:
https://www.zhuxichi.com/2017/ ... al05/
 
轻轻松松做个强大的搜索引擎06 -- 搜索词建议:
https://www.zhuxichi.com/2017/ ... al06/
 
轻轻松松做个强大的搜索引擎07 -- Boosting:
https://www.zhuxichi.com/2017/ ... al07/
 
欢迎交流哈
继续阅读 »
我把自己建立一个简单的中文搜索引擎的过程写成了几篇博客。在这里和大家分享一下:
 
轻轻松松做个强大的搜索引擎01 -- 数据库安装:
https://www.zhuxichi.com/2017/ ... al01/
 
轻轻松松做个强大的搜索引擎02 -- 数据录入:
https://www.zhuxichi.com/2017/ ... al02/
 
轻轻松松做个强大的搜索引擎03 -- 全文搜索:
https://www.zhuxichi.com/2017/ ... al03/
 
轻轻松松做个强大的搜索引擎04 -- 分词:
https://www.zhuxichi.com/2017/ ... al04/
 
轻轻松松做个强大的搜索引擎05 -- 关键词高亮:
https://www.zhuxichi.com/2017/ ... al05/
 
轻轻松松做个强大的搜索引擎06 -- 搜索词建议:
https://www.zhuxichi.com/2017/ ... al06/
 
轻轻松松做个强大的搜索引擎07 -- Boosting:
https://www.zhuxichi.com/2017/ ... al07/
 
欢迎交流哈 收起阅读 »

Elasticsearch Suggester详解

现代的搜索引擎,一般会具备"Suggest As You Type"功能,即在用户输入搜索的过程中,进行自动补全或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度。例如在Google上输入部分关键词,甚至输入拼写错误的关键词时,它依然能够提示出用户想要输入的内容:

google_completion_suggester.png


google_terms_suggester.png



如果自己亲手去试一下,可以看到Google在用户刚开始输入的时候是自动补全的,而当输入到一定长度,如果因为单词拼写错误无法补全,就开始尝试提示相似的词。

那么类似的功能在Elasticsearch里如何实现呢? 答案就在Suggesters API。 Suggesters基本的运作原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回。 根据使用场景的不同,Elasticsearch里设计了4种类别的Suggester,分别是:
  • Term Suggester
  • Phrase Suggester
  • Completion Suggester
  • Context Suggester


在官方的参考文档里,对这4种Suggester API都有比较详细的介绍,但苦于只有英文版,部分国内开发者看完文档后仍然难以理解其运作机制。 本文将在Elasticsearch 5.x上通过示例讲解Suggester的基础用法,希望能帮助部分国内开发者快速用于实际项目开发。限于篇幅,更为高级的Context Suggester会被略过。


首先来看一个Term Suggester的示例:
准备一个叫做blogs的索引,配置一个text字段。


PUT /blogs/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "text"
        }
      }
    }
  }
}


通过bulk api写入几条文档


POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "elk rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{  "body": "elasticsearch is rock solid"}



此时blogs索引里已经有一些文档了,可以进行下一步的探索。为帮助理解,我们先看看哪些term会存在于词典里。
将输入的文本分析一下:


POST _analyze
{
  "text": [
    "Lucene is cool",
    "Elasticsearch builds on top of lucene",
    "Elasticsearch rocks",
    "Elastic is the company behind ELK stack",
    "elk rocks",
    "elasticsearch is rock solid"
  ]
}



(由于结果太长,此处略去)

这些分出来的token都会成为词典里一个term,注意有些token会出现多次,因此在倒排索引里记录的词频会比较高,同时记录的还有这些token在原文档里的偏移量和相对位置信息。
执行一次suggester搜索看看效果:


POST /blogs/_search

  "suggest": {
    "my-suggestion": {
      "text": "lucne rock",
      "term": {
        "suggest_mode": "missing",
        "field": "body"
      }
    }
  }
}



suggest就是一种特殊类型的搜索,DSL内部的"text"指的是api调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的lucne是错误的拼写,模拟用户输入错误。 "term"表示这是一个term suggester。 "field"指定suggester针对的字段,另外有一个可选的"suggest_mode"。 范例里的"missing"实际上就是缺省值,它是什么意思?有点挠头... 还是先看看返回结果吧:


{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": 0,
    "hits":
  },
  "suggest": {
    "my-suggestion": [
      {
        "text": "lucne",
        "offset": 0,
        "length": 5,
        "options": [
          {
            "text": "lucene",
            "score": 0.8,
            "freq": 2
          }
        ]
      },
      {
        "text": "rock",
        "offset": 6,
        "length": 4,
        "options":
      }
    ]
  }
}



在返回结果里"suggest" -> "my-suggestion"部分包含了一个数组,每个数组项对应从输入文本分解出来的token(存放在"text"这个key里)以及为该token提供的建议词项(存放在options数组里)。  示例里返回了"lucne","rock"这2个词的建议项(options),其中"rock"的options是空的,表示没有可以建议的选项,为什么? 上面提到了,我们为查询提供的suggest mode是"missing",由于"rock"在索引的词典里已经存在了,够精准,就不建议啦。 只有词典里找不到词,才会为其提供相似的选项。

如果将"suggest_mode"换成"popular"会是什么效果?
尝试一下,重新执行查询,返回结果里"rock"这个词的option不再是空的,而是建议为rocks。


 "suggest": {
    "my-suggestion": [
      {
        "text": "lucne",
        "offset": 0,
        "length": 5,
        "options": [
          {
            "text": "lucene",
            "score": 0.8,
            "freq": 2
          }
        ]
      },
      {
        "text": "rock",
        "offset": 6,
        "length": 4,
        "options": [
          {
            "text": "rocks",
            "score": 0.75,
            "freq": 2
          }
        ]
      }
    ]
  }



回想一下,rock和rocks在索引词典里都是有的。 不难看出即使用户输入的token在索引的词典里已经有了,但是因为存在一个词频更高的相似项,这个相似项可能是更合适的,就被挑选到options里了。 最后还有一个"always" mode,其含义是不管token是否存在于索引词典里都要给出相似项。

有人可能会问,两个term的相似性是如何判断的? ES使用了一种叫做Levenstein edit distance的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。 Term suggester还有其他很多可选参数来控制这个相似性的模糊程度,这里就不一一赘述了。

Term suggester正如其名,只基于analyze过的单个term去提供建议,并不会考虑多个term之间的关系。API调用方只需为每个token挑选options里的词,组合在一起返回给用户前端即可。 那么有无更直接办法,API直接给出和用户输入文本相似的内容? 答案是有,这就要求助Phrase Suggester了。

Phrase suggester在Term suggester的基础上,会考量多个term之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等等。看个范例就比较容易明白了:


POST /blogs/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne and elasticsear rock",
      "phrase": {
        "field": "body",
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}



返回结果:


"suggest": {
    "my-suggestion": [
      {
        "text": "lucne and elasticsear rock",
        "offset": 0,
        "length": 26,
        "options": [
          {
            "text": "lucene and elasticsearch rock",
            "highlighted": "<em>lucene</em> and <em>elasticsearch</em> rock",
            "score": 0.004993905
          },
          {
            "text": "lucne and elasticsearch rock",
            "highlighted": "lucne and <em>elasticsearch</em> rock",
            "score": 0.0033391973
          },
          {
            "text": "lucene and elasticsear rock",
            "highlighted": "<em>lucene</em> and elasticsear rock",
            "score": 0.0029183894
          }
        ]
      }
    ]
  }



options直接返回一个phrase列表,由于加了highlight选项,被替换的term会被高亮。因为lucene和elasticsearch曾经在同一条原文里出现过,同时替换2个term的可信度更高,所以打分较高,排在第一位返回。Phrase suggester有相当多的参数用于控制匹配的模糊程度,需要根据实际应用情况去挑选和调试。


最后来谈一下Completion Suggester,它主要针对的应用场景就是"Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个Suggester采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引,FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找,这也是Completion Suggester的局限所在。

为了使用Completion Suggester,字段的类型需要专门定义如下:


PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion"
        }
      }
    }
  }
}



用bulk API索引点数据:


POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "the elk stack rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "elasticsearch is rock solid"}




查找:


POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}



结果:


"suggest": {
    "blog-suggest": [
      {
        "text": "elastic i",
        "offset": 0,
        "length": 9,
        "options": [
          {
            "text": "Elastic is the company behind ELK stack",
            "_index": "blogs_completion",
            "_type": "tech",
            "_id": "AVrXFyn-cpYmMpGqDdcd",
            "_score": 1,
            "_source": {
              "body": "Elastic is the company behind ELK stack"
            }
          }
        ]
      }
    ]
  }



值得注意的一点是Completion Suggester在索引原始数据的时候也要经过analyze阶段,取决于选用的analyzer不同,某些词可能会被转换,某些词可能被去除,这些会影响FST编码结果,也会影响查找匹配的效果。

比如我们删除上面的索引,重新设置索引的mapping,将analyzer更改为"english":


PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion",
          "analyzer": "english"
        }
      }
    }
  }
}



bulk api索引同样的数据后,执行下面的查询:


POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}



居然没有匹配结果了,多么费解!  原来我们用的english analyzer会剥离掉stop word,而is就是其中一个,被剥离掉了!
用analyze api测试一下:


POST _analyze?analyzer=english
{
  "text": "elasticsearch is rock solid"
}

会发现只有3个token:
{
  "tokens": [
    {
      "token": "elasticsearch",
      "start_offset": 0,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "rock",
      "start_offset": 17,
      "end_offset": 21,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "solid",
      "start_offset": 22,
      "end_offset": 27,
      "type": "<ALPHANUM>",
      "position": 3
    }
  ]
}



FST只编码了这3个token,并且默认的还会记录他们在文档中的位置和分隔符。 用户输入"elastic i"进行查找的时候,输入被分解成"elastic"和"i",FST没有编码这个“i” , 匹配失败。

好吧,如果你现在还足够清醒的话,试一下搜索"elastic is",会发现又有结果,why?  因为这次输入的text经过english analyzer的时候is也被剥离了,只需在FST里查询"elastic"这个前缀,自然就可以匹配到了。

其他能影响completion suggester结果的,还有诸如"preserve_separators","preserve_position_increments"等等mapping参数来控制匹配的模糊程度。以及搜索时可以选用Fuzzy Queries,使得上面例子里的"elastic i"在使用english analyzer的情况下依然可以匹配到结果。

因此用好Completion Sugester并不是一件容易的事,实际应用开发过程中,需要根据数据特性和业务需要,灵活搭配analyzer和mapping参数,反复调试才可能获得理想的补全效果。

回到篇首Google搜索框的补全/纠错功能,如果用ES怎么实现呢?我能想到的一个的实现方式:
  1. 在用户刚开始输入的过程中,使用Completion Suggester进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能Completion Suggester的结果已经够好,用户已经可以看到理想的备选项了。 
  2. 如果Completion Suggester已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下Phrase Suggester。
  3. 如果Phrase Suggester没有找到任何option,开始尝试term Suggester。


精准程度上(Precision)看: Completion >  Phrase > term, 而召回率上(Recall)则反之。从性能上看,Completion Suggester是最快的,如果能满足业务需求,只用Completion Suggester做前缀匹配是最理想的。 Phrase和Term由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制suggester用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量map到内存。
继续阅读 »
现代的搜索引擎,一般会具备"Suggest As You Type"功能,即在用户输入搜索的过程中,进行自动补全或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度。例如在Google上输入部分关键词,甚至输入拼写错误的关键词时,它依然能够提示出用户想要输入的内容:

google_completion_suggester.png


google_terms_suggester.png



如果自己亲手去试一下,可以看到Google在用户刚开始输入的时候是自动补全的,而当输入到一定长度,如果因为单词拼写错误无法补全,就开始尝试提示相似的词。

那么类似的功能在Elasticsearch里如何实现呢? 答案就在Suggesters API。 Suggesters基本的运作原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回。 根据使用场景的不同,Elasticsearch里设计了4种类别的Suggester,分别是:
  • Term Suggester
  • Phrase Suggester
  • Completion Suggester
  • Context Suggester


在官方的参考文档里,对这4种Suggester API都有比较详细的介绍,但苦于只有英文版,部分国内开发者看完文档后仍然难以理解其运作机制。 本文将在Elasticsearch 5.x上通过示例讲解Suggester的基础用法,希望能帮助部分国内开发者快速用于实际项目开发。限于篇幅,更为高级的Context Suggester会被略过。


首先来看一个Term Suggester的示例:
准备一个叫做blogs的索引,配置一个text字段。


PUT /blogs/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "text"
        }
      }
    }
  }
}


通过bulk api写入几条文档


POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{ "body": "elk rocks"}
{ "index" : { "_index" : "blogs", "_type" : "tech" } }
{  "body": "elasticsearch is rock solid"}



此时blogs索引里已经有一些文档了,可以进行下一步的探索。为帮助理解,我们先看看哪些term会存在于词典里。
将输入的文本分析一下:


POST _analyze
{
  "text": [
    "Lucene is cool",
    "Elasticsearch builds on top of lucene",
    "Elasticsearch rocks",
    "Elastic is the company behind ELK stack",
    "elk rocks",
    "elasticsearch is rock solid"
  ]
}



(由于结果太长,此处略去)

这些分出来的token都会成为词典里一个term,注意有些token会出现多次,因此在倒排索引里记录的词频会比较高,同时记录的还有这些token在原文档里的偏移量和相对位置信息。
执行一次suggester搜索看看效果:


POST /blogs/_search

  "suggest": {
    "my-suggestion": {
      "text": "lucne rock",
      "term": {
        "suggest_mode": "missing",
        "field": "body"
      }
    }
  }
}



suggest就是一种特殊类型的搜索,DSL内部的"text"指的是api调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的lucne是错误的拼写,模拟用户输入错误。 "term"表示这是一个term suggester。 "field"指定suggester针对的字段,另外有一个可选的"suggest_mode"。 范例里的"missing"实际上就是缺省值,它是什么意思?有点挠头... 还是先看看返回结果吧:


{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "hits": {
    "total": 0,
    "max_score": 0,
    "hits":
  },
  "suggest": {
    "my-suggestion": [
      {
        "text": "lucne",
        "offset": 0,
        "length": 5,
        "options": [
          {
            "text": "lucene",
            "score": 0.8,
            "freq": 2
          }
        ]
      },
      {
        "text": "rock",
        "offset": 6,
        "length": 4,
        "options":
      }
    ]
  }
}



在返回结果里"suggest" -> "my-suggestion"部分包含了一个数组,每个数组项对应从输入文本分解出来的token(存放在"text"这个key里)以及为该token提供的建议词项(存放在options数组里)。  示例里返回了"lucne","rock"这2个词的建议项(options),其中"rock"的options是空的,表示没有可以建议的选项,为什么? 上面提到了,我们为查询提供的suggest mode是"missing",由于"rock"在索引的词典里已经存在了,够精准,就不建议啦。 只有词典里找不到词,才会为其提供相似的选项。

如果将"suggest_mode"换成"popular"会是什么效果?
尝试一下,重新执行查询,返回结果里"rock"这个词的option不再是空的,而是建议为rocks。


 "suggest": {
    "my-suggestion": [
      {
        "text": "lucne",
        "offset": 0,
        "length": 5,
        "options": [
          {
            "text": "lucene",
            "score": 0.8,
            "freq": 2
          }
        ]
      },
      {
        "text": "rock",
        "offset": 6,
        "length": 4,
        "options": [
          {
            "text": "rocks",
            "score": 0.75,
            "freq": 2
          }
        ]
      }
    ]
  }



回想一下,rock和rocks在索引词典里都是有的。 不难看出即使用户输入的token在索引的词典里已经有了,但是因为存在一个词频更高的相似项,这个相似项可能是更合适的,就被挑选到options里了。 最后还有一个"always" mode,其含义是不管token是否存在于索引词典里都要给出相似项。

有人可能会问,两个term的相似性是如何判断的? ES使用了一种叫做Levenstein edit distance的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。 Term suggester还有其他很多可选参数来控制这个相似性的模糊程度,这里就不一一赘述了。

Term suggester正如其名,只基于analyze过的单个term去提供建议,并不会考虑多个term之间的关系。API调用方只需为每个token挑选options里的词,组合在一起返回给用户前端即可。 那么有无更直接办法,API直接给出和用户输入文本相似的内容? 答案是有,这就要求助Phrase Suggester了。

Phrase suggester在Term suggester的基础上,会考量多个term之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等等。看个范例就比较容易明白了:


POST /blogs/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne and elasticsear rock",
      "phrase": {
        "field": "body",
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}



返回结果:


"suggest": {
    "my-suggestion": [
      {
        "text": "lucne and elasticsear rock",
        "offset": 0,
        "length": 26,
        "options": [
          {
            "text": "lucene and elasticsearch rock",
            "highlighted": "<em>lucene</em> and <em>elasticsearch</em> rock",
            "score": 0.004993905
          },
          {
            "text": "lucne and elasticsearch rock",
            "highlighted": "lucne and <em>elasticsearch</em> rock",
            "score": 0.0033391973
          },
          {
            "text": "lucene and elasticsear rock",
            "highlighted": "<em>lucene</em> and elasticsear rock",
            "score": 0.0029183894
          }
        ]
      }
    ]
  }



options直接返回一个phrase列表,由于加了highlight选项,被替换的term会被高亮。因为lucene和elasticsearch曾经在同一条原文里出现过,同时替换2个term的可信度更高,所以打分较高,排在第一位返回。Phrase suggester有相当多的参数用于控制匹配的模糊程度,需要根据实际应用情况去挑选和调试。


最后来谈一下Completion Suggester,它主要针对的应用场景就是"Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个Suggester采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引,FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找,这也是Completion Suggester的局限所在。

为了使用Completion Suggester,字段的类型需要专门定义如下:


PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion"
        }
      }
    }
  }
}



用bulk API索引点数据:


POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "the elk stack rocks"}
{ "index" : { "_index" : "blogs_completion", "_type" : "tech" } }
{ "body": "elasticsearch is rock solid"}




查找:


POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}



结果:


"suggest": {
    "blog-suggest": [
      {
        "text": "elastic i",
        "offset": 0,
        "length": 9,
        "options": [
          {
            "text": "Elastic is the company behind ELK stack",
            "_index": "blogs_completion",
            "_type": "tech",
            "_id": "AVrXFyn-cpYmMpGqDdcd",
            "_score": 1,
            "_source": {
              "body": "Elastic is the company behind ELK stack"
            }
          }
        ]
      }
    ]
  }



值得注意的一点是Completion Suggester在索引原始数据的时候也要经过analyze阶段,取决于选用的analyzer不同,某些词可能会被转换,某些词可能被去除,这些会影响FST编码结果,也会影响查找匹配的效果。

比如我们删除上面的索引,重新设置索引的mapping,将analyzer更改为"english":


PUT /blogs_completion/
{
  "mappings": {
    "tech": {
      "properties": {
        "body": {
          "type": "completion",
          "analyzer": "english"
        }
      }
    }
  }
}



bulk api索引同样的数据后,执行下面的查询:


POST blogs_completion/_search?pretty
{ "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}



居然没有匹配结果了,多么费解!  原来我们用的english analyzer会剥离掉stop word,而is就是其中一个,被剥离掉了!
用analyze api测试一下:


POST _analyze?analyzer=english
{
  "text": "elasticsearch is rock solid"
}

会发现只有3个token:
{
  "tokens": [
    {
      "token": "elasticsearch",
      "start_offset": 0,
      "end_offset": 13,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "rock",
      "start_offset": 17,
      "end_offset": 21,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "solid",
      "start_offset": 22,
      "end_offset": 27,
      "type": "<ALPHANUM>",
      "position": 3
    }
  ]
}



FST只编码了这3个token,并且默认的还会记录他们在文档中的位置和分隔符。 用户输入"elastic i"进行查找的时候,输入被分解成"elastic"和"i",FST没有编码这个“i” , 匹配失败。

好吧,如果你现在还足够清醒的话,试一下搜索"elastic is",会发现又有结果,why?  因为这次输入的text经过english analyzer的时候is也被剥离了,只需在FST里查询"elastic"这个前缀,自然就可以匹配到了。

其他能影响completion suggester结果的,还有诸如"preserve_separators","preserve_position_increments"等等mapping参数来控制匹配的模糊程度。以及搜索时可以选用Fuzzy Queries,使得上面例子里的"elastic i"在使用english analyzer的情况下依然可以匹配到结果。

因此用好Completion Sugester并不是一件容易的事,实际应用开发过程中,需要根据数据特性和业务需要,灵活搭配analyzer和mapping参数,反复调试才可能获得理想的补全效果。

回到篇首Google搜索框的补全/纠错功能,如果用ES怎么实现呢?我能想到的一个的实现方式:
  1. 在用户刚开始输入的过程中,使用Completion Suggester进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能Completion Suggester的结果已经够好,用户已经可以看到理想的备选项了。 
  2. 如果Completion Suggester已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下Phrase Suggester。
  3. 如果Phrase Suggester没有找到任何option,开始尝试term Suggester。


精准程度上(Precision)看: Completion >  Phrase > term, 而召回率上(Recall)则反之。从性能上看,Completion Suggester是最快的,如果能满足业务需求,只用Completion Suggester做前缀匹配是最理想的。 Phrase和Term由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制suggester用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量map到内存。 收起阅读 »

2017 Elastic 官方及社区国内活动日程安排

Elastic-Heart-2.png


【线上活动】

在线直播
直播工具Zoom:https://elastic.zoom.us/j/522710614,房间号:522710614(密码进群索取)
  • 《Elastic{ON}17 Keynote 回顾》,QQ 群(190605846),2017-3-17 21:00 PM,回放
  • 《What's new in Elasticsearch 5》,QQ 群(190605846),2017-3-20 21:00 PM,回放
  • 《What's new in Logstash 5》,QQ 群(190605846),2017-3-21 21:00 PM,回放 

 
 
 
【线下活动】

 Workshop
  • Elastic Workshop,北京,2017-04-10【报名结束】
  • Elastic Workshop,上海,2017-04-17【报名结束】
  • Elastic Workshop,深圳,2017-04-20【报名结束】
  • Elastic Workshop,上海,2017-06-29【报名结束】
  • Elastic Workshop,广州,2017-07-04【报名结束】
  • Elastic Workshop,深圳,2017-07-06【报名结束】

 
Meetup
  • Elastic Meetup Shanghai ,上海,2017.5.14, 【报名结束】【日程
  • Elastic Meetup Beijing ,北京,2017.5.21  【报名结束】【日程】【直播回放
  • Elastic Meetup Nanjing,南京,2017.6.10 【报名结束】【日程】【直播回放
  • Elastic Meetup Hangzhou,杭州,2017.6.25 【报名结束​】【日程】【直播回放
  • Elastic Meetup Changsha,长沙,2017.10.28 【报名结束​】【日程
  • Elastic Meetup Wuhan,武汉,2017.11.4 【报名结束​】【日程
  • Elastic Meetup Guangzhou,广州,2017.11.25 【报名结束​】【日程
  • Elastic Meetup Shenzhen,深圳,2017.12.16 【报名结束​】【日程

 
【会议参展】

Elastic 今年继续赞助和支持各种开发者会议,欢迎届时来展台交流。
  • Gopher China,上海,2017.04.15-2017.04.16
  • OSC Shanghai ,上海,2017.5.13
  • The China-R Conf,北京,2017.5.19-2017.5.21
  • OSC Hangzhou,杭州,2017.6.24
  • ArchSummit Shenzhen,深圳,2017.7.7-2017.7.8
  • OSC Jinan,济南,2017.7.22
  • OSC Zhuhai,珠海,2017.8.27
  • RubyConf China,杭州,2017.9.16-17
  • OSC Chengdu, 成都,2017.9.23
  • OSC Chongqing,重庆,2017.9.24
  • ArchSummit Beijing,2017.12.8-2017.12.9

 
上面是暂时确定的活动,部分活动报名链接晚点放出来,请关注本页面。
欢迎各个不同的城市的同学一起帮忙举办线下活动。
各个城市的线下活动欢迎报名分享,大家多交流,话题无论大小。
继续阅读 »
Elastic-Heart-2.png


【线上活动】

在线直播
直播工具Zoom:https://elastic.zoom.us/j/522710614,房间号:522710614(密码进群索取)
  • 《Elastic{ON}17 Keynote 回顾》,QQ 群(190605846),2017-3-17 21:00 PM,回放
  • 《What's new in Elasticsearch 5》,QQ 群(190605846),2017-3-20 21:00 PM,回放
  • 《What's new in Logstash 5》,QQ 群(190605846),2017-3-21 21:00 PM,回放 

 
 
 
【线下活动】

 Workshop
  • Elastic Workshop,北京,2017-04-10【报名结束】
  • Elastic Workshop,上海,2017-04-17【报名结束】
  • Elastic Workshop,深圳,2017-04-20【报名结束】
  • Elastic Workshop,上海,2017-06-29【报名结束】
  • Elastic Workshop,广州,2017-07-04【报名结束】
  • Elastic Workshop,深圳,2017-07-06【报名结束】

 
Meetup
  • Elastic Meetup Shanghai ,上海,2017.5.14, 【报名结束】【日程
  • Elastic Meetup Beijing ,北京,2017.5.21  【报名结束】【日程】【直播回放
  • Elastic Meetup Nanjing,南京,2017.6.10 【报名结束】【日程】【直播回放
  • Elastic Meetup Hangzhou,杭州,2017.6.25 【报名结束​】【日程】【直播回放
  • Elastic Meetup Changsha,长沙,2017.10.28 【报名结束​】【日程
  • Elastic Meetup Wuhan,武汉,2017.11.4 【报名结束​】【日程
  • Elastic Meetup Guangzhou,广州,2017.11.25 【报名结束​】【日程
  • Elastic Meetup Shenzhen,深圳,2017.12.16 【报名结束​】【日程

 
【会议参展】

Elastic 今年继续赞助和支持各种开发者会议,欢迎届时来展台交流。
  • Gopher China,上海,2017.04.15-2017.04.16
  • OSC Shanghai ,上海,2017.5.13
  • The China-R Conf,北京,2017.5.19-2017.5.21
  • OSC Hangzhou,杭州,2017.6.24
  • ArchSummit Shenzhen,深圳,2017.7.7-2017.7.8
  • OSC Jinan,济南,2017.7.22
  • OSC Zhuhai,珠海,2017.8.27
  • RubyConf China,杭州,2017.9.16-17
  • OSC Chengdu, 成都,2017.9.23
  • OSC Chongqing,重庆,2017.9.24
  • ArchSummit Beijing,2017.12.8-2017.12.9

 
上面是暂时确定的活动,部分活动报名链接晚点放出来,请关注本页面。
欢迎各个不同的城市的同学一起帮忙举办线下活动。
各个城市的线下活动欢迎报名分享,大家多交流,话题无论大小。 收起阅读 »