阿粉最近遇到一个需求,因为数据量没有达到需要使用 ElasticSearch 的级别,也不想单独部署一套集群,所以准备自己基于 Lucene 实现一个简易的搜索服务。下面我们一起来看一下吧。
背景
**Lucene **是一套用于全文检索和搜索的开放源码程序库,由 Apache 软件基金会支持和提供。Lucene 提供了一个简单却强大的应用程序接口,能够做全文索引和搜索。Lucene 是现在最受欢迎的免费 Java 信息检索程序库。
上面的解释是来自维基百科,我们只需要知道 Lucene 可以进行全文索引和搜索就行了,这里的索引是动词,意思是我们可以将文档或者文章或者文件等数据进行索引记录下来,索引过后,我们查询起来就会很快。
索引这个词有的时候是动词,表示我们要索引数据,有的时候是名词,我们需要根据上下文场景来判断。新华字典前面的字母表或者书籍前面的目录本质上都是索引。
接入
引入依赖
首先我们创建一个 SpringBoot 项目,然后在 pom 文件中加入如下内容,我这里使用的 lucene 版本是 7.2.1,
<properties> <lucene.version>7.2.1</lucene.version> </properties> <!-- 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 之前我们需要先索引一些文件,然后再通过关键词查询出来,下面我们来模拟整个过程。为了方便我们这里模拟一些数据,正常的数据应该是从数据库或者文件中加载的,我们的思路是这样的:
- 生成多条实体数据;
- 将实体数据映射成 Lucene 的文档形式;
- 索引文档;
- 根据关键词查询文档;
第一步我们先创建一个实体如下:
import lombok.Data; @Data public class ArticleModel { private String title; private String author; private String content; }
我们再写一个工具类,用来索引数据,代码如下:
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; 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.store.Directory; import org.apache.lucene.store.FSDirectory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Map; public class LuceneIndexUtil { private static String INDEX_PATH = "/opt/lucene/demo"; private static IndexWriter writer; public static LuceneIndexUtil getInstance() { return SingletonHolder.luceneUtil; } private static class SingletonHolder { public final static LuceneIndexUtil luceneUtil = new LuceneIndexUtil(); } private LuceneIndexUtil() { this.initLuceneUtil(); } private void initLuceneUtil() { try { Directory dir = FSDirectory.open(Paths.get(INDEX_PATH)); Analyzer analyzer = new StandardAnalyzer(); IndexWriterConfig iwc = new IndexWriterConfig(analyzer); writer = new IndexWriter(dir, iwc); } catch (IOException e) { log.error("create luceneUtil error"); if (null != writer) { try { writer.close(); } catch (IOException ioException) { ioException.printStackTrace(); } finally { writer = null; } } } } /** * 索引单个文档 * * @param doc 文档信息 * @throws IOException IO 异常 */ public void addDoc(Document doc) throws IOException { if (null != doc) { writer.addDocument(doc); writer.commit(); writer.close(); } } /** * 索引单个实体 * * @param model 单个实体 * @throws IOException IO 异常 */ public void addModelDoc(Object model) throws IOException { Document document = new Document(); List<Field> fields = luceneField(model.getClass()); fields.forEach(document::add); writer.addDocument(document); writer.commit(); writer.close(); } /** * 索引实体列表 * * @param objects 实例列表 * @throws IOException IO 异常 */ public void addModelDocs(List<?> objects) throws IOException { if (CollectionUtils.isNotEmpty(objects)) { List<Document> docs = new ArrayList<>(); objects.forEach(o -> { Document document = new Document(); List<Field> fields = luceneField(o); fields.forEach(document::add); docs.add(document); }); writer.addDocuments(docs); } } /** * 清除所有文档 * * @throws IOException IO 异常 */ public void delAllDocs() throws IOException { writer.deleteAll(); } /** * 索引文档列表 * * @param docs 文档列表 * @throws IOException IO 异常 */ public void addDocs(List<Document> docs) throws IOException { if (CollectionUtils.isNotEmpty(docs)) { long startTime = System.currentTimeMillis(); writer.addDocuments(docs); writer.commit(); log.info("共索引{}个 Document,共耗时{} 毫秒", docs.size(), (System.currentTimeMillis() - startTime)); } else { log.warn("索引列表为空"); } } /** * 根据实体 class 对象获取字段类型,进行 lucene Field 字段映射 * * @param modelObj 实体 modelObj 对象 * @return 字段映射列表 */ public List<Field> luceneField(Object modelObj) { Map<String, Object> classFields = ReflectionUtils.getClassFields(modelObj.getClass()); Map<String, Object> classFieldsValues = ReflectionUtils.getClassFieldsValues(modelObj); List<Field> fields = new ArrayList<>(); for (String key : classFields.keySet()) { Field field; String dataType = StringUtils.substringAfterLast(classFields.get(key).toString(), "."); switch (dataType) { case "Integer": field = new IntPoint(key, (Integer) classFieldsValues.get(key)); break; case "Long": field = new LongPoint(key, (Long) classFieldsValues.get(key)); break; case "Float": field = new FloatPoint(key, (Float) classFieldsValues.get(key)); break; case "Double": field = new DoublePoint(key, (Double) classFieldsValues.get(key)); break; case "String": String string = (String) classFieldsValues.get(key); if (StringUtils.isNotBlank(string)) { if (string.length() <= 1024) { field = new StringField(key, (String) classFieldsValues.get(key), Field.Store.YES); } else { field = new TextField(key, (String) classFieldsValues.get(key), Field.Store.NO); } } else { field = new StringField(key, StringUtils.EMPTY, Field.Store.NO); } break; default: field = new TextField(key, JsonUtils.obj2Json(classFieldsValues.get(key)), Field.Store.YES); break; } fields.add(field); } return fields; } public void close() { if (null != writer) { try { writer.close(); } catch (IOException e) { log.error("close writer error"); } writer = null; } } public void commit() throws IOException { if (null != writer) { writer.commit(); writer.close(); } } }
有了工具类,我们再写一个 demo 来进行数据的索引
import java.util.ArrayList; import java.util.List; /** * <br> * <b>Function:</b><br> * <b>Author:</b>@author Silence<br> * <b>Date:</b>2020-10-17 21:08<br> * <b>Desc:</b>无<br> */ public class Demo { public static void main(String[] args) { LuceneIndexUtil luceneUtil = LuceneIndexUtil.getInstance(); List<ArticleModel> articles = new ArrayList<>(); try { //索引数据 ArticleModel article1 = new ArticleModel(); article1.setTitle("Java 极客技术"); article1.setAuthor("鸭血粉丝"); article1.setContent("这是一篇给大家介绍 Lucene 的技术文章,必定点赞评论转发!!!"); ArticleModel article2 = new ArticleModel(); article2.setTitle("极客技术"); article2.setAuthor("鸭血粉丝"); article2.setContent("此处省略两千字..."); ArticleModel article3 = new ArticleModel(); article3.setTitle("Java 极客技术"); article3.setAuthor("鸭血粉丝"); article3.setContent("最后邀请你加入我们的知识星球,Today is big day!"); articles.add(article1); articles.add(article2); articles.add(article3); luceneUtil.addModelDocs(articles); luceneUtil.commit(); } catch (Exception e) { e.printStackTrace(); } } }
上面的 content 内容可以自行进行替换,阿粉这边避免凑字数的嫌疑就不贴了。
展示
运行结束过后,我们用过 Lucene 的可视化工具 luke 来查看下索引的数据内容,下载过后解压我们可以看到有.bat 和 .sh 两个脚本,根据自己的系统进行运行就好了。阿粉这边是 mac 用的是 sh 脚本运行,运行后打开设置的索引目录即可。
luke 在 GitHub 上面有,但是下载很慢,阿粉这边通过国外服务器下载下来,并上传的百度网盘中了,公众号回复【luke】获取。