Elasticsearch7.x(一)——lucene的详细介绍和使用


前言

前段时间由于工作需要,用到ES做数据索引框架,便学习了一阵子。最近想想忘得也差不多了,于是便决定写下文章帮助自己复习一下~

讲Elasticsearch分布式检索框架前,我们先重点学习一下elasticsearch中的核心——lucene框架。


一、搜索是什么?

简单来说,就是通过关键词,去搜索位置的数据源,得到自己想要的相关信息

普通搜索

比如:
        程序猿老王,掏出自己一堆银行卡和数十把钥匙,来到相亲大会。这个不速之客,立马引来一堆妹纸簇拥而上。
        但是,面对这么多的妹纸,老王慌了(他必须选出最满意的那个姑娘~)。
        如何在一群姑娘们的信息里,找到自己想要的那个呢?
        于是老王根据肤白貌美大长腿,对手里的妹纸信息筛选,淘汰一批。然后越发膨胀,身高低于170不要,结果就剩几张了。最后选择了年龄最小的那个妹纸,刚拉上小手,该死的闹钟响了~

上面的搜索其实就是遍历所有信息后,做匹配查询

非普通搜索

      生活中很多场景下,我们搜索的对象并非都是关系型结构化的信息。我们无法像数据库模糊查询那样模糊匹配,跟不能遍历所有内容做匹配,毕竟查询是为快速找到想要的信息。

举个栗子:

我们在百度中搜索关键字,结果出现内容却是来自不同网站,不同内容体的结果,并标红显示关键词。
而且还有更重要的一点就是 非常非常的快~

那么问题来了,网上的数据各式各样并非结构化的,而面对这样的业务查询,请问该如何匹配呢?

显然不可能一片一片的内容做全局字符匹配吧~

想知道,就接者看下去吧~

倒排索引

      上面说到,我们无法根据常见的搜索,快速匹配到不同的文档,并显示出来。那么接下来,我们提供一个新的实现思路来解决这个问题。

举个栗子:

原文档:

doc1:小明说他看见他的女朋友了
doc2:纳尼~他哪里来的女朋友?
doc3:在梦里~
doc4:哦,那没事了

文档分词

ID文档内容分词
1doc1小明说他看见他的女朋友了[小明、看见、女朋友]
2doc2纳尼~他哪里来的女朋友?[纳尼、哪里、女朋友]
3doc3在梦里看见的~[梦里、看见]
4doc4哦,那没事了[没事]

建立倒排索引

关键词ID
小明1
看见1,3
女朋友1 ,2
纳尼2
哪里2
梦里3
没事3

比较分析

  • 建立完毕后,我再来分析下这种方式的查询实现。

假设:现在需求是搜索“女朋友”

普通方式:

  • 遍历四个文档,分别对文档中的内容进行关键词匹配。如果匹配上了,就输出。

倒排索引:

  • 根据关键词,搜索建立好的索引库。我们可以直接拿到“女朋友”出现在文档1和文档2中。于是我们直接去文档1和2 ,找到有这个关键字的部分。

显然倒排的方式要明显优于普通的方式。

二、Lucene介绍

什么是lucene

什么是全文检索

luncene 通过对文档内容,全部进行分词,并对单词建立倒排索引的过程。

工作流程

下面这幅图来自《Lucene in action》,但却不仅仅描述了Lucene的检索过程,而是描述了全文检索的一般过程。

在这里插入图片描述
根据上图显示可知:

  1. 各种数据源的数据(结构的或非结构的),创建索引文档
  2. 通过分词、倒排索引创建索引,存入索引库中。
  3. 用户查询,通过查询索引,找到文档,进而找到数据源信息。

部分组成介绍

1. Field

 //1 创建文档对象
 Document document1 = new Document();
 // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
 document1.add(new StringField("id", "1", Field.Store.YES));
  • Document文档中有很多字段组成,每个字段就是一个Field,且有不同的类型。
  • 子类: DoubleField、FloatField、IntField、LongField、StringField、TextField等
  • TextField即创建索引,又会被分词。
  • StringField会创建索引,但是不会被分词。
  • StoreField一定会被存储,但是一定不创建索引。

