站内搜索引擎

这篇具有很好参考价值的文章主要介绍了站内搜索引擎。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.什么是搜索引擎?

如图所示:

站内搜索引擎

我用的是谷歌浏览器,但是我的搜索引擎可以跟换 。切换到bing主页站内搜索引擎

在搜索框中我们输入一段话,跳到一个带有搜索结果的页面如下:

站内搜索引擎

站内搜索引擎

搜索引擎的核心功能:查找用户输入的词/一句话 相关联的网页。 

搜索结果页一条记录包含的信息如下:

站内搜索引擎

 当我们点击标题,会跳转到落地页,如下图:

站内搜索引擎

相信大家对搜索引擎都有一定的了解了。

2.实现搜索引擎的核心思路:

  • 首先我需要很多网页。
  • 再根据用户输入的词/一句话,在这些网页中进行查找。

搜索引擎的网页又该如何获取呢?

像百度,搜狗,这样的大型搜索引擎,每天会有很多爬虫去爬取大量网页,在存储起来。

我们这里先直接去官网下载jdk文档(之后抽取时间改进利用爬虫获取网页)。

用户输入查询词之后,如何去让查询词语和当前的这些网页进行匹配呢?

搜索引擎工作原理

利用这些网页(.html文件解析过后,生成的文档DocInfo)构建正排索引和倒排索引。

3.正排索引和倒排索引原理:

正排,倒排原理理解

1.正排索引:文档 id        ->        文档内容                        (1个id对应1个文档内容)

2.倒排索引:词                ->        和词相关联的文档id        (1个词对应一个/多个文档id)

举个例子:

//正排索引

1                        我上街买了一份炸鸡

2                        我晚饭吃了一份牛肉

//倒排索引(先分词)

我                                1,2

上街                            1

买                                1

了                                1,2

一份                            1,2

炸鸡                            1 

晚饭                            2

吃                                2

牛肉                            2

4.我要把搜索引擎做到什么程度?

 我们还是先做一个“站内搜索”,类似于哔哩哔哩里的搜索框,搜索站内的东西。我们要做一个java,jdk8文档的搜索引擎,比如输入一个ArrayList,会在页面显示一系列关于ArrayList文档的信息,通过点击标题或者utl跳转到官网jdk8文档。

5.获取文档(网页)

Java Downloads | Oracle

站内搜索引擎

先看看在线文档的内容以及url站内搜索引擎

站内搜索引擎

 https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html

 在对比看一下离线后的文档路径

站内搜索引擎

 file:///C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html

站内搜索引擎

本地链接和在线网站链接相比得出结论:

我们可以基于离线文档来制作索引,实现搜索 ,当用户在搜索结果页面点击具体的搜索结果的时候,就自动跳转到在线文档的页面。

6.模块粗略划分:

1.索引模块:

  • 扫描下载的文档,分析文档内容,构造出正排索引+倒排索引。并且把索引内容保存到文件中
  • 加载制作好的索引,并提供一些API实现查正排和查倒排这样的功能。

2.搜索模块:

  • 调用索引模块,实现一个搜索的完整过程
  • 输入:用户输入词/一句话
  • 输出:完整的搜索结果(包含了很多条记录,每个记录都有标题,描述,展示url,并且点击能够跳转)

3.web模块:

  • 需要实现一个简单的web程序,能够通过网页的形式来和用户交互~

7.创建项目

用到的工具:

idea专业版2022.2.1

webstorm专业版2022.2.1

8.介绍一下什么是分词

用户在使用时侯,往往会输入一句话,比如:

站内搜索引擎

会出现一下结果页:

站内搜索引擎

用户输入的话被分成好多个词,每个词又对应很多文档id,所以结果页面会有很多条记录,每条记录都包括标题,url,描述这三个基本信息 。

我上街买了一份炸鸡        可分词为:

上街

一份

炸鸡

我们要使用第三方库ansj来进行分词。ansj地址

说一下分词的原理,有助于编写代码。

1.基于词库(跟不上时代发展,每隔一段时间会出现新词)

  • 尝试把所有词都进行穷举~把这些穷举结果放到词典文件中。
  • 然后就可以依次的取句子中的内容,每隔一个字,在词典里查一下,每隔俩个字,查一下.....

2.基于统计(该方法更加科学,用的多)

  • 收集到很多很多的“语料库” => "人工标注/直接统计  => "也就知道了哪些字放在一起的概率比较大~";
  • 分词的实现,就是属于“人工智能”典型应用场景~

分词原理

写个测试类用一下ansj这个第三方库

站内搜索引擎

说一下爆红的原因:

在分词的时候,会加载一些词典文件,通过这些词典文件能够加快分词速度和准确率,但是没有这些词典文件ansj仍然能快速分出词。 (可配置词典文件)

我们在来看一下分词结果:

站内搜索引擎

