搜索引擎项目开发过程以及重难点整理(一)

简介: 搜索引擎项目开发过程以及重难点整理(一)

认识搜索引擎

像百度搜索这样,输入要搜索的内容,点击搜索,就会出现若干条结果,每条结果包含标题,内容描述,展示url,图片,图标等相关的内容。如下图输入"恰似故人归"点击百度一下,就会出现如下的页面。


搜索引擎的本质就是输入一个查询词,得到若干个搜索结果,其中包含了标题,描述,展示url和点击url。


搜索的核心思路

我们把每一个网页称为一个文档,每一次的搜索就是在所有文档中查找查询词,检查文档中是否包含要查询的词或字符串。但是细想这种方法直接暴力且开销巨大,随着文档的增多,搜索的时间也随之延长,而搜索引擎往往对效率要求非常高,试想如果你的每一次搜索都需要1分钟时间才会有相应的搜素结果你还会用这个搜索引擎吗?

因此我们想到倒排索引,这是一种专门针对搜素引擎场景而设计的数据结构。


倒排索引介绍

文档:被检索的html页面

正排索引:一个文档中包含了那些词,描述一个文档的基本信息,将文档中的词进行分词处理并存储。

倒排索引:一个词被那些文档所引用,描述一个词的基本信息,存储了这个词都存在在那些文档中,这个词在文档中的重要程度。


项目简介

如今,大大小小的网站以及软件都可能会涉及到搜索这一功能,因此,计划写出一个基于web的搜索相关文档的搜索引擎项目。以锻炼自己的能力,加强对技术的应用,能够更好的掌握相关的技术,了解互联网的发展。众所周知,搜索本身看起来貌似很简单,很多方法其实都可以做出来,比如简简单单的sql语句的模糊匹配也就可以实现这一功能,为什么非要做一个项目来实现搜索引擎呢?最主要的原因还是相关的性能问题,试想一下,若在平时生活中搜索一个词的时间长达几分钟甚至更长,这是人们所接受不了的,因此,设计一个搜索引擎就显现的非常的重要,能够加快搜索的性能,能够实现很快出结果,在性能上能够进行日常的应用

下面我们对Java API文档实现一个搜索引擎。

首先我们需要在官方页面下载Java API文档到本地,此处附上下载地址:java api文档下载地址


模块管理

2.png


构建索引模块

数据库设计

该设计的数据库主要作用是进行正排索引和倒排索引的保存。

建立searcher_refactor数据库

CREATE SCHEMA `searcher_refactor` DEFAULT CHARACTER SET utf8mb4 ;

正排索引

正排索引的字段主要有:


docId --文章的id


title --文章的标题


url --文章对应的url


content --文章的正文


表的结构如图所示。

2.png

建表语句如下:

CREATE TABLE `searcher_refactor`.`forward_indexes` (
    `docid` INT NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(100) NOT NULL,
    `url` VARCHAR(200) NOT NULL,
    `content` LONGTEXT NOT NULL,
    PRIMARY KEY (`docid`))
COMMENT = '存放正排索引\ndocid -> 文档的完整信息';

倒排索引

id --对应单词的id


word --对应的单词


docid --对应的文章id


weight --该单词在该文章对应的权重


表的结构如下图所示。

2.png

建表语句如下:

CREATE TABLE `searcher_refactor`.`inverted_indexes` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `word` VARCHAR(100) NOT NULL,
    `docid` INT NOT NULL,
    `weight` INT NOT NULL,
    PRIMARY KEY (`id`))
COMMENT = '倒排索引\n通过 word -> [ { docid + weight }, { docid + weight }, ... ]';

程序入口类Indexer类

存储构建索引模块代码的整体逻辑:


扫描出来所有的 html 文件,并把他们放到一个list集合里面


针对每个 html 文件,得到其 标题、URL、正文信息,把这些信息封装成一个对象(文档 Document)


进行正排索引的保存


进行倒排索引的生成保存


关闭线程池

