lucene

一、 lucene简介

1. Lucene

Luceneapache下的一个开源的全文检索引擎工具包。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能。

官网: http://lucene.apache.org/

 

2. 全文检索

全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

总结:先建索引再通过索引进行查询

 

3. 全文检索的应用场景

注意:Lucene和搜索引擎是不同的,Lucene是一套用java或其它语言写的全文检索的

工具包。它为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库搜索引擎是一个全文检索系统,它是一个单独运行的软件系统。

 

4. 为什么要使用全文检索

1.搜索速度:将数据源中的数据都通过全文索引

2.匹配效果:词语进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。

3.相关度:有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。

4.适用场景:关系数据库中进行模糊查询时,数据库自带的索引将不起作用,此时需要通过全文检索来提高速度;比如:网站系统中针对内容的模糊查询select * from article where content like %广州

5. lucene全文检索流程

 

全文检索的流程分为两大部分:索引流程、搜索流程。

索引流程:即采集数据构建文档对象分析文档(分词)创建索引。

搜索流程:即用户通过搜索界面创建查询执行搜索,搜索器从索引库搜索渲染搜索结.

 

6. 索引流程

对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。

6.1 采集数据

全文检索要搜索的数据信息格式多种多样,拿搜索引擎(百度, google)来说,通过搜索引擎网站能搜索互联网站上的网页(html)、互联网上的音乐(mp3..)、视频(avi..)pdf电子书等。

全文检索搜索的这些数据称为非结构化数据。

6.1.1 结构化数据和非结构化数

结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。

非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。

6.1.2 结构化数据搜索

由于结构化数据是固定格式,所以就可以针对固定格式的数据设计算法来搜索,比如数据库like查询,like查询采用顺序扫描法,使用关键字匹配内容,对于内容量大的like查询速度慢。

6.1.3 非结构化数据搜索

需要将所有要搜索的非结构化数据通过技术手段采集到一个固定的地方,将这些非结构化的数据想办法组成结构化的数据,再以一定的算法去搜索。

6.2 采集数据技术有哪些

对于互联网上网页采用http将网页抓取到本地生成html文件。

数据在数据库中就连接数据库读取表中的数据。

数据是文件系统中的某个文件,就通过文件系统读取文件的内容。

6.2.1 网页采集(了解)

因为目前搜索引擎主要搜索数据的来源是互联网,搜索引擎使用一种爬虫程序抓取网页( 通过http抓取html网页信息),以下是一些爬虫项目:

Solrhttp://lucene.apache.org/solr solrapache的一个子项目,支持从关系数据库、xml文档中提取原始数据。

Nutchhttp://lucene.apache.org/nutch, Nutchapache的一个子项目,包括大规模爬虫工具,能够抓取和分辨web网站数据。

jsouphttp://jsoup.org/ ),jsoup 是一款Java HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOMCSS以及类似于jQuery的操作方法来取出和操作数据。

heritrixhttp://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由 java 开发的、开源的网络爬虫,用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性,方便用户实现自己的抓取逻辑。

6.3 数据库采集(掌握)

针对电商站内搜索功能,全文检索的数据源在数据库中,需要通过jdbc或者orm框架访问数据库中book表的内容。

6.4 索引文件逻辑结构

 

文档域:对非结构化的数据统一格式为document文档格式,一个文档有多个field域,不同的文档其field的个数可以不同,建议相同类型的文档包括相同的field。本例子一个document对应一 条 book表的记录。

索引域:用于搜索,搜索程序将从索引域中搜索一个一个词,根据词找到对应的文档将Document中的Field的内容进行分词,将分好的词创建索引,索引=Field域名:

倒排索引表

传统方法是先找到文件,如何在文件中找内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大就搜索慢。

倒排索引结构是根据内容(词语)找文档,倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它是在索引中匹配搜索关键字,由于索引内容量有限并且采用固定优化算法搜索速度很快,找到了索引中的词汇,词汇与文档关联,从而最终找到了文件

6.5 创建索引流程

 

分词器Analyzer进行分词 ,主要过程就是分词、过滤两步。

分词就是将采集到的文档内容切分成一个一个的词,具体应该说是将DocumentFieldvalue值切分成一个一个的词。

This is a the book.

