Spring Boot中集成Lucence

在线体验各类最新模型,更有模型 免费Token 额度领取!
立即体验
简介: Lucene是Apache开源的全文检索库,通过分词、建立索引实现高效搜索。本文介绍其原理,并在Spring Boot中集成,实现中文分词、高亮显示等实战功能,适用于文件搜索、内容检索等场景。
  1. Lucence 和全文检索
    Lucene 是什么?看一下百度百科:
    Lucene是一套用于全文检索和搜寻的开源程式库,由 Apache 软件基金会支持和提供。Lucene 提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。——《百度百科》
    1.1 全文检索
    这里提到了全文检索的概念,我们先来分析一下什么是全文检索,理解了全文检索之后,再理解 Lucene 的原理就非常简单了。
    何为全文检索?举个例子,比如现在要在一个文件中查找某个字符串,最直接的想法就是从头开始检索,查到了就OK,这种对于小数据量的文件来说,很实用,但是对于大数据量的文件来说,就有点吃力了。或者说找包含某个字符串的文件,也是这样,如果在一个拥有几十个 G 的硬盘中找那效率可想而知,是很低的。
    文件中的数据是属于非结构化数据,也就是说它没有什么结构可言,要解决上面提到的效率问题,首先我们得将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对这些有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这就叫全文搜索。即先建立索引,再对索引进行搜索的过程。
    1.2 Lucene 建立索引的方式
    那么 Lucene 中是如何建立索引的呢?假设现在有两篇文章,内容如下:
    文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too. 文章2的内容为:He once lived in Shanghai.
    首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符号和停词。所谓的停词指的是没有特别意义的词,比如英文中的 a,the,too 等。经过分词后,得到词元(Token) 。如下:
    文章1经过分词后的结果:[Tom] [lives] [Guangzhou] [I] [live] [Guangzhou] 文章2经过分词后的结果:[He] [lives] [Shanghai]
    然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小写,将单词缩减为词根形式,如 ”lives” 到 ”live” 等,将单词转变为词根形式,如 ”drove” 到 ”drive” 等。然后得到词(Term)。如下:
    文章1经过处理后的结果:[tom] [live] [guangzhou] [i] [live] [guangzhou] 文章2经过处理后的结果:[he] [live] [shanghai]
    最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:
    关键词 文章号[出现频率] 出现位置
    guangzhou 1[2] 3,6
    he 2[1] 1
    i 1[1] 4
    live 1[2],2[1] 2,5,2
    shanghai 2[1] 3
    tom 1[1] 1
    以上就是Lucene 索引结构中最核心的部分。它的关键字是按字符顺序排列的,因此 Lucene 可以用二元搜索算法快速定位关键词。实现时 Lucene 将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息。 搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果,然后就可以在具体的文章中根据出现位置找到该词了。所以 Lucene 在第一次建立索引的时候可能会比较慢,但是以后就不需要每次都建立索引了,就快了。
    理解了 Lucene 的分词原理,接下来我们在 Spring Boot 中集成 Lucene 并实现索引和搜索的功能。
  2. Spring Boot 中集成 Lucence
    2.1 依赖导入
    首先需要导入 Lucene 的依赖,它的依赖有好几个,如下:
    org.apache.lucene
    lucene-core
    5.3.1

org.apache.lucene
lucene-queryparser
5.3.1

org.apache.lucene
lucene-analyzers-common
5.3.1

org.apache.lucene
lucene-highlighter
5.3.1


org.apache.lucene
lucene-analyzers-smartcn
5.3.1