9.实现索引模块Parser类的整体流程 

  • 指定本地jdk8文档的路径INPUT_PATH
  • Parser类作为入口,main函数调用run方法
  • run函数包含三个功能:
  • 1.根据指定路径,罗列出该文件夹下所有的.html文件,包括子文件夹中的.html的路径
  • 2.根据上边罗列的所有html文件路径,打开文件,读取文件,解析文件,构建索引(在内存中)
  • 3.把在内存中构造好的索引数据结构,保存到指定的文件中(即序列化,反序列化)

10.递归枚举文件

import java.io.File;
import java.util.ArrayList;

public class Parser {
    public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";

    public void run() {
        //1.枚举出所有的.html文件,包括子目录中的文件
        ArrayList<File> filelist = new ArrayList<>();
        enumFile(INPUT_PATH, filelist);
//        System.out.println(filelist);
//        System.out.println(filelist.size());
        //2.针对上面罗列出的文档路径,打开文件,读取文件内容,进行解析,构建索引
        //3.把在内存中构造好的索引数据结构,保存到指定的文件中
    }

    private void enumFile(String inputPath, ArrayList<File> filelist) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();
        for (File f : files) {
            if(f.isDirectory()){
                enumFile(f.getAbsolutePath(),filelist);
            }else {
                filelist.add(f);
            }
        }
    }

    public static void main(String[] args) {
        Parser parser = new Parser();
        parser.run();
    }
}

现在有一个问题,我要的是html文件,其他文件例如css,js等其他文件是我不想要它出现在结果里边,用户也看不懂,所以filelist里边要去掉这些前端文件。

站内搜索引擎

11.除去不是html的文件 

我怎么实现?

我的思路是得到所有文件的目录,它的后缀不是html的就不加到那个filelist里边

private void enumFile(String inputPath, ArrayList<File> filelist) {
        File rootPath = new File(inputPath);
        File[] files = rootPath.listFiles();
        for (File f : files) {
            if(f.isDirectory()){
                enumFile(f.getAbsolutePath(),filelist);
            }else {
                if(f.getAbsolutePath().endsWith(".html")){
                    filelist.add(f);
                }
            }
        }
    }

这里我想说的是,后续的开发中会遇到各种各样的问题,但是只要思想不滑坡,办法总比困难多。

浩大的工程,都是由一个一个小小的函数堆积起来的。好了,现在我们把所有的html文件都加载到filelist里边了,现在的问题是,我怎么根据上边的路径打开文件去解析里边的文件呢?解析,我要解析什么?(1.标题,2.url,3.描述(描述是根据正文来的,所以因该是解析正文)),我为什么要这三个东西?我要把这三个东西用一个弄一个实体类来表示,即一个DocInfo,里边有id,title,url,content,然后通过一个ArrayList<DocInfo>构建出一个正排索引,然后根据这个正排索引去构建倒排索引,目前先考虑这么多。

12.解析html

站内搜索引擎

站内搜索引擎

我们打开文件发现html的名字和文件中的title里的名字相似,但是html的名字更加获取,代码实现上更加容易,我们就以html前边的单词作为标题 。

站内搜索引擎

 站内搜索引擎

这样的话,我们解析html的标题的功能完成了。我们继续往下做。

13.解析url

站内搜索引擎

我们所期望的结果就是,用户点击搜索结果,就能够跳转到对应的线上文档的页面。

思路:

把本地路径的后半段提取出来作为后缀         java/util/ArrayList.html

以在线路径前半部分为前缀             https://docs.oracle.com/javase/8/docs/api/

拼接后就是完整的url         https://docs.oracle.com/javase/8/ docs/api/java/util/ArrayList.html

写一个测试代码:

import java.io.File;

public class TestUrl {
    public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";

    public static void main(String[] args) {
        File f = new File("C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html");
        String part1 = "https://docs.oracle.com/javase/8/docs/api/";
        String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
        System.out.println(part1+part2);
    }
}

站内搜索引擎

这里虽然是反斜杠,但是粘贴在浏览器上还能打开,体现出浏览器的鲁棒性。

我们优化一下吧,把\都替换成/.

具体方法:

站内搜索引擎

注意:

站内搜索引擎

 虽然没看懂,但是能用!接下来我们开始解析正文,先讲讲解析正文的思路:

14.解析正文

我们先打开一个html文件观察观察

站内搜索引擎

这里边有script的代码,还有css的代码style,还有html的标签

这里我们使用正则表达式去除一下这些标签

具体代码:

