2020/03/18-全文检索技术

1.全文检索

1.1数据的分类

结构化数据:

​ mysql:表 字段类型和大小都是固定

非结构化数据:

​ 全文检索:

1.2普通检索和全文检索的比较

普通检索(mysql)(增删改) 全文检索(查)
数据类型 结构化数据 结构化数据和非结构化数据
过程 先创建索引,然后根据id查询 先创建倒排索引,然后根据倒排索引查询
查询速度 有时快,有时慢 一定快
结果范围 普通 广
事务 支持 不支持事务

1.3.全文检索应用场景

(1) 站内搜索

例如:微博 智联招聘 boss直聘

(2) 垂直搜索

比如说腾讯视频能搜到搜狐视频

(3) 搜索引擎

百度 谷歌

2.lucene(了解)

lucene:所有流行的实现全文检索的框架的底层都是lucene,实现全文检索的一套jar包库(类库) 官网:https://lucene.apache.org/

solr:封装lucene这套jar包库的框架,数据库

elastic search:封装lucene这套jar包库的框架,更强,比solr更专业,更简单

2.1.实现全文检索

(1) 创建一个空的项目:

起名:full-text-searching

(2) 创建新模块,lucene

(3) 添加pom依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.shenyian.demo</groupId>
    <artifactId>lucene</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 版本锁定-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- lucene 依赖-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>4.10.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>4.10.3</version>
        </dependency>
        <!-- mybatis plus 的起步依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>
        <!-- mysql 依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- 单元测试的起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!-- ik分词器 -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>

    </dependencies>

</project>

sql脚本:(在代码文件夹中)

(4) 编辑配置文件application.yml

spring: 
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.176.109:3306/elastic_search?useUnicode=true&characterEncoding=UTF8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: ****

(5) 创建启动类,添加MapperScan注解

package com.shenyian;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.shenyian.mapper")
public class LuceneApplication {

    public static void main(String[] args) {
        SpringApplication.run(LuceneApplication.class, args);
    }
}
(6) 创建JobInfo 实体类
package com.shenyian.domain;

import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import lombok.Data;

@Data
@TableName("job_info")
public class JobInfo {
    @TableId
    private Long id;
    //公司名称
    private String companyName;
    //职位名称
    private String jobName;
    //薪资范围,最小
    private Integer salaryMin;
    //招聘信息详情页
    private String url;
}

(7) 创建mapper
package com.shenyian.mapper;

import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.shenyian.domain.JobInfo;

public interface JobInfoMapper extends BaseMapper<JobInfo> {
}

(8) 单元测试类:

创建索引库,添加文档:

  @Test
    public void test() throws Exception {

        List<JobInfo> jobInfos = jobInfoMapper.selectList(null);

        //Directory d, IndexWriterConfig conf
        Directory directory = FSDirectory.open(new File("H:\\lucene\\index"));//指定索引库保存的地址
        //Version matchVersion, Analyzer analyzer
        Analyzer analyzer = new IKAnalyzer();//中文分词器
        //Analyzer analyzer = new StandardAnalyzer();//标准分词器 对于英文识别,不识别中文
        //Analyzer analyzer = new CJKAnalyzer();//中日韩分词器 分词的不准
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer);
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); //用来创建索引库的工具
        for (JobInfo jobInfo : jobInfos) {
            Document document = new Document();
            document.add(new TextField("companyName", jobInfo.getCompanyName(), Field.Store.YES));
            document.add(new TextField("jobName", jobInfo.getJobName(), Field.Store.YES));
            document.add(new DoubleField("salaryMin", jobInfo.getSalaryMin(), Field.Store.YES));
            document.add(new StringField("url", jobInfo.getUrl(), Field.Store.YES));
            indexWriter.addDocument(document);//添加document 文档
        }
        indexWriter.close();//io关闭

    }

通过luke工具查看索引库:

选择代码中index所在文件夹:


点击ok:

索引库:

(9) 实现检索
    @Test
    public void search() throws Exception {
        IndexReader indexReader = DirectoryReader.open(FSDirectory.open(new File("H:\\lucene\\index")));//用来读取索引库的信息
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);//是用来检索
        TopDocs topDocs = indexSearcher.search(new TermQuery(new Term("jobName", "java")), 10);//通过term查询,最多显示10条
        int totalHits = topDocs.totalHits;
        System.out.println("匹配到的数据条数:" + totalHits);
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;//通过倒排索引查询到的id数组
        for (ScoreDoc scoreDoc : scoreDocs) {
            int doc = scoreDoc.doc;//文档的id
            Document document = indexSearcher.doc(doc);//通过id查询到文档
            System.out.println(document.get("companyName"));
            System.out.println(document.get("jobName"));
            System.out.println(document.get("salayMin"));
            System.out.println(document.get("url"));
            System.out.println("=====================================");
        }
    }

