admin管理员组文章数量:1577830
Java API 文档搜索引擎:
- 1、项目介绍:
- (1)认识搜索引擎:
- (2)搜索的核心思想:
- (3)项目的目标:
- 2、项目准备:
- (1)需要的资源:
- (2)创建web项目:
- 3、开发步骤:
- (1)创建三个 JavaBean 公共模块
- (2)预处理:解析本地 html 文件
- (3)构建索引:
- (4)搜索模块
- (5)前端
- 4、测试:
1、项目介绍:
(1)认识搜索引擎:
比如火狐浏览器的搜索引擎就包括:百度、谷歌
先观察,百度搜索引擎的搜索结果页中,包含了若干条结果,每一个结果中,又包含了图标,标题,描述,展示url,时间,子链,图片等。
搜索引擎的本质:
- 输入一个查询词,得到若干搜索结果,每个搜索结果包含了标题、描述、展示url和点击url
(2)搜索的核心思想:
当前我们有很多网页(假设上亿个),每个 网页 我们称为是一个 文档
如何高效进行检索?查找出有哪些网页是和查询词具有一定的相关性呢?
- 我们可以认为,网页中包含了查询词(或者查询词的一部分),就认为具有相关性
解决方案:
① 方案一 : 暴力搜索
每次处理搜索请求时,拿着查询词去所有的网页中搜索一遍,检查每个网页是否包含查询词字符串。。。
显然,这个方案的开销非常大,并且随着文档数量的增多,这样的开销会线性增长,是一种不适合的搜索方案。
② 方案二 : 倒排索引
(这是一种专门针对搜索引擎场景而设计的数据结构)
- 文档(doc):被检索的 html 页面(经过预处理)
- 正排索引:“一个文档包含了哪些词”。描述一个文档的基本信息,包括文档标题,文档正文,文档标题和正文分词(断句结果)
- 倒排索引:“一个词被哪些文档引用了”。描述了一个词的基本信息,包括了词都被哪些文档引用,这个词在该文档的重要程度,以及这个词的出现位置等。
(3)项目的目标:
实现一个 Java API 文档的简单的搜索引擎
2、项目准备:
项目全部源码(项目配置) GitHub 链接:
https://github/JACK-QBS/Project
代码框架如下:
简单介绍一下:
① java包 下的代码是我们的 后端 代码,用来响应来自前端的请求和与数据库的交互;
② webapp包 下的代码是我们的 前端 代码,即用户界面的设计。
(1)需要的资源:
Maven、IDEA、Chrome浏览器、Fiddler4抓包工具(可使用浏览器自带的开发者工具)
(2)创建web项目:
具体创建步骤和环境配置:
https://blog.csdn/qq_45658339/article/details/112249187
这个项目中 pop.xml 的配置源码放到 GitHub 中:
https://github/JACK-QBS/Project
3、开发步骤:
(1)创建三个 JavaBean 公共模块
1、每一个本地 html 文件对应一个文档对象(文档对应的结构)
public class DocInfo {
private Integer id;//类似数据库主键(识别不同文档)
private String title;//标题:html文件名作为标题
private String url; //oracle官网api文档下html的url
private String content;//网页正文:<标签>内容</标签>,内容为正文
}
2、倒排索引 Map<String,List>中,关键词对应的信息
public class Weight {
private DocInfo doc;
private int weight;//权重值:通过标题和正文中,关键词的数量计算
private String keyword;//关键词
}
3、返回结果集对象
public class Result {
//合并文档,排序用
private Integer id;//docInfo的id,文档合并时,文档身份的标识
private int weight;//权重:同一个文档合并后,权限相加,再排序
//返回给前端用
private String title;//文档(docInfo)的标题
private String url;//文档(docInfo)的url
private String desc;//docInfo的content(超长时,截取指定长度)
}
(2)预处理:解析本地 html 文件
遍历 api 目录下所有的文件, 并读取每个文件的内容, 把所有文件整理成一个行文本文件( 每行对应一个 html)
每一个 文件 转化为 DocInfo 对象:
- ① url : 官网url的前缀 + 本地 api 目录下 html 文件的相对路径
- ② title : 简单处理为文件名
- ③ 内容:输入流读取html内容(不读取标签本身,读取标签内容)
输出流保存到 本地 raw_data.txt 文件中
public class Parser {
//api目录
public static final String API_PATH = "D:\\Code\\Project\\docs\\api";
//构建的本地文件正排索引
public static final String RAW_DATA = "D:\\Code\\Project/raw_data.txt";
//官方api文档的根路径(拼接本地api路径)
public static final String API_BASE_PATH = "https://docs.oracle/javase/8/docs/api";
public static void main(String[] args) throws IOException {
//找到api本地路径下所有的html文件
List<File> htmls = listHtml(new File(API_PATH));
FileWriter fw = new FileWriter(RAW_DATA);
PrintWriter pw = new PrintWriter(fw,true); //打印输出流,自动刷新缓冲区
for (File html : htmls) {
//一个html解析DocInfo有的属性(输入)
DocInfo doc = parseHtml(html);
//保存本地正排索引文件(输出)(行号代表id)
//格式:一行为一个doc,title+\3 + url + content
String uri = html.getAbsolutePath().substring(API_PATH.length());
System.out.println("Parse: "+uri);
if(doc.getTitle().contains("�")){
System.out.println("title====================="+doc.getTitle());
}
if(doc.getContent().contains("�")){
System.out.println("content====================="+doc.getContent());
}
pw.println(doc.getTitle()+"\3"+doc.getUrl()+"\3"+doc.getContent());
}
}
private static DocInfo parseHtml(File html) {
DocInfo doc = new DocInfo();
//ArrayList.html长度-5
doc.setTitle(html.getName().substring(0,html.getName().length()-".html".length()));
//获取相对路径
String uri = html.getAbsolutePath().substring(API_PATH.length());
doc.setUrl(API_BASE_PATH + uri);
doc.setContent(parseContent(html));
//目前是从本地api目录的html文件解析为文档对象,这步不需要设置id
return doc;
}
/**
* 解析 html 内容
* <标签>内容</标签>
* 只取内容,有多个标签就拼接
*/
private static String parseContent(File html) {
StringBuilder sb = new StringBuilder();
try {
FileReader fr = new FileReader(html);
int i;
boolean isContent = false;//判断是标签还是内容
//一个字符一个字符来读取
while ((i = fr.read()) != -1) {
char c = (char) i;
if (isContent) {
if (c == '<') {
//当前标签的内容读取结束 <标签>内容<
isContent = false;
continue;
} else if (c == '\n' || c == '\r') { // 换行符 \r\n
sb.append(" ");
} else {
sb.append(c);//拼接标签内容
}
} else if (c == '>'){
//当前不是正文,并且读取到>,之后就是正文 <标签
isContent = true;
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
//递归遍历html文件(根据传入的目录)
private static List<File> listHtml(File dir) {
List<File> list = new ArrayList<>();
//列出目录中的子文件和子文件夹
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory()) {
//若是子文件夹:递归调用获取子文件夹内的html文件
list.addAll(listHtml(child));
} else if (child.getName().endsWith(".html")) {
list.add(child);
}
}
}
return list;
}
}
(3)构建索引:
-
正排索引:从本地文件数据中读取到 java 内存(类似于数据库保存的数据),从本地 raw_data.txt 中读取并保存
-
倒排索引:构建 Map<String,List<信息>>(类似数据库 hash 索引)
map键:关键词(分词来做)
map值: 信息:- (1) docInfo 对象引用或是 docInfo 的 id
- (2) 权重(标题对应的关键词数量10 + 正文对应关键词数量1)(自定义)
- (3) 关键词
-
正排转倒排:
- (1)分词操作
- (2)List,Map操作
- (3)遍历每一个DocInfo对象,对标题和内容分词,遍历临时保存的Map,保存到倒排
public class Index {
//正排索引:
private static final List<DocInfo> FORWARD_INDEX = new ArrayList<>();
//倒排索引:
private static final Map<String,List<Weight>> INVERTED_INDEX = new HashMap<>();
/**
* 构建正排索引的内容:从本地 raw_data.txt 中读取并保存
*/
public static void buildForwardIndex() {
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 对象,类似数据库一行数据对应Java对象
DocInfo doc = new DocInfo();
doc.setId(++id);
//行文本文件每一行中有三列, 用 \3 分割. 分别是标题, url, 正文.
String[] parts = line.split("\3");//每一行按 \3 间隔符切开
doc.setTitle(parts[0]);
doc.setUrl(parts[1]);
doc.setContent(parts[2]);
//添加到正排索引
System.out.println(doc);
FORWARD_INDEX.add(doc);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 构建倒排索引:从java内存中正排索引获取文档来构建
*/
public static void buildInvertedIndex() {
for (DocInfo doc : FORWARD_INDEX) {//doc+分词 对应 weight(doc和分词一对多,分词和weight一对一)
//一个doc,分别对标题和正文分词,每一个分词生成一个weight对象,需要计算权重
//如标题为:清华大学/计算机/专业/使用/计算机/炒菜
//第一次出现的关键词,要new Weight对象,之后出现相同分词关键词时
// 要获取之前已经拿到的相同关键词weight对象,再更新权重(把自己的权限加进去)
//实现逻辑:先构造一个HashMap,保存分词(键)和weight对象(value)
Map<String,Weight> cache = new HashMap<>();
//标题 分词遍历处理
List<Term> titleFenCis = ToAnalysis.parse(doc.getTitle()).getTerms();
for (Term titleFenCi : titleFenCis) {
Weight w = cache.get(titleFenCi.getName());//获取标题分词键对应的weight
//如果没有,就创建一个并放到map中
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) {
Weight w = cache.get(contentFenCi.getName());//获取标题分词键对应的weight
//如果没有,就创建一个并放到map中
if (w == null) {
w = new Weight();
w.setDoc(doc);
w.setKeyword(contentFenCi.getName());
cache.put(contentFenCi.getName(),w);
}
//正文分词,权重就+1
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);
}
//System.out.println(keyword+": ("+w.getDoc().getId()+", "+w.getWeight()+") ");
weights.add(w);//倒排中,添加当前文档每个分词对应的weight对象
}
}
}
//通过关键词(分词)在倒排中查找映射的文档(多个文档,倒排拉链)
public static List<Weight> get(String keyword) {
return INVERTED_INDEX.get(keyword);
}
public static void main(String[] args) {
Index.buildForwardIndex();
//测试倒排内容是否正确
for (Map.Entry<String,List<Weight>> e : INVERTED_INDEX.entrySet()) {
String keyword = e.getKey();
System.out.print(keyword+": ");
List<Weight> weights = e.getValue();
weights.stream()
.map(w->{//map操作:把list中每一个对应转换为其他对象
return "("+w.getDoc().getId()+", "+w.getWeight()+")";
})//转换完,会变成List<String>
.forEach(System.out::print);
System.out.println();
}
}
}
(4)搜索模块
- (1)根据搜索内容,进行分词,遍历每个分词
- (2)每个分词,在倒排中查找对应的文档(一个分词对应多个文档)
- (3)一个文档转换为一个Result(不同分词可能存在相同的文档,需要合并)
- (4)合并完成后,对List排序:权重降序排序
- (5)设置响应体内容
//根据前端请求路径,定义后端服务路径,loadOnStartup属性表示是否在启动时初始化(默认-1启动不初始化,第一次请求初始化)
@WebServlet(value = "/search",loadOnStartup = 0)
public class SearchServlet extends HttpServlet {
@Override
public void init(ServletConfig config) throws ServletException {
//初始化工作:先构建正排索引,再根据正排索引构建倒排
Index.buildForwardIndex();
Index.buildInvertedIndex();
System.out.println("init complete!");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");//ajax请求,响应json格式
//构造返回给前端的内容:使用对象,之后再序列化为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();//搜索的分词
//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()<=150 ? content : content.substring(0,60)+"...");
results.add(r);
}
}
//4、合并完成后,对List<Result>排序:权重降序排序
results.sort(new Comparator<Result>() {
@Override
public int compare(Result o1, Result 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","未知的错误");
}
PrintWriter pw = resp.getWriter();//获取输出流
//设置响应体内容:map对象序列化为json字符串
pw.println(new ObjectMapper().writeValueAsString(map));
}
}
(5)前端
前端页面结构比较简单, 只要包含一个输入框和一个按钮即可.
创建一个 index.html. 这个就不说了吧, 直接发给大家:
https://github/JACK-QBS/Project/blob/master/%E9%A1%B9%E7%9B%AE2%EF%BC%9A%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E/src/main/webapp/index.html
4、测试:
运行程序:
当不输入搜索内容时:
随便输入一个 Java 中的 API
版权声明:本文标题:手把手带你做项目2:搜索引擎(附源码) 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/dongtai/1727828232a1132360.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论