过滤包括去除标点符号、去除停用词(的、是、aanthe等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。 (停用词)

IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。 IndexWriter需要通过Directory对索引进行存储操作。

Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常 用的包括FSDirectory(在文件系统存储索引)、

RAMDirectory(在内存存储索引)

6.6 lucene的使用

Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。

官方网站:http://lucene.apache.org/

下载地址:http://archive.apache.org/dist/lucene/java/

可以使用maven直接添加依赖,本教程使用这一种

7. 搜索流程

 

查询对象Query:用户定义查询语句,用户确定查询什么内容(输入什么关键字)

指定查询语法,相当于sql语句。

IndexSearcher索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索。

IndexReader索引读取对象,它对应的索引维护对象IndexWriterIndexSearcher

通过IndexReader读取索引目录中的索引文件

Directory索引流对象,IndexReader需要Directory读取索引库,使用

FSDirectory文件系统流对象

IndexSearcher搜索完成,返回一个TopDocs(匹配度高的前边的一些记录)

二、 Hello lucene

业务需求:使用Lucene实现电商项目中图书类商品的索引和搜索功能。

1. 前期准备

数据初始化准备:

book.sql

导入到数据库中

操作数据库准备(使用SpringBoot+MyBatis

导入book.sql(过程省略,显示效果)

 

 

2. 添加依赖

 1 <dependency>
 2     <groupId>org.mybatis.spring.boot</groupId>
 3     <artifactId>mybatis-spring-boot-starter</artifactId>
 4     <version>2.0.0</version>
 5 </dependency>
 6 
 7 <dependency>
 8     <groupId>mysql</groupId>
 9     <artifactId>mysql-connector-java</artifactId>
10     <version>5.1.37</version>
11     <scope>runtime</scope>
12 </dependency>
13 <dependency>
14     <groupId>org.springframework.boot</groupId>
15     <artifactId>spring-boot-starter-test</artifactId>
16     <scope>test</scope>
17 </dependency>

 

  

3. 编写实体bin

 
public class Book {
    /**
     * 编号
     */
    private int id;
    /**
     * 书名
     */
    private String bookName;
    /**
     * 价格
     */
    private double price;
    /**
     * 图片路径
     */
    private String pic;
    /**
     * 描述
     */
    private String description;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public String getPic() {
        return pic;
    }

    public void setPic(String pic) {
        this.pic = pic;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", bookName='" + bookName + '\'' +
                ", price=" + price +
                ", pic='" + pic + '\'' +
                ", description='" + description + '\'' +
                '}';
    }
}

 

 

4. MyBatis配置

  
mybatis.type-aliases-package=com.hx.springbootmybatis
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.10.120:3306/test_db?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
#日志配置
logging.path=D:/log
logging.level.org.springframework.web=INFO
logging.level.com.hx.springbootmybatis.domain=DEBUG

 

5. 编写Mapper

 
@Mapper
public interface BookMapper {

    /**
     * 查询所有书籍信息
     * @return
     */
    @Select("select id,bookname,price,pic,description from book")
    public List<Book> getAllBook();
}

 


 
 

 

6. 测试Mybatis

 
@RunWith(SpringRunner.class)
@SpringBootTest
public class LuceneApplicationTests {

    @Autowired
    private BookMapper bookMapper;
    @Test
    public void test1() {
        List<Book> allBook = bookMapper.getAllBook();
        System.out.println(allBook);
     }
}

 

 
  

7. lucene配置

7.1 添加依赖

 
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>7.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>7.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>7.5.0</version>
</dependency>

 

 
 
  

8. 创建索引

 
@Test
public void test2() throws IOException {
    //1采集数据
    List<Book> allBook = bookMapper.getAllBook();

    //2创建索引

    //Document集合对象
    List<Document> documents = new ArrayList<Document>();

    //将非结构化数据结构化,创建索引域和文档域
    Document doc;
    for (Book book : allBook) {
        doc = new Document();
        Field id = new TextField("id", String.valueOf(book.getId()), Field.Store.YES);
        Field bookName = new TextField("name", book.getBookName().toString(), Field.Store.YES);
        Field price = new TextField("price", String.valueOf(book.getPrice()), Field.Store.YES);
        Field pic = new TextField("pic", book.getPic(), Field.Store.YES);
        Field description = new TextField("description", book.getDescription(), Field.Store.YES);
        doc.add(id);
        doc.add(bookName);
        doc.add(price);
        doc.add(pic);
        doc.add(description);
        documents.add(doc);
    }
    //构建分词器
    Analyzer analyzer=new StandardAnalyzer();

    //构建存储目录和配置参数

    Directory directory= FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //构建存储目录和配置
    IndexWriterConfig cfg=new IndexWriterConfig(analyzer);
    //构建IndexWriter索引写对象并添加文档对象
    IndexWriter indexWriter=new IndexWriter(directory,cfg);


    for (Document document:documents){
        indexWriter.addDocument(document);
    }

    //关闭indexWriter
    indexWriter.close();
}

 

 
  

 

 

 

9. 使用工具Luke查看索引

Luke作为Lucene工具包中的一个工具,可以通过界面来进行索引文件的查询、修改。

下载网址:http://www.getopt.org/luke/

下载对应版本:https://github.com/DmitryKey/luke/releases

打开Luke方法:

手动执行:双击lukeallxx.bat

 

使用代码查询

 

 

v

 

10. 使用代码查询

 
@Test
public void test3() throws ParseException, IOException {
    //一.构建查询对象
    //1.创建分词器
    Analyzer analyzer = new StandardAnalyzer();
    //2.构建查询解析器
    QueryParser queryParser = new QueryParser("description", analyzer);
    //3.构建查询对象
    Query query = queryParser.parse("description: 魔法 and 斗气");

    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);
        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        System.out.println("图书description:" + doc.get("description"));
    }
    //6.关闭indexReader
    indexReader.close();
}

 

 
  

 

 

 