2.2.IK分词器的扩展词和停用词

​ 在resources下创建文件: IKAnalyzer.cfg.xml

​ 然后再在resources下创建文件ext.dic 和 stopword.dic

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">  
<properties>  
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict">ext.dic</entry>
	<!--用户可以在这里配置自己的停止词字典-->
	<entry key="ext_stopwords">stopword.dic</entry>
	
</properties>    

(1) 添加扩展词和停用词的配置文件

(2) 添加扩展词

(3) 再次创建索引库,可以先清除之前的索引库

(4) 配置之后,需要重新加载文档(加载之前要删除旧的):

(5) 那么扩展词就能查到,停止词查不到了

2.3.竞价排名(关键代码如下)

默认的排序是根据匹配度:如果匹配度一样,那么根据id

竞价排名的优先级高于匹配度 ,通过field的属性来设置:textField.setBoost(10000); //打分

     @Test
    public void test() throws Exception {

        List<JobInfo> jobInfos = jobInfoMapper.selectList(null);

        //Directory d, IndexWriterConfig conf
        Directory directory = FSDirectory.open(new File("H:\\lucene\\index"));//指定索引库保存的地址
        //Version matchVersion, Analyzer analyzer
        Analyzer analyzer = new IKAnalyzer();//中文分词器
        //Analyzer analyzer = new StandardAnalyzer();//标准分词器 对于英文识别,不识别中文
        //Analyzer analyzer = new CJKAnalyzer();//中日韩分词器 分词的不准
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer);
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); //用来创建索引库的工具
        indexWriter.deleteAll();//清除原先的索引库
        for (JobInfo jobInfo : jobInfos) {
            Document document = new Document();
            document.add(new TextField("companyName", jobInfo.getCompanyName(), Field.Store.YES));
            document.add(new TextField("jobName", jobInfo.getJobName(), Field.Store.YES));
            document.add(new DoubleField("salaryMin", jobInfo.getSalaryMin(), Field.Store.YES));
            document.add(new StringField("url", jobInfo.getUrl(), Field.Store.YES));
            indexWriter.addDocument(document);//添加document 文档
        }
        //单独添加一个给钱的公司
        Document document = new Document();
        TextField textField = new TextField("companyName", "给钱的随便写的给了排名第一的有限公司", Field.Store.YES);
        textField.setBoost(10000); //打分
        document.add(textField);
        document.add(new TextField("jobName", "java", Field.Store.YES));
        document.add(new DoubleField("salaryMin", 30000, Field.Store.YES));
        document.add(new StringField("url", "www.suibian.com", Field.Store.YES));
        indexWriter.addDocument(document);
        indexWriter.close();//io关闭

    }

es有两个端口:9200(通过浏览器http协议可以访问)9300(集群的es 互相访问,tcp协议访问)

安装:node.js》6.2.4 版本es 》6.2.4版本 kibana可视化工具》谷歌浏览器es插件

1,安装node:

(1) 一路next即可,智能安装

(2) 检查,在cmd命令窗口中输入:果出现版本,说明安装成功

2, 安装es和kibana:

(1) es和kibana的压缩包,解压缩

(2) 在es的解压缩之后的软件中找到config目录,然后修改如下配置:

​ 1)elasticsearch.yml: 数据保存地址和日志保存地址

​ 2)jvm.options:配置启动占用的内存

(3) 在es的plugins文件夹中添加ik分词器插件,如果已经有,那么就直接第四步:

(4) 启动es:

(5) 启动了两个端口9200是可以通过浏览器访问,9300是为了集群内部使用

(6) 验证:如果浏览器显示如下信息,那么代表es已经安装成功

(6) 如果启动es的时候报错,查看报错信息:

(7) 在kibana的安装软件中找到bin目录,启动kibana:

(8) 如果控制台打印如下:

(9) 在浏览器中打 开kibana页面,点击Dev Tools:http://localhost:5601

(9) 谷歌浏览器插件安装:

​ 1)谷歌安装插件地址: google ---》更多工具----》扩展程序

​ 2)

解压此文件;

​ 3) 如果没有这个加载已压缩的扩展程序,那么点击开发者模式

​ 4) 添加扩展程序

​ 5) 查看谷歌浏览器

​ 6) 点击之后:

3.1.验证ik分词器是否生效

GET /_analyze
{
  "text": "我是一个好学生",
  "analyzer": "ik_smart"
}
或者
GET /_analyze
{
  "text": "我是一个好学生",
  "analyzer": "ik_max_word" 推荐的
}    

kibana 支持restful风格:

PUT 一般为创建索引库、类型

POST 一般为添加和修改文档

