Lucene轻量级搜索引擎,真的太强了!!!Solr 和 ES 都是基于它

一、基础知识

1、Lucene 是什么

Lucene 是一个本地全文搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封装

Lucene 适合那种轻量级的全文搜索,我就是服务器资源不够,如果上 ES 的话会很占用服务器资源,所有就选择了 Lucene 搜索引擎

2、倒排索引原理

全文搜索的原理是使用了倒排索引,那么什么是倒排索引呢?

  1. 先通过中文分词器,将文档中包含的关键字全部提取出来,比如我爱中国,会通过分词器分成我,爱,中国,然后分别对应'我爱中国'
  2. 然后再将关键字与文档的对应关系保存起来
  3. 最后对关键字本身做索引排序

3、与传统数据库对比

Lucene DB
数据库表(table) 索引(index)
行(row) 文档(document)
列(column) 字段(field)

4、数据类型

常见的字段类型

  1. StringField:这是一个++不可分词的字符串字段类型,适用于精确匹配和排序++。
  2. TextField:这是一个++可分词的字符串字段类型,适用于全文搜索和模糊匹配++。
  3. IntField、LongField、FloatField、DoubleField:这些是数值字段类型,用于存储整数和浮点数。
  4. DateField:这是一个日期字段类型,用于存储日期和时间。
  5. BinaryField:这是一个二进制字段类型,用于存储二进制数据,如图片、文件等。
  6. StoredField:这是一个存储字段类型,用于存储不需要被索引的原始数据,如文档的内容或其他附加信息。

Lucene 分词器是将文本内容分解成单独的词汇(term)的工具。Lucene 提供了多种分词器,其中一些常见的包括

  1. StandardAnalyzer:这是 Lucene 默认的分词器,它使用 UnicodeText 解析器将文本转换为小写字母,并且根据空格、标点符号和其他字符来进行分词。
  2. CJKAnalyzer:这个分词器专门为中日韩语言设计,它可以正确地处理中文、日文和韩文的分词。
  3. KeywordAnalyzer:这是一个不分词的分词器,它将输入的文本作为一个整体来处理,常用于处理精确匹配的情况。
  4. SimpleAnalyzer:这是一个非常简单的分词器,它仅仅按照非字母字符将文本分割成小写词汇。
  5. WhitespaceAnalyzer:这个分词器根据空格将文本分割成小写词汇,不会进行任何其他的处理。

但是对于中文分词器,我们一般常用第三方分词器IKAnalyzer,需要引入它的POM文件

二、最佳实践

1、依赖导入

复制代码
<lucene.version>8.1.1</lucene.version>
<IKAnalyzer-lucene.version>8.0.0</IKAnalyzer-lucene.version>

<!--============lucene start================-->
<!-- Lucene核心库 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-core</artifactId>
    <version>${lucene.version}</version>
</dependency>

<!-- Lucene的查询解析器 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-queryparser</artifactId>
    <version>${lucene.version}</version>
</dependency>

<!-- Lucene的默认分词器库 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analyzers-common</artifactId>
    <version>${lucene.version}</version>
</dependency>

<!-- Lucene的高亮显示 -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-highlighter</artifactId>
    <version>${lucene.version}</version>
</dependency>

<!-- ik分词器 -->
<dependency>
    <groupId>com.jianggujin</groupId>
    <artifactId>IKAnalyzer-lucene</artifactId>
    <version>${IKAnalyzer-lucene.version}</version>
</dependency>
<!--============lucene end================-->