三、 Field 域实战讲解

1. Field属性

Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个FieldDocument只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。

是否分词(tokenized)

是:作分词处理,即将Field值进行分词,分词的目的是为了索引。

比如:商品名称、商品简介等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元索引。否:不作分词处理

比如:商品id、订单号、身份证号等

是否索引(indexed)

是:进行索引。将Field分词后的词或整个Field值进行索引,索引的目的是为了搜索。

比如:商品名称、商品简介分词后进行索引,订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。

否:不索引。该域的内容无法搜索到比如:商品id、文件路径、图片路径等,不用作为查询条件的不用索引。

是否存储(stored)

是:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取。

比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。

否:不存储Field值,不存储的Field无法通过Document获取比如:商品简介,内容较大不用存储。如果要向用户展示商品简介可以从系统的关系数据库中获取商品简

2.  Field常用类型

Field类型

数据类型

说明

StringField(FieldName, FieldValue, Store.YES)

字符串

N

Y

Y/N

字符串类型Field, 不分词, 作为一个整体进行索引(: 身份证号, 订单编号), 是否需要存储由Store.YESStore.NO决定

LongField(FieldName, FieldValue, Store.YES)

数值型代表

Y

Y

Y/N

Long数值型Field代表, 分词并且索引(: 价格), 是否需要存储由Store.YESStore.NO决定

StoredField(FieldName, FieldValue)

重载方法, 支持多种类型

N

N

Y

构建不同类型的Field, 不分词, 不索引, 要存储. (: 商品图片路径)

TextField(FieldName, FieldValue, Store.NO)

文本类型

Y

Y

Y/N

文本类型Field, 分词并且索引, 是否需要存储由Store.YESStore.NO决定

Field(FieldName, FieldValue, FieldType)

自定义类型

Y

Y

Y/N

自定义是否存储、索引、分类、设置权重等

2.1 Field代码修改

图书id

是否分词:不用分词,因为不会根据商品id来搜索商品

是否索引:不索引,因为不需要根据图书ID进行搜索

是否存储:要存储,因为查询结果页面需要使用id这个值。

图书名称:

是否分词:要分词,因为要将图书的名称内容分词索引,根据关键搜索图书名称抽取的词。

是否索引:要索引。

是否存储:要存储。

图书价格:

是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容 要特殊分词处理,本例子可能要根据价格范围搜索,需要

分词和索引。

是否索引:要索引

是否存储:要存储

图书图片地址:

是否分词:不分词

是否索引:不索引

是否存储:要存储

图书描述:

是否分词:要分词

是否索引:要索引

是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。

不在lucene的索引文件中记录,节省lucene的索引文件空间,如果要在详情页面显示描述,思路:

lucene中取出图书的id,根据图书的id查询关系数据库中book表得到描述信息。

2.2 代码

 
@Test
public void test4() throws IOException {
    //1采集数据
    List<Book> allBook = bookMapper.getAllBook();

    //2创建索引

    //Document集合对象
    List<Document> documents = new ArrayList<Document>();

    //将非结构化数据结构化,创建索引域和文档域
    Document doc;
    for (Book book : allBook) {
        doc = new Document();
        // id:不分词、不索引、要存储
        Field id = new StoredField("id", String.valueOf(book.getId()));
        // name:分词、索引、存储
        Field bookName = new TextField("name", book.getBookName().toString(), Field.Store.YES);
        // price:分词、索引、存储
        Field price = new TextField("price", String.valueOf(book.getPrice()), Field.Store.YES);
        //pic:不分词、不索引、要存储
        Field pic = new TextField("pic", book.getPic(), Field.Store.YES);
        // desc:分词、索引、不存储
        Field description = new TextField("description", book.getDescription(),Field.Store.NO);
        doc.add(id);
        doc.add(bookName);
        doc.add(price);
        doc.add(pic);
        doc.add(description);
        documents.add(doc);


    }
    //构建分词器
    Analyzer analyzer = new StandardAnalyzer();

    //构建存储目录和配置参数

    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //构建存储目录和配置
    IndexWriterConfig cfg = new IndexWriterConfig(analyzer);
    //构建IndexWriter索引写对象并添加文档对象
    IndexWriter indexWriter = new IndexWriter(directory, cfg);


    for (Document document : documents) {
        indexWriter.addDocument(document);
    }

    //关闭indexWriter
    indexWriter.close();
}

 

 
  

