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

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

 [[347248]]

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

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> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

索引数据

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

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

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

import lombok.Data; 
 
@Data 
public class ArticleModel { 
    private String title; 
    private String author; 
    private String content; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

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(); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.

有了工具类,我们再写一个 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(); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

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

展示

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

进入过后,我们可以看到下图显示的内容,选择 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
    } 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.

在 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); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

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

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

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

 

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2010-05-06 13:32:59

2011-05-17 18:01:52

搜索引擎优化

2011-11-15 08:40:17

百度

2011-10-28 16:19:21

百度搜索

2022-11-18 12:06:48

App接口搜索

2009-08-07 08:29:36

百度搜索引擎

2023-01-11 12:49:49

AIChatGPT

2021-01-28 06:07:32

百度搜索搜索引擎 应用

2020-06-04 13:41:35

大数据疫情技术

2018-10-19 10:05:14

区块链百度百度搜索

2009-12-04 09:27:02

搜索引擎色情

2012-11-25 15:42:47

互联网百度搜索

2018-06-13 17:43:46

熊掌号

2012-04-06 09:23:09

百度苹果

2020-03-17 08:23:47

搜索引擎百度Google

2023-09-21 15:05:12

ChatGPT搜索引擎

2010-08-12 22:20:14

百度搜索营销培训

2011-06-29 16:02:40

jQuery
点赞
收藏

51CTO技术栈公众号