談談個人網站的建立(二)—— lucene的使用


首先,幫忙點擊一下我的網站http://www.wenzhihuai.com/ 。謝謝啊,如果可以,GitHub上麻煩給個star,以后面試能講講這個項目,GitHub地址https://github.com/Zephery/newblog

Lucene的整體架構

image

搜索引擎的幾個重要概念:

  1. 倒排索引:將文檔中的詞作為關鍵字,建立詞與文檔的映射關系,通過對倒排索引的檢索,可以根據詞快速獲取包含這個詞的文檔列表。倒排索引一般需要對句子做去除停用詞。

  2. 停用詞:在一段句子中,去掉之后對句子的表達意向沒有印象的詞語,如“非常”、“如果”,中文中主要包括冠詞,副詞等。

  3. 排序:搜索引擎在對一個關鍵詞進行搜索時,可能會命中許多文檔,這個時候,搜索引擎就需要快速的查找的用戶所需要的文檔,因此,相關度大的結果需要進行排序,這個設計到搜索引擎的相關度算法。

Lucene中的幾個概念

  1. 文檔(Document):文檔是一系列域的組合,文檔的域則代表一系列域文檔相關的內容。
  2. 域(Field):每個文檔可以包含一個或者多個不同名稱的域。
  3. 詞(Term):Term是搜索的基本單元,與Field相對應,包含了搜索的域的名稱和關鍵詞。
  4. 查詢(Query):一系列Term的條件組合,成為TermQuery,但也有可能是短語查詢等。
  5. 分詞器(Analyzer):主要是用來做分詞以及去除停用詞的處理。

索引的建立

索引的搜索

lucene在本網站的使用:

  1. 搜索 2. 自動分詞

一、搜索

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

<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. 構建索引
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();
  1. 更新與刪除
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();
  1. 查詢
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());
    }
}
  1. 高亮
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);
}

結果

<b><font color='red'>Java</font></b>堆.棧和常量池 筆記
  1. 分頁
    目前lucene分頁的方式主要有兩種:
    (1). 每次都全部查詢,然后通過截取獲得所需要的記錄。由於采用了分詞與倒排索引,所有速度是足夠快的,但是在數據量過大的時候,占用內存過大,容易造成內存溢出
    (2). 使用searchAfter把數據保存在緩存里面,然后再去取。這種方式對大量的數據友好,但是當數據量比較小的時候,速度會相對慢。
    lucene中使用searchafter來篩選順序
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)
  1. 使用效果
    全部代碼放在這里,代碼寫的不太好,光從代碼規范上就不咋地。在網頁上的使用效果如下:

二、lucene自動補全

百度、谷歌等在輸入文字的時候會彈出補全框,如下圖:

在搭建lucene自動補全的時候,也有考慮過使用SQL語句中使用like來進行,主要還是like對數據庫壓力會大,而且相關度沒有lucene的高。主要使用了官方suggest庫以及autocompelte.js這個插件。
suggest的原理看這,以及索引結構看這

使用:

  1. 導入maven包
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-suggest</artifactId>
    <version>6.6.0</version>
</dependency>
  1. 如果想將結果反序列化,聲明實體類的時候要加上:
public class Blog implements Serializable {
  1. 實現InputIterator接口
    InputIterator的幾個方法:
    long weight():返回的權重值,大小會影響排序,默認是1L
    BytesRef payload():對某個對象進行序列化
    boolean hasPayloads():是否有設置payload信息
    Set contexts():存入context,context里可以是任意的自定義數據,一般用於數據過濾
    boolean hasContexts():判斷是否有下一個,默認為false
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
    }
}
  1. ajax 建立索引
/**
 * 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!");
    }
}
  1. 查找
    因為有些文章的標題是一樣的,先對list排序,將標題短的放前面,長的放后面,然后使用LinkHashSet來存儲。
@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;
    }
}
  1. controller層
@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
}
  1. ajax來提交請求
    autocomplete.js源代碼與介紹:https://github.com/xdan/autocomplete
<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>
  1. 效果:

歡迎訪問我的個人網站

參考:
https://www.ibm.com/developerworks/cn/java/j-lo-lucene1/

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM