全文检索示例:全文检索接口编写(9)

简介

本文在上一章节springboot集成es基础上完成全文检索接口编写
完整代码github地址

添加maven依赖

代码使用了commons包的一些工具类,添加依赖

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8</version>
</dependency>
<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.2</version>
    <scope>compile</scope>
</dependency>

创建VO

package app.vo;

import lombok.Data;

/**
 * @author faith.huan 2019-07-21 11:01
 */
@Data
public class EsPageVO {

    /**
     * 搜索评分
     */
    private Float score;

    /**
     * 文件名
     */
    private String fileName;

    /**
     * 标题
     */
    private String title;

    /**
     * 内容
     */
    private String content;

    /**
     * 原文地址
     */
    private String  url;

    /**
     * 爬取时间
     */
    private String crawlDate;

    /**
     * 写入ES时间
     */
    private String toEsDate;

}

编写Service

高亮显示

/**
 * 高亮字段
 */
private final HighlightBuilder.Field[] highlightFields = new HighlightBuilder.Field[]{
        new HighlightBuilder.Field("content").fragmentSize(500).numOfFragments(1).noMatchSize(500).preTags("<font color='red'>").postTags("</font>"),
        new HighlightBuilder.Field("title").fragmentSize(150).numOfFragments(1).noMatchSize(150).preTags("<font color='red'>").postTags("</font>")
};

字段筛选

因为content字段内容很长,我们在查询是筛选掉content字段,不进行查询该字段

/**
 * 显示字段筛选,不显示content字段
 */
private final SourceFilter sourceFilter = new FetchSourceFilterBuilder().withExcludes("content").build();

构建查询

根据关键词和查询类型(phrase|term)构建查询

/**
 * 根据关键词和检索类型构建查询
 *
 * @param keyword 关键词
 * @param type    查询类型
 * @return QueryBuilder
 */
private QueryBuilder buildKeywordQuery(String keyword, String type) {
    if ("phrase".equals(type)) {
        // 使用短语匹配查询
        log.debug("matchPhraseQuery,keyword:{}", keyword);
        BoolQueryBuilder builder = QueryBuilders.boolQuery();
        builder.should().add(QueryBuilders.matchPhraseQuery("title", keyword));
        builder.should().add(QueryBuilders.matchPhraseQuery("content", keyword));
        return builder;
    } else {
        // 使用分词查询
        log.debug("multiMatchQuery,keyword:{}", keyword);
        return QueryBuilders.multiMatchQuery(keyword, "content", "title");
    }

}

全文检索方法

/**
 * 全文检索
 *
 * @param keyword  关键词
 * @param type     检索类型  phrase|term
 * @param page     页码
 * @param pageSize 一页条数
 */
public AggregatedPage<EsPageVO> fullTextSearch(String keyword, String type,
                                             int page, int pageSize) {
    try {
        QueryBuilder queryBuilder = buildKeywordQuery(keyword, type);

        SearchQuery searchQuery = new NativeSearchQueryBuilder().withIndices(INDEX_NAME).withTypes(DOC_TYPE)
                .withQuery(queryBuilder)
                // 设置字段筛选
                .withSourceFilter(sourceFilter)
                // 设置高亮字段
                .withHighlightFields(highlightFields)
                // 设置分页
                .withPageable(PageRequest.of(page, pageSize)).build();

        return elasticsearchRestTemplate.queryForPage(searchQuery, EsPageVO.class,new SearchResultMapper() {
            @Override
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
                List<EsPageVO> list = new ArrayList<>();
                SearchHits hits = response.getHits();
                for (SearchHit searchHit : hits) {
                    EsPageVO doc = new EsPageVO();
                    Map<String, Object> sourceMap = searchHit.getSourceAsMap();
                    doc.setScore(searchHit.getScore());
                    doc.setFileName(MapUtils.getString(sourceMap, "fileName"));
                    doc.setUrl(MapUtils.getString(sourceMap, "url"));
                    doc.setCrawlDate(MapUtils.getString(sourceMap, "crawlDate"));
                    doc.setToEsDate(MapUtils.getString(sourceMap, "toEsDate"));

                    // 高亮字段处理
                    Map<String, HighlightField> highlightFields = searchHit.getHighlightFields();
                    // 标题高亮
                    if (highlightFields.containsKey("title")) {
                        Text[] titles = highlightFields.get("title").getFragments();
                        doc.setTitle(titles[0].string());
                    } else {
                        log.warn("未找到标题高亮内容");
                        doc.setTitle(MapUtils.getString(sourceMap, "title"));
                    }
                    // 正文高亮
                    if (highlightFields.containsKey("content")) {
                        Text[] contents = highlightFields.get("content").getFragments();
                        doc.setContent(contents[0].string());
                    } else {
                        log.warn("未找到正文高亮内容");
                        doc.setContent("无正文内容");
                    }
                    list.add(doc);
                }

                return new AggregatedPageImpl<T>((List<T>) list, pageable, hits.getTotalHits(),  hits.getMaxScore());

            }

            @Override
            public <T> T mapSearchHit(SearchHit searchHit, Class<T> type) {
                return null;
            }
        });

    } catch (Exception e) {
        log.error("高级查询发生异常", e);
        return null;
    }
}

编写Controller

package app.web;

import app.service.SearchService;
import app.vo.EsPageVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.web.bind.annotation.*;

/**
 * 全文检索Controller
 *
 * @author faith.huan 2019-10-21 21:29:26
 */
