springCloud-day08

一:了解搜索技术
1.搜索引擎
搜索引擎的功能主要是三部分:
①:爬行和抓取数据(爬虫多用Python来编写)
②:对数据做预处理(提取文字,中文分词、建立倒排索引)
③:提供搜索功能(用户输入关键词后,去索引库搜索数据)
在上述三个步骤中,java要解决的往往是后两个步骤:数据处理和搜索。

2.数据库搜索的问题
①:数据库数据单表存储能力有限,无法存储海量数据

  • 解决大数据,可以进行分库分表。但是分库分表会增加业务复杂度
    ②:搜索只能通过模糊匹配,效率极低
  • 模糊搜索可能导致全表扫描,效率非常差

3.什么是倒排索引(搜索与数据库的根本区别–传统查找和倒排索引
①:传统查找(线性查找):采用数据按行存储,查找时逐行扫描,或者根据索引查找,然后匹配搜索条件,效率较差。概括来讲是先找到文档,然后看是否匹配。
②:倒排索引:首先对文档数据按照id进行索引存储,然后对文档中的数据分词,记录对词条进行索引,并记录词条在文档中出现的位置。这样查找时只要找到了词条,就找到了对应的文档。概括来讲是先找到词条,然后看看哪些文档包含这些词条。
③:创建倒排索引流程
在这里插入图片描述

当我们需要把这些数据创建倒排索引时,会分为两步:
1)创建文档列表
首先给每一条原始的文档数据创建文档编号(docID),创建索引,形成文档列表:
在这里插入图片描述
2)创建倒排索引列表(词条列表)
然后对文档中的数据进行分词,得到词条。对词条进行编号,并以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。
在这里插入图片描述
④:搜索流程
1)当用户输入任意的内容时,首先对用户输入的内容进行分词,得到用户要搜索的所有词条
2)然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。
3) 然后根据这些编号去文档列表中找到文档
举例:
例如用户要搜索关键词:拉斯跳槽

  • 首先对这句话进行分词,得到3个词条:拉斯、跳槽
  • 然后去倒排索引列表搜索(有索引,速度快),得到三个词所在的文档编号:0、2、3、4
  • 然后根据编号到文档列表查找(有索引,速度快),即可得到原始文档信息了。

总结:(重要)
1.传统的查询是:先找到这条记录(文档),然后在这条记录(文档)中找对应的词语
2.倒排索引是:先用这个词语找到对应的文档id,再用这个id查询出这个文档信息
注意:并且倒排索引的俩次查询都是有索引的(词语有索引,文档id有索引–俩个条件都是唯一的)
3.为什么搜索很快的原因:就是应运了倒排索引(俩次查询都是有索引的)
4.倒排索引的步骤:
①:创建文档列表(给每一条原始的文档数据创建文档编号(docID),创建索引)
②:创建倒排索引列表(对文档中的数据进行分词,得到词条。对词条进行编号,并以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。)
③:搜索流程(a.当用户输入任意的内容时,首先对用户输入的内容进行分词,得到用户要搜索的所有词条 b.然后拿着这些词条去倒排索引列表中进行匹配 c.找到这些词条就能找到包含这些词条的所有文档的编号。然后根据这些编号去文档列表中找到文档)

二:Lucene概述
1.概述:在java语言中,对倒排索引的实现中最广为人知的就是Lucene了,目前主流的java搜索框架都是依赖Lucene来实现的。
①:Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
②:Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
③:Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品

2.Lucene的底层使用原理就是–倒排索引(重要)

3.Lucene版本
①:最新版本到9版本了
②:我们一般用4版本的,比较稳定和支持成熟的框架使用(比如es)

三:Lucene的基本使用(创建,查询,修改,删除)
1.创建索引
在这里插入图片描述
①:准备要添加的文档数据:Document
②:初始化索引写出工具:IndexWriter

  • 设定索引存储目录Directory
  • 设定其他配置:IndexWriterConfig
    • 设定分词器:Analyzer
    • 设定Lucene版本
      ③:写出索引
      ④:快速入门
      a.添加依赖
