# 3.2 Lucene的使用.md

首先，帮忙点击一下我的网站<http://www.wenzhihuai.com/> 。谢谢啊，如果可以，GitHub上麻烦给个star，以后面试能讲讲这个项目，GitHub地址<https://github.com/Zephery/newblog> 。

### Lucene的整体架构

![image](https://github-images.wenzhihuai.com/images/lucenejoijfoaiwheifuwhifu.png)

### 搜索引擎的几个重要概念：

1. 倒排索引：将文档中的词作为关键字，建立词与文档的映射关系，通过对倒排索引的检索，可以根据词快速获取包含这个词的文档列表。倒排索引一般需要对句子做去除停用词。 ![](https://github-images.wenzhihuai.com/images/fewfwaefawe20170903183124.png)
2. 停用词：在一段句子中，去掉之后对句子的表达意向没有印象的词语，如“非常”、“如果”，中文中主要包括冠词，副词等。
3. 排序：搜索引擎在对一个关键词进行搜索时，可能会命中许多文档，这个时候，搜索引擎就需要快速的查找的用户所需要的文档，因此，相关度大的结果需要进行排序，这个设计到搜索引擎的相关度算法。

### Lucene中的几个概念

1. 文档（Document）：文档是一系列域的组合，文档的域则代表一系列域文档相关的内容。
2. 域（Field）：每个文档可以包含一个或者多个不同名称的域。
3. 词（Term）：Term是搜索的基本单元，与Field相对应，包含了搜索的域的名称和关键词。
4. 查询（Query）：一系列Term的条件组合，成为TermQuery，但也有可能是短语查询等。
5. 分词器（Analyzer）：主要是用来做分词以及去除停用词的处理。

索引的建立

![](https://github-images.wenzhihuai.com/images/fweafwe170903175148.png)

索引的搜索

![](https://github-images.wenzhihuai.com/images/fwefawfagergwerg20170903184512.png)

### lucene在本网站的使用：

1. 搜索 2. 自动分词

## 一、搜索

注意：本文使用最新的lucene，版本6.6.0。lucene的版本更新很快，每跨越一次大版本，使用方式就不一样。首先需要导入lucene所使用的包。使用maven：

```java
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId><!--lucene核心-->
    <version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId><!--分词器-->
    <version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-smartcn</artifactId><!--中文分词器-->
    <version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId><!--格式化-->
    <version>${lucene.version}</version>
</dependency>
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-highlighter</artifactId><!--lucene高亮-->
    <version>${lucene.version}</version>
</dependency>
```

1. 构建索引

```java
Directory dir = FSDirectory.open(Paths.get("blog_index"));//索引存储的位置
SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();//简单的分词器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
IndexWriter writer = new IndexWriter(dir, config);
Document doc = new Document();
doc.add(new TextField("title", blog.getTitle(), Field.Store.YES)); //对标题做索引
doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));//对文章内容做索引
writer.addDocument(doc);
writer.close();
```

2. 更新与删除

```java
IndexWriter writer = getWriter();
Document doc = new Document();
doc.add(new TextField("title", blog.getTitle(), Field.Store.YES));
doc.add(new TextField("content", Jsoup.parse(blog.getContent()).text(), Field.Store.YES));
writer.updateDocument(new Term("blogid", String.valueOf(blog.getBlogid())), doc);   //更新索引
writer.close();
```

3. 查询

```java
private static void search_index(String keyword) {
    try {
        Directory dir = FSDirectory.open(Paths.get("blog_index")); //获取要查询的路径，也就是索引所在的位置
        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher searcher = new IndexSearcher(reader);
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        QueryParser parser = new QueryParser("content", analyzer); //查询解析器
        Query query = parser.parse(keyword); //通过解析要查询的String，获取查询对象
        TopDocs docs = searcher.search(query, 10);//开始查询，查询前10条数据，将记录保存在docs中,
        for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
            Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
            System.out.println(doc.get("title")); //fullPath是刚刚建立索引的时候我们定义的一个字段
        }
        reader.close();
    } catch (IOException | ParseException e) {
        logger.error(e.toString());
    }
}
```

4. 高亮

```java
Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>");
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
highlighter.setTextFragmenter(fragmenter);
for (ScoreDoc scoreDoc : docs.scoreDocs) { //取出每条查询结果
    Document doc = searcher.doc(scoreDoc.doc); //scoreDoc.doc相当于docID,根据这个docID来获取文档
    String title = doc.get("title");
    TokenStream tokenStream = analyzer.tokenStream("title", new StringReader(title));
    String hTitle = highlighter.getBestFragment(tokenStream, title);
    System.out.println(hTitle);
}
```

结果

```html
<b><font color='red'>Java</font></b>堆.栈和常量池 笔记
```

5. 分页 目前lucene分页的方式主要有两种： (1). 每次都全部查询，然后通过截取获得所需要的记录。由于采用了分词与倒排索引，所有速度是足够快的，但是在数据量过大的时候，占用内存过大，容易造成内存溢出 (2). 使用searchAfter把数据保存在缓存里面，然后再去取。这种方式对大量的数据友好，但是当数据量比较小的时候，速度会相对慢。 lucene中使用searchafter来筛选顺序

```java
ScoreDoc lastBottom = null;//相当于pageSize
BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
QueryParser parser1 = new QueryParser("title", analyzer);//对文章标题进行搜索
Query query1 = parser1.parse(q);
booleanQuery.add(query1, BooleanClause.Occur.SHOULD);
TopDocs hits = search.searchAfter(lastBottom, booleanQuery.build(), pagehits);  //lastBottom（pageSize），pagehits（pagenum）
```

6. 使用效果 全部代码放在[这里](https://github.com/Zephery/newblog/blob/master/src/main/java/com/myblog/lucene/BlogIndex.java)，代码写的不太好，光从代码规范上就不咋地。在网页上的使用效果如下：

![](https://github-images.wenzhihuai.com/images/bervererfwaefewf20170903200915.png)

## 二、lucene自动补全

百度、谷歌等在输入文字的时候会弹出补全框，如下图：

![](https://github-images.wenzhihuai.com/images/QQ%E6%88%AA%E5%9B%BE20170728102929.png)

在搭建lucene自动补全的时候，也有考虑过使用SQL语句中使用like来进行，主要还是like对数据库压力会大，而且相关度没有lucene的高。主要使用了官方suggest库以及[autocompelte.js](https://github.com/xdan/autocomplete/)这个插件。 suggest的原理[看这](http://iamyida.iteye.com/blog/2205114),以及索引结构[看这](http://blog.csdn.net/u011389474/article/details/69458445)。

#### 使用：

1. 导入maven包

```xml
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-suggest</artifactId>
    <version>6.6.0</version>
</dependency>
```

2. 如果想将结果反序列化，声明实体类的时候要加上：

```java
public class Blog implements Serializable {
```

3. 实现InputIterator接口

```
InputIterator的几个方法：
long weight():返回的权重值，大小会影响排序，默认是1L
BytesRef payload():对某个对象进行序列化
boolean hasPayloads():是否有设置payload信息
Set<BytesRef> contexts():存入context，context里可以是任意的自定义数据，一般用于数据过滤
boolean hasContexts():判断是否有下一个，默认为false
```

```java
public class BlogIterator implements InputIterator {
    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(BlogIterator.class);
    private Iterator<Blog> blogIterator;
    private Blog currentBlog;

    public BlogIterator(Iterator<Blog> blogIterator) {
        this.blogIterator = blogIterator;
    }

    @Override
    public boolean hasContexts() {
        return true;
    }

    @Override
    public boolean hasPayloads() {
        return true;
    }

    public Comparator<BytesRef> getComparator() {
        return null;
    }

    @Override
    public BytesRef next() {
        if (blogIterator.hasNext()) {
            currentBlog = blogIterator.next();
            try {
                //返回当前Project的name值，把blog类的name属性值作为key
                return new BytesRef(Jsoup.parse(currentBlog.getTitle()).text().getBytes("utf8"));
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * 将Blog对象序列化存入payload
     * 可以只将所需要的字段存入payload，这里对整个实体类进行序列化，方便以后需求，不建议采用这种方法
     */
    @Override
    public BytesRef payload() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(currentBlog);
            out.close();
            BytesRef bytesRef = new BytesRef(bos.toByteArray());
            return bytesRef;
        } catch (IOException e) {
            logger.error("", e);
            return null;
        }
    }

    /**
     * 文章标题
     */
    @Override
    public Set<BytesRef> contexts() {
        try {
            Set<BytesRef> regions = new HashSet<BytesRef>();
            regions.add(new BytesRef(currentBlog.getTitle().getBytes("UTF8")));
            return regions;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Couldn't convert to UTF-8");
        }
    }

    /**
     * 返回权重值，这个值会影响排序
     * 这里以产品的销售量作为权重值，weight值即最终返回的热词列表里每个热词的权重值
     */
    @Override
    public long weight() {
        return currentBlog.getHits();   //change to hits
    }
}
```

4. ajax 建立索引

```java
/**
 * ajax建立索引
 */
@Override
public void ajaxbuild() {
    try {
        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
        //创建Blog测试数据
        List<Blog> blogs = blogMapper.getAllBlog();
        suggester.build(new BlogIterator(blogs.iterator()));
    } catch (IOException e) {
        System.err.println("Error!");
    }
}
```

5. 查找\
   因为有些文章的标题是一样的，先对list排序，将标题短的放前面，长的放后面，然后使用LinkHashSet来存储。

```java
@Override
public Set<String> ajaxsearch(String keyword) {
    try {
        Directory dir = FSDirectory.open(Paths.get("autocomplete"));
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        AnalyzingInfixSuggester suggester = new AnalyzingInfixSuggester(dir, analyzer);
        List<String> list = lookup(suggester, keyword);
        list.sort(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                if (o1.length() > o2.length()) {
                    return 1;
                } else {
                    return -1;
                }
            }
        });
        Set<String> set = new LinkedHashSet<>();
        for (String string : list) {
            set.add(string);
        }
        ssubSet(set, 7);
        return set;
    } catch (IOException e) {
        System.err.println("Error!");
        return null;
    }
}
```

6. controller层

```java
@RequestMapping("ajaxsearch")
public void ajaxsearch(HttpServletRequest request, HttpServletResponse response) throws IOException {
    String keyword = request.getParameter("keyword");
    if (StringUtils.isEmpty(keyword)) {
        return;
    }
    Set<String> set = blogService.ajaxsearch(keyword);
    Gson gson = new Gson();
    response.getWriter().write(gson.toJson(set));//返回的数据使用json
}
```

7. ajax来提交请求 autocomplete.js源代码与介绍：<https://github.com/xdan/autocomplete>

```js
<link rel="stylesheet" href="js/autocomplete/jquery.autocomplete.css">
<script src="js/autocomplete/jquery.autocomplete.js" type="text/javascript"></script>
<script type="text/javascript">
    /******************** remote start **********************/
    $('#remote_input').autocomplete({
        source: [
            {
                url: "ajaxsearch.html?keyword=%QUERY%",
                type: 'remote'
            }
        ]
    });
    /********************* remote end ********************/
</script>
```

8. 效果：\
   ![](https://github-images.wenzhihuai.com/images/QQ%E6%88%AA%E5%9B%BE20170728105628.png)

#### 欢迎访问我的[个人网站](http://www.wenzhihuai.com/)

参考：\
<https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/>

<http://iamyida.iteye.com/blog/2205114>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gitbook.wenzhihuai.com/ge-ren-wang-zhan/2.lucene-de-shi-yong.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