2、创建索引

  1. 先制定索引的基本数据,包括索引名称和字段

    /**

    • @author: sunhhw

    • @date: 2023/12/25 17:39

    • @description: 定义文章文档字段和索引名称
      */
      public interface IArticleIndex {

      /**

      • 索引名称
        */
        String INDEX_NAME = "article";

      // --------------------- 文档字段 ---------------------
      String COLUMN_ID = "id";
      String COLUMN_ARTICLE_NAME = "articleName";
      String COLUMN_COVER = "cover";
      String COLUMN_SUMMARY = "summary";
      String COLUMN_CONTENT = "content";
      String COLUMN_CREATE_TIME = "createTime";
      }

  2. 创建索引并新增文档

    /**

    • 创建索引并设置数据
    • @param indexName 索引地址
      */
      public void addDocument(String indexName, List<Document> documentList) {
      // 配置索引的位置 例如:indexDir = /app/blog/index/article
      String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
      try {
      File file = new File(indexDir);
      // 若不存在,则创建目录
      if (!file.exists()) {
      FileUtils.forceMkdir(file);
      }
      // 读取索引目录
      Directory directory = FSDirectory.open(Paths.get(indexDir));
      // 中文分析器
      Analyzer analyzer = new IKAnalyzer();
      // 索引写出工具的配置对象
      IndexWriterConfig conf = new IndexWriterConfig(analyzer);
      // 创建索引
      IndexWriter indexWriter = new IndexWriter(directory, conf);
      long count = indexWriter.addDocuments(documentList);
      log.info("[批量添加索引库]总数量:{}", documentList.size());
      // 提交记录
      indexWriter.commit();
      // 关闭close
      indexWriter.close();
      } catch (Exception e) {
      log.error("[创建索引失败]indexDir:{}", indexDir, e);
      throw new UtilsException("创建索引失败", e);
      }
      }
  1. 注意这里有个坑,就是这个indexWriter.close();必须要关闭, 不然在执行其他操作的时候会有一个write.lock文件锁控制导致操作失败
  2. indexWriter.addDocuments(documentList)这是批量添加,单个添加可以使用indexWriter.addDocument()
  1. 单元测试

    @Test
    public void create_index_test() {
    ArticlePO articlePO = new ArticlePO();
    articlePO.setArticleName("git的基本使用" + i);
    articlePO.setContent("这里是git的基本是用的内容" + i);
    articlePO.setSummary("测试摘要" + i);
    articlePO.setId(String.valueOf(i));
    articlePO.setCreateTime(LocalDateTime.now());
    Document document = buildDocument(articlePO);
    LuceneUtils.X.addDocument(IArticleIndex.INDEX_NAME, document);
    }

    private Document buildDocument(ArticlePO articlePO) {
    Document document = new Document();
    LocalDateTime createTime = articlePO.getCreateTime();
    String format = LocalDateTimeUtil.format(createTime, DateTimeFormatter.ISO_LOCAL_DATE);

    复制代码
     // 因为ID不需要分词,使用StringField字段
     document.add(new StringField(IArticleIndex.COLUMN_ID, articlePO.getId() == null ? "" : articlePO.getId(), Field.Store.YES));
     // 文章标题articleName需要搜索,所以要分词保存
     document.add(new TextField(IArticleIndex.COLUMN_ARTICLE_NAME, articlePO.getArticleName() == null ? "" : articlePO.getArticleName(), Field.Store.YES));
     // 文章摘要summary需要搜索,所以要分词保存
     document.add(new TextField(IArticleIndex.COLUMN_SUMMARY, articlePO.getSummary() == null ? "" : articlePO.getSummary(), Field.Store.YES));
      // 文章内容content需要搜索,所以要分词保存
     document.add(new TextField(IArticleIndex.COLUMN_CONTENT, articlePO.getContent() == null ? "" : articlePO.getContent(), Field.Store.YES));
     // 文章封面不需要分词,但是需要被搜索出来展示
     document.add(new StoredField(IArticleIndex.COLUMN_COVER, articlePO.getCover() == null ? "" : articlePO.getCover()));
     // 创建时间不需要分词,仅需要展示
     document.add(new StringField(IArticleIndex.COLUMN_CREATE_TIME, format, Field.Store.YES));
     return document;

    }

3、更新文档

  1. 更新索引方法

    /**

    • 更新文档
    • @param indexName 索引地址
    • @param document 文档
    • @param condition 更新条件
      */
      public void updateDocument(String indexName, Document document, Term condition) {
      String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
      try {
      // 读取索引目录
      Directory directory = FSDirectory.open(Paths.get(indexDir));
      // 中文分析器
      Analyzer analyzer = new IKAnalyzer();
      // 索引写出工具的配置对象
      IndexWriterConfig conf = new IndexWriterConfig(analyzer);
      // 创建索引
      IndexWriter indexWriter = new IndexWriter(directory, conf);
      indexWriter.updateDocument(condition, document);
      indexWriter.commit();
      indexWriter.close();
      } catch (Exception e) {
      log.error("[更新文档失败]indexDir:{},document:{},condition:{}", indexDir, document, condition, e);
      throw new ServiceException();
      }
      }
  2. 单元测试

    @Test
    public void update_document_test() {
    ArticlePO articlePO = new ArticlePO();
    articlePO.setArticleName("git的基本使用=编辑");
    articlePO.setContent("这里是git的基本是用的内容=编辑");
    articlePO.setSummary("测试摘要=编辑");
    articlePO.setId("2");
    articlePO.setCreateTime(LocalDateTime.now());
    Document document = buildDocument(articlePO);
    LuceneUtils.X.updateDocument(IArticleIndex.INDEX_NAME, document, new Term("id", "2"));
    }

  1. 更新的时候,如果存在就更新那条记录,如果不存在就会新增一条记录
  2. new Term("id", "2")搜索条件,跟数据库里的where id = 2差不多
  3. IArticleIndex.INDEX_NAME = article 索引名称