2. Directory

//2 索引目录类,指定索引在硬盘中的位置
Directory directory = FSDirectory.open(new File("D:\\work\\index"));
  • 索引存储的地方
  • 子类:FSDirectory 和 RAMDirectory两种
  • FSDirectory :文件系统目录,会把索引库指向本地磁盘。速度慢,但是数据安全,程序挂了索引不会消息。
  • RAMDirectory:内存目录,会把索引库保存在内存。索引数据没有持久化,挂死后,数据丢失。

3. Analyzer

//3 创建分词器对象--使用IK分词器
Analyzer analyzer = new IKAnalyzer();
  • 分词解析器,给文档数据分词使用
  • 默认的分词,只能对英文进行分词,无法分词中文。
  • 我们使用的IK分词器

三、基本使用

1 添加依赖

        <!-- 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>
        <!--中文分词器-->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>

2. 创建索引

创建索引的流程

  1. 创建文档对象
  2. 指定索引存放目录
  3. 创建分词对象
  4. 索引写出配置
  5. 创建索引的写出工具类
  6. 添加文档到索引类里
  7. 提交
  8. 关闭

结合代码理解一下流程~

// 创建索引
    @Test
    public void testCreateIndex() throws Exception{
        // 创建文档的集合
        Collection<Document> docs = new ArrayList<>();
        //1 创建文档对象
        Document document1 = new Document();
        // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
        document1.add(new StringField("id", "1", Field.Store.YES));
        // 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词
        document1.add(new TextField("title", "小明说他看见他的女朋友了", Field.Store.YES));
        docs.add(document1);
        Document document2 = new Document();
        document2.add(new StringField("id", "1", Field.Store.YES));
        document2.add(new TextField("title", "纳尼~他哪里来的女朋友?", Field.Store.YES));
        docs.add(document2);
        Document document3 = new Document();
        document3.add(new StringField("id", "1", Field.Store.YES));
        document3.add(new TextField("title", "在梦里看到的~", Field.Store.YES));
        docs.add(document3);
        Document document4 = new Document();
        document4.add(new StringField("id", "1", Field.Store.YES));
        document4.add(new TextField("title", "哦,那没事了", Field.Store.YES));
        docs.add(document4);



        //2 索引目录类,指定索引在硬盘中的位置
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        //3 创建分词器对象--使用IK分词器
        Analyzer analyzer = new IKAnalyzer();
        //4 索引写出工具的配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
        //设置每次索引都是重新创建---还有其他集中模式 append 追加
        conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
        //5 创建索引的写出工具类。参数:索引的目录和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);

        //6 把文档交给IndexWriter
        indexWriter.addDocuments(docs);
        //7 提交
        indexWriter.commit();
        //8 关闭
        indexWriter.close();
    }

3.查询索引

3.1 流程

  1. 获取索引目录,建立对象
  2. 读取索引目录对象,建立索引读取工具
  3. 根据索引读取工具,创建索引搜索工具
  4. 最重要的,创建查询解析器
  5. 创建查询对象
  6. 根据查询对象,搜索数据
  7. 获取查询结果

3.2 测试代码

 @Test
    public void testIndexSearch() throws Exception {
        // 1.索引目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 2.索引读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 3.索引搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        // 4.创建查询解析器,两个参数:默认要查询的字段的名称,分词器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 5.创建查询对象---女朋友
        Query query = parser.parse("女朋友");

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

3.3 扩展查询

方式一:通过QueryParser 解析关键字,得到查询对象(会使用分词)

        // 4.创建查询解析器,两个参数:默认要查询的字段的名称,分词器
        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        // 5.创建查询对象---女朋友
        Query query = parser.parse("女朋友");

方式二:最小Trem单元查询(不可再分词。值必须是字符串!)

Query query = new TermQuery(new Term("title", "女朋友"));

方式三:匹配查询( ? 可以代表任意一个字符 * 可以任意多个任意字符)

Query query = new WildcardQuery(new Term("title", "*朋*"));

方式四:错误匹配(允许查询的key有一定偏差,会帮你查询正确的结果。)

Query query = new FuzzyQuery(new Term("title","facevool"),1);

方式五: 数值范围查询(查询文档id 在2-4的结果)

// 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
Query query = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);