<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>cn.itcast.demo</groupId>
	<artifactId>lucene-demo</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<dependencies>
		<!-- Junit单元测试 -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<!-- lucene核心库 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-core</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- Lucene的查询解析器 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-queryparser</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- lucene的默认分词器库 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-analyzers-common</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- lucene的高亮显示 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-highlighter</artifactId>
			<version>4.10.2</version>
		</dependency>
	</dependencies>
</project>

b.代码实现

// 创建索引
@Test
public void testCreate() throws Exception{
    // 1.创建文档对象
    Document document = new Document();
    document.add(new StringField("", "", Store.NO));
    // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
    document.add(new StringField("id", "1", Store.YES));
    // 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词
    document.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));

    // 2.1索引目录类,指定索引在硬盘中的位置(目录)
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 2.2创建分词器对象(配置1)
    Analyzer analyzer = new StandardAnalyzer();
    // 2.3索引写出工具的配置对象(配置2)
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
    // 2.创建索引的写出工具类。参数:索引的目录和配置信息
    IndexWriter indexWriter = new IndexWriter(directory, conf);

    // 3.把文档交给IndexWriter(添加文档)
    indexWriter.addDocument(document);
    // 4.提交
    indexWriter.commit();
    // 关闭
    indexWriter.close();
}

c.查看目录
在这里插入图片描述
d.索引查看工具
在这里插入图片描述
在这里插入图片描述

2.创建索引时的细节
1):覆盖或追加
我们在写索引时,可以在IndexConfigWriter中配置写入模式:覆盖或者追加:
可以有3种模式:

  • CREATE:每次写入都覆盖以前的数据
  • APPEND:不覆盖数据,而是使用以前的索引数据后追加
  • CREATE_OR_APPEND:如果不存在则创建新的,如果存在则追加数据
    在这里插入图片描述
    2):Field字段类型(重要
    灵魂4连问–选择存储的字段类型
    在这里插入图片描述
    3):分词器
    在这里插入图片描述
    ①.a.IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X,我们可以通过maven引入其依赖:
<dependency>
  <groupId>com.janeluo</groupId>
  <artifactId>ikanalyzer</artifactId>
  <version>2012_u6</version>
</dependency>

b.然后修改代码中的分词器类型:
在这里插入图片描述
c.再次测试
在这里插入图片描述
可以了。
②:停用词典和扩展词典(重要
a.IK分词器的词库有限,如果是词库中没有出现的词条,不会被正确分词,例如这样一句话:谷歌地图之父跳槽facebook, 加入了兴业社区,屌爆了啊
在这里插入图片描述
如图:红色的词条是没有正确分词的;蓝色的词条是没有意义的词语。
我们期待:兴业社区、屌爆了能作为一个完整词条;并且一些无关词语如:了、啊、额、入了可以不被分词。

b.新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除。
在这里插入图片描述
在这里插入图片描述
4)批量创建索引(多个文档新增)

// 批量创建索引
@Test
public void testCreate2() throws Exception{
    // 创建文档的集合
    Collection<Document> docs = new ArrayList<>();
    // 创建文档对象
    Document document1 = new Document();
    document1.add(new StringField("id", "1", Store.YES));
    document1.add(new TextField("title", "谷歌地图之父跳槽facebook", Store.YES));
    docs.add(document1);
    // 创建文档对象
    Document document2 = new Document();
    document2.add(new StringField("id", "2", Store.YES));
    document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Store.YES));
    docs.add(document2);
    // 创建文档对象
    Document document3 = new Document();
    document3.add(new StringField("id", "3", Store.YES));
    document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Store.YES));
    docs.add(document3);
    // 创建文档对象
    Document document4 = new Document();
    document4.add(new StringField("id", "4", Store.YES));
    document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Store.YES));
    docs.add(document4);
    // 创建文档对象
    Document document5 = new Document();
    document5.add(new StringField("id", "5", Store.YES));
    document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Store.YES));
    docs.add(document5);

    // 索引目录类,指定索引在硬盘中的位置
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 引入IK分词器
    Analyzer analyzer = new IKAnalyzer();
    // 索引写出工具的配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
    // 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。
    // OpenMode.CREATE会先清空原来数据,再提交新的索引
    conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

    // 创建索引的写出工具类。参数:索引的目录和配置信息
    IndexWriter indexWriter = new IndexWriter(directory, conf);
    // 把文档集合交给IndexWriter
    indexWriter.addDocuments(docs); 
    // 提交
    indexWriter.commit();
    // 关闭
    indexWriter.close();
}