GET 代表获得数据

Delete 一般为删除

3.2.操作索引库

创建索引库:PUT /shenyian

查询索引库:GET /shenyian

删除索引库:DELETE /shenyian


3.3.在索引库中创建类型(type)(不推荐)

PUT /shenyian
PUT /shenyian/_mapping/goods  //创建索引库中的类型
{
  "properties": { //固定写法
     "goodsName":{// 类型中的字段
       "type": "text", //字段的field类型
       "index": true, //是否会检索
       "store": true, //是否在文档中保存
       "analyzer": "ik_max_word" //用哪个分词器
     }
  }
}

3.4.同时创建索引库和类型(type)(推荐)

PUT /shenyian
{
  "mappings": {
    "goods":{
      "properties": {
         "goodsName":{
          "type": "text",
          "index": true,
          "store": true,
          "analyzer": "ik_max_word"
        },
        "price":{
          "type": "double", //double的field类型
          "index": true,
          "store": true
        },
        "image":{
          "type": "keyword", //和lucene的stringField一样,保存字符串,但是不分词
          "index": true,
          "store": true
        }
      }
    }
  }
}

模板的创建(了解)

PUT /shenyian2
{
  "mappings": {
    "goods":{			
      "properties": {
        "goodsName":{ 
          "type": "text",  
          "index": true,
          "store": true,  
          "analyzer": "ik_max_word" 
        } 
    },  
    "dynamic_templates":[
        {
          "myStringTemplate":{ //自定义的模板名称
            "match_mapping_type": "string", //匹配到的字段类型
            "mapping":{
               "type": "text",//如果匹配的是字符串,那么自动textfiled类型
               "analyzer": "ik_max_word" //默认的ik_max_word分词器
            }
          }
        }
    ]
  }
 } 
}

3.5.文档的操作

添加文档:

POST /shenyian/goods  
{
  "goodsName": "小米9手机",
  "price": 2999,
  "image": "www.xiaomi9.com/9.jpg"
}
或
POST /shenyian/goods/1 如果自己给id,那么es会用我们给的id
{
  "goodsName": "小米9手机",
  "price": 2999,
  "image": "www.xiaomi9.com/9.jpg"
}

通过id修改文档:

POST /shenyian/goods/7uHXmXAB2jTsz9zVCTTF //通过自动生成的id进行修改
{
  "goodsName": "小米9pro手机",
  "price": 3999,
  "image": "www.xiaomi9.com/9.jpg"
}

通过id查询:

GET /shenyian/goods/7uHXmXAB2jTsz9zVCTTF

通过id删除:

DELETE /shenyian/goods/7uHXmXAB2jTsz9zVCTTF

3.7.各种查询(重点)

数据准备:

PUT /shenyian
{
  "mappings": {
    "goods":{
      "properties": {
        "goodsName":{
          "type": "text",
          "index": true,
          "store": true,
          "analyzer": "ik_max_word"
        },
        "price":{
          "type": "double",
          "index": true,
          "store": true
        },
        "image":{
          "type": "keyword",
          "store": true
        }
      }
    }
  }
}
POST /shenyian/goods/1 
{
  "goodsName": "小米9 手机",
  "price": 2999,
  "image":"www.xiaomi.9.jpg"
}
POST /shenyian/goods/2
{
  "goodsName": "华为 p30 手机",
  "price": 2999,
  "image":"www.huawei.p30.jpg"
}
POST /shenyian/goods/3
{
  "goodsName": "华为 p30 plus",
  "price": 3999,
  "image":"www.huawei.p30plus.jpg"
}
POST /shenyian/goods/4
{
  "goodsName": "苹果 iphone 11 手机",
  "price": 5999,
  "image":"www.iphone.11.jpg"
}
POST /shenyian/goods/5
{
  "goodsName": "苹果 iphone xs",
  "price": 6999,
  "image":"www.iphone.xs.jpg"
}
POST /shenyian/goods/6
{
  "goodsName": "一加7 手机",
  "price": 3999,
  "image":"www.yijia.7.jpg"
}
(1)查询所有
POST /shenyian/goods/_search 如果不通过id来查询,那么需要添加_search固定语法
{
  "query": { 也是固定语法
    "match_all": {}
  }
}
(2)term 查询

(根据倒排索引中的term来查询)

POST /shenyian/goods/_search
{
  "query": {
    "term": {
      "goodsName": "手机"
    }
  }
}
(3) match 查询

(将查询数据分词,然后每个词都term查询,将结果合集)

POST /shenyian/goods/_search
{
  "query": {
    "match": {
      "goodsName": "手机 小米"
    }
  }
}
(4)范围查询

根据某个字段的区间范围

POST /shenyian/goods/_search
{
  "query": {
    "range": {
      "price": { //通过price这个字段
        "gte": 2000,  gte:greate than equals
        "lte": 4000   lte:less than equals
      }
    }
  }
}
(5)模糊查询

(容错查询,可以允许打错字,最多2个)

POST /shenyian/goods/_search
{
  "query": {
    "fuzzy": { 容错查询关键字
      "goodsName": {
        "value": "iphoww",
        "fuzziness": 2 容错率,最多是2
      }
    }
  }
}
(6)布尔查询

(组合查询,组合上面提到的查询)

POST /shenyian/goods/_search
{
  "query": {
    
    "bool": {
      "must": [    //下面的match查询的结构和range查询的结果的交集
        {
          "match": {      
            "goodsName": "手机 小米"
          }
        },
        
        {
          "range": {
            "price": {
              "gte": 2000,
              "lte": 4000
            }
          }
        }
      ]
    }
  }
}
POST /shenyian/goods/_search
{
  "query": {
    
    "bool": {
      "should": [ //下面的match查询的结构和range查询的结果的并集
        {
          "match": {
            "goodsName": "手机 小米"
          }
        },
        
        {
          "range": {
            "price": {
              "gte": 2000,
              "lte": 4000
            }
          }
        }
      ]
    }
  }
}
POST /shenyian/goods/_search
{
  "query": {
    
    "bool": {     must中查询出来的结果然后排除must_not中的结果
      "must": [   
        {
          "match": {
            "goodsName": "手机 小米"
          }
        },
        
        {
          "range": {
            "price": {
              "gte": 2000,
              "lte": 4000
            }
          }
        }
      ],
      "must_not": [
        {
          "term": {
            "goodsName": "华为"
          }
        }
      ]
    }
  }
}

关键字有:

must:组合term,match,range, fuzzy 等等查询结果的交集 match&& range

must_not:根据must或者should的查询结果,然后剔除must_not查询的结果

should:组合term,match,range等等查询结果的并集 match||range

一般来说:must和must_not一起用,或者should和must_not一起用

filter(这个filter的术语是过滤,作用和must一样......所以就认为等于must)

3.8.过滤

显示字段的过滤,如果不想显示那些字段,可以过滤....

includes:

POST /shenyian/goods/_search
{
  "query": {
    "match": {
      "goodsName": "华为"
    }
  },
  "_source": {
    "includes": ["goodsName","price"]
  }
}

excludes:

POST /shenyian/goods/_search
{
  "query": {
    "match": {
      "goodsName": "华为"
    }
  },
  "_source": {
    "excludes": ["image"] 不想显示的字段
  }
}

3.8.排序,分页

POST /shenyian/goods/_search
{
  "query": {
    "match": {
      "goodsName": "手机"
    }
  },
  "sort": [   排序
    {
      "price": {
        "order": "desc"
      }
    }
  ],
  "from": 0,  分页
  "size": 2
}

3.9.高亮(被搜索的关键字变色)

POST /shenyian/goods/_search
{
  "query": {
    "match": {
      "goodsName": "手机"
    }
  },
  "highlight": {
    "fields": {
      "goodsName": {}  //需要高亮的字段和上面的查询字段要一致
    },
    "pre_tags": "<font color=red>",  //前置html标签
    "post_tags": "</font>"			//闭合html标签
  }
}

3.10.聚合(分组)

聚合的字段field类型必须是:keyword

elastic search mysql
聚合(分组) 桶(bucket) group by
avg,max,min,count(*) 分组之后的计算 度量 聚合函数

mysql聚合:

准备数据:

PUT /car
{
  "mappings": {
    "orders": {
      "properties": {
        "color": {
          "type": "keyword"
        },
        "make": {
          "type": "keyword"
        }
      }
    }
  }
}
POST /car/orders/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "红", "make" : "本田", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "绿", "make" : "福特", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "蓝", "make" : "丰田", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "绿", "make" : "丰田", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "红", "make" : "本田", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "红", "make" : "宝马", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "蓝", "make" : "福特", "sold" : "2014-02-12" }
GET /car/orders/_search
{
  "from": 0,
  "size": 0, //为了不显示查询结果,不影响聚合
  "aggs": {
    "my_aggs_color": {//聚合起名
      "terms": {//固定写法
        "field": "color", //用什么字段来进行分组
        "size": 10 //最多显示多少组
      },
      "aggs": { 
        "my_avg": {//给聚合函数起个名字
          "avg": {  //根据什么聚合函数来计算:avg  max  min
            "field": "price" //什么字段来进行计算
          }
        }
      }
    }
  }
}


(本文致谢王浩,我特别敬佩的大神,浩哥。)

posted @ 2020-03-18 20:38  老沈  阅读(298)  评论(0编辑  收藏  举报