package com.peixinchen.searcher.indexer.command;
import com.peixinchen.searcher.indexer.core.IndexManager;
import com.peixinchen.searcher.indexer.model.Document;
import com.peixinchen.searcher.indexer.util.FileScanner;
import com.peixinchen.searcher.indexer.properties.IndexerProperties;
import lombok.extern.slf4j.Slf4j;
import org.ansj.splitWord.analysis.ToAnalysis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
/**
 * 构建索引的模块,是整个程序的逻辑入口
 */
@Slf4j      // 添加 Spring 日志的使用
@Component  // 注册成 Spring 的 bean
//@Profile("run") // 让跑测试的时候不加载这个 bean(run != test)
public class Indexer implements CommandLineRunner {
    // 需要依赖 FileScanner 对象
    private final FileScanner fileScanner;
    private final IndexerProperties properties;
    private final IndexManager indexManager;
    private final ExecutorService executorService;
    @Autowired  // 构造方法注入的方式,让 Spring 容器,注入 FileScanner 对象进来 —— DI
    public Indexer(FileScanner fileScanner, IndexerProperties properties, IndexManager indexManager, ExecutorService executorService) {
        this.fileScanner = fileScanner;
        this.properties = properties;
        this.indexManager = indexManager;
        this.executorService = executorService;
    }
    @Override
    public void run(String... args) throws Exception {
        ToAnalysis.parse("随便分个什么,进行预热,避免优化的时候计算第一次特别慢的时间");
        log.info("这里的整个程序的逻辑入口");
        // 1. 扫描出来所有的 html 文件,并把他们放到一个list集合里面
        log.debug("开始扫描目录,找出所有的 html 文件。{}", properties.getDocRootPath());
        List<File> htmlFileList = fileScanner.scanFile(properties.getDocRootPath(), file -> {
            //定义我们索要搜寻的文件名的后缀为.html
            return file.isFile() && file.getName().endsWith(".html");
        });
        log.debug("扫描目录结束,一共得到 {} 个文件。", htmlFileList.size());
        // 2. 针对每个 html 文件,得到其 标题、URL、正文信息,把这些信息封装成一个对象(文档 Document)
        File rootFile = new File(properties.getDocRootPath());
        List<Document> documentList = htmlFileList.stream()
                .parallel()         // 【注意】由于我们使用了 Stream 用法,所以,可以通过添加 .parallel(),使得整个操作变成并行,利用多核增加运行速度
                .map(file -> new Document(file, properties.getUrlPrefix(), rootFile))
                .collect(Collectors.toList());
        log.debug("构建文档完毕,一共 {} 篇文档", documentList.size());
        // 3. 进行正排索引的保存
        indexManager.saveForwardIndexesConcurrent(documentList);
        log.debug("正排索引保存成功。");
        // 4. 进行倒排索引的生成保存
        indexManager.saveInvertedIndexesConcurrent(documentList);
        log.debug("倒排索引保存成功。");
        // 5. 关闭线程池
        executorService.shutdown();
    }
}

扫描文档子模块

2.png


FileScanner类

以 rootPath 作为根目录,开始进行文件的扫描,把所有符合条件的 File 对象,作为结果,以 List 形式返回

rootPath 根目录的路径,调用者需要确保这个目录存在 && 一定是一个目录

filter 通过针对每个文件调用 filter.accept(file) 就知道,文件是否满足条件

返回满足条件的所有文件