最后一个依赖是用来支持中文分词的,因为默认是支持英文的。那个高亮的分词依赖是最后我要做一个搜索,然后将搜到的内容高亮显示,模拟当前互联网上的做法,大家可以运用到实际项目中去。
2.2 快速入门
根据上文的分析,全文检索有两个步骤,先建立索引,再检索。所以为了测试这个过程,我新建两个 java 类,一个用来建立索引的,另一个用来检索。
2.2.1 建立索引
我们自己弄几个文件,放到 D:\lucene\data 目录下,新建一个 Indexer 类来实现建立索引功能。首先在构造方法中初始化标准分词器和写索引实例。
public class Indexer {

/**
 * 写索引实例
 */
private IndexWriter writer;

/**
 * 构造方法,实例化IndexWriter
 * @param indexDir
 * @throws Exception
 */
public Indexer(String indexDir) throws Exception {
    Directory dir = FSDirectory.open(Paths.get(indexDir));
    //标准分词器,会自动去掉空格啊,is a the等单词
    Analyzer analyzer = new StandardAnalyzer();
    //将标准分词器配到写索引的配置中
    IndexWriterConfig config = new IndexWriterConfig(analyzer);
    //实例化写索引对象
    writer = new IndexWriter(dir, config);
}

}
在构造放发中传一个存放索引的文件夹路径,然后构建标准分词器(这是英文的),再使用标准分词器来实例化写索引对象。接下来就开始建立索引了,我将解释放到代码注释里,方便大家跟进。
/**

  • 索引指定目录下的所有文件
  • @param dataDir
  • @return
  • @throws Exception
    */
    public int indexAll(String dataDir) throws Exception {
    // 获取该路径下的所有文件
    File[] files = new File(dataDir).listFiles();
    if (null != files) {
     for (File file : files) {
         //调用下面的indexFile方法,对每个文件进行索引
         indexFile(file);
     }
    
    }
    //返回索引的文件数
    return writer.numDocs();
    }

/**

  • 索引指定的文件
  • @param file
  • @throws Exception
    */
    private void indexFile(File file) throws Exception {
    System.out.println("索引文件的路径:" + file.getCanonicalPath());
    //调用下面的getDocument方法,获取该文件的document
    Document doc = getDocument(file);
    //将doc添加到索引中
    writer.addDocument(doc);
    }

