文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-03 18:29

关注

本文转载自微信公众号「Java极客技术」,作者鸭血粉丝 。转载本文请联系Java极客技术公众号。  

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

阿粉最近遇到一个需求,因为数据量没有达到需要使用 ElasticSearch 的级别,也不想单独部署一套集群,所以准备自己基于 Lucene 实现一个简易的搜索服务。下面我们一起来看一下吧。

背景

**Lucene **是一套用于全文检索和搜索的开放源码程序库,由 Apache 软件基金会支持和提供。Lucene 提供了一个简单却强大的应用程序接口,能够做全文索引和搜索。Lucene 是现在最受欢迎的免费 Java 信息检索程序库。

上面的解释是来自维基百科,我们只需要知道 Lucene 可以进行全文索引和搜索就行了,这里的索引是动词,意思是我们可以将文档或者文章或者文件等数据进行索引记录下来,索引过后,我们查询起来就会很快。

索引这个词有的时候是动词,表示我们要索引数据,有的时候是名词,我们需要根据上下文场景来判断。新华字典前面的字母表或者书籍前面的目录本质上都是索引。

接入

引入依赖

首先我们创建一个 SpringBoot 项目,然后在 pom 文件中加入如下内容,我这里使用的 lucene 版本是 7.2.1,

  1.  
  2.     7.2.1 
  3.  
  4.  
  5. -- Lucene核心库 --> 
  6.  
  7.  org.apache.lucene 
  8.  lucene-core 
  9.  ${lucene.version} 
  10.  
  11. -- Lucene解析库 --> 
  12.  
  13.  org.apache.lucene 
  14.  lucene-queryparser 
  15.  ${lucene.version} 
  16.  
  17. -- Lucene附加的分析库 --> 
  18.  
  19.  org.apache.lucene 
  20.  lucene-analyzers-common 
  21.  ${lucene.version} 
  22.  

索引数据

在使用 Lucene 之前我们需要先索引一些文件,然后再通过关键词查询出来,下面我们来模拟整个过程。为了方便我们这里模拟一些数据,正常的数据应该是从数据库或者文件中加载的,我们的思路是这样的:

  1. 生成多条实体数据;
  2. 将实体数据映射成 Lucene 的文档形式;
  3. 索引文档;
  4. 根据关键词查询文档;

