1. 业务开发

基本步骤:

  1. 配置字典 ES索引 增加索引配置
  2. 使用注解,在需要同步es的方法上面使用注解
  3. 创建模型Bean,创建业务es文档模型
  4. 创建Service,实现对es的curd
  5. 创建拦截Aop,创建业务es同步aop
  6. 创建搜索action,创建控制层对外部提供查询接口

[!tip]
下面代码片段截取自政务版本的内容搜索

1.1. 字典配置

通过配置字典ES索引来增加es索引,特别注意字典的数据值的json格式

{"name":"GovESContentBean","url":"/gov/es/sync.do"}

name:模型bean的名称,具体开发参考模型Bean,必须与ESBaseBean的子类上的@Component名称一致;

url:es同步请求的方法,具体开发参考 同步方法

1.2. 注解

在对应的业务代码使用

1.2.1. @ESSave、@ESDelete

在对应的方法上进行注解,提供给aop进行拦截,

[!tip] 主要提供AOP拦截,在AOP中进行ES库的同步,所有涉及数据更新的地方都需要使用此注解

1.2.2. 实例

...
//保存方法
@ESSave
public ResultData save(@ModelAttribute ContentEntity content) {
        contentBiz.saveEntity(_product);
        return ResultData.build().success(_product);
}

//更新方法
@ESSave
public ResultData update(@ModelAttribute ContentEntity content) {
        contentBiz.updateEntity(_product);
        return ResultData.build().success(_product);
}


//删除方法
@ESDelete
public ResultData delete(@RequestBody List<ContentEntity> contents,HttpServletResponse response, HttpServletRequest request) {
    int[] ids = new int[contents.size()];
    for(int i = 0;i<contents.size();i++){

        ids[i] =Integer.parseInt(contents.get(i).getId()) ;
    }


    contentBiz.delete(ids);
    return ResultData.build().success();
}

...

1.3. 模型Bean

创建业务中需要的ES文档模型 public class *Bean extends ESBaseBean,业务Bean继承ESBaseBean

注解参考

es文档定义,indexName可以定义不同的es库,注意:高版本es容器已经废弃了type的设置
@Document(indexName = "cms",type = "content")

es关键id注解
@Id

es关键字定义,例如:我是中国人,只能通过我是中国人搜索到,
@Field(type = FieldType.Keyword)

es关键字定义,分词搜索,例如:我是中国人,可以通过我是、中国人、中国、都能搜索到,
@Field(type = FieldType.Keyword, analyzer = "ik_max_word")

es长文本关键字定义,推荐加上analyzer = "ik_max_word"
@Field(type = FieldType.Text, analyzer = "ik_max_word")

es数值定义
@Field(type = FieldType.Integer )

es日期定义
@Field(type = FieldType.Date)

1.3.1. 实例

以文章为例,创建es文档模型bean,只需要定义需要存入es库的字段

@Document(indexName = "cms-gov")
@Component("GovESContentBean")
public class ESContentBean extends ESBaseBean {

    /**
     * 存储文章作者
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer="ik_smart")
    private String author;

    /**
     * 存储自定义扩张模型
     */
    @Field(type = FieldType.Object)
    private Map<String, Object> MDiyModel;

    @Field(type = FieldType.Keyword)
    private String typeId;

    @Field(type = FieldType.Text)
    private String litPic;

    @Field(type = FieldType.Keyword)
    private String flag;

    /**
     * 存储文章发布到
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer="ik_smart")
    private String styles;

    /**
     * 存储类型
     */
    private String type;

