SSM(二)Lucene全文檢索

前言

大家平時(shí)肯定都有用過全文檢索工具,最常用的百度谷歌就是其中的典型萨惑。如果自己能夠做一個(gè)那是不是想想就逼格滿滿呢更舞。Apache就為我們提供了這樣一個(gè)框架,以下就是在實(shí)際開發(fā)中加入Lucene的一個(gè)小Demo拙吉。


獲取Maven依賴

首先看一下實(shí)際運(yùn)行的效果圖:



這個(gè)項(xiàng)目是基于之前使用IDEA搭建的SSM的基礎(chǔ)上進(jìn)行增加的潮孽,建議小白先看下一我。上一篇博客筷黔,以及共享在Github上的源碼往史。
以下是Lucene所需要的依賴:

<!--加入lucene-->
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>${lucene.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>${lucene.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <!--lucene中文分詞-->
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-smartcn -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>${lucene.version}</version>
        </dependency>

        <!--lucene高亮-->
        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-highlighter -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>${lucene.version}</version>
        </dependency>

具體的用途我都寫有注釋。
在IDEA中修改了Pom.xml文件之后只需要點(diǎn)擊如圖所示的按鈕即可重新獲取依賴:



編寫Lucene工具類

這個(gè)工具類中的具體代碼我就不單獨(dú)提出來說了佛舱,每個(gè)關(guān)鍵的地方我都寫有注釋椎例,不清楚的再討論。

package com.crossoverJie.lucene;

import com.crossoverJie.pojo.User;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

import java.io.StringReader;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import com.crossoverJie.util.*;

/**
 * 博客索引類
 * @author Administrator
 *
 */
public class LuceneIndex {

    private Directory dir=null;

    /**
     * 獲取IndexWriter實(shí)例
     * @return
     * @throws Exception
     */
    private IndexWriter getWriter()throws Exception{
        /**
         * 生成的索引我放在了C盤请祖,可以根據(jù)自己的需要放在具體位置
         */
        dir= FSDirectory.open(Paths.get("C://lucene"));
        SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer();
        IndexWriterConfig iwc=new IndexWriterConfig(analyzer);
        IndexWriter writer=new IndexWriter(dir, iwc);
        return writer;
    }

    /**
     * 添加博客索引
     * @param user
     */
    public void addIndex(User user)throws Exception{
        IndexWriter writer=getWriter();
        Document doc=new Document();
        doc.add(new StringField("id",String.valueOf(user.getUserId()), Field.Store.YES));
        /**
         * yes是會(huì)將數(shù)據(jù)存進(jìn)索引订歪,如果查詢結(jié)果中需要將記錄顯示出來就要存進(jìn)去,如果查詢結(jié)果
         * 只是顯示標(biāo)題之類的就可以不用存肆捕,而且內(nèi)容過長(zhǎng)不建議存進(jìn)去
         * 使用TextField類是可以用于查詢的刷晋。
         */
        doc.add(new TextField("username", user.getUsername(), Field.Store.YES));
        doc.add(new TextField("description",user.getDescription(), Field.Store.YES));
        writer.addDocument(doc);
        writer.close();
    }

    /**
     * 更新博客索引
     * @param user
     * @throws Exception
     */
    public void updateIndex(User user)throws Exception{
        IndexWriter writer=getWriter();
        Document doc=new Document();
        doc.add(new StringField("id",String.valueOf(user.getUserId()), Field.Store.YES));
        doc.add(new TextField("username", user.getUsername(), Field.Store.YES));
        doc.add(new TextField("description",user.getDescription(), Field.Store.YES));
        writer.updateDocument(new Term("id", String.valueOf(user.getUserId())), doc);
        writer.close();
    }

    /**
     * 刪除指定博客的索引
     * @param userId
     * @throws Exception
     */
    public void deleteIndex(String userId)throws Exception{
        IndexWriter writer=getWriter();
        writer.deleteDocuments(new Term("id", userId));
        writer.forceMergeDeletes(); // 強(qiáng)制刪除
        writer.commit();
        writer.close();
    }

    /**
     * 查詢用戶
     * @param q 查詢關(guān)鍵字
     * @return
     * @throws Exception
     */
    public List<User> searchBlog(String q)throws Exception{
        /**
         * 注意的是查詢索引的位置得是存放索引的位置,不然會(huì)找不到。
         */
        dir= FSDirectory.open(Paths.get("C://lucene"));
        IndexReader reader = DirectoryReader.open(dir);
        IndexSearcher is=new IndexSearcher(reader);
        BooleanQuery.Builder booleanQuery = new BooleanQuery.Builder();
        SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer();
        /**
         * username和description就是我們需要進(jìn)行查找的兩個(gè)字段
         * 同時(shí)在存放索引的時(shí)候要使用TextField類進(jìn)行存放眼虱。
         */
        QueryParser parser=new QueryParser("username",analyzer);
        Query query=parser.parse(q);
        QueryParser parser2=new QueryParser("description",analyzer);
        Query query2=parser2.parse(q);
        booleanQuery.add(query, BooleanClause.Occur.SHOULD);
        booleanQuery.add(query2, BooleanClause.Occur.SHOULD);
        TopDocs hits=is.search(booleanQuery.build(), 100);
        QueryScorer scorer=new QueryScorer(query);
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
        /**
         * 這里可以根據(jù)自己的需要來自定義查找關(guān)鍵字高亮?xí)r的樣式喻奥。
         */
        SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color='red'>","</font></b>");
        Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer);
        highlighter.setTextFragmenter(fragmenter);
        List<User> userList=new LinkedList<User>();
        for(ScoreDoc scoreDoc:hits.scoreDocs){
            Document doc=is.doc(scoreDoc.doc);
            User user=new User();
            user.setUserId(Integer.parseInt(doc.get(("id"))));
            user.setDescription(doc.get(("description")));
            String username=doc.get("username");
            String description=doc.get("description");
            if(username!=null){
                TokenStream tokenStream = analyzer.tokenStream("username", new StringReader(username));
                String husername=highlighter.getBestFragment(tokenStream, username);
                if(StringUtil.isEmpty(husername)){
                    user.setUsername(username);
                }else{
                    user.setUsername(husername);
                }
            }
            if(description!=null){
                TokenStream tokenStream = analyzer.tokenStream("description", new StringReader(description));
                String hContent=highlighter.getBestFragment(tokenStream, description);
                if(StringUtil.isEmpty(hContent)){
                    if(description.length()<=200){
                        user.setDescription(description);
                    }else{
                        user.setDescription(description.substring(0, 200));
                    }
                }else{
                    user.setDescription(hContent);
                }
            }
            userList.add(user);
        }
        return userList;
    }
}

查詢Controller的編寫

接下來是查詢Controller:

    @RequestMapping("/q")
    public String search(@RequestParam(value = "q", required = false,defaultValue = "") String q,
                         @RequestParam(value = "page", required = false, defaultValue = "1") String page,
                         Model model,
                         HttpServletRequest request) throws Exception {
        LuceneIndex luceneIndex = new LuceneIndex() ;
        List<User> userList = luceneIndex.searchBlog(q);
        /**
         * 關(guān)于查詢之后的分頁我采用的是每次分頁發(fā)起的請(qǐng)求都是將所有的數(shù)據(jù)查詢出來,
         * 具體是第幾頁再截取對(duì)應(yīng)頁數(shù)的數(shù)據(jù)捏悬,典型的拿空間換時(shí)間的做法撞蚕,如果各位有什么
         * 高招歡迎受教。
         */
        Integer toIndex = userList.size() >= Integer.parseInt(page) * 5 ? Integer.parseInt(page) * 5 : userList.size();
        List<User> newList = userList.subList((Integer.parseInt(page) - 1) * 5, toIndex);
        model.addAttribute("userList",newList) ;
        String s = this.genUpAndDownPageCode(Integer.parseInt(page), userList.size(), q, 5, request.getServletContext().
                getContextPath());
        model.addAttribute("pageHtml",s) ;
        model.addAttribute("q",q) ;
        model.addAttribute("resultTotal",userList.size()) ;
        model.addAttribute("pageTitle","搜索關(guān)鍵字'" + q + "'結(jié)果頁面") ;

        return "queryResult";
    }

其中有用到一個(gè)genUpAndDownPageCode()方法來生成分頁的Html代碼过牙,如下:

    /**
     * 查詢之后的分頁
     * @param page
     * @param totalNum
     * @param q
     * @param pageSize
     * @param projectContext
     * @return
     */
    private String genUpAndDownPageCode(int page,Integer totalNum,String q,Integer pageSize,String projectContext){
        long totalPage=totalNum%pageSize==0?totalNum/pageSize:totalNum/pageSize+1;
        StringBuffer pageCode=new StringBuffer();
        if(totalPage==0){
            return "";
        }else{
            pageCode.append("<nav>");
            pageCode.append("<ul class='pager' >");
            if(page>1){
                pageCode.append("<li><a href='"+projectContext+"/q?page="+(page-1)+"&q="+q+"'>上一頁</a></li>");
            }else{
                pageCode.append("<li class='disabled'><a href='#'>上一頁</a></li>");
            }
            if(page<totalPage){
                pageCode.append("<li><a href='"+projectContext+"/q?page="+(page+1)+"&q="+q+"'>下一頁</a></li>");
            }else{
                pageCode.append("<li class='disabled'><a href='#'>下一頁</a></li>");
            }
            pageCode.append("</ul>");
            pageCode.append("</nav>");
        }
        return pageCode.toString();
    }

代碼比較簡(jiǎn)單诈豌,就是根據(jù)的頁數(shù)、總頁數(shù)來生成分頁代碼抒和,對(duì)了我前端采用的是現(xiàn)在流行的Bootstrap矫渔,這個(gè)有不會(huì)的可以去他官網(wǎng)看看,比較簡(jiǎn)單易上手摧莽。接下來只需要編寫顯示界面就大功告成了庙洼。


顯示界面

我只貼關(guān)鍵代碼,具體的可以去Github上查看镊辕。

<c:choose>
                    <c:when test="${userList.size()==0 }">
                        <div align="center" style="padding-top: 20px"><font color="red">${q}</font>未查詢到結(jié)果油够,請(qǐng)換個(gè)關(guān)鍵字試試!</div>
                    </c:when>
                    <c:otherwise>
                        <div align="center" style="padding-top: 20px">
                            查詢<font color="red">${q}</font>關(guān)鍵字征懈,約${resultTotal}條記錄石咬!
                        </div>
                        <c:forEach var="u" items="${userList }" varStatus="status">
                            <div class="panel-heading ">

                                <div class="row">
                                    <div class="col-md-6">
                                        <div class="row">
                                            <div class="col-md-12">
                                                <b>
                                                    <a href="<%=path %>/user/showUser/${u.userId}">${u.username}</a>
                                                </b>
                                                <br/>
                                                    ${u.description}
                                            </div>
                                        </div>
                                    </div>
                                    <div class="col-md-4 col-md-offset-2">
                                        <p class="text-muted text-right">
                                                ${u.password}
                                        </p>
                                    </div>
                                </div>
                            </div>
                            <div class="panel-footer">
                                <p class="text-right">
                            <span class="label label-default">
                            <span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
                             ${u.password}
                            </span>
                                </p>
                            </div>
                        </c:forEach>
                    </c:otherwise>
                </c:choose>

利用JSTL標(biāo)簽即可將數(shù)據(jù)循環(huán)展示出來,關(guān)鍵字就不需要單獨(dú)做處理了卖哎,在后臺(tái)查詢的時(shí)候已經(jīng)做了修改了鬼悠。


總結(jié)

關(guān)于全文檢索的框架不止Lucene還有solr,具體誰好有什么區(qū)別我也不太清楚亏娜,準(zhǔn)備下來花點(diǎn)時(shí)間研究下焕窝。哦對(duì)了,最近又有點(diǎn)想做Android開發(fā)了维贺,感覺做點(diǎn)東西能夠?qū)崒?shí)在在的摸得到逼格確實(shí)要高些(現(xiàn)在主要在做后端開發(fā))它掂,感興趣的朋友可以關(guān)注下。哦對(duì)了溯泣,直接運(yùn)行我代碼的朋友要下注意:

  • 首先要將數(shù)據(jù)庫倒到自己的MySQL上
  • 之后在首次運(yùn)行的時(shí)候需要點(diǎn)擊
    重新生成索引按鈕生成一遍索引之后才能進(jìn)行搜索虐秋,因?yàn)楝F(xiàn)在的數(shù)據(jù)是直接存到數(shù)據(jù)庫中的,并沒有在新增的時(shí)候就增加索引垃沦,在實(shí)際開發(fā)的時(shí)候需要在新增數(shù)據(jù)那里再生成一份索引客给,就直接調(diào)用LuceneIndex類中的addIndex方法傳入實(shí)體即可,再做更新栏尚、刪除操作的時(shí)候也同樣需要對(duì)索引做操作起愈。