@RestController
@RequestMapping("/search")
@Slf4j
public class SearchController {

    private final SearchService searchService;

    public SearchController(SearchService searchService) {
        this.searchService = searchService;
    }


    /**
     * 全文检索方法
     *
     * @param keyword  关键字
     * @param type     检索类型
     * @param page     页码
     * @param pageSize 一页条数
     */
    @CrossOrigin
    @PostMapping("/fullTextSearch")
    public AggregatedPage<EsPageVO> fullTextSearch(@RequestParam(value = "keyword", required = false, defaultValue = "") String keyword,
                                                   @RequestParam(value = "type", required = false, defaultValue = "") String type,
                                                   @RequestParam(value = "page", required = false, defaultValue = "0") int page,
                                                   @RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize
    ) {

        log.info("keyword:{}, type:{},page:{},pageSize:{}", keyword, type, page, pageSize);

        return searchService.fullTextSearch(keyword, type, page, pageSize);
    }


}

接口测试

我使用的是谷歌插件RESTer进行的测试,你也可以选择postman等其他测试rest接口的工具。

按照下图设置方法地址headers内容
在这里插入图片描述

phrase测试

设置关键词为权威指南,查询类型为phrase,点击send进行测试
在这里插入图片描述
返回json,只查询到一个文档,文档包含权威指南这个短语

{
    "content": [
        {
            "score": 12.266467,
            "fileName": "f927fb4b4850e60ff42bcbd4d9d7bb96.json",
            "title": "如何读这本书",
            "content": "这本<font color='red'>权威</font><font color='red'>指南</font>不仅会帮助你学习 Elasticsearch,而且希望能够带你接触一些更深入、更有趣的话题,如  、  、  和  ,这些虽然不是必要的阅读却能让你深入理解其内在机制。 本书的第一部分应该按章节顺序阅读,因为每一章建立在上一章的基础上(尽管你也可以浏览刚才提到的章节)。 后续各章节如  和  相对独立,你可以按需选择性参阅。",
            "url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/_how_to_read_this_book.html",
            "crawlDate": "2019-10-20T09:56:54.924",
            "toEsDate": "2019-10-20T10:27:47.135+0800"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "offset": 0,
        "pageSize": 10,
        "pageNumber": 0,
        "paged": true,
        "unpaged": false
    },
    "facets": [],
    "aggregations": null,
    "scrollId": null,
    "maxScore": 12.266467,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0,
    "size": 10,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "numberOfElements": 1,
    "first": true,
    "last": true,
    "empty": false
}

term测试

关键词仍然用权威指南,查询类型改为term,点击send进行测试
在这里插入图片描述
查询返回json,查询到三个文档,其中第二、三个文档只包含指南关键字

{
    "content": [
        {
            "score": 12.266468,
            "fileName": "f927fb4b4850e60ff42bcbd4d9d7bb96.json",
            "title": "如何读这本书",
            "content": "这本<font color='red'>权威</font><font color='red'>指南</font>不仅会帮助你学习 Elasticsearch,而且希望能够带你接触一些更深入、更有趣的话题,如  、  、  和  ,这些虽然不是必要的阅读却能让你深入理解其内在机制。 本书的第一部分应该按章节顺序阅读,因为每一章建立在上一章的基础上(尽管你也可以浏览刚才提到的章节)。 后续各章节如  和  相对独立,你可以按需选择性参阅。",
            "url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/_how_to_read_this_book.html",
            "crawlDate": "2019-10-20T09:56:54.924",
            "toEsDate": "2019-10-20T10:27:47.135+0800"
        },
        {
            "score": 6.866167,
            "fileName": "6e937165ca8899fab217ff535cb3335b.json",
            "title": "部署",
            "content": "这一章不是在生产中运行集群的详尽<font color='red'>指南</font>,但是它涵盖了集群上线之前需要考虑的关键事项。 主要包括三个方面:",
            "url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/deploy.html",
            "crawlDate": "2019-10-20T09:57:07.270",
            "toEsDate": "2019-10-20T10:27:47.063+0800"
        },
        {
            "score": 5.19139,
            "fileName": "dff9ac1115ab6ffdd6141c8b2b7ce91e.json",
            "title": "前言",
            "content": "无论你是需要全文搜索,还是结构化数据的实时统计,或者两者结合,这本<font color='red'>指南</font>都能帮助你了解其中最基本的概念, 从最基本的操作开始学习 Elasticsearch。之后,我们还会逐渐开始探索更加高级的搜索技术,不断提升搜索体验来满足你的需求。 Elasticsearch 不仅仅只是全文搜索,我们还将介绍结构化搜索、数据分析、复杂的人类语言处理、地理位置和对象间关联关系等。 我们还将探讨为了充分利用 Elasticsearch 的水平伸缩性,应当如何建立数据模型,以及在生产环境中如何配置和监控你的集群。",
            "url": "https://www.elastic.co/guide/cn/elasticsearch/guide/current/preface.html",
            "crawlDate": "2019-10-20T09:56:55.336",
            "toEsDate": "2019-10-20T10:27:47.120+0800"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "offset": 0,
        "pageSize": 10,
        "pageNumber": 0,
        "paged": true,
        "unpaged": false
    },
    "facets": [],
    "aggregations": null,
    "scrollId": null,
    "maxScore": 12.266468,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0,
    "size": 10,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "numberOfElements": 3,
    "first": true,
    "last": true,
    "empty": false
}

```

至此全文检索接口编写、测试结束。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值