当前位置 博文首页 > 令狐_JackieHao的博客:【原来那么简单/大数据】随随便便开发一

    令狐_JackieHao的博客:【原来那么简单/大数据】随随便便开发一

    作者:[db:作者] 时间:2021-07-13 10:04

    【1K数据集+SpringBoot+Thymeleaf】基于全文检索技术lucene开发的搜索引擎

    ? 小编最近写的项目=============》有问题咨询可以加小哥微信:JL1714873054。如果觉得不错可以点一波关注,谢谢,后台看到私信一定会回复,没有回复请谅解。可以关注我的微信公众号:CodeLinghu ,预约毕设,预约项目,查看精彩的项目博客分享。

    一、需求分析

    • 实现一个搜索框,能够索引指定数据集(数据取自数据库中)
    • 实现索引内容的展示,图片展示
    • 实现文本分类,排序等基础功能
    • 索引数据量>1K (可自行爬取)
    • 每次完成搜索能够进行一次评价一次检索效率
    • 可做毕设/可做项目/大创

    1.1、项目效果图展示

    1.1.1前端页面展示:


    1.1.2数据库数据展示:

    二、所用技术栈

    1. SpringBoot
    2. Thymeleaf模板渲染
    3. mysql
    4. ik中文分词
    5. lucene 索引
    6. json转换
    7. HTML+CSS+JavaScript

    2.1项目版本信息:

    1. jdk1.8
    2. mysql 5.7.29
    3. lucene 7.7.2
    4. maven 3.6.3
    5. Windows 10系统
    6. IntellJ IDEA 2019 2.14
    7. Navicat for mysql 12.1.2

    三、项目技术理论基础篇

    本项目的重点是搜索/索引。所以我们首先认识一下所谓的搜索功能。

    传统的搜索功能流程如图01-00.

    ? 图01-00

    以上搜索功能是目前企业中较为传统的一种搜索方式,其特点就是数据量少,承载不了高并发。

    本项目所采用的搜索理论基础方案如图01-01.

    ? 图01-01

    使用新方案的优势:

    1. 降低了数据库压力
    2. 提升了数据库访问速度
    3. 通过lucene的API操作索引库访问数据库实现了业务与数据的有效隔离

    数据查询有两种方案:

    • 顺序查询

    所谓顺序查询就是通过用户检索的内容进行字符串匹配,遍历所有的文档,当匹配到相同字符串便查询到当前文件,没有查到则继续扫描下一个文件,直到扫描完成所有文件。

    • 倒排索引

    倒排索引是指先将海量数据进行分词,形成一个索引表,查询时先查询索引表,通过索引表查询指定文件,这样可以做到有效去重查询相同内容文本的时间。为了做到倒排索引,我们才用的则是全文检索技术------lucene

    3.1、Lucene相关认识(需要你认识到)

    1. lucene是一种技术架构,不是一个成型的技术产品,而是半产品。
    2. lucene是一个工具包,我们可以利用它完成索引工具的开发,制作属于自己的搜索引擎产品
    3. Lucene在Java开发环境里是一个免费成熟的源代码工具
    4. Lucene可以通过官方网站下载,当然我也会提供下载包链接()
    5. Lucene是Apache公司的产品
    6. Lucene实现全文检索的基本流程图:

    • 原始文档数据:

      • 可以自行爬取数据,也可以用小哥提供的文档数据
      • 文档数据放在小哥配套的文件夹(DataSources)里,是一个mysql文件,大家可以直接导入mysql即可。
    • 文档:

      • 拿到原始文档数据是为了建立索引,在索引前需要将原始内容创建文当 Document文档 Document中包含了许多 Field

    • 分析文档(分词):

      分析文档就是分词。将文档中的内容进行词组划分。

    • 索引文档:

      索引文档是为了更好地搜索。分词形成了词汇单元,通过索引词汇单元快速找到需要被索引到的内容。

    四、项目实战篇

    4.1 Lucene的下载

    可以通过官方网站下载lucene,也可以在小哥留的资料包里下载

    解压后:

    PS:queryparser:查询解析器

    使用以上三个文件就可以实现本次项目中Lucene的功能。

    4.2数据源下载

    也在这个文件夹下面:

    导入到mysql的效果:

    4.3Java工程的创建

    使用 DAO接口实现类获取mysql中的数据:

    package cn.linghu.dao;
    
    import cn.linghu.pojo.Sku;
    
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     *
     */
    public class SkuDaoImpl implements SkuDao {
    
        public List<Sku> querySkuList() {
            // 数据库链接
            Connection connection = null;
            // 预编译statement
            PreparedStatement preparedStatement = null;
            // 结果集
            ResultSet resultSet = null;
            // 商品列表
            List<Sku> list = new ArrayList<Sku>();
    
            try {
                // 加载数据库驱动
                Class.forName("com.mysql.jdbc.Driver");
                // 连接数据库
                connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/lucene", "root", "123456");
    
                // SQL语句
                String sql = "SELECT * FROM tb_sku";
                // 创建preparedStatement
                preparedStatement = connection.prepareStatement(sql);
                // 获取结果集
                resultSet = preparedStatement.executeQuery();
                // 结果集解析
                while (resultSet.next()) {
                    Sku sku = new Sku();
                    sku.setId(resultSet.getString("id"));
                    sku.setName(resultSet.getString("name"));
                    sku.setSpec(resultSet.getString("spec"));
                    sku.setBrandName(resultSet.getString("brand_name"));
                    sku.setCategoryName(resultSet.getString("category_name"));
                    sku.setImage(resultSet.getString("image"));
                    sku.setNum(resultSet.getInt("num"));
                    sku.setPrice(resultSet.getInt("price"));
                    sku.setSaleNum(resultSet.getInt("sale_num"));
                    list.add(sku);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
            return list;
        }
    }
    
    

    4.3.1核心代码------实现索引流程:

    \1. 采集数据
    
    \2. 创建Document文档对象
    
    \3. 创建分析器(分词器)
    
    \4. 创建IndexWriterConfifig配置信息类
    
    \5. 创建Directory对象,声明索引库存储位置
    
    \6. 创建IndexWriter写入对象
    
    \7. 把Document写入到索引库中
    
    \8. 释放资源
        
    package cn.linghu.test;
    
    import cn.linghu.dao.SkuDao;
    import cn.linghu.dao.SkuDaoImpl;
    import cn.linghu.pojo.Sku;
    import org.apache.lucene.analysis.Analyzer;
    import org.apache.lucene.analysis.standard.StandardAnalyzer;
    import org.apache.lucene.document.*;
    import org.apache.lucene.index.IndexWriter;
    import org.apache.lucene.index.IndexWriterConfig;
    import org.apache.lucene.index.Term;
    import org.apache.lucene.store.Directory;
    import org.apache.lucene.store.FSDirectory;
    import org.apache.lucene.store.MMapDirectory;
    import org.junit.Test;
    import org.wltea.analyzer.lucene.IKAnalyzer;
    
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 索引库维护
     */
    public class TestIndexManager {
    
    
        /**
         * 创建索引库
         */
        @Test
        public void createIndexTest() throws Exception {
            //1. 采集数据
            SkuDao skuDao = new SkuDaoImpl();
            List<Sku> skuList = skuDao.querySkuList();
    
            //文档集合
            List<Document> docList = new ArrayList<>();
    
            for (Sku sku : skuList) {
                //2. 创建文档对象
                Document document = new Document();
    
                //创建域对象并且放入文档对象中
                /**
                 * 是否分词: 否, 因为主键分词后无意义
                 * 是否索引: 是, 如果根据id主键查询, 就必须索引
                 * 是否存储: 是, 因为主键id比较特殊, 可以确定唯一的一条数据, 在业务上一般有重要所用, 所以存储
                 *      存储后, 才可以获取到id具体的内容
                 */
                document.add(new StringField("id", sku.getId(), Field.Store.YES));
    
                /**
                 * 是否分词: 是, 因为名称字段需要查询, 并且分词后有意义所以需要分词
                 * 是否索引: 是, 因为需要根据名称字段查询
                 * 是否存储: 是, 因为页面需要展示商品名称, 所以需要存储
                 */
                document.add(new TextField("name", sku.getName(), Field.Store.YES));
    
                /**
                 * 是否分词: 是(因为lucene底层算法规定, 如果根据价格范围查询, 必须分词)
                 * 是否索引: 是, 需要根据价格进行范围查询, 所以必须索引
                 * 是否存储: 是, 因为页面需要展示价格
                 */
                document.add(new IntPoint("price", sku.getPrice()));
                document.add(new StoredField("price", sku.getPrice()));
    
                /**
                 * 是否分词: 否, 因为不查询, 所以不索引, 因为不索引所以不分词
                 * 是否索引: 否, 因为不需要根据图片地址路径查询
                 * 是否存储: 是, 因为页面需要展示商品图片
                 */
                document.add(new StoredField("image", sku.getImage()));
    
                /**
                 * 是否分词: 否, 因为分类是专有名词, 是一个整体, 所以不分词
                 * 是否索引: 是, 因为需要根据分类查询
                 * 是否存储: 是, 因为页面需要展示分类
                 */
                document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));
    
                /**
                 * 是否分词: 否, 因为品牌是专有名词, 是一个整体, 所以不分词
                 * 是否索引: 是, 因为需要根据品牌进行查询
                 * 是否存储: 是, 因为页面需要展示品牌
                 */
                document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));
    
                //将文档对象放入到文档集合中
                docList.add(