package com.peixinchen.searcher.indexer.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.List;
@Slf4j      // 添加日志
@Service    // 注册成 Spring bean
public class FileScanner {
    /**
     * 以 rootPath 作为根目录,开始进行文件的扫描,把所有符合条件的 File 对象,作为结果,以 List 形式返回
     * @param rootPath 根目录的路径,调用者需要确保这个目录存在 && 一定是一个目录
     * @param filter 通过针对每个文件调用 filter.accept(file) 就知道,文件是否满足条件
     * @return 满足条件的所有文件
     */
    public List<File> scanFile(String rootPath, FileFilter filter) {
        List<File> resultList = new ArrayList<>();
        File rootFile = new File(rootPath);
        // 针对目录树进行遍历,深度优先 or 广度优先即可,确保每个文件都没遍历到即可
        // 我们这里采用深度优先遍历,使用递归完成
        traversal(rootFile, filter, resultList);
        return resultList;
    }
    private void traversal(File directoryFile, FileFilter filter, List<File> resultList) {
        // 1. 先通过目录,得到该目录下的孩子文件有哪些
        File[] files = directoryFile.listFiles();
        if (files == null) {
            // 说明有问题,我们不管(一般是权限等的问题),通常咱们遇不到这个错误
            return;
        }
        // 2. 遍历每个文件,检查是否符合条件
        for (File file : files) {
            // 通过 filter.accept(file) 的返回值,判断是否符合条件
            if (filter.accept(file)) {
                // 说明符合条件,需要把该文件加入到结果 List 中
                resultList.add(file);
            }
        }
        // 3. 遍历每个文件,针对是目录的情况,继续深度优先遍历(递归)
        for (File file : files) {
            if (file.isDirectory()) {
                traversal(file, filter, resultList);
            }
        }
    }
}

构建文档子模块

2.png


构建标题

parseTitle方法

  private String parseTitle(File file) {
        // 从文件名中,将 .html 后缀去掉,剩余的看作标题
        String name = file.getName();
        String suffix = ".html";
        return name.substring(0, name.length() - suffix.length());
    }

构建url

parseUrl方法

 @SneakyThrows
    private String parseUrl(File file, String urlPrefix, File rootFile) {
        // 需要得到一个相对路径,file 相对于 rootFile 的相对路径
        // 比如:rootFile 是 D:\课程\2022-08-02-Java19-22班-项目-2\docs\api
        //      file 是     D:\课程\2022-08-02-Java19-22班-项目-2\docs\api\javax\sql\DataSource.html
        // 则相对路径就是:javax\sql\DataSource.html
        // 把所有反斜杠(\) 变成正斜杠(/)
        // 最终得到 java/sql/DataSource.html
        String rootPath = rootFile.getCanonicalPath();
        //如果本地目录中存在/,就把他变为\
        rootPath = rootPath.replace("/", "\\");
        if (!rootPath.endsWith("\\")) {
            rootPath = rootPath + "\\";
        }
        String filePath = file.getCanonicalPath();
        String relativePath = filePath.substring(rootPath.length());
        relativePath = relativePath.replace("\\", "/");
        return urlPrefix + relativePath;
    }

构建内容

parseContent方法

 @SneakyThrows
    private String parseContent(File file) {
        StringBuilder contentBuilder = new StringBuilder();
        try (InputStream is = new FileInputStream(file)) {
            try (Scanner scanner = new Scanner(is, "ISO-8859-1")) {
                while (scanner.hasNextLine()) {
                    String line = scanner.nextLine();
                    contentBuilder.append(line).append(" ");
                }
                return contentBuilder.toString()
                        .replaceAll("<script.*?>.*?</script>", " ")
                        .replaceAll("<.*?>", " ")
                        .replaceAll("\\s+", " ")
                        .trim();
            }
        }
    }

构建正排索引子模块

构建正排索引的过程其实就是向数据库中插入正排索引的过程


正排索引实体类

public class Document {
    private Integer docId;  // docId 会在正排索引插入后才会赋值
    private String title;   // 从文件名中解析出来
    // 注意这里的url是两个信息(1. 官网的https://docs.oracle.com/javase/8/docs/api/  2. 相对路径的相对位置)
    private String url;
    private String content; // 从文件中读取出来,并且做一定的处理
    // 专门给测试用例用的构造方法
    public Document(String title, String url, String content) {
        this.title = title;
        this.url = url;
        this.content = content;
    }
    public Document(File file, String urlPrefix, File rootFile) {
        this.title = parseTitle(file);
        this.url = parseUrl(file, urlPrefix, rootFile);
        this.content = parseContent(file);
    }

mapper接口

   void batchInsertForwardIndexes(@Param("list") List<Document> documentList);

xml