public String parseContent(File f) {
        try (FileReader fileReader = new FileReader(f)) {
            StringBuilder content = new StringBuilder();
            while (true) {
                int ret = fileReader.read();
                if (ret == -1) {
                    break;
                }
                char c = (char) ret;
                content.append(c);
            }
            String scriptRegex = "<script[^>]*?>[\\s\\S]*?<\\/script>";
            //定义style的正则表达式,去除style样式,防止css代码过多时只截取到css样式代码
            String styleRegex = "<style[^>]*?>[\\s\\S]*?<\\/style>";
            //定义HTML标签的正则表达式,去除标签,只提取文字内容
            String htmlRegex = "<[^>]+>";
            //定义回车,换行符,制表符
            String spaceRegex = "\t|\r|\n";

            String content_final = content.toString();
            content_final = content_final.replaceAll(scriptRegex, "");
            content_final = content_final.replaceAll(styleRegex, "");
            content_final = content_final.replaceAll(htmlRegex, "");
            content_final = content_final.replaceAll(spaceRegex,"");

            return content_final;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

效果展示:

站内搜索引擎

 嗯不错不错~(蜜汁自信)

现在的问题是,我们已经把东西解析出来了,我们现在需要一个索引类去把解析出来的东西加入到索引,然后将制作好的索引保存到指定文件中去。

15.实现索引模块

先创建一个Index类,这个类的主要实现方法:

//给定一个docId,在正排索引中,查询文档的详细信息
//给定一个词,在倒排索引中,查哪些文档和这个词相关联
//往索引中新增一个文档
//把内存中的索引结构保存到磁盘中
//把磁盘中的索引数据加载到内存中

然后创建一个DocInfo类,这个实体类来描述docId,title,url,content.

站内搜索引擎

这里有一个问题,返回值是List<Integer>行不行?用户输入一个词/一句话,我返回了文章的id难道不对吗?如过这样做的话,就体现不出文章的相关性了,排在前边的永远是docId小的文章。为了解决这样的问题,修改一下返回值。

创建一个类Weight,权重的意思,Weight里边包含docId,和weight这俩个属性,weight值越大,我们就表示,用户输入的值和文章的相关性越强,在浏览器显示的越靠前。这个Weight类就是把文档id和文档与词的相关性进行一个包裹。

站内搜索引擎

具体实现代码:

import java.util.List;

//通过这个类在内存中构造出索引
public class Index {
    //给定一个docId,在正排索引中,查询文档的详细信息
    public DocInfo getDocInfo(int docId) {
        return null;
    }

    //给定一个词,在倒排索引中,查哪些文档和这个词相关联
    public List<Weight> getInverted(String term) {
        return null;
    }

    //往索引中新增一个文档
    public void addDoc(String title, String url, String content) {

    }
    //把内存中的索引结构保存到磁盘中
    public void save(){

    }
    //把磁盘中的索引数据加载到内存中
    public void load(){
        
    }
}

我们现在还需要俩个结构表示正排索引和倒排索引:

现在在想一下,正排索引是通过一个docId返回一个DocInfo,而倒排索引可以通过一个词返回一组docId,为了让词和docId有相关性,所以进行如下编码:

private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();

现在我有一个问题,查正排,和查倒排效率高吗?时间复杂度是多少?

查正排,我们用的是ArrayList,时间复杂度是O(1)

查倒排,我们用的是HashMap,时间复杂度是O(1)

所以说,查询的很快,我们用这样的结构来保证查询的够快,够高效。

还有就是,我们现在的数据都是放在内存上的,这是查询快的第二个原因。

目前,实现的代码如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

//通过这个类在内存中构造出索引
public class Index {
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();
    //给定一个docId,在正排索引中,查询文档的详细信息
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    //给定一个词,在倒排索引中,查哪些文档和这个词相关联
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    //往索引中新增一个文档
    public void addDoc(String title, String url, String content) {
        //构建正排索引
        DocInfo docInfo = buildForward(title,url,content);
        //构建倒排索引
        buildInverted(docInfo);
    }

    private void buildInverted(DocInfo docInfo) {
        
    }

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setDocId(forwardIndex.size());
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        forwardIndex.add(docInfo);
        return docInfo;
    }

    //把内存中的索引结构保存到磁盘中
    public void save(){

    }
    //把磁盘中的索引数据加载到内存中
    public void load(){

    }
}

目前,我们新增文档的功能还没实现完,正排索引构建好了,现在实现构建倒排索引,怎么构建?

怎么做?倒排索引就是词与docId的映射,我准备,把传入的docInfo,把它的title,和content进行分词,我倒排索引用的是HashMap<String,ArrayList<Weight>>,也就是词和权重的键值对。权重里边包括docId,weight,我们又如何确定这个权重的值, (权重这个值,描述了词个文档之间的相关性,权重值越大,我们认为词和文章id相关性越强,在网页上显示的也就越靠前)。

在真实的搜索引擎中,这里的相关性,是一个非常复杂的逻辑,往往是一个专门的算法团队来进行负责的。根据文档中提取的特征,训练模型,最终借助机器学习的方式来衡量相关性。

现在我对构建倒排索引做如下规划:

1.针对文档标题进行分词

2.遍历分词结果,统计每个词出现的次数

3.针对正文进行分词

4.遍历分词结果,统计每个词出现的次数

5.把上面的结果汇总到一个HashMap里面。

6.遍历刚才这个HashMap,依次来更新倒排索引中的结构。

现在,我们想一想,标题中出现的词,和正文出现的词相比,标题出现的词是不是权重更大一些?

标题的词少,但是这里的词更能表达文章的中心思想,

 正文的词多,但是这里的词更不能表达文章的中心思想。

一句话:最终文档的权重,就设定成 : 标题中出现的次数 * 10 + 正文中出现的次数。

这里的权重公式可以改进。如何改进?

点击率 = 点击次数/展示次数

在实际开发中,比如我们的服务器,每天有一亿访问量,然后可以把这一亿访问量拆成若干份,

30%        使用A公式

30%        使用B公式

30%        使用C公式

10%        使用D公式

分别统计,这些情况的点击率如何

公式会越来越复杂,同时也会让点击效果提升

最终我们看看这几个公式哪个更好,就会留下哪个公式继续迭代。

实际的搜索引擎,这里的计算公式非常复杂,并且要持续调整,反复迭代。

画个图理解一下倒排索引的数据结构

站内搜索引擎

 具体代码实现:

private void buildInverted(DocInfo docInfo) {
        class WordCnt {
            public int titleCount;
            public int contentCount;
        }

        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();

        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCnt.titleCount += 1;
            }

        }

        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            } else {
                wordCnt.contentCount += 1;
            }

        }

        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            //倒排拉链 invertedList
            ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());

            if (invertedList == null) {
                ArrayList<Weight> newInvertedList = new ArrayList<>();
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                newInvertedList.add(weight);
                invertedIndex.put(entry.getKey(), newInvertedList);
            } else {
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                invertedList.add(weight);
            }
        }
    }

        