個(gè)人博客地址:http://crossoverjie.top只恨。
GitHub地址:https://github.com/crossoverJie

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抬虽,一起剝皮案震驚了整個(gè)濱河市官觅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阐污,老刑警劉巖休涤,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異笛辟,居然都是意外死亡功氨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門手幢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捷凄,“玉大人,你說我怎么就攤上這事围来《宓樱” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵监透,是天一觀的道長(zhǎng)桶错。 經(jīng)常有香客問我,道長(zhǎng)胀蛮,這世上最難降的妖魔是什么院刁? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮粪狼,結(jié)果婚禮上退腥,老公的妹妹穿的比我還像新娘。我一直安慰自己鸳玩,他們只是感情好阅虫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著不跟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪米碰。 梳的紋絲不亂的頭發(fā)上窝革,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音吕座,去河邊找鬼虐译。 笑死,一個(gè)胖子當(dāng)著我的面吹牛吴趴,可吹牛的內(nèi)容都是我干的漆诽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼厢拭!你這毒婦竟也來了兰英?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤供鸠,失蹤者是張志新(化名)和其女友劉穎畦贸,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楞捂,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薄坏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寨闹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胶坠。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖繁堡,靈堂內(nèi)的尸體忽然破棺而出涵但,到底是詐尸還是另有隱情,我是刑警寧澤帖蔓,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布矮瘟,位于F島的核電站,受9級(jí)特大地震影響塑娇,放射性物質(zhì)發(fā)生泄漏澈侠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一埋酬、第九天 我趴在偏房一處隱蔽的房頂上張望哨啃。 院中可真熱鬧,春花似錦写妥、人聲如沸拳球。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祝峻。三九已至,卻和暖如春扎筒,著一層夾襖步出監(jiān)牢的瞬間莱找,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工嗜桌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奥溺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓骨宠,卻偏偏與公主長(zhǎng)得像浮定,于是被迫代替她去往敵國(guó)和親相满。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,515評(píng)論 25 707
  • 今天只有五十多公里的路程桦卒,到達(dá)毛埡大草原禾尼鄉(xiāng)品嘗所波大叔的牛肉燉土豆立美,難度系數(shù)兩顆星。就在昨天下午我們的隊(duì)伍又結(jié)...
    風(fēng)塵垢閱讀 346評(píng)論 0 1
  • 彩泥柔軟好塑型闸盔,今天給大家展示用彩泥做卡通小動(dòng)物哦悯辙,背后粘上磁鐵,可以放在冰箱上面迎吵,可愛*^o^* 今天給大家展示...
    多恩美術(shù)工作室閱讀 6,569評(píng)論 3 7