 <insert id="batchInsertForwardIndexes" useGeneratedKeys="true" keyProperty="docId" keyColumn="docid">
        insert into forward_indexes (title, url, content) values
        <!-- 一共有多少条记录,得根据用户传入的参数来决定,所以这里采用动态 SQL 特性 -->
        <foreach collection="list" item="doc" separator=", ">
            (#{doc.title}, #{doc.url}, #{doc.content})
        </foreach>
    </insert>

注意这里我们使用了动态sql的特性,为了提高构建正排索引的效率,我们在插入正排索引的时候使用了批量插入(待会我们会在构建正排索引中介绍)的方式,那么在mybatis实现的过程中,我们就需要使用动态sql中对集合的遍历

2.png2.png2.png



application.yml配置文件:

2.png


在Spring的配置文件中,指定mybatis查找mapper xml文件的路径

classpath:就代表从src/main/resources下进行查找【这个实际上是错误的理解,但现在这么理解关系不大】


完整路径: src/main/resources/mapper/index-mapper.xml


AppConfig类

这个类是给后序创建索引中的多线程版本提供服务的

这个类负责创建一个线程池,把线程池交给bean去管理,

为什么要把线程池交给bean去管理呢?


每次使用appconfig去配置这个线程池的代码的时候就还是相当于是一个单例模式,其实我可以将创建线程池的方法放到idexManager类当中去,但是每次都要new实例,如果放到一个配置类里交给spring去管理就意味着每次我只需要在需要的地方注入我这个bean对象即可,相当于我ThreadPoolExecutor这个类从始至终都只new了一个实例,这就是单例模式,也就是说只要被spring管理的,没有经过特殊处理的,都是单例模式


package com.peixinchen.searcher.indexer.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
@Configuration
public class AppConfig {
    @Bean
    public ExecutorService executorService() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                8, 20, 30, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5000),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable task) {
                        Thread thread = new Thread(task);
                        thread.setName("批量插入线程");
                        return thread;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy()
        );
        return executor;
    }
}

构建索引方法(两种)

saveForwardIndexes方法(构建正排索引,单线程版本)

// 先批量生成、保存正排索引(单线程版本)
    public void saveForwardIndexes(List<Document> documentList) {
        // 1. 批量插入时,每次插入多少条记录(由于每条记录比较大,所以这里使用 10 条就够了)
        int batchSize = 10;
        // 2. 一共需要执行多少次 SQL?   向上取整(documentList.size() / batchSize)
        int listSize = documentList.size();
        int times = (int) Math.ceil(1.0 * listSize / batchSize);    // ceil(天花板): 向上取整
        log.debug("一共需要 {} 批任务。", times);
        // 3. 开始分批次插入
        for (int i = 0; i < listSize; i += batchSize) {
            // 从 documentList 中截取这批要插入的 文档列表(使用 List.subList(int from, int to)
            int from = i;
            int to = Integer.min(from + batchSize, listSize);
            List<Document> subList = documentList.subList(from, to);
            // 针对这个 subList 做批量插入
            mapper.batchInsertForwardIndexes(subList);
        }
    }

2.png

下面我们可以对上述的单线程的插入方法进行一个改动,加入多线程


saveForwardIndexesConcurrentf方法(构建 + 保存正排索引 多线程版本)

2.png

@Timing("构建 + 保存正排索引 —— 多线程版本")
    @SneakyThrows
    public void saveForwardIndexesConcurrent(List<Document> documentList) {
        // 1. 批量插入时,每次插入多少条记录(由于每条记录比较大,所以这里使用 10 条就够了)
        int batchSize = 10;
        // 2. 一共需要执行多少次 SQL?   向上取整(documentList.size() / batchSize)
        int listSize = documentList.size();
        int times = (int) Math.ceil(1.0 * listSize / batchSize);    // ceil(天花板): 向上取整
        log.debug("一共需要 {} 批任务。", times);
        CountDownLatch latch = new CountDownLatch(times);   // 统计每个线程的完全情况,初始值是 times(一共多少批)
        // 3. 开始分批次插入
        for (int i = 0; i < listSize; i += batchSize) {
            // 从 documentList 中截取这批要插入的 文档列表(使用 List.subList(int from, int to)
            int from = i;
            int to = Integer.min(from + batchSize, listSize);
            Runnable task = new Runnable() {
                @Override
                public void run() { // 内部类 / lambda 表达式里如果用到了外部变量,外部变量必须的 final(或者隐式 final 的变量)
                    List<Document> subList = documentList.subList(from, to);
                    // 针对这个 subList 做批量插入
                    mapper.batchInsertForwardIndexes(subList);
                    latch.countDown();      //  每次任务完成之后,countDown(),让 latch 的个数减一
                }
            };
            executorService.submit(task);   // 主线程只负责把一批批的任务提交到线程池,具体的插入工作,由线程池中的线程完成
        }
        // 4. 循环结束,只意味着主线程把任务提交完成了,但任务有没有做完是不知道的
        // 主线程等在 latch 上,直到 latch 的个数变成 0,也就是所有任务都已经执行完了
        latch.await();
    }

构建倒排索引子模块

引入Ansj分词库

我们在将单词存入倒排索引表中的时候,其实是将正排索引表中存储的标题还有内容进行分词,统计权重后才存入表中的,而分词的操作中,我们需要引入分词库ansj

        <!-- Java 版本的一个分词库,本身是支持中文分词的,只是咱的文档中没有中文。但英文分词它也支持 -->
    <!-- https://github.com/NLPchina/ansj_seg -->
    <dependency>
      <groupId>org.ansj</groupId>
      <artifactId>ansj_seg</artifactId>
      <version>5.1.6</version>
    </dependency>

倒排索引实体类

package com.peixinchen.searcher.indexer.model;
import lombok.Data;
// 这个对象映射 inverted_indexes 表中的一条记录(我们不关心表中的 id,就不写 id 了)
@Data
public class InvertedRecord {
    private String word;
    private int docId;
    private int weight;
    public InvertedRecord(String word, int docId, int weight) {
        this.word = word;
        this.docId = docId;
        this.weight = weight;
    }
}

mapper接口

   void batchInsertInvertedIndexes(@Param("list") List<InvertedRecord> recordList);

xml文件