    /**
     * 存储栏目父ID集合
     */
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer="ik_smart")
    private String parentIds;

    /**
     * 储存站点ID
     */
    @Field(type = FieldType.Keyword)
    private String appId;

    /**
     * 存储栏目标题
     */
    @Field(type = FieldType.Text)
    private String typeTitle;

    /**
     * 自定义排序
     */
    @Field(type = FieldType.Integer)
    private Integer sort;

    @Field(type = FieldType.Text)
    private String descrip;

    /**
     * 文章审批进度
     */
    @Field(type = FieldType.Keyword)
    private String progressStatus;

    @Field(type = FieldType.Date,format = DateFormat.custom, pattern="yyyy-MM-dd HH:mm:ss")
    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    private Date date;

    /**
     * 链接地址
     */
    @Field(type = FieldType.Text,index=false)
    private String url;

   // .. 省略 set get 方法

}

1.4. 同步方法

通过业务代码查询出所有的数据再通过 `Service` 提供的方法同步到es库

...
    @Autowired
    private IContentBiz contentBiz;

    @Autowired
    private IModelBiz modelBiz;

    @Autowired
    private ICategoryBiz categoryBiz;

    @Autowired
    private IESContentService esContentService;



    /**
     * 同步es文章
     * @return
     */
    @ApiOperation(value = "同步es文章接口")
    @PostMapping("/sync")
    @RequiresPermissions("es:sync")
    @ResponseBody
    public ResultData sync() {
        // 获取栏目列表
        LambdaQueryWrapper<CategoryEntity> categoryWrapper = new LambdaQueryWrapper<>();
        categoryWrapper.ne(CategoryEntity::getCategoryType, CategoryTypeEnum.LINK.toString());
        List<CategoryEntity> categoryList = categoryBiz.list(categoryWrapper);
        for (CategoryEntity category : categoryList) {
            // 获取文章列表
            LambdaQueryWrapper<ContentEntity> contentWrapper = new LambdaQueryWrapper<>();
            contentWrapper.eq(ContentEntity::getCategoryId, category.getId());
            List<ContentEntity> contentList = contentBiz.list(contentWrapper);
            boolean hasModel = false;

            ModelEntity model = null;
            if (StringUtils.isNotBlank(category.getMdiyModelId())) {
                hasModel = true;
                // 获取模型实体
                model = modelBiz.getById(category.getMdiyModelId());
            }
            List<ESContentBean> beanList = new ArrayList<>();
            for (ContentEntity content : contentList) {
                ESContentBean esContentBean = new ESContentBean();
                ESContentBeanUtil.fixESContentBean(esContentBean, content, category);
                if (hasModel) {
                    //配置link_id Map
                    Map<String, String> modelMap = new HashMap<>(1);
                    modelMap.put("link_id", content.getId());
                    List<Map<String, Object>> modelFieldList = contentBiz.queryBySQL(
                            model.getModelTableName(),
                            null ,
                            modelMap, null, null, null, null, null);
                    // 防止NP
                    if (CollUtil.isNotEmpty(modelFieldList)) {
                        // 模型字段MAP
                        Map<String, Object> objectMap = modelFieldList.get(0);
                        for (String key : objectMap.keySet()) {
                            // 类型为BigInteger时需要转换为long防止es类型构造器转换错误
                            if (objectMap.get(key) instanceof BigInteger) {
                                objectMap.put(key, ((BigInteger) objectMap.get(key)).longValue());
                            }
                        }
                        esContentBean.setMDiyModel(objectMap);
                    }
                }
                beanList.add(esContentBean);
            }
            if (CollUtil.isNotEmpty(beanList)) {
                try {
                    esContentService.saveAll(beanList);
                }catch (DataAccessResourceFailureException e) {
                    return ResultData.build().error("未找到当前ES信息,请检查当前ES链接是否正常");
                }catch (NoSuchIndexException e) {
                    return ResultData.build().error("未找到当前ES索引信息,请检查是否创建索引");
                }
            }

        }
        return ResultData.build().success().msg("全部同步完成!");
    }
...

1.5. Service继承ElasticsearchRepository

继承后能进行简单的增删改查操作,具体可以参考CrudRepository

@Service("govIESContentService")
public interface IESContentService extends ElasticsearchRepository<ESContentBean, String> {

}

[!tip] 基于spring boot data 快速实现ES的CURD