目前倒排索引构建好了,但这些索引都保存在内存中。而且构建索引的过程是比较耗时间的。我们的文档大概也有一万多条,addDoc这个比较耗时间。

因此,我们不因该在服务器启动的时候才构建索引(启动服务器会被拖慢很多很多)

我的想法是:

把这些耗时间的操作,单独去执行,单独执行完了之后,然后让线上服务器直接加载构造好的索引。

因此才要实现save和load的操作。

我们接下来就去把内存中的索引结构,变成一个字符串,然后写入文件,也就是序列化的过程,当我们加载文档也就是反序列化的过程。

 序列化与反序列化也有很多方法,此处我们就用json格式来进行序列化与反序列化

我们先把jackson库引入

站内搜索引擎

具体实现代码:

public void save() {
        System.out.println("保存索引开始!");
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("保存索引完成!");
    }

现在说一下json的格式:

站内搜索引擎

 当我们实现加载功能的时候,jackson库提供了TypeReference这样的类来帮助我们解决,json格式的数据转换成什么类型的数据,由于forwardIndex 的返回值类型是ArrayList<DocInfo>,所以进行以下编码:

public void load() {
        System.out.println("加载索引开始!");
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("加载索引结束!");
    }

现在我们的索引模块基本完成了,我们看看效果,在看看能否进行一下优化。

16.优化运行时间

先运行一下项目:保存索引大概是560ms这个样子,也就是save的运行时间

站内搜索引擎

 站内搜索引擎

文件大小第一个大约是70MB,第二个文件大小大约是64MB 

站内搜索引擎

站内搜索引擎

我们前边的解析html也是比较消耗时间的,我们先写个日志打印一下消耗的时间。

 public void run() {
        long begin = System.currentTimeMillis();
        System.out.println("索引制作开始!");
        //1.枚举出所有的.html文件,包括子目录中的文件
        ArrayList<File> filelist = new ArrayList<>();
        enumFile(INPUT_PATH, filelist);
        long endEnumFile = System.currentTimeMillis();
        System.out.println("枚举文件完成!消耗时间: " + (endEnumFile - begin) + "ms");
//        System.out.println(filelist);
//        System.out.println(filelist.size());
        //2.针对上面罗列出的文档路径,打开文件,读取文件内容,进行解析,构建索引
        for (File f : filelist) {
            System.out.println("开始解析:" + f.getAbsolutePath());
            parseHtml(f);
        }
        long endFor = System.currentTimeMillis();
        System.out.println("遍历文件完成!消耗时间:" + (endFor - endEnumFile) + "ms");
        //3.把在内存中构造好的索引数据结构,保存到指定的文件中
        index.save();
        long end = System.currentTimeMillis();
        System.out.println("索引制作完成!消耗时间: " + (end - begin) + " ms");
    }

站内搜索引擎

 可以清楚的看到制作索引需要消耗18s的时间,还是挺长的。我们该怎么办?

说一下性能优化:

要想优化一段程序的性能,就需要先通过测试的手段找到其中的“性能瓶颈”。现在问题主要出现在

 for (File f : filelist) {
            System.out.println("开始解析:" + f.getAbsolutePath());
            parseHtml(f);
        }

这一段代码上,也就是