第一步我们先创建一个实体如下:

  1. import lombok.Data; 
  2.  
  3. @Data 
  4. public class ArticleModel { 
  5.     private String title; 
  6.     private String author; 
  7.     private String content; 

我们再写一个工具类,用来索引数据,代码如下:

  1. import org.apache.commons.collections.CollectionUtils; 
  2. import org.apache.commons.lang.StringUtils; 
  3. import org.apache.lucene.analysis.Analyzer; 
  4. import org.apache.lucene.analysis.standard.StandardAnalyzer; 
  5. import org.apache.lucene.document.*; 
  6. import org.apache.lucene.index.IndexWriter; 
  7. import org.apache.lucene.index.IndexWriterConfig; 
  8. import org.apache.lucene.store.Directory; 
  9. import org.apache.lucene.store.FSDirectory; 
  10. import org.springframework.beans.factory.annotation.Value; 
  11. import org.springframework.stereotype.Component; 
  12.  
  13. import java.io.IOException; 
  14. import java.nio.file.Paths; 
  15. import java.util.ArrayList; 
  16. import java.util.List; 
  17. import java.util.Map; 
  18.  
  19. public class LuceneIndexUtil { 
  20.  
  21.     private static String INDEX_PATH = "/opt/lucene/demo"
  22.     private static IndexWriter writer; 
  23.  
  24.     public static LuceneIndexUtil getInstance() { 
  25.         return SingletonHolder.luceneUtil; 
  26.     } 
  27.  
  28.     private static class SingletonHolder { 
  29.         public final static LuceneIndexUtil luceneUtil = new LuceneIndexUtil(); 
  30.     } 
  31.  
  32.     private LuceneIndexUtil() { 
  33.         this.initLuceneUtil(); 
  34.     } 
  35.  
  36.     private void initLuceneUtil() { 
  37.         try { 
  38.             Directory dir = FSDirectory.open(Paths.get(INDEX_PATH)); 
  39.             Analyzer analyzer = new StandardAnalyzer(); 
  40.             IndexWriterConfig iwc = new IndexWriterConfig(analyzer); 
  41.             writer = new IndexWriter(dir, iwc); 
  42.         } catch (IOException e) { 
  43.             log.error("create luceneUtil error"); 
  44.             if (null != writer) { 
  45.                 try { 
  46.                     writer.close(); 
  47.                 } catch (IOException ioException) { 
  48.                     ioException.printStackTrace(); 
  49.                 } finally { 
  50.                     writer = null
  51.                 } 
  52.             } 
  53.         } 
  54.     } 
  55.  
  56.      
  57.     public void addDoc(Document doc) throws IOException { 
  58.         if (null != doc) { 
  59.             writer.addDocument(doc); 
  60.             writer.commit(); 
  61.             writer.close(); 
  62.         } 
  63.     } 
  64.  
  65.      
  66.     public void addModelDoc(Object model) throws IOException { 
  67.         Document document = new Document(); 
  68.         List fields = luceneField(model.getClass()); 
  69.         fields.forEach(document::add); 
  70.         writer.addDocument(document); 
  71.         writer.commit(); 
  72.         writer.close(); 
  73.     } 
  74.  
  75.      
  76.     public void addModelDocs(List objects) throws IOException { 
  77.         if (CollectionUtils.isNotEmpty(objects)) { 
  78.             List docs = new ArrayList<>(); 
  79.             objects.forEach(o -> { 
  80.                 Document document = new Document(); 
  81.                 List fields = luceneField(o); 
  82.                 fields.forEach(document::add); 
  83.                 docs.add(document); 
  84.             }); 
  85.             writer.addDocuments(docs); 
  86.         } 
  87.     } 
  88.  
  89.      
  90.     public void delAllDocs() throws IOException { 
  91.         writer.deleteAll(); 
  92.     } 
  93.  
  94.      
  95.     public void addDocs(List docs) throws IOException { 
  96.         if (CollectionUtils.isNotEmpty(docs)) { 
  97.             long startTime = System.currentTimeMillis(); 
  98.             writer.addDocuments(docs); 
  99.             writer.commit(); 
  100.             log.info("共索引{}个 Document,共耗时{} 毫秒", docs.size(), (System.currentTimeMillis() - startTime)); 
  101.         } else { 
  102.             log.warn("索引列表为空"); 
  103.         } 
  104.     } 
  105.  
  106.      
  107.     public List luceneField(Object modelObj) { 
  108.         Map classFields = ReflectionUtils.getClassFields(modelObj.getClass()); 
  109.         Map classFieldsValues = ReflectionUtils.getClassFieldsValues(modelObj); 
  110.  
  111.         List fields = new ArrayList<>(); 
  112.         for (String key : classFields.keySet()) { 
  113.             Field field; 
  114.             String dataType = StringUtils.substringAfterLast(classFields.get(key).toString(), "."); 
  115.             switch (dataType) { 
  116.                 case "Integer"
  117.                     field = new IntPoint(key, (Integer) classFieldsValues.get(key)); 
  118.                     break; 
  119.                 case "Long"
  120.                     field = new LongPoint(key, (Long) classFieldsValues.get(key)); 
  121.                     break; 
  122.                 case "Float"
  123.                     field = new FloatPoint(key, (Float) classFieldsValues.get(key)); 
  124.                     break; 
  125.                 case "Double"
  126.                     field = new DoublePoint(key, (Double) classFieldsValues.get(key)); 
  127.                     break; 
  128.                 case "String"
  129.                     String string = (String) classFieldsValues.get(key); 
  130.                     if (StringUtils.isNotBlank(string)) { 
  131.                         if (string.length() <= 1024) { 
  132.                             field = new StringField(key, (String) classFieldsValues.get(key), Field.Store.YES); 
  133.                         } else { 
  134.                             field = new TextField(key, (String) classFieldsValues.get(key), Field.Store.NO); 
  135.                         } 
  136.                     } else { 
  137.                         field = new StringField(key, StringUtils.EMPTY, Field.Store.NO); 
  138.                     } 
  139.                     break; 
  140.                 default
  141.                     field = new TextField(key, JsonUtils.obj2Json(classFieldsValues.get(key)), Field.Store.YES); 
  142.                     break; 
  143.             } 
  144.             fields.add(field); 
  145.         } 
  146.         return fields; 
  147.     } 
  148.     public void close() { 
  149.         if (null != writer) { 
  150.             try { 
  151.                 writer.close(); 
  152.             } catch (IOException e) { 
  153.                 log.error("close writer error"); 
  154.             } 
  155.             writer = null
  156.         } 
  157.     } 
  158.  
  159.     public void commit() throws IOException { 
  160.         if (null != writer) { 
  161.             writer.commit(); 
  162.             writer.close(); 
  163.         } 
  164.     } 

有了工具类,我们再写一个 demo 来进行数据的索引

  1. import java.util.ArrayList; 
  2. import java.util.List; 
  3.  
  4.  
  5. public class Demo { 
  6.     public static void main(String[] args) { 
  7.         LuceneIndexUtil luceneUtil = LuceneIndexUtil.getInstance(); 
  8.         List articles = new ArrayList<>(); 
  9.         try { 
  10.             //索引数据 
  11.             ArticleModel article1 = new ArticleModel(); 
  12.             article1.setTitle("Java 极客技术"); 
  13.             article1.setAuthor("鸭血粉丝"); 
  14.             article1.setContent("这是一篇给大家介绍 Lucene 的技术文章,必定点赞评论转发!!!"); 
  15.             ArticleModel article2 = new ArticleModel(); 
  16.             article2.setTitle("极客技术"); 
  17.             article2.setAuthor("鸭血粉丝"); 
  18.             article2.setContent("此处省略两千字..."); 
  19.             ArticleModel article3 = new ArticleModel(); 
  20.             article3.setTitle("Java 极客技术"); 
  21.             article3.setAuthor("鸭血粉丝"); 
  22.             article3.setContent("最后邀请你加入我们的知识星球,Today is big day!"); 
  23.             articles.add(article1); 
  24.             articles.add(article2); 
  25.             articles.add(article3); 
  26.             luceneUtil.addModelDocs(articles); 
  27.             luceneUtil.commit(); 
  28.              
  29.         } catch (Exception e) { 
  30.             e.printStackTrace(); 
  31.         } 
  32.     } 

上面的 content 内容可以自行进行替换,阿粉这边避免凑字数的嫌疑就不贴了。

展示

运行结束过后,我们用过 Lucene 的可视化工具 luke 来查看下索引的数据内容,下载过后解压我们可以看到有.bat 和 .sh 两个脚本,根据自己的系统进行运行就好了。阿粉这边是 mac 用的是 sh 脚本运行,运行后打开设置的索引目录即可。

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

搜索数据

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

  1. import org.apache.commons.collections.MapUtils; 
  2. import org.apache.lucene.analysis.Analyzer; 
  3. import org.apache.lucene.analysis.standard.StandardAnalyzer; 
  4. import org.apache.lucene.index.DirectoryReader; 
  5. import org.apache.lucene.queryparser.classic.QueryParser; 
  6. import org.apache.lucene.search.*; 
  7. import org.apache.lucene.store.Directory; 
  8. import org.apache.lucene.store.FSDirectory; 
  9. import org.springframework.beans.factory.annotation.Value; 
  10.  
  11. import java.io.IOException; 
  12. import java.nio.file.Paths; 
  13. import java.util.Map; 
  14.  
  15.  
  16. public class LuceneSearchUtil { 
  17.  
  18.     private static String INDEX_PATH = "/opt/lucene/demo"
  19.     private static IndexSearcher searcher; 
  20.  
  21.     public static LuceneSearchUtil getInstance() { 
  22.         return LuceneSearchUtil.SingletonHolder.searchUtil; 
  23.     } 
  24.  
  25.     private static class SingletonHolder { 
  26.         public final static LuceneSearchUtil searchUtil = new LuceneSearchUtil(); 
  27.     } 
  28.  
  29.     private LuceneSearchUtil() { 
  30.         this.initSearcher(); 
  31.     } 
  32.  
  33.     private void initSearcher() { 
  34.         Directory directory; 
  35.         try { 
  36.             directory = FSDirectory.open(Paths.get(INDEX_PATH)); 
  37.             DirectoryReader reader = DirectoryReader.open(directory); 
  38.             searcher = new IndexSearcher(reader); 
  39.         } catch (IOException e) { 
  40.             e.printStackTrace(); 
  41.         } 
  42.     } 
  43.  
  44.     public TopDocs searchByMap(Map queryMap) throws Exception { 
  45.         if (null == searcher) { 
  46.             this.initSearcher(); 
  47.         } 
  48.         if (MapUtils.isNotEmpty(queryMap)) { 
  49.             BooleanQuery.Builder builder = new BooleanQuery.Builder(); 
  50.             queryMap.forEach((key, value) -> { 
  51.                 if (value instanceof String) { 
  52.                     Query queryString = new PhraseQuery(key, (String) value); 
  53. //                    Query queryString = new TermQuery(new Term(key, (String) value)); 
  54.                     builder.add(queryString, BooleanClause.Occur.MUST); 
  55.                 } 
  56.             }); 
  57.             return searcher.search(builder.build(), 10); 
  58.         } 
  59.         return null
  60.     } 
  61.  

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

  1. //查询数据 
  2.    Map map = new HashMap<>(); 
  3.    map.put("title""Java 极客技术"); 
  4. //   map.put("title""极客技术"); 
  5. //   map.put("content""最"); 
  6.    LuceneSearchUtil searchUtil = LuceneSearchUtil.getInstance(); 
  7.    TopDocs topDocs = searchUtil.searchByMap(map); 
  8.    System.out.println(topDocs.totalHits); 

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

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

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

 

来源:Java极客技术内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