<!-- 不关心自增 id -->
    <insert id="batchInsertInvertedIndexes">
        insert into inverted_indexes (word, docid, weight) values
        <foreach collection="list" item="record" separator=", ">
            (#{record.word}, #{record.docId}, #{record.weight})
        </foreach>
    </insert>

构建索引方法(两种)

saveInvertedIndexes方法(构建倒排索引,单线程版本)

首先在Document类中构建我们文档的分词以及计算权重方法:

private final static HashSet<String> ignoredWordSet = new HashSet<>();
    static {
        ignoredWordSet.add(" ");
        ignoredWordSet.add("\t");
        ignoredWordSet.add("。");
        ignoredWordSet.add(".");
        ignoredWordSet.add(",");
        ignoredWordSet.add("(");
        ignoredWordSet.add(")");
        ignoredWordSet.add("/");
        ignoredWordSet.add("-");
        ignoredWordSet.add(";");
    }
    // 针对文档进行分词,并且分别计算每个词的权重
    public Map<String, Integer> segWordAndCalcWeight() {
        // 统计标题中的每个词出现次数 | 分词:标题有哪些词
        List<String> wordInTitle = ToAnalysis.parse(title)
                .getTerms()
                .stream()
                .parallel()
                .map(Term::getName)
                .filter(s -> !ignoredWordSet.contains(s))
                .collect(Collectors.toList());
        // 统计标题中,每个词的出现次数 | 统计次数
        Map<String, Integer> titleWordCount = new HashMap<>();
        for (String word : wordInTitle) {
            int count = titleWordCount.getOrDefault(word, 0);
            titleWordCount.put(word, count + 1);
        }
        // 统计内容中的词,以及词的出现次数
        List<String> wordInContent = ToAnalysis.parse(content)
                .getTerms()
                .stream()
                .parallel()
                .map(Term::getName)
                .filter(s -> !ignoredWordSet.contains(s))
                .collect(Collectors.toList());
        Map<String, Integer> contentWordCount = new HashMap<>();
        for (String word : wordInContent) {
            int count = contentWordCount.getOrDefault(word, 0);
            contentWordCount.put(word, count + 1);
        }
        // 计算权重值
        Map<String, Integer> wordToWeight = new HashMap<>();
        // 先计算出有哪些词,不重复
        Set<String> wordSet = new HashSet<>(wordInTitle);
        wordSet.addAll(wordInContent);
        for (String word : wordSet) {
            int titleCount = titleWordCount.getOrDefault(word, 0);
            int contentCount = contentWordCount.getOrDefault(word, 0);
            int weight = titleCount * 10 + contentCount;
            wordToWeight.put(word, weight);
        }
        return wordToWeight;
    }

然后写saveInvertedIndexes方法:

//保存倒排索引,单线程版本
    @SneakyThrows
    public void saveInvertedIndexes(List<Document> documentList) {
        int batchSize = 10000;  /* 批量插入时,最多 10000 条 */
        List<InvertedRecord> recordList = new ArrayList<>();    // 放这批要插入的数据
        for (Document document : documentList) {
            Map<String, Integer> wordToWeight = document.segWordAndCalcWeight();
            for (Map.Entry<String, Integer> entry : wordToWeight.entrySet()) {
                String word = entry.getKey();
                //这个docid是我们构建正排索引的时候数据库的自增id
                int docId = document.getDocId();
                int weight = entry.getValue();
                InvertedRecord record = new InvertedRecord(word, docId, weight);
                recordList.add(record);
                // 如果 recordList.size() == batchSize,说明够一次插入了
                if (recordList.size() == batchSize) {
                    // 每次批量插入1000条数据
                    mapper.batchInsertInvertedIndexes(recordList);
                    // 批量插入后清空 list,视为让 recordList.size() = 0
                    recordList.clear();
                }
            }
        }
        // recordList 还剩一些,之前放进来,但还不够 batchSize 个的,所以最后再批量插入一次
        mapper.batchInsertInvertedIndexes(recordList);  // 批量插入
        recordList.clear();
    }

saveForwardIndexesConcurrent方法(构建倒排索引,多线程版本)

2.png


  static class InvertedInsertTask implements Runnable {
        private final CountDownLatch latch;
        private final int batchSize;
        private final List<Document> documentList;
        private final IndexDatabaseMapper mapper;
        InvertedInsertTask(CountDownLatch latch, int batchSize, List<Document> documentList, IndexDatabaseMapper mapper) {
            this.latch = latch;
            this.batchSize = batchSize;
            this.documentList = documentList;
            this.mapper = mapper;
        }
        @Override
        public void run() {
            List<InvertedRecord> recordList = new ArrayList<>();    // 放这批要插入的数据
            for (Document document : documentList) {
                Map<String, Integer> wordToWeight = document.segWordAndCalcWeight();
                for (Map.Entry<String, Integer> entry : wordToWeight.entrySet()) {
                    String word = entry.getKey();
                    //这个id是前面插入正排索引的时候赋给Document实体类的id
                    int docId = document.getDocId();
                    int weight = entry.getValue();
                    InvertedRecord record = new InvertedRecord(word, docId, weight);
                    recordList.add(record);
                    // 如果 recordList.size() == batchSize,说明够一次插入了
                    if (recordList.size() == batchSize) {
                        mapper.batchInsertInvertedIndexes(recordList);  // 批量插入
                        recordList.clear();                             // 清空 list,视为让 list.size() = 0
                    }
                }
            }
            // recordList 还剩一些,之前放进来,但还不够 batchSize 个的,所以最后再批量插入一次
            mapper.batchInsertInvertedIndexes(recordList);  // 批量插入
            recordList.clear();
            latch.countDown();
        }
    }
    @Timing("构建 + 保存倒排索引 —— 多线程版本")
    @SneakyThrows
    public void saveInvertedIndexesConcurrent(List<Document> documentList) {
        int batchSize = 10000;  // 批量插入时,最多 10000 条
        //每个线程每次处理50篇文档
        int groupSize = 50;
        int listSize = documentList.size();
        int times = (int) Math.ceil(listSize * 1.0 / groupSize);
        CountDownLatch latch = new CountDownLatch(times);
        for (int i = 0; i < listSize; i += groupSize) {
            int from = i;
            int to = Integer.min(from + groupSize, listSize);
            //每次只截取50个
            List<Document> subList = documentList.subList(from, to);
            Runnable task = new InvertedInsertTask(latch, batchSize, subList, mapper);
            executorService.submit(task);
        }
        latch.await();
    }

统计正派倒排索引的构建时间

我们使用aop进行记时操作

定义接口

package com.peixinchen.searcher.indexer.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 用来统计时间的
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Timing {
    String value();
}

定义切面类

package com.peixinchen.searcher.indexer.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Slf4j
@Aspect
@Component
public class TimingAspect {
    @Pointcut("@annotation(com.peixinchen.searcher.indexer.aop.Timing)")
    public void timingPointcut() {}
    @Around("timingPointcut()")
    public Object timing(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Timing annotation = method.getAnnotation(Timing.class);
        String value = annotation.value();
        long b = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            long e = System.currentTimeMillis();
            double ms = e * 1.0 - b;
            double s = ms / 1000;
            log.info("{} 耗时 {} 秒。", value, s);
        }
    }
}