每次循环都要针对一个文件进行解析,读文件+分词+解析内容(这里面主要还是卡在cpu运算上)

单个线程的情况下,这些任务,都是串行执行的,多个线程,这些任务就可以并发执行了。

我们的速度就会有较大提升。

现在我们修改一下代码,改成多线程制作索引。

什么时候会有线程安全问题?

多个线程同时修改一个对象,就会出现线程安全问题。

目前碰到一个问题,如何保证线程安全? 

线程安不安全主要在parseHtml(f);这个函数

parseTitle不涉及多个线程同时修改一个对象,

parseUrl也同样不涉及多个线程同时修改一个对象,

parseContent也不涉及多个线程同时修改一个对象,

上边三个函数都是多个线程玩各自的对象。

问题出现在addDoc这个函数,画个图理解一下。

站内搜索引擎

八个线程同时玩forwardIndex,invertedIndex这俩个对象,势必会出现线程不安全问题。

那你为啥不给addDoc加个锁,这样不就行了吗?

我们引入线程的目的是为了让串行变成并发,你给addDoc加上锁不就又成了串行执行了吗?

通俗点来说,脱裤子放屁,多费手续。

我们的目的是,串行的让它并发,实在并发不了的让它串行。让加锁的粒度在小一些。

所以如果直接把synchronized加到parseHTML或者addDoc上,这样做锁的粒度太粗了,并发程度不高,就是提高的效率有限。

现在发现,buildForward 和 buildInverted其实是在操作不同的对象。也就不存在锁竞争。

新建俩个锁对象,针对俩个锁对象进行加锁。

多线程的效果:

站内搜索引擎

 多线程的使用极大的提高了,索引制作时间。

那么线程池中的线程个数设置成几合适呢?我们只能通过实验来进行确定。

不同的代码并发程度是不一样的。

并不是线程数目越多越好,线程多确实可以提升效率,但是太多的话,提升效果就不明显了,在多加线程,白白浪费计算机资源。

目前又出现一个问题:

站内搜索引擎

 程序执行完了,进程没有关闭。

这个问题的主要原因就是:

如果要是一个线程是守护线程,此时这个线程的运行状态,不会影响到进程结束。

如果要是一个线程不是守护线程,这个线程的运行状态,就会影响到进程结束。

默认创建出来的都是非守护线程,需要通过setDaemon方法手动设置,才能成为守护线程。

通过线程池创建的线程,并不是守护线程,

当main方法执行完了,这些线程仍然在工作。(仍然在等待新任务的到来)

我们可以使用线程池提供的方法,让线程停止。

目前又碰到一个问题,首次制作索引比较慢,尤其是开机之后,首次制作索引特别慢,后面就会快了,重启机器,第一次制作就会特别慢。

通过排查parseHtml函数定位到,parseContent这个函数有问题,

计算机读取文件,是一个开销比较大的操作。

站内搜索引擎

我为什么选择atomiclong来计时间?

多线程环境下,可以简单使用AtomicXXX 使代码变得线程安全。 

atomiclong相当于加了synchronized的long。

开机第一次运行:

站内搜索引擎

第二次运行:

站内搜索引擎

可以明显看到,t1的时间变化,    

站内搜索引擎    

注意此处是八个线程累加的时间。 

(第一次开机运行,t2用了51s ,第二次运行t2用了25s,搞不懂为啥出现这种情况)

这种情况的话,我猜测是因为addDoc之前,你就得把title,url,content解析出来,parsecontent没解析完,adddoc就得等着,t2的时间前后也是2倍关系。