方式六:组合查询(多个条件组合)

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

4.修改索引

流程:

  1. 根据ID 查找到文档
  2. 删除要修改的ID 文档
  3. 添加此ID修改后的文档
  4. 生成索引

代码:

    @Test
    public void testUpdateIndex() throws Exception{
        // 创建目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 创建配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
        // 创建索引写出工具
        IndexWriter writer = new IndexWriter(directory, conf);

        // 创建新的文档数据
        Document doc = new Document();
        doc.add(new StringField("id","1", Field.Store.YES));
        doc.add(new TextField("title","我不信他会有女朋友~", Field.Store.YES));
        /* 修改索引。参数:
         * 	词条:根据这个词条匹配到的所有文档都会被修改
         * 	文档信息:要修改的新的文档数据
         */
        writer.updateDocument(new Term("id","1"), doc);
        // 提交
        writer.commit();
        // 关闭
        writer.close();
    }

注意事项:

  • Lucene修改功能底层会先删除,再把新的文档添加。
  • 修改功能会根据Term进行匹配,所有匹配到的都会被删除。这样不好
  • 因此,一般我们修改时,都会根据一个唯一不重复字段进行匹配修改。例如ID
  • 但是词条搜索,要求ID必须是字符串。如果不是,这个方法就不能用。
  • 如果ID是数值类型,我们不能直接去修改。可以先手动删除deleteDocuments(数值范围查询锁定ID),再添加。

5.删除索引

删除方式:

  • 根据文档id找到文档,然后删除
  • 根据Term查询,把查询的结果删除
  • 删除所有

代码:

    @Test
    public void testDelete() throws Exception {
        // 创建目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 创建配置对象
        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, 4L, true, true);
        //		writer.deleteDocuments(query);

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

6. 高亮显示:

 /**
     * 高亮显示
     */
    @Test
    public void testHighlighter() throws Exception {
        // 目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 创建读取工具
        IndexReader reader = DirectoryReader.open(directory);
        // 创建搜索工具
        IndexSearcher searcher = new IndexSearcher(reader);

        QueryParser parser = new QueryParser("title", new IKAnalyzer());
        Query query = parser.parse("女朋友");

        // 格式化器
        Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
        QueryScorer scorer = new QueryScorer(query);
        // 准备高亮工具
        Highlighter highlighter = new Highlighter(formatter, scorer);
        // 搜索
        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 = reader.document(docID);
            System.out.println("id: " + doc.get("id"));

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

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

    }

6.排序

/**
     * 排序
     * @throws Exception
     */
    @Test
    public void testSortQuery() throws Exception {
        // 目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 创建读取工具
        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.LONG, 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 = reader.document(docID);
            System.out.println("id: " + doc.get("id"));
            System.out.println("title: " + doc.get("title"));
        }
    }

7.分页

    /**
     * 分页
     * @throws Exception
     */
    @Test
    public void testPageQuery() throws Exception {
        // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
        int pageSize = 1;// 每页条数
        int pageNum = 1;// 当前页码
        int start = (pageNum - 1) * pageSize;// 当前页的起始条数
        int end = start + pageSize;// 当前页的结束条数(不能包含)

        // 目录对象
        Directory directory = FSDirectory.open(new File("D:\\work\\index"));
        // 创建读取工具
        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.LONG, false));
        // 搜索数据,查询0~end条
        TopDocs topDocs = searcher.search(query, end,sort);
        System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

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

8.得分算法

        Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前!

打分公式有些复杂,这里我也没弄太明白。这里就不写了,待日后有时间沉下心来仔细研究一番。

有兴趣请参考另一方大佬写的博客:

https://www.cnblogs.com/forfuture1978/archive/2010/03/07/1680007.html


总结

本章主要内容:

  1. 理解倒排索引的实现
  2. 明白lucene的基本组成和工作流程
  3. 熟练使用lucene的方法 对索引操作
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值