2.3 测试

运行上面代码test3搜索

 

description没有存储

 

使用Luke查看

 

id没有分词

四、 lucene索引维护(CUD

业务需求:管理人员通过电商系统更改图书信息,这时更新的是数据库,如果使用

lucene搜索图书信息需要在数据库表book信息变化时及时更新lucene索引库。

1.  添加索引

调用 indexWriter.addDocumentdoc)添加索引(上面已有例子)

2. 删除索引

 

@Test
public void test5() throws IOException {
    // 1 构建分词器
    Analyzer analyzer=new StandardAnalyzer();

    //2构建存储目录和配置参数
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));
    IndexWriterConfig cfg = new IndexWriterConfig(analyzer);

    // 3 构建IndexWriter对象
    IndexWriter indexWriter=new IndexWriter(directory,cfg);

    indexWriter.deleteDocuments(new Term("name","仙逆"),new
            Term("price","12.0"));
    // indexWriter.deleteAll(); //删除全部索引(慎用)

    //4.关闭indexWriter
    indexWriter.close();

}

 

 

 

 
  

 删除之前

 

删除之后没变化(实际删除了,工具有问题),更换词再删

 

3. 修改索引

更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索

引执行更新,可以先查询出来,确定更新记录存在执行更新操作。

更新前

 

 
/**
 * 更新索引
 * @throws IOException
 */
@Test
public void test6() throws IOException {
    // 1 构建分词器
    Analyzer analyzer=new StandardAnalyzer();

    //2构建存储目录和配置参数
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));
    IndexWriterConfig cfg = new IndexWriterConfig(analyzer);

    // 3 构建IndexWriter对象
    IndexWriter indexWriter=new IndexWriter(directory,cfg);

    Document document=new Document();
    Field name = new TextField("name", "mybook", Field.Store.YES);
    document.add(name);

    indexWriter.updateDocument(new Term("name","斗破苍穹"),document);

    //4.关闭indexWriter
    indexWriter.close();

}

 

 
 

 

 

五、 lucene索引查询

1. 创建查询的两种方法

对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。类似关系数据库Sql语法一样,Lucene也有自己的查询语法,比如:“name:lucene”表示查询Fieldname为“lucene”的文档信息。

可通过两种方法创建查询对象:

1.使用Lucene提供Query子类

Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,如下代码:Query query = new TermQuery(new Term("name", "lucene"));

2.使用QueryParse解析查询表达式

QueryParser会将用户输入的查询表达式解析成Query对象实例。

如下代码:QueryParser queryParser = new QueryParser("name", newStandardAnalyzer());Query query = queryParser.parse("name:lucene"

2. 通过Query的子类TermQuery搜索

2.1 清空索引

运行test2重新建立 分词、索引、存储

2.2 代码

 
@Test
public void test7() throws ParseException, IOException {
    //一.构建查询对象
    //1.创建分词器
     // Analyzer analyzer = new StandardAnalyzer();
    //2.构建查询解析器


    //3.构建查询对象
    Query query = new TermQuery(new Term("name","斗"));

    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);
        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        System.out.println("图书description:" + doc.get("description"));
    }

    //6.关闭indexReader
    indexReader.close();


}

  

 
  

2.3 运行

 

3. BooleanQuery:布尔查询,实现组合条件查询

 
BooleanClause.Occur的值
 //MUST:查询条件必须满足,相当于AND
    //SHOULD:查询条件可选,相当于OR
    //MUST_NOT:查询条件不能满足,相当于NOT非
/**
 *  BooleanQuery:搜索
 * @throws ParseException
 * @throws IOException
 */
@Test
public void test8() throws ParseException, IOException {
    //一.构建查询对象
    //1.构建查询对象
       Query query1 = new TermQuery(new Term("price", "10.0"));
    Query query2 = new TermQuery(new Term("name", "斗"));
    BooleanQuery.Builder builder=new BooleanQuery.Builder().add(query1,BooleanClause.Occur.MUST).add(query2, BooleanClause.Occur.MUST);
    BooleanQuery query = builder.build();

    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);
        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        System.out.println("图书description:" + doc.get("description"));
    }

    //6.关闭indexReader
    indexReader.close();


}

  

 
  

MUST  MUST

 

修改代码

MUST  MUST_NOT

 

其他测试不一一列举

4. 通过QueryParser搜索

 
/**
 * 搜索
 * @throws ParseException
 * @throws IOException
 */