接下来我们可以优化一下parsecontent这个函数。

    public String parseContent(File f) {
        try (FileReader fileReader = new FileReader(f)) {
            StringBuilder content = new StringBuilder();
            while (true) {
                int ret = fileReader.read();
                if (ret == -1) {
                    break;
                }
                char c = (char) ret;
                content.append(c);
            }

我们读取文件用的是FileReader这个方法,fileReader.read()每次从文件中读取一个字符,每次read都是在读磁盘,它的速度就会很慢。

思路:我们可以先让filereader提前把文件读取到内存里面,然后每次调用一次bufferedReader.read() 就可以从内存中读取.

BufferedReader 可以搭配FileReader来使用。

BufferedReader内部就会内置一个缓冲区。就能够自动的把FileReader中的一些内容预读到内存中,从而减少直接访问磁盘的次数。

其实FileReader也有缓冲区,只是BufferedReader对缓冲支持更好。

此刻的心情:优化了个寂寞。

但是,开机第一次运行的效果肯定比没优化前好,我们开机重新试一下。

站内搜索引擎

此时用了19秒 比前开机少用了10来秒,只能说还行。

17.实现搜索模块

首先,搜索模块就是调用索引模块,来完成搜索的核心过程~

1.分词。针对用户输入的词/句进行分词

2.触发,拿着每个分词结果,去倒排索引中查,找到具有相关性的文档,

3.排序,针对上边触发出来的结果,进行排序(按照相关性,降序排序)

4.包装结果。根据排序后的结果,依次去查正排,获取到每个文档的详细信息,包装成一定的数据结构返回出去。

目前遇到一个问题,返回结果中的描述该如何返回?

我们只是得到了正文。

说一下描述包含什么?描述是正文的一段摘要,这个摘要来源于正文,同时要包含查询词或者查询词的一部分。

思路:

目前我们可以获取所有的查询词的分词结果

站内搜索引擎

遍历分词结果,看看哪个结果在正文中出现

for (Weight weight : allTermTesult) {
            DocInfo docInfo = index.getDocInfo(weight.getDocId());
            Result result = new Result();
            result.setTitle(docInfo.getTitle());
            result.setUrl(docInfo.getUrl());
            result.setDesc(GenDesc(docInfo.getContent(), terms));
            results.add(result);
        }

这里抽象出了一个方法GenDesc,根据正文和分词结果来生成描述。

针对当前文档来说,不一定包含所有的分词结果

就针对这个被包含的分词结果,去正文中查找,找到对应的位置

就以这个词的位置为中心,往前截取60个字符,作为描述的开始。

然后再从这个描述开始,截取160个字符,作为整个描述。

如果用户查询的时候输入了Hello,我们的分词结果都是小写的,所以用户不管输入什么,都会被转换成小写字母,那么正文中的大写字母也得转换成小写字母,不然就查不到。我们这里统一弄成小写的。

private String GenDesc(String content, List<Term> terms) {
        int firstPos = -1;
        for (Term term : terms) {
            String word = term.getName();
            firstPos = content.toLowerCase().indexOf(word);
            if (firstPos >= 0) {
                //找到了位置
                break;
            }
        }
    }

还有一个问题,如果用户输入的查询词是List,而正文中出现的是ArrayList,在生成描述的时候,此处拿着这个List去正文中indexOf,此时是否会把ArrayList当作结果呢?

这就会导致生成的描述,里面就是带ArrayList的,而不是带List的了

我要查List结果描述里边是ArrayList 和 List,这样设计就不科学。

类似这样的情况,在查倒排的时候,是否会存在呢?

倒排索引中的key都是分词结果,ArrayList不会被分成Array + List ,仍然把ArrayList视为是一个单词。List和ArrayList并不能匹配,使用List这个词不能查出包含ArrayList的结果。这是科学的

因此我们希望,在生成描述的过程中能够找到整个词都匹配的结果,才算是找到了。而不是只找到词的一部分。

如何让word独立成词?而不是只作为正文词的一部分。

站内搜索引擎

我的想法是直接在word的前后加个空格,让这个词独立出来。

站内搜索引擎

 仅仅是这么一处代码,却需要思考这么多的东西。

验证一下docsearcher类

站内搜索引擎

 站内搜索引擎

结果符合我们的预期要求。 

18.实现web模块

提供一个web接口,最终以网页的形式,把我们的程序呈现给用户。

前端(html+css+js)后端(servlet/spring)先用servlet实现,在升级成spring

现在约定一下前后端的通信接口。

请求:

GET/searcher?query=[查询词] HTTP/1.1

响应:(json格式的数据)

Http/1.1 200 ok

[

        {

                title:"这是标题1",

                url:"这是url1",

                desc:"这是描述1",

        },

        {     

                title:"这是标题2",

                url:"这是url2",

                desc:"这是描述2",

        },

]

具体实现:

package api;

import com.fasterxml.jackson.databind.ObjectMapper;
import searcher.DocSearcher;
import searcher.Result;

import javax.servlet.ServletException;
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 {
    //全局唯一,用static修饰一下
    private static DocSearcher docSearcher = new DocSearcher();
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //拿到用户提交的查询词
        String query = req.getParameter("query");
        if (query == null || query.equals("")) {
            String msg = "您的参数非法,没有获取到query的值!";
            System.out.println(msg);
            resp.sendError(404, msg);
            return;
        }

        System.out.println("query=" + query);

        List<Result> results = docSearcher.search(query);

        //把结果进行打包
        resp.setContentType("application/json;charset=utf-8");

        objectMapper.writeValue(resp.getWriter(),results);

    }
}

验证一下效果:

站内搜索引擎

后端这边我们已经做好了,我们接下来做一个好看点的前端页面。

19.前端页面的制作 

我们利用ajax进行前后端交互,

当用户点击搜索按钮的时候,浏览器就会获取到搜索框的内容,基于ajax构造HTTP请求,然后发给搜索服务器。浏览器获取到搜索结果之后,在根据结果的json数据,把页面生成出来。

目前遇到一个巨坑,呜呜呜~调试了2个小时终于让我给逮住了。

站内搜索引擎

ajax构造请求的路径没有理清楚,造成时间上的浪费。

站内搜索引擎

我把index. html放在了html这个文件夹中,所以ajax构造请求url的路径得修改一下,

站内搜索引擎

 如果我放在webapp下,就不用加../了

我是看了这个博客解决的:

ajax关于url路径问题

目前又遇到一个问题:

站内搜索引擎

内容太多,超出了一个屏幕,该怎么解决?

可以通过修改css代码,

你可以定位到代码问题出现在哪里吗?

最大的div类名是container.,就让在这个div内部滚动。

站内搜索引擎

又遇到一个问题:

第一次搜索和第二次搜索结果会累加到一起

站内搜索引擎

 站内搜索引擎

出现这个问题的原因是啥?

 每次点击按钮,都是把结果往.result里面进行追加,没有清理过内容

更科学的方法,应该是在每次搜索之前,都把之前的旧的结果给清空掉。

现在我们在实现一下标红逻辑。

1.修改后端代码,生成搜索结果的时候,就需要把其中包含查询词的部分,给加上一个标记。

例如,给这个部分套上一层<i>标签~

2.然后在前端这里针对<i>标签设置样式,然后浏览器就可以根据<i>标签的样式来进行显示了,

(比如给<i>标签的字体颜色设置为红色即可)

具体实现:

private String GenDesc(String content, List<Term> terms) {
        int firstPos = -1;
        for (Term term : terms) {
            String word = term.getName();
            firstPos = content.toLowerCase().indexOf(" " + word + " ");
            if (firstPos >= 0) {
                //找到了位置
                break;
            }
        }

        if (firstPos == -1) {
            //所有的分词结果都不在正文中存在 我们返回空的描述就不合适,所以我们返回正文的前160个字符
            return content.substring(0, 160) + "...";
        }

        //
        String desc = "";
        int descBeg = firstPos < 60 ? 0 : firstPos - 60;
        if (descBeg + 160 > content.length()) {
            desc = content.substring(descBeg);//从起始位置截到末尾
        } else {
            desc = content.substring(descBeg, descBeg + 160) + "...";
        }

        for (Term term : terms) {
            String word = term.getName();
            //此处进行全字匹配,也就是当查询词为list的时候不能把Arraylist中的List给单独标红。(?i) 不区分大小写替换
            desc = desc.replaceAll("(?i) " + word + " ", "<i> " + word + " </i>");
        }
        return desc;
    }

站内搜索引擎

现在测试一下更加复杂的情况:

站内搜索引擎

站内搜索引擎

 俩次的搜索结果一样,

站内搜索引擎

服务器500,原因是代码执行过程中抛出异常了。

就需要找到 

站内搜索引擎

 站内搜索引擎

 数组越界异常:

排查发现如果正文没有160个字符,还要截取160个字符必然会导致错误。

站内搜索引擎

改正:

站内搜索引擎

站内搜索引擎

站内搜索引擎

又出现一个问题:

下边的itemDiv都没有标红的字 

 我想是因为分词把空格也看成一个词了,导致代码会拿空格去查找倒排索引,我们想个办法把空格去掉。

这里用停用词表,修改一下代码。

站内搜索引擎

思路:

让搜索程序加载这个停用词表,到内存中,

使用一个HashSet把这些词都存起来,

在针对分词结果,在停用词中进行筛选,

如果某个结果在词表中存在,就直接去掉。

验证结果:

站内搜索引擎

符合预期要求。 

又发现一个bug

站内搜索引擎

还是有不标红的情况,出现这种情况的原因

站内搜索引擎

我点进去一个不标红的文档,搜索关键字,结果发现该文档确实存在这个关键字,哪里出现了问题?

站内搜索引擎

前边后边都有空格才去匹配这个firstPos,那要是没有空格就匹配不到了,关键是全文就只有这一个字 ,所以firstPos直接返回的是-1,我们要解决这里的问题,就得使用正则表达式。

站内搜索引擎

站内搜索引擎

站内搜索引擎

站内搜索引擎

成功解决bug。

目前又发现一个问题:

站内搜索引擎

站内搜索引擎

站内搜索引擎

1576 + 1350  = 2926 = 2926

站内搜索引擎

同一个文档即出现了array 又出现了list,也就是说,ArrayList出现了俩次,重复了。

前面算权重,针对分词结果依次进行促发,

array => 触发一组docId

list => 触发一组docId 

同时包含多个分词结果,意味着这个文档的相关性更高,

像这种情况,就应该提高这个文档的权重,既然相关性更高,提高权重之后就能排的更靠前。

把权重进行相加。把多个分词结果触发的文档,按照docId进行去重,同时进行权重合并。

去重思路: 

合并俩个有序列表,合并俩个数组。

先把分词结果进行一个排序处理(按照docId升序排序)在进行合并,合并的时候就可以根据docId值相同的情况,做权重相加。

可是用户输入的仅仅就是俩个词吗?

所以应该是N个数组合并。

对比多个数组值的大小关系,找到小的,插入到结果中。

站内搜索引擎

谁对应的值越小,就把这个值取出来,插入到结果数组中 ,同时针对这个下标进行++。

我们这里使用堆/优先级队列 

站内搜索引擎

end.扩散一下思维,那么一些小型网站的搜索引擎我们是否就会做了呢?

我们是否还能通过es来做一下?

爬虫能否实现一下?文章来源地址https://www.toymoban.com/news/detail-410636.html

到了这里,关于站内搜索引擎的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [C++项目] Boost文档 站内搜索引擎(4): 搜索的相关接口的实现、线程安全的单例index接口、cppjieba分词库的使用、综合调试...

    有关 Boost 文档搜索引擎的项目的前三篇文章, 已经分别介绍分析了: 项目背景: 🫦[C++项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍… 文档解析、处理模块 parser 的实现: 🫦[C++项目] Boost文档 站内搜索引擎(2): 文档文本解析模块parser的实现、如何对

    2024年02月14日
    浏览(53)
  • [C++项目] Boost文档 站内搜索引擎(3): 建立文档及其关键字的正排 倒排索引、jieba库的安装与使用...

    之前的两篇文章: 第一篇文章介绍了本项目的背景, 获取了 Boost 库文档 🫦[C++项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍… 第二篇文章 分析实现了 parser 模块. 此模块的作用是 对所有文档 html 文件, 进行清理并汇总 🫦[C++项目] Boost文档 站内搜

    2024年02月07日
    浏览(58)
  • [C++项目] Boost文档 站内搜索引擎(2): 文档文本解析模块parser的实现、如何对文档文件去标签、如何获取文档标题...

    在上一篇文章中, 已经从 Boost 官网获取了 Boost 库的源码. 相关文章: 🫦[C++项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍… 接下来就要编写代码了. 不过还需要做一些准备工作. 创建项目目录 所有的项目文件肯定要在一个目录下, 找一个位置执行下

    2024年02月14日
    浏览(46)
  • 什么是搜索引擎?2023 年搜索引擎如何运作?

    在当今的数字时代,搜索引擎已经成为人们获取信息的主要途径之一。然而,你是否知道搜索引擎是如何工作的,以及它们为什么如此重要? 搜索引擎是一种计算机程序,通过互联网或企业内部网络检索信息。用户输入或短语后,搜索引擎会扫描网络上的网页、文件、

    2024年02月16日
    浏览(52)
  • Elasticsearch:什么是搜索引擎?

    搜索引擎是一种软件程序或系统,旨在帮助用户查找存储在互联网或特定数据库中的信息。 搜索引擎的工作原理是对各种来源的内容进行索引和编目,然后根据用户的搜索查询向用户提供相关结果列表。 搜索引擎对于希望快速有效地查找特定信息的用户来说是有用的工具。

    2024年02月21日
    浏览(43)
  • 除了百度还有什么搜索引擎比较好

    百度是国内最大的搜索引擎,也是国内最好的搜索引擎。但是由于其广告比较多,有些同学不太喜欢使用百度搜索。那么除了百度还有什么搜索引擎比较好?小编就来和大家分享几款国内可以使用的其他搜索引擎。 1.谷歌搜索,谷歌搜索是全球最受欢迎的搜索引擎,在国内无法

    2024年02月11日
    浏览(50)
  • 为什么选择elasticsearch分布式搜索引擎

    elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容 例如: 在CSDN上搜索代码 在电商网站搜索商品 在百度搜索答案 elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监

    2024年02月12日
    浏览(54)
  • 【迅搜02】究竟什么是搜索引擎?正式介绍XunSearch

    啥?还要单独讲一下啥是搜索引擎?不就是百度、Google嘛,这玩意天天用,还轮的到你来说? 额,好吧,虽然大家天天都在用,但是我发现,其实很多人对搜索引擎并不是特别了解,更别提搜索引擎开发了。不信?那么咱们就一步一步来看。 上来先官方,看看百科中对搜索

    2024年02月05日
    浏览(39)
  • 什么是搜索引擎(SEO)爬虫&它们是如何工作的?

    你的网站上有蜘蛛🕷️。别抓狂!我说的不是真正的八条腿的蜘蛛🕷️。 我指的是搜索引擎优化爬虫。他们是实现SEO的机器人。每个主要的搜索引擎都使用爬虫来对可感知的互联网进行分类。正是通过这些爬虫(有时被称为爬行爬虫或爬行器)的工作,你的网站才在谷歌、

    2024年04月09日
    浏览(60)
  • 入门ElasticSearch :为什么选择ES作为搜索引擎?

    随着数据量的不断增长,搜索和分析大规模数据集变得越来越重要。传统数据库在面对这种需求时往往表现不佳,这时候就需要一种专门用于搜索和分析的引擎。ElasticSearch (简称ES)就是这样一款强大的搜索引擎,它具有许多优势,使得它成为许多企业和开发者的首选。 简

    2024年02月09日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包