Java文檔搜索引擎

項目詳情: https://github.com/BlackerGod/Java_search_api
成品展示: 點擊查看
(ps:受限於服務器帶寬和處理器,會導致這個有點慢。。。)

一、項目需求

當我們遇到Java內一些不熟悉的函數或者寶,我們都需要去查詢官方文檔,那麼問題就來了,我們可以看到的是官方文檔並沒有提供一個查詢接口,我們每次使用時還要知道在哪個包下,然後再去一個一個查找,費時費力。那麼我們今天就做一個項目,來實現對Java官方文檔的查詢功能;

二、項目分析

當我們需要查找的時候,必須要已經獲取到了整個頁面的資源,我們要知道每個資源都在哪裏,這塊我們可以用爬蟲實現(比較麻煩),我們先下載好整個文檔的資源,然後對這個資源做一些處理,然後從文件中查找,最終返回他的url、描述、題目等信息。

三、項目設計

1.預處理模塊:把下載好的html文檔進行一次初步的處理(簡單分析結構並且幹掉其中的html標籤)
把api目錄中的所有html進行處理 =》 得到一個單個文件。使用行文本的方式進行組織(爲了製作索引方便)

第一列:文檔標題
第二列:文檔url
第三列:文檔正文(去掉HTML標籤)

2.索引模塊:預處理得到的結果,構建正排+倒排索引
3.搜索模塊:完成一次搜索過程基本流程(從用戶輸入查詢詞,得到最終的搜索結果)
4.前端模塊:有一個頁面,展示結果並且讓用戶輸入數據

四、編碼

1.預處理模塊

我們先下載好文檔:
在這裏插入圖片描述我們只錄入api,可以看到都是一些html頁面,這其實是對應着官方的頁面的
在這裏插入圖片描述那麼我們先找完整個文件夾,並且把每個html的title、url、content(除了標籤的文段)全部保存下來。

package parser;

import java.io.*;
import java.util.ArrayList;

/**
 * 遍歷文檔目錄,讀取所有的html文檔內容,把結果解析成行文本文件
 * 每一行對應一個文檔,每一行都包含文檔信息
 * Parser是一個單獨可以執行的類(含main)
 */
public class Parser {
    private static final String INPUT_PATH = null; //下載的api路徑
    private static final String OUTPUT_PATH = null; //輸出處理文件的路徑

    /**
     * //完成預處理
     * 1.枚舉INPUT_PATH下所有html文件(遞歸)
     * 2.對html文件路徑進行遍歷,一次打開每個文件,並讀取內容
     * 3.把內容轉換成需要結構化的數據(DocInfo對象),然後寫出文件
     * @param args
     */
    public static void main(String[] args) throws IOException {
        FileWriter fileWriter = new FileWriter(new File(OUTPUT_PATH));
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH,fileList);
        for (File f : fileList){
            //System.out.println("converting" + f.getAbsolutePath() + "...");
            String line = convertLine(f);
            //System.out.println(line);
            fileWriter.write(line);
        }
        fileWriter.close();
    }

    /**
     *
     * @param f 文件
     * @return  根據文件來獲取標題.url.content;
     */
    private static String convertLine(File f) throws IOException {
        String title = convertTitle(f);
        String url = convertUrl(f);
        String content = convertContent(f);
        // \3起到分隔三個部分的效果. \3爲ascii碼爲3的字符
        return title + "\3" + url + "\3" + content + "\n";
    }

    private static String convertContent(File f) throws IOException {
    //把標籤和\n去掉
        FileReader fileReader = new FileReader(f);
        boolean isContent = true;
        StringBuilder output = new StringBuilder();
        while (true){
            int ret = fileReader.read();
            if(ret == -1){
                break;
            }
            char c = (char)ret;
            if(isContent){//是正文
                if(c == '<'){
                    isContent = false;
                    continue;
                }
                if(c == '\n' || c == '\r'){  //\n換行,\r表示回車
                    c = ' ';
                }
                output.append(c);
            } else { // 是標籤
                if(c == '>'){
                    isContent = true;
                }
            }
        }
        fileReader.close();
        return output.toString();
    }
    private static String convertUrl(File f) {
        //線上文檔對應的Url
        String prev = "https://docs.oracle.com/javase/8/docs/api";
        String text = f.getAbsolutePath().substring(INPUT_PATH.length());
        text = text.replaceAll("\\\\","/");//轉不轉換都可以的
        return prev + text;
    }
    private static String convertTitle(File f) {
        //把文件名當做標題就可以了(去掉.html)
        String name = f.getName();
        return name.substring(0,name.length() - ".html".length());
    }

    /**
     *
     * @param inputPath  當前目錄
     * @param fileList     已經保存的文件列表
     */
    private static void enumFile(String inputPath, ArrayList<File> fileList) {
        //遞歸把inputPath對應的全部目錄和文件都遍歷一遍
        File root = new File(inputPath);
        File[] files = root.listFiles(); //查看當前路徑下的所有文件(包括文件夾)
        for (File f : files){
            if(f.isDirectory()){
                enumFile(f.getAbsolutePath(),fileList);
                //遞歸向下
            } else if(f.getAbsolutePath().endsWith(".html")){
                //是否是.html,是的話就添加
                fileList.add(f);
            }
        }
    }
}

