搜索引擎技术Lucene7 入门

java零基础自学网站,点击了解:https://how2j.cn

阿里云服务器,点击了解:https://www.aliyun.com/minisite/goods

步骤 1 : 关于JDK版本

至少使用JDK8版本,请下载JDK8或者更高版本: 下载以及配置JDK环境

步骤 2 : Lucene 概念

Lucene 这个开源项目,使得 Java开发人员可以很方便地得到像搜索引擎google baidu那样的搜索效果。

步骤 3 : 先运行,看到效果,再学习

老规矩,先下载下载区(点击进入)的可运行项目,配置运行起来,确认可用之后,再学习做了哪些步骤以达到这样的效果。 
运行TestLucene类,期望看到如图所示的效果。
一共是10条数据,通过关键字查询出来6条命中结果,不同的命中结果有不同的匹配度得分,比如第一条,命中都就很高,既有 护眼, 也有 带光源。 其他的命中度就比较低,没有护眼关键字的匹配,只有光源关键字的匹配。

先运行,看到效果,再学习

步骤 4 : 模仿和排错

在确保可运行项目能够正确无误地运行之后,再严格照着教程的步骤,对代码模仿一遍。 
模仿过程难免代码有出入,导致无法得到期望的运行结果,此时此刻通过比较正确答案 ( 可运行项目 ) 和自己的代码,来定位问题所在。 
采用这种方式,学习有效果,排错有效率,可以较为明显地提升学习速度,跨过学习路上的各个槛。 

推荐使用diffmerge软件,进行文件夹比较。把你自己做的项目文件夹,和我的可运行项目文件夹进行比较。 
这个软件很牛逼的,可以知道文件夹里哪两个文件不对,并且很明显地标记出来 
这里提供了绿色安装和使用教程:diffmerge 下载和使用教程

步骤 5 : Lucene 版本

当前使用的Lucene版本是截至2018.3.9最新版本 7.2.1

步骤 6 : jar 包

一系列需要的jar包都放在项目里了,直接使用就好了,包括兼容 lucene 7.2.1 的中文分词器

jar 包

步骤 7 : TestLucene.java

这是TestLucene.java 的完整代码,后续会对代码详细讲解

package com.how2java;

 

import java.io.IOException;

import java.io.StringReader;

import java.util.ArrayList;

import java.util.List;

 

import org.apache.lucene.analysis.TokenStream;

import org.apache.lucene.document.Document;

import org.apache.lucene.document.Field;

import org.apache.lucene.document.TextField;

import org.apache.lucene.index.DirectoryReader;

import org.apache.lucene.index.IndexReader;

import org.apache.lucene.index.IndexWriter;

import org.apache.lucene.index.IndexWriterConfig;

import org.apache.lucene.index.IndexableField;

import org.apache.lucene.queryparser.classic.QueryParser;

import org.apache.lucene.search.IndexSearcher;

import org.apache.lucene.search.Query;

import org.apache.lucene.search.ScoreDoc;

import org.apache.lucene.search.highlight.Highlighter;

import org.apache.lucene.search.highlight.QueryScorer;

import org.apache.lucene.search.highlight.SimpleHTMLFormatter;

import org.apache.lucene.store.Directory;

import org.apache.lucene.store.RAMDirectory;

import org.wltea.analyzer.lucene.IKAnalyzer;

 

public class TestLucene {

 

    public static void main(String[] args) throws Exception {

        // 1. 准备中文分词器

        IKAnalyzer analyzer = new IKAnalyzer();

 

        // 2. 索引

        List<String> productNames = new ArrayList<>();

        productNames.add("飞利浦led灯泡e27螺口暖白球泡灯家用照明超亮节能灯泡转色温灯泡");

        productNames.add("飞利浦led灯泡e14螺口蜡烛灯泡3W尖泡拉尾节能灯泡暖黄光源Lamp");

        productNames.add("雷士照明 LED灯泡 e27大螺口节能灯3W球泡灯 Lamp led节能灯泡");

        productNames.add("飞利浦 led灯泡 e27螺口家用3w暖白球泡灯节能灯5W灯泡LED单灯7w");

        productNames.add("飞利浦led小球泡e14螺口4.5w透明款led节能灯泡照明光源lamp单灯");

        productNames.add("飞利浦蒲公英护眼台灯工作学习阅读节能灯具30508带光源");

        productNames.add("欧普照明led灯泡蜡烛节能灯泡e14螺口球泡灯超亮照明单灯光源");

        productNames.add("欧普照明led灯泡节能灯泡超亮光源e14e27螺旋螺口小球泡暖黄家用");

        productNames.add("聚欧普照明led灯泡节能灯泡e27螺口球泡家用led照明单灯超亮光源");    

        Directory index = createIndex(analyzer, productNames);

 

        // 3. 查询器

        String keyword = "护眼带光源";

        Query query = new QueryParser("name", analyzer).parse(keyword);

         

        // 4. 搜索

        IndexReader reader = DirectoryReader.open(index);

        IndexSearcher searcher = new IndexSearcher(reader);

        int numberPerPage = 1000;

        System.out.printf("当前一共有%d条数据%n",productNames.size());

        System.out.printf("查询关键字是:\"%s\"%n",keyword);

        ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;

 

        // 5. 显示查询结果

        showSearchResults(searcher, hits, query, analyzer);

        // 6. 关闭查询

        reader.close();

    }

 

    private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)

            throws Exception {

        System.out.println("找到 " + hits.length + " 个命中.");

        System.out.println("序号\t匹配度得分\t结果");

        for (int i = 0; i < hits.length; ++i) {

            ScoreDoc scoreDoc= hits[i];

            int docId = scoreDoc.doc;

            Document d = searcher.doc(docId);

            List<IndexableField> fields = d.getFields();

            System.out.print((i + 1));

            System.out.print("\t" + scoreDoc.score);

            for (IndexableField f : fields) {

                System.out.print("\t" + d.get(f.name()));

            }

            System.out.println();

        }

    }

 

    private static Directory createIndex(IKAnalyzer analyzer, List<String> products) throws IOException {

        Directory index = new RAMDirectory();

        IndexWriterConfig config = new IndexWriterConfig(analyzer);

        IndexWriter writer = new IndexWriter(index, config);

 

        for (String name : products) {

            addDoc(writer, name);

        }

        writer.close();

        return index;

    }

 

    private static void addDoc(IndexWriter w, String name) throws IOException {

        Document doc = new Document();

        doc.add(new TextField("name", name, Field.Store.YES));

        w.addDocument(doc);

    }

}

步骤 8 : 分词器

准备中文分词器,关于分词器更多概念在分词器概念 中有详细讲解,这里先使用

// 1. 准备中文分词器

IKAnalyzer analyzer = new IKAnalyzer();

步骤 9 : 创建索引

1. 首先准备10条数据
这10条数据都是字符串,相当于产品表里的数据
2. 通过createIndex方法,把它加入到索引当中

创建内存索引,为什么Lucene会比数据库快?因为它是从内存里查,自然就比数据库里快多了呀

Directory index = new RAMDirectory();


根据中文分词器创建配置对象

IndexWriterConfig config = new IndexWriterConfig(analyzer);


创建索引 writer

IndexWriter writer = new IndexWriter(index, config);


遍历那10条数据,把他们挨个放进索引里

for (String name : products) {

    addDoc(writer, name);

}


每条数据创建一个Document,并把这个Document放进索引里。 这个Document有一个字段,叫做"name"。 TestLucene.java 第49行创建查询器,就会指定查询这个字段

private static void addDoc(IndexWriter w, String name) throws IOException {

    Document doc = new Document();

    doc.add(new TextField("name", name, Field.Store.YES));

    w.addDocument(doc);

}

// 2. 索引

List<String> productNames = new ArrayList<>();

productNames.add("飞利浦led灯泡e27螺口暖白球泡灯家用照明超亮节能灯泡转色温灯泡");

productNames.add("飞利浦led灯泡e14螺口蜡烛灯泡3W尖泡拉尾节能灯泡暖黄光源Lamp");

productNames.add("雷士照明 LED灯泡 e27大螺口节能灯3W球泡灯 Lamp led节能灯泡");

productNames.add("飞利浦 led灯泡 e27螺口家用3w暖白球泡灯节能灯5W灯泡LED单灯7w");

productNames.add("飞利浦led小球泡e14螺口4.5w透明款led节能灯泡照明光源lamp单灯");

productNames.add("飞利浦蒲公英护眼台灯工作学习阅读节能灯具30508带光源");

productNames.add("欧普照明led灯泡蜡烛节能灯泡e14螺口球泡灯超亮照明单灯光源");

productNames.add("欧普照明led灯泡节能灯泡超亮光源e14e27螺旋螺口小球泡暖黄家用");

productNames.add("聚欧普照明led灯泡节能灯泡e27螺口球泡家用led照明单灯超亮光源");    

Directory index = createIndex(analyzer, productNames);

