API 文档搜索引擎(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
简介: API 文档搜索引擎(上)

1. 认识搜索引擎:

在搜狗搜索的搜索结果页中, 包含了若干条结果, 每一个结果包含了图标, 标题, 描述, 展示URL等

搜索引擎的本质:输入一个查询词, 得到若干个搜索结果, 每个搜索结果包含了标题, 描述, 展示URL和点击URL

2. 搜索引擎思路:

2.1 搜索的核心思路:

当前我们有很多的网页(假设上亿个), 每个网页我们称为是一个文档

如何高效进行检索? 查找出有哪些网页是和查询词具有一定的相关性呢?

我们可以认为, 网页中包含了查询词(或者查询词的一部分), 就认为具有相关性.

那么我们就有了一个直观的解决思路

方案一 -- 暴力搜索

每次处理搜索请求的时候, 拿着查询词去所有的网页中搜索一遍, 检查每个网页是否包含查询词字符串.

这个方法是否可行?

显然, 这个方案的开销非常大. 并且随着文档数量的增多, 这样的开销会线性增长. 而搜索引擎往往对于效率的要求非常高.

方案二 -- 倒排索引

这是一种专门针对搜索引擎场景而设计的数据结构.

文档(doc): 被检索的html页面(经过预处理)

正排索引: "一个文档包含了哪些词". 描述一个文档的基本信息, 包括文档标题, 文档正文, 文档标题和正文的分词 /断句结果

倒排索引: "一个词被哪些文档引用了". 描述了一个词的基本信息, 包括这个词都被哪些文档引用, 这个词在该文档 中的重要程度, 以及这个词的出现位置等.

2.2 项目目标:

  • 实现一个 Java API 文档的简单的搜索引擎.
  • 最终效果

2.3 核心流程

  1. 索引模块: 扫描下载到的文档, 分析数据内容构建正排+倒排索引, 并保存到文件中.
  2. 搜索模块: 加载索引. 根据输入的查询词, 基于正排+倒排索引进行检索, 得到检索结果.
  3. web模块: 编写一个简单的页面, 展示搜索结果. 点击其中的搜索结果能跳转到对应的 Java API 文档页面.

3. 实现搜索引擎:

3.. Weight, Result, DocInfo类

/**
 * 这个类就是把 文档id 和 文档与词的相关性 权重 进行一个包裹
 */
@Data
public class Weight {
    private int docId;
    // 这个 weight 就表示 文档 和 词 之间的"相关性"
    // 这个值越大, 就认为相关性越强
    private int weight;
}
/**
 * 这个类来表示一个搜索结果
 */
@Data
public class Result {
    private String title;
    private String url;
    // 描述是正文的一段摘要
    private String desc;
}
/**
 * 表示一个文档对象(HTML对象)
 * 根据这些内容后面才能制作索引, 完成搜索过程.
 */
@Data
public class DocInfo {
    // docId 文档的唯一身份标识(不能重复)
    private int docId;
    // 该文档的标题. 简单粗暴的使用文件名来表示.
    // Collection.html => Collection
    private String title;
    // 该文档对应的线上文档的 URL. 根据本地文件路径可以构造出线上文档的 URL
    private String url;
    // 该文档的正文. 把 html 文件中的 html 标签去掉, 留下的内容
    private String content;
}

3.1 分词:

分词是搜索中的一个核心操作. 尤其是中文分词, 比较复杂(当然, 咱们此处暂不涉及中文分词)

我们可以使用现成的分词库 ansj.

注意: 当 ansj 对英文分词时, 会自动把单词转为小写.

  • 导入依赖:
<dependency>
    <groupId>org.ansj</groupId>
    <artifactId>ansj_seg</artifactId>
    <version>5.1.6</version>
</dependency>
  • 实例代码:
public static void main(String[] args) {
    // 准备一个比较长的话, 用来分词
    String str = "小明毕业于清华大学";
    // Term 就表示一个分词结果
    List<Term> terms = ToAnalysis.parse(str).getTerms();
    for (Term term : terms){
        System.out.println(term.getName());
    }
}

3.2 实现 Parser 类:

Parser 构建一个可执行程序, 负责读取 html 文档, 制作并生成索引数据(输出到文件中)

  • 从制定的路径中枚举出所有的文件
  • 读取每个文件, 从文件中解析出 HTML 的标题, 正文, URL

  • 先指定一个加载文档的路径
private static final String INPUT_PATH = 
            "C:/Users/LEO/Desktop/jdk-8u361-docs-all/docs/api";
  • 创建一个 index 实例
1. 
2. private Index index = new Index();

(1) run 方法

public void run(){
    long beg = System.currentTimeMillis();
    System.out.println("*** 索引制作开始! ***");
    // 整个 searcher.Parser 类的入口
    // 1. 根据上面指定的路径, 枚举出该路径中所有的文件(html), 这个过程需要把所有子目录中的文件都能获取到
    ArrayList<File> fileList = new ArrayList<>();
    enumFile(INPUT_PATH,fileList);
    /**
         * 获取到 INPUT_PATH 下的所有文件
         * System.out.println(fileList);
         * System.out.println(fileList.size());
         */
    // 2. 针对上面罗列出的文件的路径, 打开文件, 读取文件内容, 并进行解析, 并构建索引
    for (File f : fileList){
        // 通过这个方法来解析单个的html文件
        System.out.println("开始解析: " + f.getAbsolutePath());
        parseHTML(f);
    }
    // 3. 把在内存中构造好的索引数据结构, 保存到指定的文件中
    index.save();
    long end = System.currentTimeMillis();
    System.out.println("**** 索引制作完成! " + (end - beg) + "ms ****");
}

(2) enumFile() 枚举出该路径中所有的文件

<code class="language-plaintext hljs">// 第一个参数表示: 从哪个目录开始进行递归遍历
// 第二个参数表示: 递归得到的结果
// inputPath: C:/Users/LEO/Desktop/jdk-8u361-docs-all/docs/api
private void enumFile(String inputPath, ArrayList<File> fileList) {
    File rootPath = new File(inputPath);
    // listFiles 能够获取到 rootPath 当前目录下所包含的文件/目录
    // 使用 listFiles 只能看到一级目录, 看不到子目录里的内容
    // 要想看到子目录中的内容, 还需要进行递归
    File[] files = rootPath.listFiles();
    for (File f : files){
        // 根据当前 f 的类型, 来决定是否要递归
        // 如果 f 是一个普通文件, 就把 f 加入到 fileList 结果中
        // 如果 f 是一个目录, 就递归的调用 enumFile 方法, 进一步的获取子目录中的内容
        if(f.isDirectory()){
            enumFile(f.getAbsolutePath(), fileList);
        } else {
            // 排除非html文件
            // endsWith是String类的方法
            if(f.getAbsolutePath().endsWith(".html")){
                fileList.add(f);
            }
        }
    }
}</code>

(3) parseHTML() 通过这个方法来解析单个的html文件

private void parseHTML(File f) {
    // 1. 解析出 HTML 的标题
    String title = parseTitle(f);
    // 2. 解析出 HTML 对应的 URL
    String url = parseUrl(f);
    // 3. 解析出 HTML 对应的正文(有了正文才有后续的描述)
//        String content = parseContent(f);
    String content = parseContentByRegex(f); // 使用正则的版本
    // 4. 把解析出来的这些信息加入到索引当中
    index.addDoc(title, url, content);
}

(4) parseTitle() 解析出html文件的标题

private String parseTitle(File f) {
    String name = f.getName();
    return name.substring(0, name.length() - ".html".length());
}

(5) parseUrl() 解析出html文件的URL

private String parseUrl(File f) {
//  String part1 = "file:///C:/Users/LEO/Desktop/jdk-8u361-docs-all/docs/api/";
    String part11 = "https://docs.oracle.com/javase/8/docs/api";
    String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
    return part11 + part2;
}

(6) parseContent() 解析出html文件的正文

// (边读边判断)
public String parseContent(File f) {
// 先按照一个字符一个字符的方式来读取, 以<和>来控制拷贝数据的开关
// BufferedReader bufferedReader = new BufferedReader(new FileReader(f),1024 * 1024);
    try {
        FileReader fileReader = new FileReader(f);
        // 加上一个是否要进行拷贝, 开关
        boolean isCopy = true;
        // 还得准备一个保存结果的 StringBuilder
        StringBuilder content = new StringBuilder();
        while(true){
            // 注意, 此处的 read 返回值是一个 int , 不是 char
            // 此处使用 int 作为返回值, 主要是为了表示一些非法情况
            // 如果读到了文件末尾, 继续读, 就会返回 -1
            int ret = fileReader.read();
            if(ret == -1){
                // 表示文件读完了
                break;
            }
            // 如果这个结果不是 -1, 那么就是一个合法的字符
            char c = (char) ret;
            if(isCopy) {
                // 开关打开的状态, 遇到普通字符就应该拷贝到 Stringbuilder 中
                if(c == '<'){
                    // 关闭开关
                    isCopy = false;
                    continue;
                }
                if(c == '\n' || c == '\r'){
                    // 目的是为了去掉换行, 把换行符替换成空格
                    c = ' ';
                }
                // 其他字符, 直接进行拷贝即可, 把结果给拷贝到最终的 StringBuilder 中
                content.append(c);
            } else {
                // 开关关闭的状态, 就暂时不拷贝, 直到遇到 >
                if(c == '>'){
                    isCopy = true;
                }
            }
        }
        fileReader.close();
        return content.toString();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "";
}

(7) parseContentByRegex() 使用正则获取html的正文

基于正则表达式去除 script 标签的内容

// (先全部读取完, 然后替换) readFile 是 parseContentByRegex 需要的读取文件的方法
private String readFile(File f){
    try(BufferedReader bufferedReader = new BufferedReader(new FileReader(f))){
        StringBuilder content = new StringBuilder();
        while(true){
            int ret = bufferedReader.read();
            if(ret == -1){
                break;
            }
            char c = (char) ret;
            if(c == '\n' || c == '\r'){
                c = ' ';
            }
            content.append(c);
        }
        return content.toString();
    } catch (IOException e){
        e.printStackTrace();
    }
    return "";
}
// 这个方法内部就基于正则表达式, 实现去标签, 以及去除 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;
}

(8) 通过这个main方法实现整个制作索引的过程

要先将api文档扫描完并保存到磁盘上, 然后再启动tomcat

public static void main(String[] args) throws InterruptedException {
    // 通过main方法来实现整个制作索引的过程
    Parser parser = new Parser();
    //        parser.run();
    parser.runByThread();
}


相关文章
|
1月前
|
敏捷开发 测试技术 API
云效产品使用常见问题之代码仓库不支持API文档如何解决
云效作为一款全面覆盖研发全生命周期管理的云端效能平台,致力于帮助企业实现高效协同、敏捷研发和持续交付。本合集收集整理了用户在使用云效过程中遇到的常见问题,问题涉及项目创建与管理、需求规划与迭代、代码托管与版本控制、自动化测试、持续集成与发布等方面。
|
1月前
|
数据可视化 Linux API
如何在Linux使用docker部署Swagger Editor并实现无公网IP远程协同编辑API文档
如何在Linux使用docker部署Swagger Editor并实现无公网IP远程协同编辑API文档
|
4天前
|
自然语言处理 安全 API
触发邮件接口有哪些?邮件API文档
**触发邮件接口**如AokSend、Mailgun、Amazon SES、Postmark和Sendinblue是自动化企业通信的关键。这些接口在特定事件时自动发送邮件,提高效率和客户体验。例如,AokSend提供详细的API文档,支持事件触发、模板管理和多语言集成;Mailgun以灵活性著称;Amazon SES适合大规模发送;Postmark专注于事务邮件;Sendinblue则提供邮件、短信和营销自动化功能。每种服务都有示例代码展示如何使用API发送邮件。选择合适的接口能提升企业通信效率和客户满意度。
|
6天前
|
前端开发 Java 程序员
Spring Boot (十五): 优雅的使用 API 文档工具 Swagger2
Spring Boot (十五): 优雅的使用 API 文档工具 Swagger2
|
5天前
|
XML 自然语言处理 API
个人微信开发api文档
个人微信开发api文档
|
13天前
|
数据可视化 Java API
【JAVA】javadoc,如何生成标准的JAVA API文档
【JAVA】javadoc,如何生成标准的JAVA API文档
11 0
|
19天前
|
XML 前端开发 Java
Spring3 MVC中使用Swagger生成API文档
Spring3 MVC中使用Swagger生成API文档
18 0
|
1月前
|
测试技术 API 开发工具
📑教你如何编写一份 API 文档
API 文档是开发者理解和使用API的关键,它提供详细的说明、代码示例和调用过程,帮助创建无缝集成。好的API文档能提升开发人员体验,减少上手时间和维护成本,同时促进产品迭代。API有面向团队、合作伙伴和最终用户三种类型。编写文档时要考虑受众,提供清晰的概述、教程、认证信息、端点定义、状态码和错误码示例。维护更新与API同步的文档至关重要,遵循通俗语言、参考文档、示例和专人负责等最佳实践,确保全面性。GitHub、Twilio和Dropbox的API文档是良好示例。
|
1月前
|
前端开发 Java 测试技术
IDEA 版 API 接口神器来了,一键生成文档,贼香!
IDEA 版 API 接口神器来了,一键生成文档,贼香!
238 0
|
1月前
|
API 开发者
1688阿里巴巴中国站平台 API接口获取商品详情 接入文档说明
1688(阿里巴巴批发网)的API接入文档是专为开发者提供的,用于指导如何集成和使用1688平台提供的API接口。这些API接口可以帮助开发者实现各种功能,如商品搜索、订单管理、用户认证等。