@Test
public void test3() throws ParseException, IOException {
    //一.构建查询对象
    //1.创建分词器
    Analyzer analyzer = new StandardAnalyzer();
    //2.构建查询解析器
    QueryParser queryParser = new QueryParser("description", analyzer);
    //3.构建查询对象
    Query query = queryParser.parse("description: 魔法 and 斗气");

    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);
        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        System.out.println("图书description:" + doc.get("description"));
    }

    //6.关闭indexReader
    indexReader.close();
}

 

 
 

5. 查询语法

5.1 基础的查询语法,关键词查询:

域名+“:”+搜索的关键字

例如:description:java

5.2 组合条件查询

Occur.MUST 查询条件必须满足,相当于and +(加号)
Occur.SHOULD 查询条件可选,相当于or 空(不用符号)
Occur.MUST_NOT 查询条件不能满足,相当于not非 ‐(减号)
+条件1 +条件2:两个条件之间是并且的关系and
    例如:+name:apache +content:apache
+条件1 条件2:必须满足第一个条件,忽略第二个条件
    例如:+name:apache content:apache
条件1 条件2:两个条件满足其一即可。
    例如:name:apache content:apache
‐条件1 条件2:必须不满足条件1,要满足条件2
    例如:‐name:apache content:apache
第二种写法:
    条件1 AND 条件2
    条件1 OR 条件2
    条件1 NOT 条件2
  

6. MultiFieldQueryParser (对多个域查询)

 
/**
 *  MultiFieldQueryParser (对多个域查询)
 * @throws ParseException
 * @throws IOException
 */
@Test
public void test9() throws ParseException, IOException {
    //一.构建查询对象
    //1.构建查询对象
    Analyzer analyzer = new StandardAnalyzer();
    String[] fields={"description","name"};
    QueryParser queryParser=new MultiFieldQueryParser(fields,analyzer);
    Query query = queryParser.parse("仙");

    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;

    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);
        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        System.out.println("图书description:" + doc.get("description"));
    }

    //6.关闭indexReader
    indexReader.close();
}

  

 

 

 

 

六、 相关度排序

0.1 什么是相关度排序

相关度排序是查询结果按照与查询关键字的相关性进行排序,越相关的越靠前。比如搜索Lucene”关键字,与该关键字最相关的文章应该排在前边

0.2 相关度打分

Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。如何打分呢?Lucene是在用户进行检索时实时根据搜索的关键字计算出来的.

分两步:

计算出词(Term)的权重

根据词的权重值,计算文档相关度得分。

词的权重

通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:

1.Term Frequency (tf)

指此Term在此文档中出现了多少次。tf 越大说明越重要。词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。

2.Document Frequency (df)

 指有多少文档包含Termdf 越大说明越不重要。

比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低

七、 中文分词器

学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句隔开。而中文则以字为单位,字又组成词,字和词再组成句子。所以对于英文,我们以简单以空格判断某个字符串是否为一个单词,比如I love Chinalove China很容易被程序区分开来;但中文“我爱中国”就不一样了,电脑不知道“中国”是一个词语还是“爱中”是一个词语。把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我 爱 中国。

Lucene自带的中文分词器

StandardAnalyzer

单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,* 效果:“我”、“爱”、“中”、“国”。

 CJKAnalyzer二分法分词:按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人”。* StandardAnalyzerCJKAnalyzer无法满足需求。

SmartChineseAnalyze* 对中文支持稍好,但扩展性差,扩展词库,禁用词库和同义词库等不好处理

第三方中文分词器(IKanalyzer

IKanalyzer: 最新版在https://code.google.com/p/ikanalyzer/上,支持Lucene 4.10200612月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开 始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词 歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是201212月后没有在更新

1. 测试StandardAnalyzer分词器

 
@Test
public void test10() throws IOException {
    Document doc = new Document();
    Field field = new TextField("s1", "我是中国人",Field.Store.YES); doc.add(field);
    // 3 构建分词器
    Analyzer analyzer = new StandardAnalyzer();
    // 4 构建存储目录和配置参数
    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));
    IndexWriterConfig cfg = new IndexWriterConfig(analyzer);
    // 5 构建IndexWriter对象并添加文档对象
    IndexWriter indexWriter = new IndexWriter(directory, cfg);
    indexWriter.addDocument(doc);
    // 6 关闭和提交IndexWriter
    indexWriter.close();
}

 


 
 

 

2. 测试CJKAnalyzer

其他部分省略

Analyzer analyzer = new CJKAnalyzer();

 

 

3. 中文分词器SmartChineseAnalyz

Analyzer analyzer=new SmartChineseAnalyzer();
 
  

3.1 添加依赖

 
 
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-smartcn</artifactId>
    <version>7.5.0</version>
</dependency>

 

