调用索引模块,来完成搜索的核心过程
简化版本的逻辑:
public List<Result> search(String query){}
这个方法用来完成整个搜索的过程。
// 通过这个类,来完成整个的搜索过程
public class DocSearcher {
// 此处要加上索引对象的实例
// 同时要完成索引加载的工作(这样才能将文件里面的索引加到内存中,不然内存中没有东西查)
private Index index = new Index();
public DocSearcher() {
index.load();
}
// 完成整个搜索过程的方法
// 参数(输入部分)就是用户给出的查询词
// 返回值(输出部分)就是搜索结果的集合
public List<Result> search(String query){
// 1. [分词] 针对 query 这个查询词进行分词
// 2. [触发] 针对分词结果来查倒排
// 3. [排序] 针对触发的结果按照权重降序排序
// 4. [包装结果] 针对排序的结果,去查正排,构造出要返回的数据
return null;
}
}
index
加载到内存中即可针对 query
这个查询词进行分词
List<Term> terms = ToAnalysis.parse(query).getTerms();
针对分词结果来查倒排
List<Weight> allTermResult = new ArrayList<>();
for(Term term : terms) {
String word = term.getName();
// 虽然倒排索引中有很多的词,但是这里的词一定都是之前的文档中存在的
List<Weight> invertedList = index.getInverted(word);
if(invertedList == null) {
// 说明这个词在所有文档中都不存在
continue;
}
// 对我们的每一个倒排拉链进行汇总
allTermResult.addAll(invertedList);
}
Terms
,提出每一个词的名字,然后去查倒排 index
里面的查倒排的方法 getInverted
方法即可(这里是直接返回 term
所对应的 value
,若不存在,就返回 null
)allTermResult
中,进行汇总针对触发的结果按照权重降序排序。此时待排序的对象是 alltermResult
allTermResult.sort(new Comparator<Weight>() {
@Override
public int compare(Weight o1, Weight o2) {
return o2.getWeight() - o1.getWeight();
}
});
sort
比较时,由于比较对象不清楚,比较规则不知道,所以我们需要制定一个比较规则Comparator
接口的类,再去重写里面的方法,最后再去 new
出实例 return o1.getWeight() - o2.getWeight();
return o2.getWeight() - o1.getWeight();
针对排序的结果,去查正排,构造出要返回的数据
List<Result> results = new ArrayList<>();
for(Weight weight : allTermResult){
DocInfo docInfo = index.getDocInfo(weight.getDocId());
Result result = new Result();
result.setTitle(docInfo.getTitle());
result.setUrl(docInfo.getUrl());
results.add(result);
}
weight
weight
里面的 DocId
,去查找文档 docInfo
docInfo
里面的 title
和 url
信息都设置到 result
里面(content
部分我们只需要一部分,所以不能直接通过 getContent
获得)result
中result
添加到链表 results
中即可构造结果的时候,需要生成“描述”
生成描述的思路: 我们可以获取到所有的查询词的分词结果。
针对当前这个文档来说,不一定会包含所有分词结果。只要包含其中一个就能被触发出来
// 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,
// 而不是只作为词的一部分(左右加空格)
firstPos = content.toLowerCase().indexOf(" " + word + " ");
word
是通过分词结果得来的,在进行分词的时候,分词库就自动地将 word
转换成小写了int firstPos = -1;
// 先遍历分词结果,看看哪个结果是在 content 中存在
for(Term term : terms) {
// 分词库直接针对词进行转小写了
// 正因如此,就必须把正文也先转成小写,然后再查询
String word = term.getName();
// 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,而不是只作为词的一部分(左右加空格)
firstPos = content.toLowerCase().indexOf(" " + word + " ");
if(firstPos >= 0){
// 找到了位置
break;
}
}
List
ArrayList
indexOf
,此时是否会把 ArrayList 当做结果呢?(肯定会) ArrayList
的,而不是带 List
的(不科学的)key
都是分词结果,ArrayList
不会被分成 Array + List
,就仍然会吧 ArrayList
视为是一个单词,所以 List
和 ArrayList
不能匹配,因此 List
这个词不能查出包含 ArrayList
的结果(科学的)因此我们希望在生成描述过程中,能够找到整个词都匹配的结果,才算是找到了,而不是知道到词的一部分
// 所有的分词结果都不在正文中存在(标题中触发)
if(firstPos == -1) {
// 此时就直接返回一个空的描述,或者也可以直接取正文的前 160 个字符
// return null;
return content.substring(0, 160) + "...";
}
// 从 firstPos 作为基准位置,往前找 60 个字符,作为描述的起始位置
String desc = "";
int descBeg = firstPos < 60 ? 0 : firstPos - 60; // 不足 60 个字符,就直接从 0 开始读
if(descBeg + 160 > content.length()) {
desc = content.substring(descBeg); // 从 descBeg 截取到末尾
}else {
desc = content.substring(descBeg, descBeg + 160) + "...";
}
return desc;
firstPos
还是 -1
的时候,就是分词结果未找到,我们可以直接返回 null
或者正文前 160
个字符firstPos
不是 -1
的时候,就是找到分词了 firstPos < 60
,则 descBeg
置为 0
;若 firstPos > 60
,则 descBeg
置为 firstPos - 60
descBeg
的长度大于正文的长度了,则直接在正文中从 descBeg
的位置截取到文末;若没有,则从 descBeg
的位置往后截取 160
个字符desc
即可private String GenDesc(String content, List<Term> terms) {
int firstPos = -1;
// 先遍历分词结果,看看哪个结果是在 content 中存在
for(Term term : terms) {
// 分词库直接针对词进行转小写了
// 正因如此,就必须把正文也先转成小写,然后再查询
String word = term.getName();
// 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,而不是只作为词的一部分(左右加空格)
firstPos = content.toLowerCase().indexOf(" " + word + " ");
if(firstPos >= 0){
// 找到了位置
break;
}
}
// 所有的分词结果都不在正文中存在(标题中触发)
if(firstPos == -1) {
// 此时就直接返回一个空的描述,或者也可以直接取正文的前 160 个字符
// return null;
return content.substring(0, 160) + "...";
}
// 从 firstPos 作为基准位置,往前找 60 个字符,作为描述的起始位置
String desc = "";
int descBeg = firstPos < 60 ? 0 : firstPos - 60; // 不足 60 个字符,就直接从 0 开始读
if(descBeg + 160 > content.length()) {
desc = content.substring(descBeg); // 从 descBeg 截取到末尾
}else {
desc = content.substring(descBeg, descBeg + 160) + "...";
}
return desc;
}
public static void main(String[] args) {
DocSearcher docSearcher = new DocSearcher();
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("-> ");
String query = scanner.next();
List<Result> results = docSearcher.search(query);
for(Result result : results) {
System.out.println("======================");
System.out.println(result);
}
}
}
JavaScript
的代码HTML
里面还包含了 script
标签JS
的代码也被整理到索引里面了Java
的 String
里面的很多方法,都是直接支持正则的(indexOf
,replace
,replaceAll
,spilt
…)这里我们主要用到的主要有:
.
:表示匹配一个非换行字符(不是 \n
或者不是 \r
)*
:表示前面的字符可以出现若干次.*
:匹配非换行字符出现若干次去掉 script
的标签和内容,正则就可以写成这样:<script.*?>(.*?)</script>
<script>
, 里面可能会包含各种属性, 有的话我们都当成任意字符来匹配
去掉普通的标签(不去掉内容):<.*?>
?
表示“非贪婪匹配”:尽可能短的去匹配,匹配一个符合条件的最短结果?
表示“贪婪匹配”:尽可能长的去匹配,匹配一个符合条件的最长结果假设有一个
content
:<div>aaa</div> <div>bbb</div>
.*
此时就把整个正文都匹配到了。进行替换,自然就把整个正文内容都给替换没了.*?
此时就是会匹配到四个标签。如果进行替换,也只是替换标签,不会替换内容此时我们就需要重新对 Parser
类的 parserContent
方法进行修改,让其能够去掉 JS
标签和内容
此时我们在 Parser
类中重新写一个方法,实现一个让正文能够去掉 JS
标签和内容的逻辑。
script
String
里面(然后才好使用正则进行匹配)这里我们实现一个 readFile
方法,用来读取文件
private String readFile(File f) {
// BufferedReader 设置缓冲区,将 f 中的内容预读到内存中
try(BufferedReader bufferedReader = new BufferedReader(new FileReader(f))){
StringBuilder content = new StringBuilder();
while(true) {
int ret = bufferedReader.read();
if(ret == -1) {
// 读完了
break;
}else {
char c = (char)ret;
if(c == '\n' || c == '\r'){
c = ' ';
}
content.append(c);
}
return content.toString();
}
}catch (IOException e){
e.printStackTrace();
}
return ""; // 抛了异常,就直接返回一个空字符串
}
script
标签content = content.replaceAll("<script.*?>(.*?)</script>", " ");
HTML
标签content = content.replaceAll("<.*?>", " ");
注意标签替换顺序不能变
content = content.replaceAll("\\s+", " ");
\s
,\\s
是转义字符+
也是表示这个符号会出现多次,还表示这个符号至少要出现一次*
只表示这个符号会出现多次,但也可以一次都不出现完整代码:
// 这个方法内部就基于正则表达式,实现去标签,以及去除 script
public String parseContentByRegex(File f) {
//1. 先把整个文件都读到 String 里面
String content = readFile(f);
// 2. 替换掉 script 标签
content = content.replaceAll("<script.*?>(.*?)</script>", " ");
// 3. 替换掉普通的 HTML 标签
content = content.replaceAll("<.*?>", " ");
// 4. 使用正则把多个空格,合并成一个空格
content = content.replaceAll("\\s+", " ");
return content;
}
再次运行 DocSearcher
,可以发现描述中的内容变规范了:
实现了 Searcher
类里面的 search
方法
这里的搜索模块实现比较简单,主要还是因为当前没有什么“业务逻辑”