4、删除文档

  1. 删除文档方法

    /**

    • 删除文档

    • @param indexName 索引名称

    • @param condition 更新条件
      */
      public void deleteDocument(String indexName, Term condition) {
      String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
      try {
      // 读取索引目录
      Directory directory = FSDirectory.open(Paths.get(indexDir));
      // 索引写出工具的配置对象
      IndexWriterConfig conf = new IndexWriterConfig();
      // 创建索引
      IndexWriter indexWriter = new IndexWriter(directory, conf);

      复制代码
      indexWriter.deleteDocuments(condition);
      indexWriter.commit();
      indexWriter.close();

      } catch (Exception e) {
      log.error("[删除文档失败]indexDir:{},condition:{}", indexDir, condition, e);
      throw new ServiceException();
      }
      }

  2. 单元测试

    @Test
    public void delete_document_test() {
    LuceneUtils.X.deleteDocument(IArticleIndex.INDEX_NAME, new Term(IArticleIndex.COLUMN_ID, "1"));
    }

  1. 删除文档跟编辑文档类似

5、删除索引

把改索引下的数据全部清空

复制代码
/**
* 删除索引
*
* @param indexName 索引地址
*/
public void deleteIndex(String indexName) {
  String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
  try {
      // 读取索引目录
      Directory directory = FSDirectory.open(Paths.get(indexDir));
      // 索引写出工具的配置对象
      IndexWriterConfig conf = new IndexWriterConfig();
      // 创建索引
      IndexWriter indexWriter = new IndexWriter(directory, conf);
      indexWriter.deleteAll();
      indexWriter.commit();
      indexWriter.close();
  } catch (Exception e) {
      log.error("[删除索引失败]indexDir:{}", indexDir, e);
      throw new ServiceException();
  }
}

6、普通查询

  1. TermQuery查询

    Term term = new Term("title", "lucene");
    Query query = new TermQuery(term);

上述代码表示通过精确匹配字段"title"中包含"lucene"的文档。

  1. PhraseQuery查询

    PhraseQuery.Builder builder = new PhraseQuery.Builder();
    builder.add(new Term("content", "open"));
    builder.add(new Term("content", "source"));
    PhraseQuery query = builder.build();

上述代码表示在字段"content"中查找包含"open source"短语的文档

  1. BooleanQuery查询

    TermQuery query1 = new TermQuery(new Term("title", "lucene"));
    TermQuery query2 = new TermQuery(new Term("author", "john"));
    BooleanQuery.Builder builder = new BooleanQuery.Builder();
    builder.add(query1, BooleanClause.Occur.MUST);
    builder.add(query2, BooleanClause.Occur.MUST);
    BooleanQuery query = builder.build();

上述代码表示使用布尔查询同时满足"title"字段包含"lucene"和"author"字段包含"john"的文档。

  1. WildcardQuery查询

    WildcardQuery示例:
    java
    WildcardQuery query = new WildcardQuery(new Term("title", "lu*n?e"));

上述代码表示使用通配符查询匹配"title"字段中以"lu"开头,且第三个字符为任意字母,最后一个字符为"e"的词项

  1. MultiFieldQueryParser查询

    String[] fields = {"title", "content", "author"};
    Analyzer analyzer = new StandardAnalyzer();

    MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer);
    Query query = parser.parse("lucene search");

a. 在"title", "content", "author"三个字段中搜索关键字"lucene search"的文本数据 b. MultiFieldQueryParser 默认使用 OR 运算符将多个字段的查询结果合并,即只要在任意一个字段中匹配成功即

可以使用MultiFieldQueryParser查询来封装一个简单的搜索工具类,这个较为常用

复制代码
/**
* 关键词搜索
*
* @param indexName 索引目录
* @param keyword   查询关键词
* @param columns   被搜索的字段
* @param current   当前页
* @param size      每页数据量
* @return
*/
public List<Document> search(String indexName, String keyword, String[] columns, int current, int size) {
  String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
  try {
      // 打开索引目录
      Directory directory = FSDirectory.open(Paths.get(indexDir));
      IndexReader reader = DirectoryReader.open(directory);
      IndexSearcher searcher = new IndexSearcher(reader);
      // 中文分析器
      Analyzer analyzer = new IKAnalyzer();
      // 查询解析器
      QueryParser parser = new MultiFieldQueryParser(columns, analyzer);
      // 解析查询关键字
      Query query = parser.parse(keyword);
      // 执行搜索,获取匹配查询的前 limit 条结果。
      int limit = current * size;
      // 搜索前 limit 条结果
      TopDocs topDocs = searcher.search(query, limit); 
      // 匹配的文档数组
      ScoreDoc[] scoreDocs = topDocs.scoreDocs;
      // 计算分页的起始 - 结束位置
      int start = (current - 1) * size;
      int end = Math.min(start + size, scoreDocs.length);
      // 返回指定页码的文档
      List<Document> documents = new ArrayList<>();
      for (int i = start; i < end; i++) {
          Document doc = searcher.doc(scoreDocs[i].doc);
          documents.add(doc);
      }
      // 释放资源
      reader.close();
      return documents;
  } catch (Exception e) {
      log.error("查询 Lucene 错误: ", e);
      return null;
  }
}

