手写 meilisearch springboot orm 框架

751 阅读5分钟

1, 什么是meiliSearch

MeiliSearch,顾名思义,美丽的搜索引擎,不仅美丽,其上手速度和小规模数据量下的体验也比 ElasticSearch 更加出色。 在大型同性交友平台 github 上面查了一下这个项目,发现是近2年火起来的,已经有将近30K+Star了,非常优秀!据说是Meili是在挪威神话中的神,指 “可爱的人”,是托尔的兄弟,果然中华文明博大精深,都流传到国外了。 MeiliSearch 是一个强大、快速、开源、易于使用和部署的搜索引擎。搜索和索引都是高度可定制的,提供开箱即用的功能,如错字容忍、过滤器和同义词。 最最最重要的是,它是支持中文搜索的,对于国人真的是太友好了。它编写的语言是 RUST,虽然小编没学过 RUST,但知道 RUST 性能非常好,可以媲美C++,那应用在搜索引擎这块,也是可以起飞了。(摘自简易百科,侵删)

2, 什么是orm框架

2.1.什么是ORM

对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。 ORM框架:为了解决面型对象与关系数据库存在的互不匹配的现象的框架。 当前ORM框架主要有五种: (1)Hibernate 全自动 需要写hql语句 (2)iBATIS 半自动 自己写sql语句,可操作性强,小巧 (3)mybatis (4)eclipseLink (5)JFinal

2.2.为什么使用ORM

当我们实现一个应用程序时(不使用O/R Mapping),我们可能会写特别多数据访问层的代码,从数据库保存、删除、读取对象信息,而这些代码都是重复的。而使用ORM则会大大减少重复性代码。对象关系映射(Object Relational Mapping,简称ORM),主要实现程序对象到关系数据库数据的映射。

3,编写基于springboot 的orm meilisearch 框架

3.1 代码思路

1,引入meilisearch 的 客户端jar包。 2, 设置配置文件,根据meilisearch 的api, 封装一下返回参数,使用泛型。 3, 在初始化的时候, 创建client 和index , 其中index 可以使用自定义注解

4, 核心代码

部分代码借鉴这个老哥的思路mvnrepository.com/artifact/co…

ponm xml 版本

<dependency>
    <groupId>com.meilisearch.sdk</groupId>
    <artifactId>meilisearch-java</artifactId>
    <version>0.11.0</version>
</dependency>
package com.ducheng.easy.ms.autoconfig;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 *meilisearch的属性配置类
 */
@Configuration
@ConfigurationProperties(prefix = "easy-ms")
public class MeiliSearchProperties {

    private static   String  default_host = "http://xxxxxxx";

    //meilisearch 的url地址
    private String host =  default_host;

    //默认的认证的签名key
    private String apiKey = "xxxxxxxxx";

    public String getHost() {
        return host ;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }
}
package com.ducheng.easy.ms.service;

import com.ducheng.easy.ms.anotation.CustomIndex;
import com.ducheng.easy.ms.entity.MeiliSearchResult;
import com.meilisearch.sdk.*;
import com.meilisearch.sdk.json.GsonJsonHandler;
import com.meilisearch.sdk.model.*;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.lang.reflect.ParameterizedType;
import java.util.*;

public class MeiliSearchRepository<T> implements InitializingBean, DocumentOperations<T> {

    /**
     *  初始化气的时候默认给索引的值
     */
    private Index index;
    private Class<T> tClass;

    private static   GsonJsonHandler jsonHandler = new GsonJsonHandler();

    @Resource
    private Client client;


    @Override
    public T get(String identifier) {
        T t = null;
        try {
            t = index.getDocument(identifier,tClass);
        } catch (Exception e) {
        }
        return t;
    }

