• 从0基础开发搜索引擎(二)------实战项目


    1.项目目标:

    我们根据JavaAPI文档,实现一个站内搜索引擎,我们会有一个网页,上面有一个搜索框,当用户输入一个查询词之后,能够得到哪些API文档和这个查询词相关,会在服务器中得到所有和查询词相关的文档,并且返回相关信息给前端页面上面,我们进行点击搜索结果,就可以跳转到对应的线上文档页面中

    2.模块划分:

    1.预处理模块:我们会把下载好的JavaAPI文档API中的html文档进行预处理,把所有API文档转化成一个行文本文件,每一行对应着一个原来的html文档,每一行中有三列,标题,URL,去掉html标签的正文,使用\3来进行分割;

    其实咱们的预处理模块的执行时间还是很长的,如果要进行性能优化,怎么进行优化呢?

    1)看看这里面有没有必要优化,必要性不高,其实我们的预处理模块操作只需要进行执行一次就够了,API文档是很稳定的,不会有太多的改变,我们的内容不变,我们的预处理模块就不会发生改变,一次性的操作也没有必要

    2)先找到性能瓶颈,给每一个步骤加上时间,看看枚举目录,转换标题,转换正文,转换url,写文件,那个过程消耗时间比较长,在上面的操作中,写文件操作和转换正文都用到了IO操作,况且没有使用缓冲流(IO操作一般比较慢)

    3)根据瓶颈分析原因(根据需求设计测试用例)

    比如说当我们的文件内容很多的时候,我们该怎么验证所有的行都是三个字段呢?

    我们的解决方法就是我们来写一个程序来进行验证,比如说我们按照文件行读取的方式来读取我们文件中的每一行,按照\3来进行分割,判断分割的结果是否是三,这也叫做自动化测试;

    2.索引模块:加载预处理模块得到的输出文件内容,在内存中制作成正排索引+倒排索引的结构,并提供一些对外的结构,正排索引是一个ArrayList,我们的DocID就是我们的数组下标,我们直接通过list.get(DocID)这个方法就可以根据DocID得到我们的文档信息了

    倒排索引通过词来得到文档信息

    是一个HashMap>,Key是分词的结果,Value是哪些文档中包含了这个词出现了几次;

    因为同一个词在不同的文档中出现的次数是不一样的,所以Weight属性里面有三个值:word:某一个词,DocID:某一个文档,Weight:权重,某个词在某个文档中相关性的权重;

    在索引模块中做完之后,我们最终会展示出两种效果:

    1)给定DocID我们可以查到对应的Document对象

    2)给定词可以查到哪些DocID和这个词相关

    3.搜索模块:

    4.展示模块:

    2.制作索引模块:根据行文本文件,创建正排索引和倒排索引

    1.我们要引入分词的第三方库,我们在这里面使用的就是ansj,我们还要安装对应的jar包

    1. <dependency>
    2. <groupId>junitgroupId>
    3. <artifactId>junitartifactId>
    4. <version>4.11version>
    5. <scope>testscope>
    6. dependency>
    7. <dependency>
    8. <groupId>org.ansjgroupId>
    9. <artifactId>ansj_segartifactId>
    10. <version>5.1.6version>
    11. dependency>

    2.为了验证它是否可以实现分词,我们写一个测试代码来进行验证;

    我们的分词库分词英文的时候会自动将我们的英文单词转化成小写,在我们的结果中,我们如果直接打印term,就会出现词性等相关信息,如果我们想要忽略这些信息,就可以直接进行打印term.getName(),这就会直接显示我们要进行分的词,但是还是小写;

    1. import org.ansj.domain.Term;
    2. import org.ansj.splitWord.analysis.ToAnalysis;
    3. import java.util.List;
    4. public class TestAll {
    5. public static void main(String[] args) {
    6. String str="我毕业于内蒙古工业大学";
    7. List list= ToAnalysis.parse(str).getTerms();
    8. for(Term term:list)
    9. {
    10. System.out.println(term.getName());
    11. }
    12. }
    13. }

    3.我们创建一个包,叫做index,创建一个类叫做OperateIndex类,我们在这个类里面进行创建正排索引和倒排索引

    构建正排索引和倒排索引:

    正排索引:读取当前的行文本数据,构建文档对象,插入到数组当中

    倒排索引:我们在进行创建倒排索引的时候,建立映射可不可以这么写呢?

    HashMap>可以吗?不太行,我们想要的是光这个词在那些文档中出想过,我们还想知道这个词在我们的每一个对应文档中的权重是多少?

    为了描述每一个词在不同的文档信息中对应的权重是多少,我们专门创建了一个类,来进行描述权重信息:

    1. //word这个词在文档中所占的权重是多少
    2. static public class Weight{
    3. public String word;
    4. public int DocID;
    5. public int weight;
    6. }
    7. 一个词会有多个Weight对象,每一个词,在每一个文章里面都对应着一个weight对象,举个例子:
    8. collection
    9. Weight对象----针对文档一:{
    10. word:weight,
    11. DocID:1
    12. weight:90
    13. }
    14. Weight对象----针对文档二:{
    15. word:weight,
    16. DocID:2
    17. weight:70
    18. }

    创建倒排索引就通过HashMap>来进行描述

    1)权重:该词和该文档之间的相关程度,相关程度越高,权重就越大,就是一个整数,就是一个数字,描述该词与文档之间的相关程度,这是和用户的需求是息息相关的;我们实际上的搜索引擎或根据查询词和文档之间的相关性进行降序排序,我们把相关性和相关程度越高的文档我们放到网页前端的位置就越靠前,这是和用户的预期是更符合的,被用户点击的概率也就越高,相关程度越低的,就排的越靠后,被用户点击的概率也就越低;

    2)相关性越强,用户点击越多,就越挣钱,我们在这里面就是用简单粗暴的方式来进行相关性的衡量,我们就看这个词在文档中的出现次数,出现次数越多,相关性就越强,但是说如果这个词在标题里面出现,我们就认为这个词的相关性比在正文里面要更强一些,我们就通过词频的大小描述相关性-------文章属性+用户的特征

    3)所以我们可以此处进行设置一个简单的公式来进行描述权重,weight=这个词在标题里面出现的次数*10+在文章正文里面出现的次数,但是相关性是一个很大的话题(文章属性+用户信息),是有专门的算法工程师来进行运算的

    在创建正排索和倒排索引之前,我们先来提供一下,这个类对外提供的接口:我们至少要写三个方法:

    1)查询我们的正排索引:根据我们的DocID来查询到我们的文档

    2)查询我们的倒排索引:倒排索引,根据我们的String类型的词,来返回我们对应的权重ArrayList(正常来说时返回一大堆DocID);

    3)构建索引:public void buildIndex{},根据我们再上一个模块写好的txtx文件,我们把它进行读取,加载到内存上面的数据结构里面,指定行文本的文件路径,我们还可以通过时间戳来进行记录一下我们创建索引过程中的执行时间

    代码框架:

    1. //返回正排索引
    2. public Document GetDocInfo(int docID){
    3. return forwordIndex.get(docID);
    4. }
    5. //返回倒排索引
    6. public List GetInverted(String key){
    7. return backwordIndex.get(key);
    8. }
    9. public void BuildIndex(String inputPath){
    10. }

    创建正排索引的过程:

    1)我们打开文件,并且读取我们的行文本文件(可以多个线程读多个文件)

    2)我们尝试读取每一行

    3)我们会构建正排索引的过程:我们读取到的每一行按照\3来进行分割,针对切分结果构建Document对象,并加入到正排索引里面(数组)

    1. 1)如果分成的String[]的长度不等于三,直接进行返回;
    2. 2)如果我们在进行构建正排索引的时候发现文件格式有问题,我们该如何进行解决呢?
    3. 我们不应该让我们的某一个问题来影响到我们的索引构建过程,而是我们直接返回,打印日志
    4. System.out.println("line");

    4)构建倒排的过程,我们要先进行遍历正排索引,针对我们的刚才构建的Document对象,Document里面的对象进行进一步处理,注意我们的Document对象里面是有一个docID,我们的这个docID就直接取我们要进行存放的数组下标

    document.setTitle(list.size());
    1. public void BuildIndex(String inputPath) throws IOException {
    2. long StartTime=System.currentTimeMillis();
    3. System.out.println("开始构建正排索引和倒排索引");
    4. //1.获得文件读取的对象
    5. BufferedReader bufferedReader=new BufferedReader(new FileReader(inputPath));
    6. //2.循环读取文件
    7. while(true){
    8. String line=bufferedReader.readLine();
    9. if(line==null){
    10. break;
    11. }
    12. //我们读取一行,分出来的三个部分就是一个文档的标题,正文,URL
    13. String[] strings=line.split("\3");
    14. if(strings.length!=3){
    15. System.out.println("当前文件格式有问题");
    16. //当前我们文件格式出问题了,我们是应该终止程序还是进行忽略呢?当前场景下有1W多个文件,如果某一个文件格式出现了问题,我们是不应该让
    17. //某一个文件的错误来进行影响到整体的索引结构
    18. continue;
    19. }
    20. Document document=new Document();
    21. document.Title=strings[0];
    22. document.url=strings[1];
    23. document.content=strings[2];
    24. document.DocID=forwordIndex.size();
    25. //我们直接把Document对象放在数组的最后一个位置
    26. forwordIndex.add(document);
    27. //构建这个文档的倒排索引
    28. CreateBackWordIndex(document);
    29. }
    30. long EndTime=System.currentTimeMillis();
    31. System.out.println("构建正派索引和倒排索引的工作完成,消耗时间"+String.valueOf(EndTime-StartTime));
    32. bufferedReader.close();
    33. }

    构建倒排索引的过程:

    1)我们现在已经知道一个Document对象,我们要根据每一个Document对象中的title和content来进行分词,再根据我们的分词结果来进行构建出Weight对象,更新倒排索引;

    2)我们先根据标题来进行分词

    3)我们进行遍历标题分词结果,统计标题中每一个词出现的次数

    1. 1)如果说这个词在哈希表中不存在查询不到:map.put(word,new WordCount(1,0));
    2. 第一个参数表示在标题中出现的次数,第二个参数表示正文中出现的次数,由于是第一次插入,第二个参数直接设置成0就可以了
    3. 2)如果这个词在原来的哈希表中出现过,我们就直接取出wordCount中的TitleCount进行++;

    4)我们再来根据正文进行分词

    5)遍历正文分词结果,统计正文中每一个词出现的次数

    (Key表示String,Value包括(标题中的出现次数,正文中的出现次数)),为了进行表示,我们创建一个类:统计一个词在标题中的出现次数和正文中的出现次数)

    6)把这两部分的结果来整理到一个HashMap里面;HashMap

    1. class WordCount{
    2. public int TitleCount;
    3. public int ContentCount;
    4. public WordCount(int titleCount, int contentCount) {
    5. TitleCount = titleCount;
    6. ContentCount = contentCount;
    7. }
    8. }

    7)遍历我们的HashMap,根据我们的WordCount对象和Key值String,依次构建我们的Weight对象并进行更新倒排索引的映射关系,我们的一个文档就对应到一个Weight对象中的一个DocID;

    8)把我们的Weight对象加入到我们的倒排索引当中,我们的倒排索引就是一个HashMap,value就是Weight构成的ArrayList,所以我们要先根据这个词,找到HashMap中的ArrayList,再把这个Weight加入到ArrayList里面

    正排索引:是一个List

     倒排索引:

    每一个String里面都对应着一个List,每一个List里面都对应着多个weight对象,下面的Value每一列,对应每一个文档的查找结果

    每当我们的查询词在一个文档中出现过,我们就在ArrayList里面追加一个weight对象

    1. private void CreateBackWordIndex(Document document) {
    2. //1.现根据文章标题来进行分词
    3. List TitleList= ToAnalysis.parse(document.Title).getTerms();
    4. //2.遍历分词结果,统计标题中每一个词出现的权重
    5. HashMap CountMap=new HashMap<>();
    6. for(Term term:TitleList){
    7. String TermData=term.getName();
    8. WordCount wordCount=CountMap.get(TermData);
    9. if(wordCount==null){
    10. //当前这个词在HashMap中不存在
    11. wordCount=new WordCount(1,0);
    12. CountMap.put(TermData,wordCount);
    13. }else{
    14. //当前这个词在哈希表中存在
    15. wordCount.TitleCount++;
    16. }
    17. }
    18. //3.再根据文章内容来进行分词
    19. List ContentList=ToAnalysis.parse(document.content).getTerms();
    20. //4.遍历分词结果,再去统计这个词在正文中出现的次数
    21. for(Term term:ContentList){
    22. String ContentData=term.getName();
    23. WordCount wordCount= CountMap.get(ContentData);
    24. if(wordCount==null){
    25. //当前这个词在哈希表中不存在
    26. wordCount=new WordCount(0,1);
    27. CountMap.put(ContentData,wordCount);
    28. }else{
    29. //当前这个词在Hash表中存在
    30. wordCount.ContentCount++;
    31. }
    32. }
    33. //5.遍历HashMap,创建Weight对象,依次构建Weight对象
    34. for(Map.Entry entry:CountMap.entrySet()){
    35. String key= entry.getKey();
    36. WordCount wordCount= entry.getValue();
    37. Weight weight=new Weight();
    38. weight.DocID=document.DocID;
    39. //标题出现次数*10+正文中出现次数
    40. //我们要将weight加入到倒排索引中,倒排索引是一个HashMap,value就是由weight组成的ArrayList,根据这个词,我们就要先找到这个词对应的
    41. //ArrayList
    42. weight.weight= wordCount.ContentCount+ wordCount.TitleCount*10;
    43. weight.word=key;
    44. ArrayList weights= (ArrayList) backwordIndex.get(key);
    45. if(weights==null){
    46. //当前键值对不存在
    47. weights=new ArrayList<>();
    48. weights.add(weight);
    49. backwordIndex.put(key,weights);
    50. }else{
    51. //到了这一步,就有一个合法的ArrayList
    52. weights.add(weight);
    53. }
    54. }
    55. }

    创建索引模块的时候出现的问题:

    1)读取行文本格式的数据的时候,转化成String格式的字符串数组之后,如果发现字符数组的长度不是3,那么说明这个行文本有问题

    2)咱们调用List TitleList= ToAnalysis.parse(document.Title).getTerms();得到的每一个Term对象里面包括了分词之后词的名字还有词的词性,我们通过term.getName()就可以得到词的具体内容了,况且里面的单词已经被转成小写了

    3)这里面的HashMap map=new HashMap<>();应该是局部变量是用来进行统计当前文档的词在这个文章中的标题和正文中出现了几次,而咱们的HashMap应该是全局变量;

    4)在来进行观察这一张图片 ,当我们在哈希表中无法查询到这个词,但是在文章标题中出现了,我们就应该给WordCount传入一个构造方法,在针对这个词进行初始化的时候,将他的这个词在文章中的出现次数也置为0(还没有遍历文章)

    1)由于索引中的词全部是小写的,那么我们进行查询的词也必须全部是小写的 

    2)现在我们进行测试一下我们的正排索引和倒排索引的构建过程,我们以arraylist为例来进行查询一下词语

    3)后来发现了错误,你前面进行文件分割的时候一定不能分割错误:

    1. Index index=new Index();
    2. //1.先进行创建索引
    3. index.BuildIndex("D:\\SelectAPI\\Result.txt");
    4. //2.开始模拟进行搜索,先找到这个词对应的权重信息,相当于先进行查找倒排索引
    5. ArrayList list= (ArrayList) index.GetInverted("arraylist");
    6. for(Weight weight:list){
    7. System.out.println(weight.DocID);
    8. System.out.println(weight.weight);
    9. System.out.println(weight.word);
    10. //3.进行查找正排索引,可以看出weight中必须含有文档ID
    11. Document document= index.GetDocInfo(weight.DocID);
    12. System.out.println(document.getTitle());
    13. System.out.println(document.getUrl());
    14. System.out.println("________________________");
    15. }

  • 相关阅读:
    linux 网络命令
    二维数组的认识和使用
    高端两轮电动车是否担得起“高端”头衔?
    【Node.js】深度解析模块化的那些事
    Excel 文件比较工具 xlCompare 11.01 Crack
    零售业迎来全新发展,这个技术少不了
    【数据分析】Python:处理缺失值的常见方法
    深度解密Go底层Map
    Machine Learning With Go 第4章:回归
    Prometheus+Grafana监控体系搭建
  • 原文地址:https://blog.csdn.net/weixin_61518137/article/details/126114212