7、关键字高亮

复制代码
@Test
public void searchArticle() throws InvalidTokenOffsetsException, IOException, ParseException {
    String keyword = "安装";
    String[] fields = {IArticleIndex.COLUMN_CONTENT, IArticleIndex.COLUMN_ARTICLE_NAME};
    // 先查询出文档列表
    List<Document> documentList = LuceneUtils.X.search(IArticleIndex.INDEX_NAME, keyword, fields, 1, 100);

    // 中文分词器
    Analyzer analyzer = new IKAnalyzer();
    // 搜索条件
    QueryParser queryParser = new MultiFieldQueryParser(fields, analyzer);
    // 搜索关键词,也就是需要高亮的字段
    Query query = queryParser.parse(keyword);
    // 高亮html语句
    Formatter formatter = new SimpleHTMLFormatter("<span style=\"color: #f73131\">", "</span>");
    QueryScorer scorer = new QueryScorer(query);
    Highlighter highlighter = new Highlighter(formatter, scorer);
    // 设置片段长度,一共展示的长度
    highlighter.setTextFragmenter(new SimpleFragmenter(50));
    List<SearchArticleVO> list = new ArrayList<>();

    for (Document doc : documentList) {
        SearchArticleVO articleVO = new SearchArticleVO();
        articleVO.setId(doc.get(IArticleIndex.COLUMN_ID));
        articleVO.setCover(doc.get(IArticleIndex.COLUMN_COVER));
        articleVO.setArticleName(doc.get(IArticleIndex.COLUMN_ARTICLE_NAME));
        articleVO.setSummary(doc.get(IArticleIndex.COLUMN_SUMMARY));
        articleVO.setCreateTime(LocalDate.parse(doc.get(IArticleIndex.COLUMN_CREATE_TIME)));
        for (String field : fields) {
            // 为文档生成高亮
            String text = doc.get(field);
            // 使用指定的分析器对文本进行分词
            TokenStream tokenStream = TokenSources.getTokenStream(field, text, analyzer);
            // 找到其中一个关键字就行了
            String bestFragment = highlighter.getBestFragment(tokenStream, text);
            if (StringUtils.isNotBlank(bestFragment)) {
                // 输出高亮结果,取第一条即可
                if (field.equals(IArticleIndex.COLUMN_ARTICLE_NAME)) {
                    articleVO.setArticleName(bestFragment);
                }
                if (field.equals(IArticleIndex.COLUMN_CONTENT)) {
                    articleVO.setSummary(bestFragment);
                }
            }
        }
        list.add(articleVO);
    }
}

我是一零贰肆,一个关注Java技术和记录生活的博主。

欢迎扫码关注"一零贰肆"的公众号,一起学习,共同进步,多看路,少踩坑。

相关推荐
草履虫建模8 小时前
Redis:高性能内存数据库与缓存利器
java·数据库·spring boot·redis·分布式·mysql·缓存
程序猿ZhangSir10 小时前
Redis 缓存进阶篇,缓存真实数据和缓存文件指针最佳实现?如何选择?
数据库·redis·缓存
段帅龙呀18 小时前
Redis构建缓存服务器
服务器·redis·缓存
夜斗小神社1 天前
【黑马点评】(二)缓存
缓存
Hello.Reader2 天前
Redis 延迟监控深度指南
数据库·redis·缓存
Hello.Reader2 天前
Redis 延迟排查与优化全攻略
数据库·redis·缓存
在肯德基吃麻辣烫2 天前
《Redis》缓存与分布式锁
redis·分布式·缓存
先睡3 天前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
CodeWithMe3 天前
【Note】《深入理解Linux内核》 Chapter 15 :深入理解 Linux 页缓存
linux·spring·缓存
大春儿的试验田3 天前
高并发收藏功能设计:Redis异步同步与定时补偿机制详解
java·数据库·redis·学习·缓存