當處理完成之後,我們就得到一個文件:
在這裏插入圖片描述在這裏插入圖片描述我們獲取到了,名字,url,正文。這個過程只生成一次就行,以後只用tmp.txt了。

2.索引模塊

先清楚兩個概念:
【正排索引】:根據文章的ID去找搜索詞是否存在
【倒排索引】:根據文章中出現了搜索詞找到文章ID
然後我們又遇到一個問題,就是這個關鍵詞可能在很多文章都出現了,那我們如何得知它就是我們最需要的呢?
那我們就需要去計算一個【權重】,根據權重去排序,這裏我就簡單的寫了一下計算方法
【權重】=關鍵詞在題目中出現的次數 x 10 + 關鍵詞在正文中出現的次數x1;

package common;

public class DocInfo {

    private int docId; //文章ID不能重複
    private String title;//文檔標題,用文件名命名
    private String url;//線上URL,根據本地構造
    private String content;//html輸出標籤的內容

    public int getDocId() {
        return docId;
    }

    public void setDocId(int docId) {
        this.docId = docId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "DocInfo{" +
                "docId=" + docId +
                ", title='" + title + '\'' +
                ", url='" + url + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

然後開始把文件加到到內存中;

package index;

import common.DocInfo;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 構建索引,正排索引(ID =》 文檔),倒排索引(文檔 =》 ID)
 */
public class Index {
    /**
     * 這個靜態類是爲了計算權重。
     */
    static public class Weight{
        public String word;
        public int docId;
        public int weight;
        //weight = titleCount*10 + textCount;
    }

    // 正排索引
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 倒排索引,不僅知道在那個docId下,而且要顯示其權重
    // 權重:該詞和文檔的關聯程度
    private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();


    /**
     * 查詢正排
     * @param docId 文章ID
     * @return  文章信息
     */
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }

    /**
     * 查詢倒排
     * @param term 目標詞
     * @return 文章列表
     */
    public ArrayList<Weight> getInverted(String term){
        return invertedIndex.get(term);
    }

    /**
     * 把txt文件內容讀取出來,加載到內存上面的數據結構
     * \3分隔
     */
    public void build(String path) throws IOException {
        long startTime = System.currentTimeMillis();
        System.out.println("build start");

        // 1.打開文件,按行讀取
        BufferedReader bw = new BufferedReader(new FileReader(new File(path)));

        // 2.接收每一行
        String line = "";

        while((line = bw.readLine()) != null){
        // 3.構造正排的過程:按照 \3來切分,切分結果構造成DocInfo對象,加入數據結構
            DocInfo docInfo = buildForward(line);

        // 4.構造倒排的過程
            buidInverted(docInfo);
            System.out.println("Build" + docInfo.getTitle() + "Finished");

        }
        bw.close();
        long finishTime = System.currentTimeMillis();
        System.out.println("build finished Time" + (finishTime - startTime)+"ms");
    }


    /**
     *
     * @param line 正排就是字符串切分
     * @return 返回docInfo
     */
    private DocInfo buildForward(String line) {
        // 把一行按照\3切分
        // 分出來的三個部分就是一個文檔的 標題 url 正文;
        String[] tokens = line.split("\3");
        if(tokens.length != 3){
            // 文件格式有問題
            System.out.println("文件格式存在問題:" + line);
            return null;
        }
        DocInfo docInfo = new DocInfo();
        // id 就是正排索引下標
        docInfo.setDocId(forwardIndex.size());
        docInfo.setTitle(tokens[0]);
        docInfo.setUrl(tokens[1]);
        docInfo.setContent(tokens[2]);
        forwardIndex.add(docInfo);
        return docInfo;
    }


    private void buidInverted(DocInfo docInfo) {
    	/**
    	*計算權重的類
    	*/
        class WordCnt{
            public int titleCount;
            public int contengtCount;

            public WordCnt(int titleCount, int contengtCount) {
                this.titleCount = titleCount;
                this.contengtCount = contengtCount;
            }
        }

        HashMap<String,WordCnt> wordCntHashMap = new HashMap<>();
        // 1.對標題分詞(分詞是靠依賴實現的)
        List<Term> titleTerms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        // 2.遍歷分詞結果,統計標題中的每個詞出現頻率
        for (Term term : titleTerms){
            // 此處word已經轉成小寫了
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if(wordCnt == null){ // 不存在
                wordCntHashMap.put(word,new WordCnt(1,0));
            } else {
                wordCnt.titleCount++;
            }
        }



        // 3.針對正文分詞
        List<Term> contentTerms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        // 4.遍歷分詞結果,統計正文中詞出現的頻率
        for (Term term : contentTerms){
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if(wordCnt == null){
                wordCntHashMap.put(word,new WordCnt(0,1));
            } else {
                wordCnt.contengtCount++;
            }
        }

        // 5.遍歷HashMap,一次構建weight對象並更新倒排索引的映射關係
        for (Map.Entry<String,WordCnt> entry : wordCntHashMap.entrySet()){
            Weight weight = new Weight();
            weight.word = entry.getKey();
            weight.docId = docInfo.getDocId();
            weight.weight = entry.getValue().titleCount * 10 + entry.getValue().contengtCount;

            //weight加入到倒排索引中
            ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());
            if(invertedList == null){
                // 不存在
                invertedList = new ArrayList<>();
                invertedIndex.put(entry.getKey(),invertedList);
            }
            invertedList.add(weight);
        }

    }
}