/**

  • 获取文档,文档里再设置每个字段,就类似于数据库中的一行记录
  • @param file
  • @return
  • @throws Exception
    */
    private Document getDocument(File file) throws Exception {
    Document doc = new Document();
    //开始添加字段
    //添加内容
    doc.add(new TextField("contents", new FileReader(file)));
    //添加文件名,并把这个字段存到索引文件里
    doc.add(new TextField("fileName", file.getName(), Field.Store.YES));
    //添加文件路径
    doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES));
    return doc;
    }
    这样就建立好索引了,我们在该类中写一个 main 方法测试一下:
    public static void main(String[] args) {

     //索引保存到的路径
     String indexDir = "D:\\lucene";
     //需要索引的文件数据存放的目录
     String dataDir = "D:\\lucene\\data";
     Indexer indexer = null;
     int indexedNum = 0;
     //记录索引开始时间
     long startTime = System.currentTimeMillis();
     try {
         // 开始构建索引
         indexer = new Indexer(indexDir);
         indexedNum = indexer.indexAll(dataDir);
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
         try {
             if (null != indexer) {
                 indexer.close();
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
     }
     //记录索引结束时间
     long endTime = System.currentTimeMillis();
     System.out.println("索引耗时" + (endTime - startTime) + "毫秒");
     System.out.println("共索引了" + indexedNum + "个文件");
    

    }
    我搞了两个 tomcat 相关的文件放到 D:\lucene\data 下了,执行完之后,看到控制台输出:
    索引文件的路径:D:\lucene\data\catalina.properties
    索引文件的路径:D:\lucene\data\logging.properties
    索引耗时882毫秒
    共索引了2个文件
    然后我们去 D:\lucene\ 目录下可以看到一些索引文件,这些文件不能删除,删除了就需要重新构建索引,否则没了索引,就无法去检索内容了。

    2.2.2 检索内容

    上面把这两个文件的索引建立好了,接下来我们就可以写检索程序了,在这两个文件中查找特定的词。
    public class Searcher {

    public static void search(String indexDir, String q) throws Exception {

     //获取要查询的路径,也就是索引所在的位置
     Directory dir = FSDirectory.open(Paths.get(indexDir));
     IndexReader reader = DirectoryReader.open(dir);
     //构建IndexSearcher
     IndexSearcher searcher = new IndexSearcher(reader);
     //标准分词器,会自动去掉空格啊,is a the等单词
     Analyzer analyzer = new StandardAnalyzer();
     //查询解析器
     QueryParser parser = new QueryParser("contents", analyzer);
     //通过解析要查询的String,获取查询对象,q为传进来的待查的字符串
     Query query = parser.parse(q);
    
     //记录索引开始时间
     long startTime = System.currentTimeMillis();
     //开始查询,查询前10条数据,将记录保存在docs中
     TopDocs docs = searcher.search(query, 10);
     //记录索引结束时间
     long endTime = System.currentTimeMillis();
     System.out.println("匹配" + q + "共耗时" + (endTime-startTime) + "毫秒");
     System.out.println("查询到" + docs.totalHits + "条记录");
    
     //取出每条查询结果
     for(ScoreDoc scoreDoc : docs.scoreDocs) {
         //scoreDoc.doc相当于docID,根据这个docID来获取文档
         Document doc = searcher.doc(scoreDoc.doc);
         //fullPath是刚刚建立索引的时候我们定义的一个字段,表示路径。也可以取其他的内容,只要我们在建立索引时有定义即可。
         System.out.println(doc.get("fullPath"));
     }
     reader.close();
    

    }
    }
    ok,这样我们检索的代码就写完了,每一步解释我写在代码中的注释上了,下面写个 main 方法来测试一下:
    public static void main(String[] args) {
    String indexDir = "D:\lucene";
    //查询这个字符串
    String q = "security";
    try {

     search(indexDir, q);
    

    } catch (Exception e) {

     e.printStackTrace();
    

    }
    }
    查一下 security 这个字符串,执行一下看控制台打印的结果:
    匹配security共耗时23毫秒
    查询到1条记录
    D:\lucene\data\catalina.properties
    可以看出,耗时了23毫秒在两个文件中找到了 security 这个字符串,并输出了文件的名称。上面的代码我写的很详细,这个代码已经比较全了,可以用在生产环境上。
    2.3 中文分词检索高亮实战
    上文已经写了建立索引和检索的代码,但是在实际项目中,我们往往是结合页面做一些查询结果的展示,比如我要查某个关键字,查到了之后,将相关的信息点展示出来,并将查询的关键字高亮等等。这种需求在实际项目中非常常见,而且大多数网站中都会有这种效果。所以这一小节我们就使用 Lucene 来实现这种效果。
    2.3.1 中文分词
    我们新建一个 ChineseIndexer 类来建立中文索引,建立过程和英文索引一样的,不同的地方在于使用的是中文分词器。除此之外,这里我们不用通过读取文件去建立索引,我们模拟一下用字符串来建立,因为在实际项目中,绝大部分情况是获取到一些文本字符串,然后根据一些关键字去查询相关内容等等。代码如下:
    public class ChineseIndexer {

    /**

    • 存放索引的位置
      */
      private Directory dir;

      //准备一下用来测试的数据
      //用来标识文档
      private Integer ids[] = {1, 2, 3};
      private String citys[] = {"上海", "南京", "青岛"};
      private String descs[] = {

       "上海是个繁华的城市。",
       "南京是一个文化的城市南京,简称宁,是江苏省会,地处中国东部地区,长江下游,濒江近海。全市下辖11个区,总面积6597平方公里,2013年建成区面积752.83平方公里,常住人口818.78万,其中城镇人口659.1万人。[1-4] “江南佳丽地,金陵帝王州”,南京拥有着6000多年文明史、近2600年建城史和近500年的建都史,是中国四大古都之一,有“六朝古都”、“十朝都会”之称,是中华文明的重要发祥地,历史上曾数次庇佑华夏之正朔,长期是中国南方的政治、经济、文化中心,拥有厚重的文化底蕴和丰富的历史遗存。[5-7] 南京是国家重要的科教中心,自古以来就是一座崇文重教的城市,有“天下文枢”、“东南第一学”的美誉。截至2013年,南京有高等院校75所,其中211高校8所,仅次于北京上海;国家重点实验室25所、国家重点学科169个、两院院士83人,均居中国第三。[8-10] 。",
       "青岛是一个美丽的城市。"
      

      };

      /**

    • 生成索引
    • @param indexDir
    • @throws Exception
      */
      public void index(String indexDir) throws Exception {
      dir = FSDirectory.open(Paths.get(indexDir));
      // 先调用 getWriter 获取IndexWriter对象
      IndexWriter writer = getWriter();
      for(int i = 0; i < ids.length; i++) {

       Document doc = new Document();
       // 把上面的数据都生成索引,分别用id、city和desc来标识
       doc.add(new IntField("id", ids[i], Field.Store.YES));
       doc.add(new StringField("city", citys[i], Field.Store.YES));
       doc.add(new TextField("desc", descs[i], Field.Store.YES));
       //添加文档
       writer.addDocument(doc);
      

      }
      //close了才真正写到文档中
      writer.close();
      }

      /**

    • 获取IndexWriter实例
    • @return
    • @throws Exception
      */
      private IndexWriter getWriter() throws Exception {
      //使用中文分词器
      SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
      //将中文分词器配到写索引的配置中
      IndexWriterConfig config = new IndexWriterConfig(analyzer);
      //实例化写索引对象
      IndexWriter writer = new IndexWriter(dir, config);
      return writer;
      }

      public static void main(String[] args) throws Exception {
      new ChineseIndexer().index("D:\lucene2");
      }
      }
      这里我们用 id、city、desc 分别代表 id、城市名称和城市描述,用他们作为关键字来建立索引,后面我们获取内容的时候,主要来获取城市描述。南京的描述我故意写的长一点,因为下文检索的时候,根据不同的关键字会检索到不同部分的信息,有个权重的概念在里面。然后执行一下 main 方法,将索引保存到 D:\lucene2\ 中。
      2.3.2 中文分词查询
      中文分词查询代码逻辑和默认的查询差不多,有一些区别在于,我们需要将查询出来的关键字标红加粗等需要处理,需要计算出一个得分片段,这是什么意思呢?比如我搜索 “南京文化” 跟搜索 “南京文明”,这两个搜索结果应该根据关键字出现的位置,返回的结果不一样才对,这在下文会测试。我们先看一下代码和注释:
      public class ChineseSearch {

      private static final Logger logger = LoggerFactory.getLogger(ChineseSearch.class);

      public static List search(String indexDir, String q) throws Exception {

      //获取要查询的路径,也就是索引所在的位置
      Directory dir = FSDirectory.open(Paths.get(indexDir));
      IndexReader reader = DirectoryReader.open(dir);
      IndexSearcher searcher = new IndexSearcher(reader);
      //使用中文分词器
      SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer();
      //由中文分词器初始化查询解析器
      QueryParser parser = new QueryParser("desc", analyzer);
      //通过解析要查询的String,获取查询对象
      Query query = parser.parse(q);

      //记录索引开始时间
      long startTime = System.currentTimeMillis();
      //开始查询,查询前10条数据,将记录保存在docs中
      TopDocs docs = searcher.search(query, 10);
      //记录索引结束时间
      long endTime = System.currentTimeMillis();
      logger.info("匹配{}共耗时{}毫秒", q, (endTime - startTime));
      logger.info("查询到{}条记录", docs.totalHits);

      //如果不指定参数的话,默认是加粗,即
      SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("","");
      //根据查询对象计算得分,会初始化一个查询结果最高的得分
      QueryScorer scorer = new QueryScorer(query);
      //根据这个得分计算出一个片段
      Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);
      //将这个片段中的关键字用上面初始化好的高亮格式高亮
      Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer);
      //设置一下要显示的片段
      highlighter.setTextFragmenter(fragmenter);

      //取出每条查询结果
      List list = new ArrayList<>();
      for(ScoreDoc scoreDoc : docs.scoreDocs) {

       //scoreDoc.doc相当于docID,根据这个docID来获取文档
       Document doc = searcher.doc(scoreDoc.doc);
       logger.info("city:{}", doc.get("city"));
       logger.info("desc:{}", doc.get("desc"));
       String desc = doc.get("desc");
      
       //显示高亮
       if(desc != null) {
           TokenStream tokenStream = analyzer.tokenStream("desc", new StringReader(desc));
           String summary = highlighter.getBestFragment(tokenStream, desc);
           logger.info("高亮后的desc:{}", summary);
           list.add(summary);
       }
      

      }
      reader.close();
      return list;
      }
      }
      每一步的注释我写的很详细,在这就不赘述了。接下来我们来测试一下效果。
      2.3.3 测试一下
      这里我们使用 thymeleaf 来写个简单的页面来展示获取到的数据,并高亮展示。在 controller 中我们指定索引的目录和需要查询的字符串,如下:
      @Controller
      @RequestMapping("/lucene")
      public class IndexController {

      @GetMapping("/test")
      public String test(Model model) {
      // 索引所在的目录
      String indexDir = "D:\lucene2";
      // 要查询的字符
      // String q = "南京文明";
      String q = "南京文化";
      try {

       List<String> list = ChineseSearch.search(indexDir, q);
       model.addAttribute("list", list);
      

      } catch (Exception e) {

       e.printStackTrace();
      

      }
      return "result";
      }
      }
      直接返回到 result.html 页面,该页面主要来展示一下 model 中的数据即可。
      <!DOCTYPE html>