3.2 代码

其他部分省略

Analyzer analyzer=new SmartChineseAnalyzer();

 

 

4. 中文分词器IKAnalyzer

IKAnalyzer继承LuceneAnalyzer抽象类,使用IKAnalyzerLucene自带的分析器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。

如果使用中文分词器ikanalyzer,就在索引和搜索程序中使用一致的分词器ikanalyzer

只支持到lucene4.0

我们在4.0以后版本Lucene中使用就需要简单集成一下。

需要做集成,是因为AnalyzercreateComponents方法API改变了IKAnalyzer提供两种分词模式:细粒度分词和智能分词

八、 ikanalyzer集成

1. 添加依赖

 

<dependency>
    <groupId>com.janeluo</groupId>
    <artifactId>ikanalyzer</artifactId>
    <version>2012_u6</version>
</dependency>

 

 
  

2. 找到 IkAnalyzer包体提供的Lucene支持类,比较IKAnalyzercreateComponets方法

可以双击shift搜索IKAnalyzer 

 

 

 

4.0及之前版本的createComponets方法

 

 
/**
 * 重载Analyzer接口,构造分词组件
 */
@Override
protected TokenStreamComponents createComponents(String fieldName, final Reader in) {
  Tokenizer _IKTokenizer = new IKTokenizer(in, this.useSmart());
  return new TokenStreamComponents(_IKTokenizer);
}
  

最新的createComponets方法

  
/**
 * Creates a new {@link TokenStreamComponents} instance for this analyzer.
 * 
 * @param fieldName
 *          the name of the fields content passed to the
 *          {@link TokenStreamComponents} sink as a reader

 * @return the {@link TokenStreamComponents} for this analyzer.
 */
protected abstract TokenStreamComponents createComponents(String fieldName);

3. 照这两个类,创建新版本的, 类里面的代码直接复制,修改参数即可

3.1 重写分析器

 1 package com.hx.lucene.analyzer;
 2 
 3 import org.apache.lucene.analysis.Analyzer;
 4 
 5 public class IKAnalyzer4Lucene7 extends Analyzer {
 6 
 7     private boolean useSmart = false;
 8 
 9     public void setUseSmart(boolean useSmart) {
10         this.useSmart = useSmart;
11     }
12 
13     /**
14      * IK分词器Lucene  Analyzer接口实现类
15      * <p>
16      * 默认细粒度切分算法
17      */
18     public IKAnalyzer4Lucene7() {
19         this(false);
20     }
21 
22     /**
23      * IK分词器Lucene Analyzer接口实现类
24      *
25      * @param useSmart 当为true时,分词器进行智能切分
26      */
27     public IKAnalyzer4Lucene7(boolean useSmart) {
28         super();
29         this.useSmart = useSmart;
30     }
31 
32     public boolean isUseSmart() {
33         return useSmart;
34     }
35 
36 
37     @Override
38     protected TokenStreamComponents createComponents(String fieldName) {
39         IKTokenizer4Lucene7 tk = new IKTokenizer4Lucene7(this.useSmart);
40         return new TokenStreamComponents(tk);
41     }
42 
43 } 

3.2 重写分词器

 1 package com.hx.lucene.analyzer;
 2 
 3 import org.apache.lucene.analysis.Tokenizer;
 4 import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
 5 import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
 6 import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
 7 import org.wltea.analyzer.core.IKSegmenter;
 8 import org.wltea.analyzer.core.Lexeme;
 9 
