1. 程式人生 > >如何使用 Lucene 做網站高亮搜尋功能?

如何使用 Lucene 做網站高亮搜尋功能?

640?wx_fmt=gif

640?wx_fmt=jpeg

作者 | 倪升武

責編 | 胡巍巍

現在基本上所有網站都支援搜尋功能,現在搜尋的工具有很多,比如Solr、Elasticsearch,它們都是基於 Lucene 實現的,各有各的使用場景。Lucene 比較靈活,中小型專案中使用的比較多,我個人也比較喜歡用。


640?wx_fmt=png

效果展示


我前段時間做了一個網站,搜尋功能用的就是 Lucene 技術,效果還可以,支援中文高亮顯示,支援標題和摘要同時檢索,若能檢索出,均高亮展示等功能,可以看下效果。

640

點選檢視更清晰

可以看出,搜尋 “微服務” 之後,可以將相關的資源全部檢索出來,不管是標題包含還是摘要包含都可以檢索出來。

這是比較精確的匹配,還有非精確的匹配也支援,比如我搜索 “Java專案實戰”,看看結果如何。

640

點選檢視更清晰

可以看出,如果不能完全精確匹配,Lucene 也可以做模糊匹配,將最接近搜尋的內容給檢索出來,展示在頁面上。

我個人還是比較喜歡使用 Lucene 的,關於 Lucene 全文檢索的原理我就不浪費篇幅介紹了,谷歌百度有一大堆原理。這篇文章主要來分享下如何使用 Lucene 做到這個功能。


640?wx_fmt=png

依賴匯入


使用 Lucene 有幾個核心的依賴需要匯入到專案中,上面展示的這個效果涉及到中文的分詞,所以中文分詞依賴也需要匯入。


  

<!-- Lucence核心包 -->


<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-core</artifactId>
  <version>5.3.1</version>
</dependency>

<!-- Lucene查詢解析包 -->
<dependency>
  <groupId>org.apache.lucene</groupId
>

  <artifactId>lucene-queryparser</artifactId>
  <version>5.3.1</version>
</dependency>

<!--支援分詞高亮  -->
<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-highlighter</artifactId>
  <version>5.3.1</version>
</dependency>

<!--支援中文分詞  -->
<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-analyzers-smartcn</artifactId>
  <version>5.3.1</version>
</dependency>



640?wx_fmt=png

建立分詞索引


使用 Lucene 首先要建立索引,然後再查詢。如何建立索引呢?為了更好的說明問題,我在這寫一個 demo:直接對字串內容建立索引。

因為在實際專案中,絕大部分情況是獲取到一些文字字串(比如從表中查詢出來的結果),然後對該文字字串建立索引。

索引建立的過程,先要獲取 IndexWriter 物件,然後將相關的內容生成索引,索引的 Key 可以自己根據專案中的情況來自定義,value 是自己處理過的文字,或者從資料庫中查詢出來的文字。生成的時候,我們需要使用中文分詞器。程式碼如下:


  

public class ChineseIndexer {
    /**
     * 存放索引的位置
     */

    private Directory dir;

    //準備一下用來測試的資料
    //用來標識文件
    private Integer ids[] = {123};
    private String citys[] = {"上海""南京""青島"};
    private String descs[] = {
            "上海是個繁華的城市。",
            "南京是一個文化的城市南京,簡稱寧,是江蘇省會,地處中國東部地區,
            長江下游,瀕江近海。全市下轄11個區,總面積6597平方公里,2013年建
            成區面積752.83平方公里,常住人口818.78萬,其中城鎮人口659.1萬人。
            [1-4] “江南佳麗地,金陵帝王州”,南京擁有著6000多年文明史、近2600
            年建城史和近500年的建都史,是中國四大古都之一,有“六朝古都”、
            “十朝都會”之稱,是中華文明的重要發祥地,歷史上曾數次庇佑華夏之正
            朔,長期是中國南方的政治、經濟、文化中心,擁有厚重的文化底蘊和豐富
            的歷史遺存。[5-7] 南京是國家重要的科教中心,自古以來就是一座崇文重
            教的城市,有“天下文樞”、“東南第一學”的美譽。截至2013年,南京有
            高等院校75所,其中211高校8所,僅次於北京上海;國家重點實驗室25所、
            國家重點學科169個、兩院院士83人,均居中國第三。[8-10] 。"
,
            "青島是一個美麗的城市。"
    };

    /**
     * 生成索引
     * @param indexDir
     * @throws Exception
     */

    public void index(String indexDir) throws Exception {
        dir = FSDirectory.open(Paths.get(indexDir));
        // 先呼叫 getWriter 獲取IndexWriter物件
        IndexWriter writer = getWriter();
        for(int i = 0; i < ids.length; i++) {
            Document doc = new Document();
            // 把上面的資料都生成索引,分別用id、city和desc來標識
            doc.add(new IntField("id", ids[i], Field.Store.YES));
            doc.add(new StringField("city", citys[i], Field.Store.YES));
            doc.add(new TextField("desc", descs[i], Field.Store.YES));
            //新增文件
            writer.addDocument(doc);
        }
        //close了才真正寫到文件中
        writer.close();
    }

    /**
     * 獲取IndexWriter例項
     * @return
     * @throws Exception
     */

    private IndexWriter getWriter() throws Exception {
        //使用中文分詞器
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        //將中文分詞器配到寫索引的配置中
        IndexWriterConfig config = new IndexWriterConfig(analyzer);
        //例項化寫索引物件
        IndexWriter writer = new IndexWriter(dir, config);
        return writer;
    }