3.索引的基本查询
1):基本流程:
①:创建索引搜索工具

  • 指定索引目录
  • 创建读取流工具
  • 创建搜索工具
    ②:创建查询条件
  • 创建查询解析器
  • 解析用户搜索语句,得到查询条件对象
    ③:搜索并解析结果
    2):代码如下:
@Test
public void testSearch() throws Exception {
    // 索引目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 索引读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 索引搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    // 创建查询对象
    Query query = parser.parse("谷歌地图之父拉斯");

    // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
    // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
    TopDocs topDocs = searcher.search(query, 10);
    // 获取总条数
    System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
    // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 取出文档编号
        int docID = scoreDoc.doc;
        // 根据编号去找文档
        Document doc = searcher.doc(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
        // 取出文档得分
        System.out.println("得分: " + scoreDoc.score);
    }
}

3):结果
在这里插入图片描述

4.索引的特殊查询
1):抽取通用查询方法
①:当我们使用各种不同查询时,其它代码几乎不动,就是查询条件在发生变化,因此我们可以把查询代码进行抽取:

public void search(Query query) throws Exception{
    // 索引目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 索引读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 索引搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
    // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
    TopDocs topDocs = searcher.search(query, 10);
    // 获取总条数
    System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
    // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 取出文档编号
        int docID = scoreDoc.doc;
        // 根据编号去找文档
        Document doc = searcher.doc(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
        // 取出文档得分
        System.out.println("得分: " + scoreDoc.score);
    }
}

②:改造之前的查询:(第一种查询方式)

@Test
public void testSearch() throws Exception {
    // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    // 创建查询对象
    Query query = parser.parse("谷歌地图之父拉斯");
	// 查询并解析
    search(query);
}

2):词条查询(第二种查询方式)

/*
 * 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串!
 */
@Test
public void testTermQuery() throws Exception {
    // 创建词条查询对象
    Query query = new TermQuery(new Term("title", "谷歌地图"));
    search(query);
}

3):通配符查询

4):FuzzyQuery(模糊查询)–(第三种查询方式)

/*
	 * 测试模糊查询
	 */
@Test
public void testFuzzyQuery() throws Exception {
    // 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2
    // 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1
    // Query query = new FuzzyQuery(new Term("title","fscevool"));
    // 可以手动指定编辑距离,但是参数必须在0~2之间
    Query query = new FuzzyQuery(new Term("title","facevool"),1);
    search(query);
}

5):数值范围查询(第四种查询方式)

/*
	 * 测试:数值范围查询
	 * 注意:数值范围查询,可以用来对非String类型的ID进行精确的查找
	 */
@Test
public void testNumericRangeQuery() throws Exception{
    // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
    Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
    search(query);
}

6):布尔查询(第五种查询方式)–用的最多的一种方式,因为要查询条件组合使用

/*
 * 布尔查询:
 * 	布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合!
 * 交集:Occur.MUST + Occur.MUST
 * 并集:Occur.SHOULD + Occur.SHOULD
 * 非:Occur.MUST_NOT
 */
@Test
public void testBooleanQuery() throws Exception{

    Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
    Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
    // 创建布尔查询的对象
    BooleanQuery query = new BooleanQuery();
    // 组合其它查询
    query.add(query1, Occur.MUST_NOT);
    query.add(query2, Occur.SHOULD);

    search(query);
}

5.修改索引(先删除之前的根据id,然后新增一条文档
1):基本流程:
①:创建索引写出对象

  • 指定目录
  • 配置
    ②:创建文档
    ③:更新数据
    2):代码
/**
     * 注意,这里的更新接收的条件时Term,即词条。需要注意两点:
     *  1)搜索条件最好唯一,例如ID,否则后果很严重
     *  2)之前说过,词条要求必须是字符串类型,那如果我们的id是Long类型怎么办?
     * @throws Exception
     */
@Test
public void testUpdate() throws Exception{
    // 创建目录对象
    Directory directory = FSDirectory.open(new File("indexDir"));
    // 创建配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 创建索引写出工具
    IndexWriter writer = new IndexWriter(directory, conf);

    // 创建新的文档数据
    Document doc = new Document();
    doc.add(new StringField("id","1",Store.YES));
    doc.add(new TextField("title","谷歌地图之父跳槽facebook 为了加入兴业社区 屌爆了啊",Store.YES));
    /* 修改索引。参数:
         * 	词条:根据这个词条匹配到的所有文档都会被修改
         * 	文档信息:要修改的新的文档数据
         */
    writer.updateDocument(new Term("id","1"), doc);
    // 提交
    writer.commit();
    // 关闭
    writer.close();
}

6.删除索引
1):基本流程:
①:创建索引写出工具
②:创建删除条件
③:删除
2):代码

/**
     * 删除的方式有多样:
     * 1)根据Term删除,需要注意:
     *    a. 词条的数据类型必须是字符串
     *    b. 最好根据id进行唯一匹配删除,如果id不是字符串类型怎么办?
     * 2)根据Query删除
     * @throws Exception
     */
@Test
public void testDelete() throws Exception {
    // 创建目录对象
    Directory directory = FSDirectory.open(new File("indexDir"));
    // 创建配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 创建索引写出工具
    IndexWriter writer = new IndexWriter(directory, conf);

    // 根据词条进行删除
    //		writer.deleteDocuments(new Term("id", "1"));

    // 根据query对象删除,如果ID是数值类型,那么我们可以用数值范围查询锁定一个具体的ID
    //		Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
    //		writer.deleteDocuments(query);

    // 删除所有
    writer.deleteAll();
    // 提交
    writer.commit();
    // 关闭
    writer.close();
}

四:Lucene的高级使用
1.高亮显示
①:后端加标签,前端加样式(高亮)
在这里插入图片描述
②:代码

@Test
public void testHighlighter() throws Exception {
    // 目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 创建搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    Query query = parser.parse("谷歌地图");

    // 格式化器(start)
    Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
    QueryScorer scorer = new QueryScorer(query);
    // 准备高亮工具
    Highlighter highlighter = new Highlighter(formatter, scorer);
    //(end)
    
    // 搜索
    TopDocs topDocs = searcher.search(query, 10);
    System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 获取文档编号
        int docID = scoreDoc.doc;
        Document doc = searcher.doc(docID);
        System.out.println("id: " + doc.get("id"));

        String title = doc.get("title");
         //(start)
        // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值
        String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title); //(end)

        System.out.println("title: " + hTitle);
        // 获取文档的得分
        System.out.println("得分:" + scoreDoc.score);
    }

}

2.排序
①:代码

@Test
public void testSortQuery() throws Exception {
    // 目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 创建搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    Query query = parser.parse("谷歌地图");

    // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
    Sort sort = new Sort(new SortField("id", SortField.Type.STRING, true));
    // 搜索
    TopDocs topDocs = searcher.search(query, 10,sort);
    System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 获取文档编号
        int docID = scoreDoc.doc;
        Document doc = searcher.doc(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
    }
}

3.分页(原始自己封装方法)
①:Lucene本身没有分页,分页需要我们自己用代码来模拟实现:
②:int page =1; int size =10;
int start =(page-1)*size;
int end = page * size;
③:获取的数组截取从start到end(或者到size的大小的数据返回)
④:用arrays.spliterator(数组,start,end)–方法

总结:搜索
1.我们原来数据库的查询是:
①:先查询这条记录,然后在这条记录中找对应的搜索的内容
②:Lucene的原理,倒排索引,先根据查询的词语(分词)-然后查询到对应的文档id-然后根据文档id查询到该条记录的全部内容(分词和文档id都是有索引的,所以查询大大加快)
2.新增索引:
①:覆盖或追加
②:Field字段类型
③:分词器(IK分词器,停用词典和扩展词典)
④:批量创建索引
3.索引的基本查询
4.索引的特殊查询
①:抽取通用查询方法
②:词条查询
③:FuzzyQuery(模糊查询)
④:数值范围查询
⑤:布尔查询
5.修改索引
6.删除索引
7.Lucene的高级使用
①:高亮显示
②:排序
③:分页
8.后续使用es会将这些功能再一步进行封装,使用更简单。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值