10 import java.io.IOException;
11 
12 /**
13  * IK分词器 Lucene Tokenizer适配器类
14  * 因为Analyzer的createComponents方法API改变了需要重新实现分词器
15  * @author THINKPAD
16  *
17  */
18 public class IKTokenizer4Lucene7 extends Tokenizer {
19 
20     // IK分词器实现
21     private IKSegmenter _IKImplement;
22 
23     // 词元文本属性
24     private final CharTermAttribute termAtt;
25     // 词元位移属性
26     private final OffsetAttribute offsetAtt;
27     // 词元分类属性(该属性分类参考org.wltea.analyzer.core.Lexeme中的分类常量)
28     private final TypeAttribute typeAtt;
29     // 记录最后一个词元的结束位置
30     private int endPosition;
31 
32     /**
33      * @param useSmart
34      */
35     public IKTokenizer4Lucene7(boolean useSmart) {
36         super();
37         offsetAtt = addAttribute(OffsetAttribute.class);
38         termAtt = addAttribute(CharTermAttribute.class);
39         typeAtt = addAttribute(TypeAttribute.class);
40         _IKImplement = new IKSegmenter(input, useSmart);
41     }
42 
43     /*
44      * (non-Javadoc)
45      * 
46      * @see org.apache.lucene.analysis.TokenStream#incrementToken()
47      */
48     @Override
49     public boolean incrementToken() throws IOException {
50         // 清除所有的词元属性
51         clearAttributes();
52         Lexeme nextLexeme = _IKImplement.next();
53         if (nextLexeme != null) {
54             // 将Lexeme转成Attributes
55             // 设置词元文本
56             termAtt.append(nextLexeme.getLexemeText());
57             // 设置词元长度
58             termAtt.setLength(nextLexeme.getLength());
59             // 设置词元位移
60             offsetAtt.setOffset(nextLexeme.getBeginPosition(),
61                     nextLexeme.getEndPosition());
62             // 记录分词的最后位置
63             endPosition = nextLexeme.getEndPosition();
64             // 记录词元分类
65             typeAtt.setType(nextLexeme.getLexemeTypeString());
66             // 返会true告知还有下个词元
67             return true;
68         }
69         // 返会false告知词元输出完毕
70         return false;
71     }
72 
73     /*
74      * (non-Javadoc)
75      * 
76      * @see org.apache.lucene.analysis.Tokenizer#reset(java.io.Reader)
77      */
78     @Override
79     public void reset() throws IOException {
80         super.reset();
81         _IKImplement.reset(input);
82     }
83 
84     @Override
85     public final void end() {
86         // set final offset
87         int finalOffset = correctOffset(this.endPosition);
88         offsetAtt.setOffset(finalOffset, finalOffset);
89     }
90 } 

4. 测试

4.1 关键代码(其余省略)

Analyzer analyzer=new IKAnalyzer4Lucene7();

4.2 测试后报错

 

 

4.3  IKTokenizer4Lucebe7final修饰

 

4.4 继续测试

 

5. 扩展 IKAnalyzer的停用词和新词

5.1 扩展 IKAnalyzer的停用词

1、在类目录下创建IK的配置文件:IKAnalyzer.cfg.xml

2、在配置文件中增加配置扩展停用词文件的节点: <entry key=ext_stopwords>my_ext_stopword.dic</entry> 如有多个,以“;”间隔

<?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_stopwords">my_ext_stopword.dic</entry>
</properties>

 

3、在类目录下创建我们的扩展停用词文件 my_ext_stopword.dic,编辑该文件加入停用词,一行一个

 

5.2 测试

package com.hx.lucene;

import com.hx.lucene.analyzer.IKAnalyzer4Lucene7;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;

import java.io.IOException;

/**
 * 扩展 IKAnalyzer的词典测试
 * 
 *
 */
public class ExtendedIKAnalyzerDicTest {

    private static void doToken(TokenStream ts) throws IOException {
        ts.reset();
        CharTermAttribute cta = ts.getAttribute(CharTermAttribute.class);
        while (ts.incrementToken()) {
            System.out.print(cta.toString() + "|");
        }
        System.out.println();
        ts.end();
        ts.close();
    }

    public static void main(String[] args) throws IOException {
        String chineseText = "厉害了我的国一经播出,受到各方好评,强烈激发了国人的爱国之情、自豪感!";
        // IKAnalyzer 细粒度切分
        try (Analyzer ik = new IKAnalyzer4Lucene7();) {
            TokenStream ts = ik.tokenStream("content", chineseText);
            System.out.println("IKAnalyzer中文分词器 细粒度切分,中文分词效果:");
            doToken(ts);
        }

        // IKAnalyzer 智能切分
        try (Analyzer ik = new IKAnalyzer4Lucene7(true);) {
            TokenStream ts = ik.tokenStream("content", chineseText);
            System.out.println("IKAnalyzer中文分词器 智能切分,中文分词效果:");
            doToken(ts);
        }
    }
}

 

运行结果:

未加停用词之前

 

加停用词之后:

6. 扩展 IKAnalyzer的新词

1.在类目录下IK的配置文件:IKAnalyzer.cfg.xml 中增加配置扩展词文件的节点: <entry key="ext_dict">ext.dic</entry> 如有多个,以“;”间隔 

 

<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">my_ext_stopword.dic</entry>

 

2. 在类目录下创建扩展词文件 ext.dic,编辑该文件加入新词,一行一个

 

3.运行test10

 

7. 细粒度和智能分词

由分词器的构造方法参数决定true智能分词

Analyzer analyzer=new IKAnalyzer4Lucene7(true);

 

 

九、 lucene高亮显示

1. 清空重新建立索引

