文章目录
lucene&ES全文搜索
一、认识全文搜索引擎
1、什么是全文搜索
对非结构化数据的搜索就叫全文检索,狭义的理解主要针对文本数据的搜索。
- 结构化数据
指关系型数据,主要是指关系型数据库形式管理得数据。
- 半结构数据
非关系模型得、有基本固定解构模式得数据,eg:日志文件、XML文档、JSON文档、Email等。
- 非结构化数据
没有固定模式的数据,如WORD、PDF、PPT、EXL,各种格式的图片、视频等。
理解:可以理解为全文检索就是把没有结构化的数据变成有结构的数据,然后进行搜索,因为有结构化的数据通常情况下可以按照某种算法进行搜索。
2、全文检索的特点
- 相关度最高的排在最前面
- 关键词的高亮
- 只处理文本,不处理语义
3、常见的全文索引
-
全文搜索工具包-Lucene(核心)
-
全文搜索服务器 ,Elastic Search(ES) / Solr等封装了lucene并扩展
二、Lucene介绍
1、Lucene是什么
Lucene是apache下的一个开源的全文检索引擎工具包(一堆jar包)。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在小型目标系统中实现全文检索的功能。
2、Lucene的核心
- 索引创建
- 索引搜索
3、索引创建分为5部(重点)
- 分词
- 词态转换,转换大小写
- 排序(自然排序)
- 合并单词(id倒排)
- 形成倒排索引文档
下面的例子可以说明上述过程
在①处分别为3个句子加上编号,然后进行分词,把每一个单词分解出来与编号对应放在②处;在搜索的过程总,对于搜索的过程中大写和小写指的都是同一个单词,在这就没有区分的必要,按规则统一变为小写放在③处;要加快搜索速度,就必须保证这些单词的排列时有一定规则,这里按照字母顺序排列后放在④处;最后再简化索引,合并相同的单词,就得到如下结果:
通常在数据库中我们都是根据文档找到内容,而这里是通过词,能够快速找到包含他的文档,这就是倒排索引文档。
4、索引搜索
还是通过上面的例子:
搜索java world两个关键词,符合java的有1,2两个文档,符合world的有1,3两个文档,在搜索引擎中直接这样排列两个词他们之间是OR的关系,出现其中一个都可以被找到,所以这里3个都会出来。全文检索中是有相关性排序的,那么结果在是怎么排列的呢?hello java world中包含两个关键字排在第一,另两个都包含一个关键字,得到结果,hello lucene world排在第二,java在最长的句子中占的权重最低排在结果集的第三。从这里可以看出相关度排序还是有一定规则的。
三、Lucene-Helloworld程序
- 创建普通maven项目
- 导入相关Lucene的jar包
- 创建测试类,分别创建索引,搜索索引
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>5.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>5.5.0</version>
</dependency>
1、创建索引
- 创建数据
- 创建索引目录以及创建索引写入的配置对象
- 创建IndexWriter
- 将数据转换为document对象,并添加到缓冲区
- 执行索引创建
public class TestLucene {
//1、创建数据
Long id1 = 1L;
String intro1 = "you are so handsome today";
Long id2 = 2L;
String intro2 = "he are so handsome today";
Long id3 = 3L;
String intro3 = "she are so handsome today";
//存放索引得路径
String dirPath = "D:\\Java0827\\Ideaworkspace\\day81_Lucene\\index";
/**
* 创建索引
* @throws IOException
*/
@Test
public void testCreatIndex() throws IOException {
//2、创建索引目录:用来存放索引得一个目录
SimpleFSDirectory directory = new SimpleFSDirectory(Paths.get(dirPath));
//创建IndexWriterConfig,指定分词器
IndexWriterConfig conf = new IndexWriterConfig(new SimpleAnalyzer());
//3、创建IndexWriter
IndexWriter indexWriter = new IndexWriter(directory, conf);
//4、将数据转换为document对象
Document doc1 = new Document();
doc1.add(new TextField("id",id1.toString(), Field.Store.YES));
doc1.add(new TextField("intro",intro1, Field.Store.YES));
Document doc2 = new Document();
doc2.add(new TextField("id",id2.toString(), Field.Store.YES));
doc2.add(new TextField("intro",intro2, Field.Store.YES));
Document doc3 = new Document();
doc3.add(new TextField("id",id3.toString(), Field.Store.YES));
doc3.add(new TextField("intro",intro3, Field.Store.YES));
//添加document到缓冲区
indexWriter.addDocument(doc1);
indexWriter.addDocument(doc2);
indexWriter.addDocument(doc3);
//5、执行索引得创建
indexWriter.commit();
//6、关闭资源
indexWriter.close();
}
}
2、搜索索引
- 准备索引目录
- 准备索引加载器
- 使用IndexSearcher进行检索
- 执行索引
/**
* 搜索索引
* @throws IOException
*/
@Test
public void testSearchIndex() throws IOException {
//1、准备索引目录
SimpleFSDirectory directory = new SimpleFSDirectory(Paths.get(dirPath));
//2、准备索引加载器
DirectoryReader reader = DirectoryReader.open(directory);
//3、使用IndexSearcher进行检索
IndexSearcher indexSearcher = new IndexSearcher(reader);
//4、执行索引
//查询单个单词
Query query = new TermQuery(new Term("intro", "so"));
//指定一个分页
int pageSize = 10;
TopDocs topDocs = indexSearcher.search(query, pageSize);
System.out.println("查询出得总条数:"+topDocs.totalHits);
//命中得所有文档
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
//文档编号
int docid = scoreDoc.doc;
//文档匹配的得分
float score = scoreDoc.score;
System.out.println("匹配得分:"+score);
Document doc = indexSearcher.doc(docid);
System.out.print("编号:"+doc.get("id"));
System.out.println(" ======> "+doc.get("intro"));
}
}
3、重点
1.哪些字段需要索引以及分词?
2.哪些字段需要索引但是不需要分词?
答:
- 需要参与搜索的字段就需要创建索引
- 不参与搜索的字段就不需要创建索引
- 要进行关键字搜索的(模糊查找)字段就要索引且要分词 类似MySQL里的like
- 而需要精确查找的(指定的条件)要索引但是不需要分词 类似where后面的确定条件
3.哪些数据要放到数据区的?
答:
- 需要进行展示的数据
四、认识ElasticSearch(简称ES)
1、为什么要使用ElasticSearch
虽然Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库,但是还是存在一些问题:
- 配置较为复杂
- 必须使用Java进行开发
- 不支持分布式集群
2、ElasticSearch特点
- 支持分布式集群
- 性能高:实时搜索
- 处理PB级别的数据
- 基于RESTful API使用简单
- 各种语言都支持
- 上手容易,屏蔽了Lucene的复杂性
3、安装与使用
官网:https://www.elastic.co/downloads/elasticsearch
运行ES
访问:http://localhost:9200/
出现上图表示ES已经能够正常使用
注意:要修改分配资源的空间(jvm.options文件中),建议改为512
4、辅助管理工具Kibana
官网:https://www.elastic.co/downloads/kibana
默认访问地址:http://localhost:5601
****Discover****:可视化查询分析器
****Visualize****:统计分析图表
****Dashboard****:自定义主面板(添加图表)
Timelion:Timelion是一个kibana时间序列展示组件(暂时不用)
Dev Tools :Console(同CURL/POSTER,操作ES代码工具,代码提示,很方便)
****Management****:管理索引库(index)、已保存的搜索和可视化结果(save objects)、设置 kibana 服务器属性。
五、文档的操作
1、相关概念理解
可以类比着MySql进行记忆
2、CRUD
获取资源:GET 索引库/类型/ID
更新资源:POST
- 全量修改 PUT 索引库/类型/ID {json数据}
- 局部修改 POST 索引库/类型/ID/_update {doc:{json数据}}
新建资源:PUT 索引库/类型/ID {json数据}
删除资源:DELETE 索引库/类型/ID
分页:
和SQL使用 LIMIT 关键字返回只有一页的结果一样,Elasticsearch接受 from 和 size 参数:
size : 每页条数,默认 10
from : 跳过开始的结果数,默认 0
eg:GET _search?size=2&from=0 (每页显示2个,从第0个开始)
3、DSL查询和过滤
DSL过滤语句和DSL查询语句非常相似,但是它们的使用目的却不同 :
-
DSL过滤 查询文档的方式更像是对于我的条件“有”或者“没有”,------精确查询
-
DSL查询语句则像是“有多像”。-----类似于模糊查询
1.过滤结果可以缓存并应用到后续请求。
2.查询语句同时 匹配文档,计算相关性,所以更耗时,且不缓存。
3.过滤语句 可有效地配合查询语句完成文档过滤。
综合查询语法
#综合语法练习 年龄 在 15~20 名字 包含zs 每页2条 从第0条开始 按照年龄升序
GET people/student/_search
{
"query": {
"bool": {
"must": [
{
"match": {//查询 与(must) 或(should) 非(must not)
"name": "zs"
}
}
],
"filter":{//过滤
"range": {
"age": {
"gte": 15,
"lte": 23
}
}
}
}
},
"from":0,
"size":2,
"sort":[
{
"age":{
"order": "asc"
}
}
]
}
4、使用DSL查询与过滤
①、match标准查询
普通搜索(匹配所有文档)
{
"query" : {
"match_all" : {}
}
}
match查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它
{
"query": {
"match": {
"fullName": "Steven King"
}
}
}
multi_match 查询允许你做 match查询的基础上同时搜索多个字段
GET people/student/_search
{
"query": {
"multi_match": {
"query": "zs",
"fields": ["name","age"]
}
}
}
上面的搜索同时在name和age字段中匹配。
②、term单词查询
单词查询,不进行分词,类似精确查找
③、range范围查找
GET people/student/_search
{
"query": {
"range": {
"age": {
"gte": 20,
"lte": 40
}
}
}
}
gt:> gte:>= lt:< lte:<=
④、prefix前缀查询
GET people/student/_search
{
"query": {
"prefix": {
"name": "w"
}
}
}
查询姓名是w开头的
⑤、wildcard通配符查询
使用*代表0~N个,使用?代表1个
GET people/student/_search
{
"query": {
"wildcard": {
"name": "*1"
}
}
}
GET people/student/_search
{
"query": {
"wildcard": {
"name": "????1"
}
}
}
⑥、bool组合多个条件
5、分词
在全文检索理论中,文档的查询是通过关键字查询文档索引来进行匹配,因此将文本拆分为有意义的单词,对于搜索结果的准确性至关重要,因此,在建立索引的过程中和分析搜索语句的过程中都需要对文本串分词
①、IK分词器
中文分词器有很多,这里使用IK
官网:https://github.com/medcl/elasticsearch-analysis-ik
解压后将文其内容放置于ES根目录/plugins/ik
测试分词器
POST _analyze
{
“analyzer”:“ik_smart”,
“text”:“中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首”
}
②、映射
- 基本字段类型
字符串:text(分词),keyword(不分词) StringField(不分词文本)
数字:long,integer,short,double,float
日期:date
逻辑:boolean
{user:{“key”:value}}
{hobbys:[xxx,xx]}
- 复杂数据类型
对象类型:object
数组类型:array
地理位置:geo_point,geo_shape
默认映射:
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” |
一般操作顺序
建库(index)----->建表并且指定类型(type)----->存数据----->查数据
简单映射配置表:
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 | 是否将该字段值组合到_all中。 |
null_value | 默认控制替换值。如空字符串替换为”NULL”,空数字替换为-1 |
store | 是否存储:默认为false。true意义不大,因为_source中已有数据 |
index | 索引模式:analyzed (索引并分词,text默认模式), not_analyzed (索引不分词,keyword默认模式),no(不索引) |
analyzer | 索引分词器:索引创建时使用的分词器,如ik_smart,ik_max_word,standard |
search_analyzer | 搜索分词器:搜索该字段的值时,传入的查询内容的分词器。 |
简单映射
#获取当前映射
GET _mapping
#删除
DELETE people
#创建索引库
PUT people
POST people/student/_mapping
{
"student":{
"properties":{
"id":{
"type":"long"
},
"name":{
"type":"text"
},
"age":{
"type":"integer"
},
"email":{
"type":"keyword"
}
}
}
}
全局映射(了解)
动态模板
六、Java操作ES
1、导入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.7</version>
</dependency>
2、连接ES获取Client对象
方式一:(推荐)把每台服务的ip 端口配上
public TransportClient creatClient(){
TransportClient client = null;
//创建客户端
try {
client = new PreBuiltTransportClient(Settings.EMPTY)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
} catch (UnknownHostException e) {
e.printStackTrace();
}
return client;
}
方式二:通过集群名称来查找
注意,如果你有一个与 ES集群不同的集群,你可以设置机器的名字。
Settings settings = Settings.builder()
.put("cluster.name", "myClusterName").build();
TransportClient client = new PreBuiltTransportClient(settings);
//添加地址到client中
方式三:
你可以设置client.transport.sniff为true来使客户端去嗅探整个集群的状态,把集群中其它机器的ip地址加到客户端中,这样做的好处是一般你不用手动设置集群里所有集群的ip到连接客户端,它会自动帮你添加,并且自动发现新加入集群的机器。
3、创建文档索引
/**
* 创建索引
*/
@Test
public void testAddIndex(){
//获取客户端
TransportClient client = creatClient();
//得到创建索引对象
IndexRequestBuilder builder = client.prepareIndex("person",
"student", "3");
//构建文档数据
Map<String, Object> map = new HashMap<>();
map.put("id", 3L);
map.put("name", "张三");
map.put("age", 14);
map.put("sex", 1);
builder.setSource(map).get();
//执行索引操作
IndexResponse indexResponse = builder.get();
System.out.println(indexResponse);
//关闭客户端
client.close();
}
4、获取文档
/**
* 查询文档
*/
@Test
public void testGet(){
//获取客户端
TransportClient client = creatClient();
//直接获取文档
System.out.println(client.prepareGet("person", "student", "1")
.get().getSource());
//关闭客户端
client.close();
}
5、更新文档
/**
* 更新文档(局部更新)
*/
@Test
public void testUpdate(){
//获取客户端
TransportClient client = creatClient();
//获取跟新文档对象
UpdateRequestBuilder builder = client.prepareUpdate("person",
"student", "1");
//跟新文档数据
Map<String, Object> map = new HashMap<>();
map.put("name", "王五");
map.put("age", 2);
map.put("sex", 1);
//执行跟新操作
UpdateResponse updateResponse = builder.setDoc(map).get();
System.out.println(updateResponse);
//关闭客户端
client.close();
}
6、删除文档
/**
* 删除指定文档
*/
@Test
public void testDelete(){
//获取客户端
TransportClient client = creatClient();
//获取删除索引对象
DeleteRequestBuilder builder = client.prepareDelete("person",
"student", "2");
DeleteResponse deleteResponse = builder.get();
System.out.println(deleteResponse);
client.close();
}
7、批量操作
/**
* 批量操作
*/
@Test
public void testBulk(){
//获取客户端
TransportClient client = creatClient();
//批量操作
BulkRequestBuilder bulk = client.prepareBulk();
//执行删除,情况文档
/*for (int i = 0 ;i<5;i++){
DeleteRequestBuilder deleteRequestBuilder = client.prepareDelete("person",
"student", String.valueOf(i));
DeleteResponse deleteResponse = deleteRequestBuilder.get();
System.out.println(deleteResponse);
bulk.add(deleteRequestBuilder).get();
}*/
//循环添加,获取创建文档对象
/*for (int i = 0 ;i < 100;i++){
IndexRequestBuilder index = client.prepareIndex("person",
"student", String.valueOf(i));
Map<String, Object> map = new HashMap<>();
map.put("id", i);
if (i % 2 == 0 ){
map.put("name", "学生"+i);
}else {
map.put("name", "老师"+i);
}
map.put("age", i);
if (i % 2 == 0 ){
map.put("sex", 0);
}else {
map.put("sex", 1);
}
IndexResponse indexResponse = index.setSource(map).get();
bulk.add(index);
System.out.println(indexResponse);
}*/
//批量修改
for (int i = 0 ;i < 100;i++){
UpdateRequestBuilder update = client.prepareUpdate("person",
"student", String.valueOf(i));
Map<String, Object> map = new HashMap<>();
map.put("height", 130 + i);
if (i % 3 == 0){
map.put("sex", 1);
} else {
map.put("sex", 0);
}
UpdateResponse updateResponse = update.setDoc(map).get();
bulk.add(update);
System.out.println(updateResponse);
}
}
8、综合搜索
/**
* 综合查询
* 按照年纪倒叙排列,name包含老师的,性别为1的,身高在150~180,年龄在30~60,查询0页码,每页10条
*/
@Test
public void testSearch(){
//获取客户端
TransportClient client = creatClient();
//批量操作
SearchRequestBuilder search = client.prepareSearch("person");
//添加排序,年龄倒叙
search.addSort("age", SortOrder.DESC);
//添加分页信息
int currentPage = 1;
int pageSize = 10;
//从第几条开始显示
search.setFrom((currentPage-1)*pageSize);
//每页显示条数
search.setSize(pageSize);
//添加综合查询对象
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder
.filter(QueryBuilders.rangeQuery("age").gte(30).lte(60))
.filter(QueryBuilders.rangeQuery("height").gte(150).lte(180));
boolQueryBuilder.must(QueryBuilders.matchQuery("name", "老师"));
//执行查询结果
SearchResponse searchResponse = search.setQuery(boolQueryBuilder).get();
//获取命中值
SearchHits hits = searchResponse.getHits();
//打印结果
System.out.println("命中总条数:"+hits.totalHits());
for (SearchHit hit : hits) {
System.out.println(hit.getSource());
}
}