几百行代码完成百度搜索引擎,真的可以吗?

it2023-12-17  66

每天早上七点三十,准时推送干货

Hello 大家好,我是鸭血粉丝,大家都叫我阿粉,搜索引擎想必大家一定不会默认,我们项目中经常使用的 ElasticSearch 就是一种搜索引擎,在我们的日志系统中必不可少,ELK 作为一个整体,基本上是运维标配了,另外目前的搜索引擎底层都是基于 Lucene 来实现的。

阿粉最近遇到一个需求,因为数据量没有达到需要使用 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】获取。

进入过后,我们可以看到下图显示的内容,选择 content 点击 show top items 可以看到右侧的索引数据,这里根据分词器的不同,索引的结果是不一样的,阿粉这里采用的分词器就是标准的分词器,小伙伴们可以根据自己的要求选择适合自己的分词器即可。

搜索数据

数据已经索引成功了,接下来我们就需要根据条件进行数据的搜索了,我们创建一个 LuceneSearchUtil.java 来操作数据。

import org.apache.commons.collections.MapUtils; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.*; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.nio.file.Paths; import java.util.Map; public class LuceneSearchUtil {     private static String INDEX_PATH = "/opt/lucene/demo";     private static IndexSearcher searcher;     public static LuceneSearchUtil getInstance() {         return LuceneSearchUtil.SingletonHolder.searchUtil;     }     private static class SingletonHolder {         public final static LuceneSearchUtil searchUtil = new LuceneSearchUtil();     }     private LuceneSearchUtil() {         this.initSearcher();     }     private void initSearcher() {         Directory directory;         try {             directory = FSDirectory.open(Paths.get(INDEX_PATH));             DirectoryReader reader = DirectoryReader.open(directory);             searcher = new IndexSearcher(reader);         } catch (IOException e) {             e.printStackTrace();         }     }     public TopDocs searchByMap(Map<String, Object> queryMap) throws Exception {         if (null == searcher) {             this.initSearcher();         }         if (MapUtils.isNotEmpty(queryMap)) {             BooleanQuery.Builder builder = new BooleanQuery.Builder();             queryMap.forEach((key, value) -> {                 if (value instanceof String) {                     Query queryString = new PhraseQuery(key, (String) value); //                    Query queryString = new TermQuery(new Term(key, (String) value));                     builder.add(queryString, BooleanClause.Occur.MUST);                 }             });             return searcher.search(builder.build(), 10);         }         return null;     } }

在 demo.java 中增加搜索代码如下:

//查询数据    Map<String, Object> map = new HashMap<>();    map.put("title", "Java 极客技术"); //   map.put("title", "极客技术"); //   map.put("content", "最");    LuceneSearchUtil searchUtil = LuceneSearchUtil.getInstance();    TopDocs topDocs = searchUtil.searchByMap(map);    System.out.println(topDocs.totalHits);

运行结果如下,表示搜索到了两条。

通过可视化工具我们可以看到 title 为"Java 极客技术"确实是有两条记录,而且我们也确认只插入了两条数据。注意这里如果根据其他字符去查询可能查询不出来,因为阿粉这里的分词器采用的是默认的分词器,小伙伴可以根据自身的情况采用相应的分词器。

至此我们可以索引和搜索数据了,不过这还是简单的入门操作,对于不同类型的字段,我们需要使用不同的查询方式,而且根据系统的特性我们需要使用特定的分词器,默认的标准分词器不一定符合我们的使用场景。而且我们索引数据的时候也需要根据字段类型进行不同 Field 的设定。上面的案例只是 demo 并不能在生产上使用,搜索引擎在互联网行业是领头羊,很多先进的互联网技术都是从搜索引擎开始发展的。

相关的代码阿粉已经放到 GitHub 上了,公众号回复【源码仓库】即可获得。

< END >

如果大家喜欢我们的文章,欢迎大家转发,点击在看让更多的人看到。也欢迎大家热爱技术和学习的朋友加入的我们的知识星球当中,我们共同成长,进步。

往期精彩回顾

乖,答应阿粉,这些 Linux 命令你一定要常用阿粉教你如何使用爬虫来对比某东上的数据

重点丨什么是双重检查锁模式?以及为何需要 volatile 关键字?

最新回复(0)