構造倒排索引這塊有點不好理解,我還是畫個圖吧
在這裏插入圖片描述這是兩篇文章,正排索引就是根據docID去返回整篇文章內容,而倒排索引就是根據裏面的內容來進行返回文章docID
構建倒排索引的過程:
【首先】針對一個docInfo,我們創建一個在這裏插入圖片描述來記錄一篇文章中,詞分別出現的次數,當結束之後,就是像我們下圖這樣了
在這裏插入圖片描述構造結束後,我們查詢總的結構
在這裏插入圖片描述就是看看,這個詞是否已經存在,如果存在就把它的docID和weigh信息放在順序表中
在這裏插入圖片描述最終我們再根據weight中的weight來排序即可

3.搜索模塊

搜索模塊就很簡單了,我們是web應用,寫一個Serverlet調用響應的查詢方法就行
我是以json格式傳輸,使用了Gson。

package api;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import searcher.Result;
import searcher.Searcher;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

public class DocSearcherServlet extends HttpServlet {

    private Searcher searcher = new Searcher();
    private Gson gson = new GsonBuilder().create();

    public DocSearcherServlet() throws IOException {
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("application/json;charset=utf-8");
        String query = req.getParameter("query");
        if(query == null){
            resp.setStatus(404);
            resp.getWriter().write("query參數非法");
            return;
        }
        List<Result> results = searcher.search(query);
        String respString = gson.toJson(results);
        resp.getWriter().write(respString);

    }
}

還有就是web.xml的配置,我們可以看到的是,由於構建比較慢,所以第一個提交請求的用戶會等很久,之後已經都加載到內存裏了,就比較快了。爲了避免這個情況,我們就在啓動時先構建一次可以避免

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <servlet-name>DocSearcherServlet</servlet-name>
    <servlet-class>api.DocSearcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>DocSearcherServlet</servlet-name>
    <url-pattern>/search</url-pattern>
  </servlet-mapping>

</web-app>
4.前端模塊

這塊我不是很擅長,查了一些東西最終才搞定

<html>
<head>
    <!-- Bootstrap 文檔: https://v3.bootcss.com/css/ -->
    <!-- Vue 文檔: https://cn.vuejs.org/v2/guide/ -->
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

    <title>Java API 搜索</title>
    <style>
        #app {
            margin-left:50px;
            margin-right:50px;
        }
        div button {
            width:100%;
        }
        .row {
            padding-top: 10px;
        }
        .col-md-5,.col-md-1 {
            padding-left:2;
            padding-right:2;
        }
        .title {
            font-size: 22px;
        }
        .desc {
            font-size: 18px;
        }
        .url {
            font-size: 18px;
            color: green;
        }
    </style>
</head>
<body>
<div id="app">
    <div class="row">
        <img src="image/1.jpg" width="55px" height="60px" />
    </div>
    <div class="row">
        <div class="col-md-5">
            <input type="text" class="form-control" placeholder="請輸入關鍵字" v-model="query">
        </div>
        <div class="col-md-1">
            <button class="btn btn-success" v-on:click="search()">搜索</button>
        </div>
    </div>
    <div class="row" v-for="result in results">
        <!--用來存放結果-->
        <div class="title"><a v-bind:href="result.clickUrl">{{result.title}}</a></div>
        <div class="desc">{{result.Desc}}</div>
        <div class="url">{{result.ShowUrl}}</div>
    </div>
</div>
</body>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
    var vm = new Vue({
        el: "#app",
        data: {
            query: "",
            results: [ ]
        },
        methods: {
            search() {
                $.ajax({
                    url:"/JavaAPI/search?query=" + this.query,
                    type: "get",
                    context: this,
                    success: function(respData, status) {
                        this.results = respData;
                    }
                })
            },
        }
    })
</script>
</html>

前端頁面也沒有啥,主要是寫css,Javascript的話,主要是把query提交給後端處理,然後接收過來的數據,分別處理。

至此,大功告成

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章