插入正排索引的时间1.005秒为:

2.png

插入倒排索引的时间为123.339秒:

2.png

相关文章
|
7月前
|
Web App开发 缓存 前端开发
前端性能优化的整理笔记(一)
前端性能优化的整理笔记(一)
165 0
|
7月前
|
人工智能 算法
【阅读】一周翻过《构建之法》,笔记整理
🚩 前言 我的阅读方式 我拿到这本书挺久了,之前已经零散地看过一部分,最近一周集中地花了一些时间,将整本书看过了一遍。看得比较粗略,正如“好读书,不求甚解”(我甚至没有去看书中提到的那些参考资料)。
52 0
|
SQL 数据采集 关系型数据库
大数据采集和抽取怎么做?这篇文章终于说明白了!
数据是数据中台\数据平台核心中的核心,因此数据汇聚必然是数据中台/平台的入口,本文详细讲述采集模块的方方面面、采集框架的使用选型以及企业真实落地
大数据采集和抽取怎么做?这篇文章终于说明白了!
|
7月前
|
缓存 前端开发 JavaScript
前端性能优化的整理笔记(二)
前端性能优化的整理笔记(二)
109 0
|
监控 项目管理
通俗易懂的方式理解项目管理的49个过程(追妹子案例)
通俗易懂的方式理解项目管理的49个过程(追妹子案例)
148 0
|
传感器
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
时隔这么长时间,我把常用的功能整理好了,再来感受VueUse工具库的优雅吧~
|
uml 开发者 Windows
推荐5款冷门小工具,看一看有没有你喜欢的?
每个人的电脑中都会安装很多软件,可能还保留着很多不为人知的冷门软件。不过虽然冷门,但绝不意味着低能,相反很多冷门软件的功能十分出色。闲话少说,接下来我就给大家推荐5款冷门小工具,看一看有没有你喜欢的。
193 0
推荐5款冷门小工具,看一看有没有你喜欢的?
|
SQL 自然语言处理 搜索推荐
搜索引擎项目开发过程以及重难点整理(二)
搜索引擎项目开发过程以及重难点整理(二)
150 0
搜索引擎项目开发过程以及重难点整理(二)
|
测试技术
初级软件测试面试题怎么找?提供的这两个地方你肯定用得上
最近几年,随着电子产品和互联网的蓬勃发展,各类科技公司如雨后春笋般出现,而软件公司作为科技类公司中的重要组成部分,在这支互联网大军中也占据了重要一席。因而,负责软件问题质检的软件测试岗位也逐渐成了这几年炙手可热的就业岗位之一。
151 0
|
前端开发 测试技术
测试领域专业术语整理-持续更新
测试领域专业术语整理-持续更新
311 0