private static Directory createIndex(IKAnalyzer analyzer, List<String> products) throws IOException {

    Directory index = new RAMDirectory();

    IndexWriterConfig config = new IndexWriterConfig(analyzer);

    IndexWriter writer = new IndexWriter(index, config);

 

    for (String name : products) {

        addDoc(writer, name);

    }

    writer.close();

    return index;

}

private static void addDoc(IndexWriter w, String name) throws IOException {

    Document doc = new Document();

    doc.add(new TextField("name", name, Field.Store.YES));

    w.addDocument(doc);

}

步骤 10 : 创建查询器

根据关键字 护眼带光源,基于 "name" 字段进行查询。 这个 "name" 字段就是在创建索引步骤里每个Document的 "name" 字段,相当于表的字段名

String keyword = "护眼带光源";

Query query = new QueryParser("name", analyzer).parse(keyword);

步骤 11 : 执行搜索

接着就执行搜索:
创建索引 reader:

IndexReader reader = DirectoryReader.open(index);


基于 reader 创建搜索器:

IndexSearcher searcher = new IndexSearcher(reader);


指定每页要显示多少条数据:

int numberPerPage = 1000;


执行搜索

ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;

// 4. 搜索

IndexReader reader = DirectoryReader.open(index);

IndexSearcher searcher = new IndexSearcher(reader);

int numberPerPage = 1000;

System.out.printf("当前一共有%d条数据%n",productNames.size());

System.out.printf("查询关键字是:\"%s\"%n",keyword);

ScoreDoc[] hits = searcher.search(query, numberPerPage).scoreDocs;

步骤 12 : 显示查询结果

每一个ScoreDoc[] hits 就是一个搜索结果,首先把他遍历出来

for (int i = 0; i < hits.length; ++i) {

ScoreDoc scoreDoc= hits[i];


然后获取当前结果的docid, 这个docid相当于就是这个数据在索引中的主键

int docId = scoreDoc.doc;


再根据主键docid,通过搜索器从索引里把对应的Document取出来

Document d = searcher.doc(docId);


接着就打印出这个Document里面的数据。 虽然当前Document只有name一个字段,但是代码还是通过遍历所有字段的形式,打印出里面的值,这样当Docment有多个字段的时候,代码就不用修改了,兼容性更好点。
scoreDoc.score 表示当前命中的匹配度得分,越高表示匹配程度越高

List<IndexableField> fields = d.getFields();

System.out.print((i + 1));

System.out.print("\t" + scoreDoc.score);

for (IndexableField f : fields) {

        System.out.print("\t" + d.get(f.name()));

}

private static void showSearchResults(IndexSearcher searcher, ScoreDoc[] hits, Query query, IKAnalyzer analyzer)

        throws Exception {

    System.out.println("找到 " + hits.length + " 个命中.");

    System.out.println("序号\t匹配度得分\t结果");

    for (int i = 0; i < hits.length; ++i) {

        ScoreDoc scoreDochits[i];

        int docId scoreDoc.doc;

        Document d searcher.doc(docId);

        List<IndexableField> fields = d.getFields();

        System.out.print((i + 1));

        System.out.print("\t" + scoreDoc.score);

        for (IndexableField f : fields) {

            System.out.print("\t" + d.get(f.name()));

        }

        System.out.println();

    }

}

步骤 13 : 运行结果

如图所示,一共是10条数据,通过关键字查询出来6条命中结果,不同的命中结果有不同的匹配度得分,比如第一条,命中都就很高,既有 护眼, 也有 带光源。 其他的命中度就比较低,没有护眼关键字的匹配,只有光源关键字的匹配。

运行结果

步骤 14 : 和 like 的区别

like 也可以进行查询,那么使用lucene 的方式有什么区别呢? 主要是两点:
1. 相关度
通过观察运行结果,可以看到不同相关度的结果都会查询出来,但是使用 like,就做不到这一点了
2. 性能
数据量小的时候,like 也会有很好的表现,但是数据量一大,like 的表现就差很多了。 在接下来的教程里会演示对 14万条数据 的查询

步骤 15 : 思路图

现在通过自己做了一遍 Lucene了,有了感性的认识,接着来整理一下做 Lucene的思路。
1. 首先搜集数据
数据可以是文件系统,数据库,网络上,手工输入的,或者像本例直接写在内存上的
2. 通过数据创建索引
3. 用户输入关键字
4. 通过关键字创建查询器
5. 根据查询器到索引里获取数据
6. 然后把查询结果展示在用户面前

思路图


更多内容,点击了解: https://how2j.cn/k/search-engine/search-engine-intro/1672.html