1.6. AOP拦截

创建AOP public class *Aop extends ESBaseAop {},需要进行es全文检索的数据,通过aop的方式同步到es库,业务Aop继承ESBaseAop

1.6.1. 实例

通过拦截的方式将模型Bean的内容同步到es库

@Component("esContentAop")
@Aspect
public class ESContentAop extends ESBaseAop {

    @Autowired
    private IESContentService esContentService;

    @Autowired
    private ICategoryBiz categoryBiz;

    //保存更新同步
    @Override
    public void save(JoinPoint joinPoint) {
        try {
            BaseEntity obj = this.getType(joinPoint, BaseEntity.class, true);
            ESContentBean bean = new ESContentBean();
            if(obj instanceof ContentEntity){
                ContentEntity content = (ContentEntity)obj;
                CategoryEntity category = categoryBiz.getById(content.getCategoryId());
                bean.setTitle(content.getContentTitle());
                bean.setContent(content.getContentDetails());
                bean.setAuthor(content.getContentAuthor());
                bean.setDate(content.getContentDatetime());
                bean.setTypeId(content.getCategoryId());
                bean.setDescrip(content.getContentDescription());
                bean.setSort(content.getContentSort());
                bean.setLitPic(content.getContentImg());
                bean.setFlag(content.getContentType());
                bean.setUrl(category.getCategoryPath();
                // 有appId则存放
                AppEntity app = BasicUtil.getWebsiteApp();
                if (app != null) {
                // 查询时会自动拼接appID,也就是当前session,所以这里可以直接设置
                bean.setAppId(app.getId());
                bean.setStyles(content.getContentStyle());
        }
            }
            LOG.info("保存es库");
            esContentService.save(bean);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //删除同步
    @Override
    public void delete(JoinPoint joinPoint, ResultData result) {
        try {
            //注意!!!默认接收切入方法第一个参数
            List<BaseEntity> arrayList = this.getType(joinPoint, ArrayList.class, true);
            BaseEntity obj ;
            obj = this.getType(joinPoint, BaseEntity.class, true);
            if (arrayList != null) {
                obj = arrayList.get(0);
            }
            // 单个删除
            if(ObjectUtil.isNotEmpty(obj)){
                esContentService.deleteById(obj.getId());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

[!tip] 所有涉及到文字内容变化的控制层都需要别拦截到,否则会出现es库数据不同步的问题产生

1.7. 搜索Action

创建 *Action ,根据业务编写对应的控制层,调用ESUtils的方法进行搜索

/**
     * 高亮搜索,保留搜索字段keyword、order、orderBy、pageNo、pageSize,其余字段根据实际业务调整
     *
     * @param keyword 关键字
     * @return 内容
     */
    @ApiOperation(value = "高亮搜索接口")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "keyword", value = "搜索关键字", required = true, paramType = "query"),
            @ApiImplicitParam(name = "order", value = "排序方式", required = false, paramType = "query"),
            @ApiImplicitParam(name = "orderBy", value = "排序的字段", required = false, paramType = "query"),
            @ApiImplicitParam(name = "pageNo", value = "页数", required = false, paramType = "query", dataType = "Integer"),
            @ApiImplicitParam(name = "pageSize", value = "页数大小", required = false, paramType = "query", dataType = "Integer"),
            @ApiImplicitParam(name = "styles", value = "皮肤,例:outsite、insite", required = false, paramType = "query"),
            @ApiImplicitParam(name = "typeId", value = "所属栏目(用于搜索指定栏目)", required = false, paramType = "query"),
            @ApiImplicitParam(name = "parentIds", value = "父栏目ID(用于搜索所有子栏目)", required = false, paramType = "query"),
            @ApiImplicitParam(name = "fields", value = "搜索关键字字段,值输入ES索引字段值,多个用逗号隔开 例:content,title", required = false, paramType = "query"),
            @ApiImplicitParam(name = "searchMethod", value = "搜索方式,不传默认分词搜索(传值参考SearchMethodEnum)", required = false, paramType = "query"),
            @ApiImplicitParam(name = "rangeField", value = "范围字段,需要传开始范围和结束范围才能生效,可以对时间进行范围筛选,值填写对应es索引的索引字段", required = false, paramType = "query"),
            @ApiImplicitParam(name = "start", value = "开始范围 时间格式:yyyy-MM-dd HH:mm:ss 例:2021-08-06 12:25:36", required = false, paramType = "query"),
            @ApiImplicitParam(name = "end", value = "结束范围 时间格式:yyyy-MM-dd HH:mm:ss 例:2021-08-06 12:25:36", required = false, paramType = "query"),
            @ApiImplicitParam(name = "noflag", value = "不等于的文章类型,多个逗号隔开", required = false, paramType = "query"),
    })
    @PostMapping("/highlightSearch")
    @ResponseBody
    public ResultData highlightSearch(String keyword) {
        ESSearchBean esSearchBean = new ESSearchBean();
        String noflag = BasicUtil.getString("noflag");
        Map<String, Object> notEquals = new HashMap<>();
        if (StringUtils.isNotBlank(noflag)) {
            List<String> collect = Arrays.stream(noflag.split(",")).collect(Collectors.toList());
            // es flag字段
            notEquals.put("flag", collect);
        }
        // 过滤条件
        String filterStr = BasicUtil.getString("filter");
        Map<Object,Object> filter = new HashMap<>();
        if (StringUtils.isNotBlank(filterStr)) {
            try {
                filter = JSONUtil.toBean(filterStr, Map.class);
            } catch (Exception e) {
                LOG.debug("过滤条件转json失败");
                e.printStackTrace();
            }
        }
        //如果有站群插件,需要根据站群插件过滤
        if (BasicUtil.getWebsiteApp() != null) {
            // 站点过滤
            LambdaQueryWrapper<AppEntity> wrapper = new QueryWrapper<AppEntity>().lambda();
            wrapper.like(AppEntity::getAppUrl, BasicUtil.getDomain());
            AppEntity appEntity = appBiz.getOne(wrapper, false);
            filter.put("net", appEntity.getId());
        }

        // 设置基本过滤字段
        String styles = BasicUtil.getString("styles");
        if (StringUtils.isNotBlank(styles)) {
            filter.put("styles", styles);
        }

        //审核过滤
        filter.put("progressStatus", "终审通过");

        esSearchBean.setFilterWhere(filter);
        esSearchBean.setKeyword(keyword);

        // 设置排序
        String order = BasicUtil.getString("order", "desc");
        String orderBy = BasicUtil.getString("orderBy", "sort");
        esSearchBean.setOrder(order);
        esSearchBean.setOrderBy(orderBy);

        //设置分页
        Integer pageNo = BasicUtil.getInt("pageNo", 1);
        Integer pageSize = BasicUtil.getInt("pageSize", 10);
        esSearchBean.setPageNo(pageNo);
        esSearchBean.setPageSize(pageSize);

        //设置高亮搜索配置
        Map<String, Object> highlightConfig = new HashMap<>();
        //设置高亮样式
        highlightConfig.put("preTags", "<span style='color:red'>");
        highlightConfig.put("postTags", "</span>");

        //设置搜索字段
        String author = BasicUtil.getString("author");
        String content = BasicUtil.getString("content");
        String title = BasicUtil.getString("title");
        LinkedHashMap<String, Float> searchFields = new LinkedHashMap<>();

        //设置搜索字段,数值f标识用于权重排序,一般多字段搜索使用,数值越大该字段的权重越大,当searchMethod等于geo_function_search生效
        if (StringUtils.isNotBlank(author)) {
            searchFields.put("author", 5f);
        }
        if (StringUtils.isNotBlank(title)) {
            searchFields.put("title", 10f);
        }
        if (StringUtils.isNotBlank(content)) {
            searchFields.put("content", 5f);
        }
        // 默认设置搜索标题
        if (CollUtil.isEmpty(searchFields)) {
            searchFields.put("title", 10f);
        }

        // 设置搜索方式,默认使用分词搜索
        String searchMethod = BasicUtil.getString("searchMethod", "");

        return esService.search(ESContentBean.class, searchFields, highlightConfig,
                notEquals, esSearchBean, searchMethod);

    }

[!tip] 注意代码中的 filter.putsearchFields.put 根据实际业务字段调整,searchMethod=should_search可以匹配完整标题

注意:fields参数里可以填写es库中已有的字段,如果传参为 title,content 表示搜索标题、文章中带有关键字的数据;styles参数为outsite或者insite

返回格式参考

{
    "result": true,
    "code": 200,
    "data": {
        "total": 1,
        "rows": [ //记录列表
            {
                "contentKeyword": "<span style='color:red'></span><span style='color:red'></span>ms",
                "sort": 0,
                "id": "1346299650146922497",
                "title": "[【网站】<span style='color:red'></span><span style='color:red'></span> 前6名 Java开源CMS建站系统]",
                "content": "[下面我们开始分享一下开源中<span style='color:red'></span>中最火的Java开源CMS建站系统]",
                "typeId": "1392359380113481730",
                "parentIds": "1382233650262241282",
                "date": "2017-08-29 20:04:31",
                "author" : "",
                "net" : "1",
                "typeId" : "1458331507951800321",
                "litPic" : """[{"url":"blob:http://192.168.0.118:8080/5e548893-1b5f-46cd-8193-fc7c48b2cc7f","name":"1637824298771.jpg","path":"/upload/1/cms/content/1637906304084.jpg","uid":1637906304036,"status":"success"}]""",
                "typeTitle" : "早间新闻",
                "flag" : "c",
                "type" : "content",
                "descrip": "-",
                "url" : """/xinwen/zaojianxinwen\c1458332611057946626.html"""
            }
        ]
    }
}

1.8. 联想搜索\热词搜索

/es/suggestSearch.do 根据输入的关键字显示相关的关键词
/es/hotSearch.do 搜索指定时间访问的热词搜索及搜索量

[!tip] 根据搜索的结果,通过前端技术处理方式进行跳转

1.9. 搜索页面

可以通过swagger接口快速调试,前端页面通过异步请求处理。

代码参考

...       
<!-- 搜索表单,取自公共头文件,作用为跳转到搜索页面 -->
<div class="hd">
    <div class="hd-inner">
        <form id="searchDataForm" action="/mcms/search.do" method="post">
            <div class="input-wrap">
                <input type="text" style="display: none;" name="style" value="{ms:global.template/}">
                <input id="content_title" name="content_title" value="${search.content_title}" content="${search.content_title}" type="text" class="search-input" placeholder="请输入关键字">
                <a onclick="document.getElementById('searchDataForm').submit();" class="search-btn">
                    <img src="{ms:global.contextpath/}/{ms:global.style/}/image/search.png" alt="">
                </a>
            </div>
        </form>
    </div>
</div>




       <div id="app">

            <div class="search-wrap">
            <#if search.content_title?? && search.content_title>
            <ul class="result-list">
                <!--列表-->
                <template v-for="item in contentList">
                    <!--列表项-start-->
                    <li class="img">
                        <img :src="item.litPic" alt="" v-if="item.litPic != null">
                        <a :href="<#if isDo?? && isDo>'{ms:global.url/}/mcms/view.do?style=out&id=' + item.id<#else>'{ms:global.url/}' + item.url</#if>" v-html="item.title" target="_blank" class="ellipsis-2">{{ item.title}}</a>
                        <p class="desc ellipse">{{ item.descrip }}</p>
                        <span>{{ item.date }}</span>
                    </li>
                </template>
            </ul>
            <!--分页-->
            <div class="news-page"></div>
            <#else>
            <span style="text-align: center;display:block;font-size: 20px;color: red">搜索关键字不可以为空</span>
            </#if>
        </div>

        </div>
...

...
        <script>
            function search(val){
            ar contextPath = '{ms:global.contextpath/}';
            // 过滤条件及范围查询,非必须
            var rangeFields = JSON.stringify(app.rangeFields);
            var filter = JSON.stringify(app.filter);
            <!--es通用搜索,支持高亮搜索,不使用es的情况下将下面代码全部注释即可-->
            //ms.http.post(ms.base+"/cms/es/search/highlightSearch.do",{
            ms.http.post(contextPath+"/es/search.do",{
                docName:'文章',
                fields: 'title,content',// 匹配哪些字段,逗号分隔
                keyword: '{ms:search.content_title/}',// 匹配关键字
                orderBy: 'date',// 排序字段
                order: 'desc',// 倒序
                filter: filter,// 过滤条件
                rangeFields: rangeFields,// 范围查询
                pageNo: val,// 页码
                pageSize: 5,// 页显示数
            }).then(res => {
                var content;
                // 由于在es环境下无法用标签获取文章相关值,在这里预先对文章图片做处理
                for (var i = 0;i<res.data.rows.length;i++){
                    content = res.data.rows[i];
                    if(content.litPic){
                        content.litPic = JSON.parse(content.litPic);
                        if(content.litPic.length > 0 ){
                            res.data.rows[i].litPic = content.litPic[0].path;
                        }
                    } else {
                        res.data.rows[i].litPic = null;
                    }
                }
                app.contentList = res.data.rows
                //getData(this.contentCount);
            }).catch(err => {
                console.log(err)
            })
        }
        var app = new Vue({
            el: '#app',
            component(){
            },
            data: {
                // 过滤条件,如下条件为查询栏目id为123456的文章
                filter: {
                    typeId: '123456',

                },
                // 范围条件,如下为查找指定时间范围内,且排序值在1-9之间的文章
                rangeFields: [
                    {
                        type: 'date',
                        field: 'date',
                        start: '2022-03-26 12:12:00',
                        end: '2022-03-26 14:36:00',
                    },
                    {
                        type: 'text',
                        field: 'sort',
                        start: '1',
                        end: '9',
                    }
                ],
                searchForm: {
                    keyword: ""
                },
                hotSearchKeywords: [],    //当前页数

                // 搜索数据
                contentList: []
            },
            watch: {
            },
            methods: {
                handleCurrentChange:function(val) {
                    <!--es通用搜索-->
                    ms.http.post(ms.base+"/es/search.do",{
                        docName:'文章',
                        fields: 'title',
                        keyword: '{ms:search.content_title/}',
                        orderBy: 'date',
                        order: 'asc',
                        pageNo: val,
                        pageSize: 5,
                    }).then(res => {
                        this.contentList = res.data.rows
                    }).catch(err => {
                        console.log(err)
                    })
                },
                switchShow:function(arr){
                    var that = this
                    arr.forEach(function(x){
                        let e = that.$el.querySelector("#key_"+x)
                        if(e){
                            e.style.display=getComputedStyle(e,null).display=='none'?'flex':'none'
                        }
                    })
                },
                show:function(arr){
                    var that = this
                    arr.forEach(function(x){
                        let e = that.$el.querySelector("#key_"+x)
                        if(e){
                            e.style.display='flex'
                        }
                    })
                },
                hide:function(arr){
                    var that = this
                    arr.forEach(function(x){
                        let e = that.$el.querySelector("#key_"+x)
                        if(e){
                            e.style.display='none'
                        }
                    })
                }
            },
            computed(){
            },
            created(){
                search();
            }
        })
        </script>
 ...

[!tip]
可以根据本页面代码来实现其他数据的全文搜索

Copyright © mingsoft.net 2012-2022 all right reserved,powered by Gitbook该文件修订时间: 2023-02-25 10:14:41

results matching ""

    No results matching ""

    results matching ""

      No results matching ""