这里注意一下,不能使用 th:test,否则字符串中的 html 标签都会被转义,不会被渲染到页面。下面启动服务,在浏览器中输入 http://localhost:8080/lucene/test,测试一下效果,我们搜索的是 “南京文化”。
image.png
再将 controller 中的搜索关键字改成 “南京文明”,看下命中的效果。
image.png
可以看出,不同的关键词,它会计算一个得分片段,也就是说不同的关键字会命中不同位置的内容,然后将关键字根据我们自己设定的形式高亮显示。从结果中可以看出,Lucene 也可以很智能的将关键字拆分命中,这在实际项目中会很好用。
相关文章
|
存储 固态存储 搜索推荐
固态硬盘分区详细指南
本文讨论了固态硬盘(SSD)的分区管理,指出分区与否取决于用户需求。固态硬盘因其高速度和耐用性而普及,但如何正确使用和管理是用户关注的问题。文章介绍了4K对齐的重要性,并提供了使用Win10/Win11系统自带磁盘管理器及第三方工具DiskGenius进行分区的步骤。此外,还解答了关于固态硬盘分区的常见疑问,如分区合并、C盘大小建议、品牌选择及寿命等。
|
SQL XML Java
【mybatis】第二篇:@Select注解中加入字段判断
【mybatis】第二篇:@Select注解中加入字段判断
|
7月前
|
自然语言处理 安全 Java
Spring Boot中集成Lucence
Lucene是Apache开源的全文检索库,基于Java开发,通过分词、建立索引实现高效搜索。本文详解其原理,并结合Spring Boot实现英文索引、中文分词及高亮查询,助你快速掌握全文检索技术。
|
6月前
|
Java Maven
【2026最新】Maven配置阿里云镜像
本文介绍在Maven 3.9与JDK 21环境下,如何配置阿里云镜像加速依赖下载。通过修改Maven的conf/settings.xml文件,在&lt;mirrors&gt;标签中添加阿里云公共仓库镜像配置,提升构建效率。
5248 1
|
8月前
|
机器学习/深度学习 人工智能 前端开发
终端里的 AI 编程助手:OpenCode 使用指南
OpenCode 是开源的终端 AI 编码助手,支持 Claude、GPT-4 等模型,可在命令行完成代码编写、Bug 修复、项目重构。提供原生终端界面和上下文感知能力,适合全栈开发者和终端用户使用。
56492 11
|
10月前
|
人工智能 Java 机器人
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
Spring AI Alibaba集成Ollama,基于Java构建本地大模型应用,支持流式对话、knife4j接口可视化,实现高隐私、免API密钥的离线AI服务。
7275 2
基于Spring AI Alibaba + Spring Boot + Ollama搭建本地AI对话机器人API
|
Cloud Native Java C++
Springboot3新特性:开发第一个 GraalVM 本机应用程序(完整教程)
文章介绍如何在Spring Boot 3中利用GraalVM将Java应用程序编译成独立的本机二进制文件,从而提高启动速度、减少内存占用,并实现不依赖JVM运行。
2605 1
Springboot3新特性:开发第一个 GraalVM 本机应用程序(完整教程)
|
自然语言处理 安全 Java
Spring Boot中集成Lucence
本节课首先详细的分析了全文检索的理论规则,然后结合 Lucene,系统的讲述了在 Spring Boot 的集成步骤,首先快速带领大家从直观上感受 Lucene 如何建立索引已经如果检索,其次通过中文检索的具体实例,展示了 Lucene 在全文检索中的广泛应用。Lucene 不难,主要就是步骤比较多,代码不用死记硬背,拿到项目中根据实际情况做对应的修改即可。
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
11592 134