    @Override
    public List<T> list() {
        Results<T> t = new Results<>();
        try {
            t = index.getDocuments(tClass);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return Arrays.asList(t.getResults());
    }

    @Override
    public List<T> list(int limit) {
        Results<T> t = null;
        try {
            DocumentsQuery documentsQuery = new DocumentsQuery();
            documentsQuery.setOffset(0);
            documentsQuery.setLimit(limit);
            t = index.getDocuments(documentsQuery,tClass);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return Arrays.asList(t.getResults());
    }

    @Override
    public List<T> list(int offset, int limit) {
        List<T> list = list(offset + limit);
        return list.subList(offset, list.size() - 1);
    }

    @Override
    public long add(T document) {
        List<T> list = Collections.singletonList(document);
        return add(list);
    }

    @Override
    public long update(T document) {
        List<T> list = Collections.singletonList(document);
        return update(list);
    }

    @Override
    public long add(List<T> documents) {
        TaskInfo taskInfo = null ;
        try {
            taskInfo = index.addDocuments(jsonHandler.encode(documents));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return taskInfo.getTaskUid();
    }

    @Override
    public long update(List<T> documents) {
        TaskInfo taskInfo = null ;
        try {
            taskInfo = index.updateDocuments(jsonHandler.encode(documents));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return taskInfo.getTaskUid();
    }


    @Override
    public long delete(String identifier) {
        TaskInfo taskInfo = null ;
        try {
            taskInfo = index.deleteDocument(identifier);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return taskInfo.getTaskUid();
    }

    @Override
    public long deleteBatch(String... documentsIdentifiers) {
        TaskInfo taskInfo = null ;
        try {
            taskInfo = index.deleteDocuments(Arrays.asList(documentsIdentifiers));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return taskInfo.getTaskUid();
    }


    @Override
    public List<T> query(String query)  {
        MeiliSearchResult<T>  meiliSearchResult = new MeiliSearchResult<>();
        try {
            String rawSearch = index.rawSearch(query);
            meiliSearchResult = jsonHandler.decode(rawSearch, MeiliSearchResult.class);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return meiliSearchResult.getHits();
    }

    @Override
    public List<T> query(SearchRequest searchRequest) {
        List<T> list =  new ArrayList<>();
        try {
            Searchable search = index.search(searchRequest);
            ArrayList<HashMap<String, Object>> hits = search.getHits();
            String encode = jsonHandler.encode(hits);
            list = jsonHandler.decode(encode, List.class);
        }catch (Exception e) {
            e.printStackTrace();
        }
        return list;
    }


    @Override
    public long deleteAll() {
        TaskInfo taskInfo = null ;
        try {
            taskInfo = index.deleteAllDocuments();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return taskInfo.getTaskUid();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initIndex();
    }

    /**
     * 初始化索引信息
     * @throws Exception
     */
    private void initIndex() throws Exception {
        Class<? extends MeiliSearchRepository> clazz = getClass();
        tClass = (Class<T>) ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[0];
        CustomIndex annoIndex = tClass.getAnnotation(CustomIndex.class);
        String uid = annoIndex.uid();
        String primaryKey = annoIndex.primaryKey();
        if (StringUtils.isEmpty(uid)) {
            uid = tClass.getSimpleName().toLowerCase();
        }
        if (StringUtils.isEmpty(primaryKey)) {
            primaryKey = "id";
        }
        Index index ;
        try {
            //如果不指定索引, 默认就使用表名称
             index = client.getIndex(uid);
        }catch (Exception e) {
            index = null;
        }
        if (ObjectUtils.isEmpty(index)) {
            TaskInfo taskInfo = client.createIndex(uid, primaryKey);
            index=  client.getIndex(uid);
        }
        this.index = index;
    }

}

5, 编写测试类代码

package com.ducheng.easy.ms;
import com.ducheng.easy.ms.anotation.MeiliSearchRepository;
@MeiliSearchRepository //使用自定义注解,也能被spring bean 识别
public class BookEntityRepository extends com.ducheng.easy.ms.service.MeiliSearchRepository<BookEntity> {
}

package com.ducheng.easy.ms;

import com.ducheng.easy.ms.anotation.CustomIndex;

import java.io.Serializable;

@CustomIndex
public class BookEntity implements Serializable {

    private String bookName;

    private String id;

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "BookEntity{" +
                "bookName='" + bookName + ''' +
                ", id='" + id + ''' +
                '}';
    }
}
package com.ducheng.easy.ms;

import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.SearchRequest;
import com.meilisearch.sdk.exceptions.MeilisearchException;
import com.meilisearch.sdk.model.Key;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@SpringBootTest(classes = MeiLiSearchApplication.class,webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class MeiLiSearchTest {

    @Autowired
    private BookEntityRepository bookEntityRepository;

    @Resource
    private Client client;

    @Test
    public void addEntity() {
        BookEntity bookEntity = new BookEntity();
        bookEntity.setBookName("我要朝鲜语");
        bookEntity.setId("400");
        long result = bookEntityRepository.add(bookEntity);
        System.out.println("添加书籍返回结果:" + result);
    }

    @Test
    public void update() {
        BookEntity bookEntity = new BookEntity();
        bookEntity.setBookName("我要朝弟弟鲜语");
        bookEntity.setId("400");
        long update = bookEntityRepository.update(bookEntity);
        System.out.println("添加书籍返回结果:" + update);
    }

    @Test
    public void list() {
        List<BookEntity> list = bookEntityRepository.list();
        list.forEach(x-> {
            System.out.println("返回的数据是:" + x.toString());
        });
    }

    @Test
    public void deleteAll() {
        long deleteAll = bookEntityRepository.deleteAll();
        System.out.println("删除数据返回长度:"+deleteAll);
    }

    @Test
    public void deleteById() {
        long deleteAll = bookEntityRepository.delete("400");
        System.out.println("删除数据返回长度:"+deleteAll);
    }

    @Test
    public void get() {
        BookEntity bookEntity = bookEntityRepository.get("400");
        System.out.println("返回的数据是:" + bookEntity.toString());
    }


    @Test
    public void getSearchRwe() throws MeilisearchException {
        List<BookEntity> entities = bookEntityRepository.query("我要");
        System.out.println("返回的数据是:" +entities);
    }


    @Test
    public void search() throws MeilisearchException {
        SearchRequest searchRequest = new SearchRequest("我要");
        List<BookEntity> entities = bookEntityRepository.query(searchRequest);
        System.out.println("返回的数据是:" +entities);
    }

    @Test
    public void  createKey() throws ParseException, MeilisearchException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        Date dateParsed = format.parse("2042-04-02T00:42:42Z");
        Key keyInfo = new Key();
        keyInfo.setDescription("Add documents: Products API key");
        keyInfo.setActions(new String[] {"*"});
        keyInfo.setIndexes(new String[] {"*"});
        keyInfo.setExpiresAt(dateParsed);
        Key key = client.createKey(keyInfo);
        System.out.println("生成的key是"+ key.getKey());
    }

}

这里有一点需要注意的事情是, 高版本的meilisearch 访问是必须要apikey的, 大家需要手动设置一下, apikey , 访问路径权限和有效期

我们看一下最后的结果

image.png bookentity 就是类名称,默认类名称小写,也就是索引index

image.png

单元测试完美,至于怎么安装的, 网上有教程, 对于小网站,博客之类的。 很友好, rust 编写的, 很稳定,性能嘎嘎的。后续我会把代码上传maven 仓库。