public void test11() throws IOException {
    //1采集数据
    List<Book> allBook = bookMapper.getAllBook();
    //2创建索引
    //Document集合对象
    List<Document> documents = new ArrayList<Document>();
    //将非结构化数据结构化,创建索引域和文档域
    Document doc;
    for (Book book : allBook) {
        doc = new Document();
        Field id = new TextField("id", String.valueOf(book.getId()), Field.Store.YES);
        Field bookName = new TextField("name", book.getBookName().toString(), Field.Store.YES);
        Field price = new TextField("price", String.valueOf(book.getPrice()), Field.Store.YES);
        Field pic = new TextField("pic", book.getPic(), Field.Store.YES);
        Field description = new TextField("description", book.getDescription(), Field.Store.YES);
        doc.add(id);
        doc.add(bookName);
        doc.add(price);
        doc.add(pic);
        doc.add(description);
        documents.add(doc);


    }
    //构建分词器
    Analyzer analyzer=new SmartChineseAnalyzer();

    //构建存储目录和配置参数

    Directory directory = FSDirectory.open(Paths.get("D:\\test\\lucene"));

    //构建存储目录和配置
    IndexWriterConfig cfg = new IndexWriterConfig(analyzer);
    //构建IndexWriter索引写对象并添加文档对象
    IndexWriter indexWriter = new IndexWriter(directory, cfg);


    for (Document document : documents) {
        indexWriter.addDocument(document);
    }

    //关闭indexWriter
    indexWriter.close();
}

  

 

2. 高亮显示测试

 
public void test12() throws ParseException, IOException, InvalidTokenOffsetsException {
    //一.构建查询对象

    //1.构建分词器

   // Analyzer analyzer = new SmartChineseAnalyzer();
    SmartChineseAnalyzer  analyzer = new SmartChineseAnalyzer();
    String[] fields = {"description", "name"};
    //2.构建查询对象
    //MultiFieldQueryParser可对多个域查询
    QueryParser queryParser = new QueryParser("description", analyzer);


    //二.执行搜索
    //1.指定索引目录
    Directory directory = FSDirectory.open( Paths.get("D:\\test\\lucene"));

    //2.创建indexWriter
    IndexReader indexReader = DirectoryReader.open(directory);

    //3.创建IndexSearcher对象
    IndexSearcher indexSearcher = new IndexSearcher(indexReader);

    //通过解析要查询的String,获取查询对象
    Query query = queryParser.parse("界");
    //计算得分,会初始化一个查询结果最高的得分
    QueryScorer scorer=new QueryScorer(query);
    //此处加入的是搜索结果的高亮部分
    SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color='red'>","</font></b>");
    //根据这个得分计算出一个片段
    Fragmenter fragmenter=new SimpleSpanFragmenter(scorer);
    Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer);
    ////设置一下要显示的片段
    highlighter.setTextFragmenter(fragmenter);


    //4.通过IndexSearcher对象执行查询索引库,返回TopDocs对象
    // 第一个参数:查询对象
    // 第二个参数:最大的n条记录
    TopDocs topDocs = indexSearcher.search(query, 10);

    //5.提取TopDocs对象中前n条记录


    System.out.println("查询出的文档个数为:" + topDocs.totalHits);

    for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
        // 文档对象ID
        int docId = scoreDoc.doc;
        Document doc = indexSearcher.doc(docId);

        //  输出文档内容
        System.out.println("===============================");
        System.out.println("文档id:" + docId);
        System.out.println("图书id:" + doc.get("id"));
        System.out.println("图书name:" + doc.get("name"));
        System.out.println("图书price:" + doc.get("price"));
        System.out.println("图书pic:" + doc.get("pic"));
        String description = doc.get("description");
        if(description!=null){
            TokenStream tokenStream=analyzer.tokenStream("description", new StringReader(description));
            /**
             * getBestFragment方法用于输出摘要(即权重大的内容)
             */
            System.out.println(highlighter.getBestFragment(tokenStream, description));
        }
        //System.out.println("图书description:" + doc.get("description"));
    }
    //6.关闭indexReader
    indexReader.close();
}

  

 

  

十、 参考链接

1. lucene官网

 http://lucene.apache.org/

2. lucene下载地址

http://archive.apache.org/dist/lucene/java/

3. Lucene查看工具Luke

下载网址:http://www.getopt.org/luke/

下载对应版本:https://github.com/DmitryKey/luke/releases  

4. 网络采集技术(爬虫)

Solrhttp://lucene.apache.org/solr

Nutchhttp://lucene.apache.org/nutch

jsouphttp://jsoup.org/ 

heritrix http://sourceforge.net/projects/archive-crawler/files/

5. 中文分词器IKanalyzer

最新版在

https://code.google.com/p/ik-analyzer/

6. IKAnalyze中文分词器集成https://www.cnblogs.com/leeSmall/p/8994176.html7. lucene高亮显示

参考链接:https://www.cnblogs.com/shyroke/p/7942152.htm

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

 



posted @ 2019-02-21 23:08  前行者鼠  阅读(539)  评论(0编辑  收藏  举报