    public static void main(String[] args) throws Exception {
        new ChineseIndexer().index("D:\\lucene2");
    }
}

這裡我們用 ID、city、desc 分別代表 ID、城市名稱和城市描述,用他們作為關鍵字來建立索引,後面我們獲取內容的時候,主要來獲取城市描述。

南京的描述我故意寫的長一點,因為下文檢索的時候,根據不同的關鍵字會檢索到不同部分的資訊,有個權重的概念在裡面。

然後執行一下 main 方法,將索引儲存到 D:\lucene2\ 中。


640?wx_fmt=png

 中文分詞查詢


中文分詞查詢效果是:將查詢出來的關鍵字標紅加粗。它的原理很簡單:需要計算出一個得分片段,這是什麼意思呢?

比如上面那個文字中我搜索 “南京文化” 跟搜尋 “南京文明”,應該會返回不同的結果,這個結果是根據計算出的得分片段來確定的。

這麼說,大家可能不太明白,我舉個更加通俗的例子,比如有一段文字:“你好,我的名字叫倪升武,科大訊飛軟體開發工程師……江湖人都叫我武哥,我一直覺得,人與人之間講的是真誠,而不是套路。……”。

如果我搜 “倪升武”,可能會給我返回結果:“我的名字叫倪升武,科大訊飛軟體開發工程師”;

如果我搜 “武哥”,可能會給我返回結果:“江湖人都叫我武哥,我一直覺得”。這就是根據搜尋關鍵字來計算一段文字不同地方的得分,將最符合的部分搜出來。

明白了原理,我們看一下程式碼,我把詳細的步驟寫在註釋中了,避免大篇幅闡述。


  

public class ChineseSearch {

    private static final Logger logger = LoggerFactory.getLogger(ChineseSearch.class);

    public static List<String> search(String indexDir, String q) throws Exception {

        //獲取要查詢的路徑,也就是索引所在的位置
        Directory dir = FSDirectory.open(Paths.get(indexDir));
        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher searcher = new IndexSearcher(reader);
        //使用中文分詞器
        SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
        //由中文分詞器初始化查詢解析器
        QueryParser parser = new QueryParser("desc", analyzer);
        //通過解析要查詢的String,獲取查詢物件
        Query query = parser.parse(q);

        //記錄索引開始時間
        long startTime = System.currentTimeMillis();
        //開始查詢,查詢前10條資料,將記錄儲存在docs中
        TopDocs docs = searcher.search(query, 10);
        //記錄索引結束時間
        long endTime = System.currentTimeMillis();
        logger.info("匹配{}共耗時{}毫秒", q, (endTime - startTime));
        logger.info("查詢到{}條記錄", docs.totalHits);

        //如果不指定引數的話,預設是加粗,即<b><b/>
        SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color=red>","</font></b>");
        //根據查詢物件計算得分,會初始化一個查詢結果最高的得分
        QueryScorer scorer = new QueryScorer(query);
        //根據這個得分計算出一個片段
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
        //將這個片段中的關鍵字用上面初始化好的高亮格式高亮
        Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
        //設定一下要顯示的片段
        highlighter.setTextFragmenter(fragmenter);

        //取出每條查詢結果
        List<String> list = new ArrayList<>();
        for(ScoreDoc scoreDoc : docs.scoreDocs) {
            //scoreDoc.doc相當於docID,根據這個docID來獲取文件
            Document doc = searcher.doc(scoreDoc.doc);
            logger.info("city:{}", doc.get("city"));
            logger.info("desc:{}", doc.get("desc"));
            String desc = doc.get("desc");

            //顯示高亮
            if(desc != null) {
                TokenStream tokenStream = analyzer.tokenStream("desc"new StringReader(desc));
                String summary = highlighter.getBestFragment(tokenStream, desc);
                logger.info("高亮後的desc:{}", summary);
                list.add(summary);
            }
        }
        reader.close();
        return list;
    }
}


640?wx_fmt=png

功能測試


到這裡,最核心的功能都實現好了,我們可以自己寫個小介面來呼叫下,看看效果。


  

@Controller
@RequestMapping("/lucene")
public class IndexController {

    @GetMapping("/test")
    public String test(Model model) {
        // 索引所在的目錄
        String indexDir = "D:\\lucene2";
        // 要查詢的字元
        String q = "南京文化";
        try {
            List<String> list = ChineseSearch.search(indexDir, q);
            model.addAttribute("list", list);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "result";
    }
}

在 result.html 頁面做一個簡單的展示操作:


  

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:each="desc : ${list}">
    <div th:utext="${desc}"></div>
</div>
</body>
</html>

上面我們搜尋的是 “南京文化”,來看下檢索出來的結果是什麼。

640

再將搜尋關鍵字改成 “南京文明”,看下命中的效果如何?

640

可以看出,不同的關鍵詞,它會計算一個得分片段,也就是說不同的關鍵字會命中同一段文字中不同位置的內容,然後將關鍵字根據我們自己設定的形式高亮顯示。

從結果中可以看出,Lucene 也可以很智慧地將關鍵字拆分命中,這在實際專案中會很好用。

作者簡介:倪升武,CSDN 部落格專家,CSDN達人課作者。碩士畢業於同濟大學,曾先後就職於 eBay、愛奇藝、華為。目前在科大訊飛從事Java領域的軟體開發,他的世界不僅只有Coding。

宣告:本文為作者投稿,版權歸其個人所有。

推薦閱讀:

640?wx_fmt=gif

640?wx_fmt=png