最近做了一个简单的搜索java api文档的项目,在这里写个文章总结一下思路。
这个项目是保存api文档到本地,通过访问本地的api文档来使用的。大思路就是遍历本地的api文档(以html和文件夹形式存在),然后将本地的html网页内容解析出来,将解析后的内容放在一个文件里面。将文件的内容构建正排索引,构建倒排索引。搜索的时候将输入的内容进行分词,按照各个分词在保存好的倒排索引权重查找。按照降序排列查找出来的内容。
然后细化每一个步骤:
1.遍历本地的HTML文件,每个HTML文件解析为一个对象。将对象的内容按照一定格式保存在一个文件里。
代码思路实现:
public static void main(String[] args) throws IOException {
//找到api本地路径下所有的HTML文件
List<File> htmls=listHtml(new File(API_PATH));
FileWriter fw= new FileWriter(RAW_DATA);
//BufferedWriter bw=new BufferedWriter(fw);
PrintWriter pw=new PrintWriter(fw,true);//自动刷新缓冲区
for(File html:htmls){
//一个HTML解析DocInfo所有属性,
DocInfo doc=parseHtml(html);
//System.out.println(doc);
// docs.add(doc);
//保存本地正排索引文件
//格式:一个行为一个doc,title+\3+url+\3+content
String url= html.getAbsolutePath().substring(API_PATH.length());
System.out.println("Parse"+url);
pw.println(doc.getTitle()+"\3"+doc.getUrl()+"\3"+doc.getContent());
}
2.建立正排索引
建立正排索引就是根据之前已经在文档里面以相应格式存储好的内容,将其按行转化为对象,存储在list里面。方便后续查看和操作;
大题思路代码:
try {
FileReader fr=new FileReader(Parser.RAW_DATA);
BufferedReader br=new BufferedReader(fr);
int id=0;//行号设置为DocInfo的id
String line;
while ((line=br.readLine())!=null){
if(line.trim().equals("")) continue;
//一行对应一个DocInfo对象
DocInfo doc=new DocInfo();
doc.setId(++id);
String[] parts=line.split("\3");//每行按照\3间隔符拆分
doc.setTitle(parts[0]);
doc.setUrl(parts[1]);
doc.setContent(parts[2]);
//添加到正排索引
FORWARDINDEX.add(doc);
}
3.根据正排索引,构建倒排索引
倒排索引的重要思想是通过关键词来搜索相关文档。所以我们首先要将关键词拿出来。然后存储关键词所在的HTML的信息。
具体做法就是:遍历正排索引,分别分割出来标题和正文内容,通过分词的方式将标题和正文的关键字拿出来,放在一个map<关键字,html信息>的map里面,每当遇到关键字标题的时候其权值+5;每当遇到关键字内容的时候权值+1。最终遍历这个map,将信息添加进Map<关键字,list<HTML信息>>里面。
局部代码展示:
for(DocInfo doc:FORWARDINDEX){
//一个doc:分别对标题和正文分词,每一个分词生成一个weight对象,需要计算权重
Map<String,Weight> cache=new HashMap<>();
List<Term> titleFencis= ToAnalysis.parse(doc.getTitle()).getTerms();
for(Term titleFenci:titleFencis){
//标题分词遍历处理
/*if(titleFenci.getName().contains("�")){
System.out.println("标题分词============"+doc.getUrl());
}*/
Weight w=cache.get(titleFenci.getName());//获取了标题分词到键对应的weight
if(w==null){
w=new Weight();
w.setDoc(doc);
w.setKeyword(titleFenci.getName());
cache.put(titleFenci.getName(),w);
}
//标题分词,权重+10;
w.setWeight(w.getWeight()+10);
}
//正文分词的逻辑和标题分词逻辑相同
List<Term> contentFencis=ToAnalysis.parse(doc.getContent()).getTerms();
for(Term contentFenci:contentFencis){
/*if(contentFenci.getName().contains("�")){
System.out.println("正文分词============"+doc.getUrl());
}*/
Weight w=cache.get(contentFenci.getName());
if(w==null){
w= new Weight();
w.setDoc(doc);
w.setKeyword(contentFenci.getName());
cache.put(contentFenci.getName(),w);
}
w.setWeight(w.getWeight()+1);
}
//把临时保存的map数据(keyWord-weight) 全部保存到倒排索引里
//遍历
for(Map.Entry<String,Weight> e :cache.entrySet()){
String keyword=e.getKey();
Weight w=e.getValue();
//更新到倒排索引Map<String,List<Weight>>---->多个文档,同一个关键次,保存在list
//先在倒排索引中,通过keyword获取已有的值;
List<Weight> weights=INVERTED_INDEX.get(keyword);
if(weights==null){
//如果拿不到就存放进去
weights=new ArrayList<>();
INVERTED_INDEX.put(keyword,weights);
}
weights.add(w);//倒排中,添加当前文档每个分词对应的weight对象
}
}
经过以上的几个步骤,初始化的工作完成了。
然后用servelet来进行处理:
servelet的工作过程:浏览器http请求------》tomcat服务器-------》到达servlet-----》执行doget,dopost方法----》返回数据。
只要在Servlet上设置@WebServlet标注,容器就会自动读取当中的信息。参数value是定位到访问的URL。
具体的方法就是:前端网页拿到后端的URL,在表单中添加接收请求的格式来接收后端以及客户的请求。当用户输入查找内容时,由后端处理输入的内容-----分词,按照关键词在倒排索引中查找,如果查找到了的话,那就拿到这个list。并且遍历list,按照list里面的权重来排序输出,后端将结果转成一个对象然后再序列化为Json字符串。最终返回给前端显示。
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");//ajax请求
//构建返回给前端的内容。使用对象,之后再序列化为json字符串
Map<String,Object> map=new HashMap<>();
//解析请求数据
String query=req.getParameter("query");
List<Result> results=new ArrayList<>();
try{
//根据搜索内容处理搜索业务
if(query==null||query.trim().length()==0){
map.put("ok",false);
map.put("msg","搜索内容为空");
}else{
//1.根据搜索内容,进行分词,遍历每个分词
for (Term t: ToAnalysis.parse(query).getTerms()){
String fenci=t.getName();//搜索的分词
//如果分词是没有意义的分词,就不执行,就跳过
//TODO 定义一个数组,包含没有意义的而关键词,if(isVAlid(fenci))continue;
//2.每个分词,在倒排中查找对应的文档(一个分词对应多个文档)
List<Weight> weights=Index.get(fenci);
//3.一个文档转为换result(不同分词可能存在相同文档,需要合并)
for(Weight w:weights){
//先转换Weight为result
Result r=new Result();
r.setId(w.getDoc().getId());
r.setTitle(w.getDoc().getTitle());
r.setWeight(w.getWeight());
r.setUrl(w.getDoc().getUrl());
//自定义:假设文档内容超过60的部分隐藏为...
String content=w.getDoc().getContent();
r.setDesc((content.length()<=60?content:content.substring(0,60)+"..."));
//TODO 合并操作:需要
// (1)在List<Result>找已有的,判断DocID相同,
// 直接在已有的result权重上加上现有的
//不存在啊,直接放进去
results.add(r);
}
}
//4.合并完成后对,list<result>排序,根据权重降序排序
results.sort((o1,o2 )-> {
//权重降序
return Integer.compare(o2.getWeight(),o1.getWeight());
});
map.put("ok",true);
map.put("data",results);
}
}catch(Exception e){
e.printStackTrace();
map.put("ok",false);//操作失败
map.put("msg",false);
}
PrintWriter pw=resp.getWriter();//获取输出流
//设置响应体内容:map对象序列化为json字符串
pw.println(new ObjectMapper().writeValueAsString(map));
}
这就是整个搜索引擎的大致思路。
下来看一下效果图: