美文网首页码出未来老男孩的成长之路Java架构技术进阶
只用了几百行代码写的百度搜索引擎,你看咋样?

只用了几百行代码写的百度搜索引擎,你看咋样?

作者: 老男孩_Misaya | 来源:发表于2020-10-23 17:28 被阅读0次

    推荐阅读:

    搜索引擎想必大家一定不会默认,我们项目中经常使用的 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 之前我们需要先索引一些文件,然后再通过关键词查询出来,下面我们来模拟整个过程。为了方便我们这里模拟一些数据,正常的数据应该是从数据库或者文件中加载的,我们的思路是这样的:

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

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

    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 脚本运行,运行后打开设置的索引目录即可。

    进入过后,我们可以看到下图显示的内容,选择 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 并不能在生产上使用,搜索引擎在互联网行业是领头羊,很多先进的互联网技术都是从搜索引擎开始发展的。

    相关文章

      网友评论

        本文标题:只用了几百行代码写的百度搜索引擎,你看咋样?

        本文链接:https://www.haomeiwen.com/subject/knmdmktx.html