一、基础知识

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";
}
  1. 创建索引并新增文档
/**
 * 创建索引并设置数据
 *
 * @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();
    }
}
  1. 单元测试
@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();
  }
}
  1. 单元测试
@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技术和记录生活的博主。

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

Lucene轻量级搜索引擎,真的太强了!!!Solr 和 ES 都是基于它的更多相关文章

  1. 搜索引擎选择: Elasticsearch与Solr

    我用过这两种搜索引擎,但也仅仅是用过而已,没有非常深入研究,以下是我的看法 lucene是完全用java实现,而sphinx是支持java api.显然这两者是有差别的,用java实现的意义在于,你可 ...

  2. 微信清理H5真的太早了?这会是应用号发布的前兆吗

    三少爷的剑  2016-04-18 21:05 收藏35 评论7   两天之内,整个 H5 游戏创业陷入了两年狂热期以来最冷的冰点. 每一个正在忙于 H5 小游戏开发的开发者都在忙于砍掉游戏代码中有关 ...

  3. Apache Solr采用Java开发、基于Lucene的全文搜索服务器

    http://docs.spring.io/spring-data/solr/ 首先介绍一下solr: Apache Solr (读音: SOLer) 是一个开源.高性能.采用Java开发.基于Luc ...

  4. 聊聊基于Lucene的搜索引擎核心技术实践

    最近公司用到了ES搜索引擎,由于ES是基于Lucene的企业搜索引擎,无意间在“聊聊架构”微信公众号里发现了这篇文章,分享给大家. 请点击链接:聊聊基于Lucene的搜索引擎核心技术实践

  5. Lucene.net 搜索引擎的中文资料

    以下是我找到的网上一些关于Lucene.net 搜索引擎的介绍资料 https://code.i-harness.com/zh-CN/tagged/lucene?page=5 http://jingp ...

  6. Delphi程序员如何找到高薪的工作?(赚不到钱,原因只有一个,就是他们没有被公司录取。Delphi必须要独自进行深入研究,才能精通,同时也不能自由性太强)

    转帖自:http://www.tommstudio.com/ViewNews.aspx?ID=187http://hi.baidu.com/rarnu/blog/ 本文翻译自<美国优秀经理观念大 ...

  7. 利用Lucene.net搜索引擎进行多条件搜索的做法

    利用Lucene.net搜索引擎进行多条件搜索的做法 2018年01月09日 ⁄ 搜索技术 ⁄ 共 613字 ⁄ 字号 小 中 大 ⁄ 评论关闭 利用Lucene.net搜索引擎进行多条件搜索的做法 ...

  8. 一文带你入门Java Stream流,太强了

    两个星期以前,就有读者强烈要求我写一篇 Java Stream 流的文章,我说市面上不是已经有很多了吗,结果你猜他怎么说:"就想看你写的啊!"你看你看,多么苍白的喜欢啊.那就&qu ...

  9. Solr和ES对比

    Solr与ES(ElasticSearch)对比 搜索引擎选择: Elasticsearch与Solr 搜索引擎选型调研文档 Elasticsearch简介* Elasticsearch是一个实时的分 ...

  10. 关于hermes与solr,es的定位与区别

    Hermes与开源的Solr.ElasticSearch的不同 谈到Hermes的索引技术,相信很多同学都会想到Solr.ElasticSearch.Solr.ElasticSearch在真可谓是大名 ...

随机推荐

  1. TienChin 渠道管理-渠道导入

    ChannelController @PostMapping("/importTemplate") void importTemplate(HttpServletResponse ...

  2. 基于知识图谱的《红楼梦》人物关系可视化及问答系统(含码源):命名实体识别、关系识别、LTP简单教学

    基于知识图谱的<红楼梦>人物关系可视化及问答系统(含码源):命名实体识别.关系识别.LTP简单教学 文件树: app.py是整个系统的主入口 templates文件夹是HTML的页面 |- ...

  3. WebAssembly入门笔记[4]:利用Global传递全局变量

    利用WebAssembly的导入导出功能可以灵活地实现宿主JavaScript程序与加载的单个wasm模块之间的交互,那么如何在宿主程序与多个wasm之间传递和共享数据呢?这就需要使用到Global这 ...

  4. [vue] 脚手架笔记

    笔记 脚手架文件结构 ├── node_modules ├── public │ ├── favicon.ico: 页签图标 │ └── index.html: 主页面 ├── src │ ├── a ...

  5. 高精度模板 大数乘以小数 vector实现

    vector<int> Mul(vector<int>& A, int &B) { vector<int>C; int T = 0; for (in ...

  6. 常用TS总结

    自己常用的 TS 写法总结,应该会一直更新.可使用 TS在线编译 校验 TS 语法. 基本用法 普通 const num: number = 10 const isStop: boolean = fa ...

  7. 【CAS学习二】CAS部署和联调

    上一篇写到服务端部署的是CAS 6.4版本,可后面与客户端集成时出现未认证授权的服务,如下: 网上查了下,要把http的访问打开.具体设置步骤是:修:%Tomcat%\webapps\cas\WEB- ...

  8. .NET Core开发实战(第3课:.NET Core的现状、未来以及环境搭建)--学习笔记

    03 | .NET Core的现状.未来以及环境搭建 .NET Core的现状 .NET Core 的应用场景:桌面端.Web端.云端.移动端.游戏.IOT 和 AI 云端指的是 .NET Core ...

  9. NC24158 [USACO 2015 Jan G]Moovie Mooving

    题目链接 题目 题目描述 Bessie is out at the movies. Being mischievous as always, she has decided to hide from ...

  10. Linux 中iostat 命令详解

    iostat命令详解 iostat 主要是统计 磁盘活动情况. iostat有以下缺陷: iostat的输出结果大多数是一段时间内的平均值,因此难以反映峰值情况iostat仅能对系统整体情况进行分析汇 ...