文章目录

  • lucene&ES全文搜索
  • 一、认识全文搜索引擎
  • 1、什么是全文搜索
  • 2、全文检索的特点
  • 3、常见的全文索引
  • 二、Lucene介绍
  • 1、Lucene是什么
  • 2、Lucene的核心
  • 3、索引创建分为5部(重点)
  • 4、索引搜索
  • 三、Lucene-Helloworld程序
  • 1、创建索引
  • 2、搜索索引
  • 3、重点
  • 四、认识ElasticSearch(简称ES)
  • 1、为什么要使用ElasticSearch
  • 2、ElasticSearch特点
  • 3、安装与使用
  • 4、辅助管理工具Kibana
  • 五、文档的操作
  • 1、相关概念理解
  • 2、CRUD
  • 3、DSL查询和过滤
  • 4、使用DSL查询与过滤
  • ①、match标准查询
  • ②、term单词查询
  • ③、range范围查找
  • ④、prefix前缀查询
  • ⑤、wildcard通配符查询
  • ⑥、bool组合多个条件
  • 5、分词
  • ①、IK分词器
  • ②、映射
  • 六、Java操作ES
  • 1、导入依赖
  • 2、连接ES获取Client对象
  • 3、创建文档索引
  • 4、获取文档
  • 5、更新文档
  • 6、删除文档
  • 7、批量操作
  • 8、综合搜索


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部(重点)

  1. 分词
  2. 词态转换,转换大小写
  3. 排序(自然排序)
  4. 合并单词(id倒排)
  5. 形成倒排索引文档

下面的例子可以说明上述过程

java 全文搜索 数据库 java实现全文检索_mysql


在①处分别为3个句子加上编号,然后进行分词,把每一个单词分解出来与编号对应放在②处;在搜索的过程总,对于搜索的过程中大写和小写指的都是同一个单词,在这就没有区分的必要,按规则统一变为小写放在③处;要加快搜索速度,就必须保证这些单词的排列时有一定规则,这里按照字母顺序排列后放在④处;最后再简化索引,合并相同的单词,就得到如下结果:

java 全文搜索 数据库 java实现全文检索_数据库_02


通常在数据库中我们都是根据文档找到内容,而这里是通过词,能够快速找到包含他的文档,这就是倒排索引文档。

4、索引搜索

还是通过上面的例子:

java 全文搜索 数据库 java实现全文检索_elasticsearch_03


搜索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、创建索引

java 全文搜索 数据库 java实现全文检索_大数据_04

  1. 创建数据
  2. 创建索引目录以及创建索引写入的配置对象
  3. 创建IndexWriter
  4. 将数据转换为document对象,并添加到缓冲区
  5. 执行索引创建
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、搜索索引

java 全文搜索 数据库 java实现全文检索_mysql_05

  1. 准备索引目录
  2. 准备索引加载器
  3. 使用IndexSearcher进行检索
  4. 执行索引
/**
     * 搜索索引
     * @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

java 全文搜索 数据库 java实现全文检索_大数据_06


访问:http://localhost:9200/

java 全文搜索 数据库 java实现全文检索_elasticsearch_07

出现上图表示ES已经能够正常使用

注意:要修改分配资源的空间(jvm.options文件中),建议改为512

4、辅助管理工具Kibana

官网:https://www.elastic.co/downloads/kibana

默认访问地址:http://localhost:5601

java 全文搜索 数据库 java实现全文检索_elasticsearch_08


****Discover****:可视化查询分析器

****Visualize****:统计分析图表

****Dashboard****:自定义主面板(添加图表)

Timelion:Timelion是一个kibana时间序列展示组件(暂时不用)

Dev Tools :Console(同CURL/POSTER,操作ES代码工具,代码提示,很方便)

****Management****:管理索引库(index)、已保存的搜索和可视化结果(save objects)、设置 kibana 服务器属性。

五、文档的操作

1、相关概念理解

java 全文搜索 数据库 java实现全文检索_大数据_09


可以类比着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

java 全文搜索 数据库 java实现全文检索_elasticsearch_10


测试分词器

POST _analyze
{
“analyzer”:“ik_smart”,
“text”:“中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首”
}

②、映射

  1. 基本字段类型

字符串:text(分词),keyword(不分词) StringField(不分词文本)

数字:long,integer,short,double,float

日期:date

逻辑:boolean

{user:{“key”:value}}

{hobbys:[xxx,xx]}

  1. 复杂数据类型

对象类型: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

搜索分词器:搜索该字段的值时,传入的查询内容的分词器。

java 全文搜索 数据库 java实现全文检索_mysql_11


简单映射

#获取当前映射 
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 全文搜索 数据库 java实现全文检索_java 全文搜索 数据库_12


动态模板

java 全文搜索 数据库 java实现全文检索_大数据_13

六、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());
        }
    }