高亮搜索案例

一. 定义分词器

PUT news
{
  "settings": {
    "analysis": {
      "analyzer": {
        "news_tag_analyzer": {
          "char_filter": "html_strip",
          "tokenizer": "keyword",
          "filter": "news_tag_filter"
        }
      },
      "filter": {
        "news_tag_filter": {
          "type": "pinyin",
          "keep_separate_first_letter" : false,
          "keep_full_pinyin" : false,
          "keep_original" : true,
          "lowercase" : true,
          "keep_joined_full_pinyin": true,
          "remove_duplicated_term": true
        }
      }
    }
  }
}

二. 定义mapping

PUT news/_mapping
{
  "properties" : {
    "content" : {
      "type" : "text",
      "analyzer": "ik_max_word",
      "search_analyzer": "ik_smart"
    },
    "id" : {
      "type" : "long"
    },
    "tags" : {
      "type": "completion",
      "analyzer": "news_tag_analyzer",
      "search_analyzer": "keyword"
    },
    "title" : {
      "type" : "text",
      // 在数据索引进ES的时候,让词分的细腻点
      "analyzer": "ik_max_word",
      // 在查询的时候,尽量不要去分
      "search_analyzer": "ik_smart"
    },
    "url" : {
      // url地址栏不进行分词
      "type" : "keyword"
    }
  }
}

三. 自定义网络词库

在某个服务器中(Tomcat或者Nginx)中放两个文件 customer.dicstopword.dic 文件(文件可以是txt文件)。customize.dic 文件中放的是业务词库,stopword.dic 中存放的是停用词。IK的一个bug问题,就是第一行不要写内容, 文件内容一次如下:

customize.dic

梅长苏
拜登
柳岩

stopword.dic

腰
的

将两个文件放到 NGINX_HOME/html 目录下,然后启动nginx, 那么我们就在浏览器地址栏访问到两个文件的内容。

四. 配置IK的动态词库

修改 ES_HOME/plugins/ik/config 目录下的 IKAnalyzer.cfg.xml, 文件的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
        <!-- <entry key="ext_dict">customer/customize.dic</entry> -->
    <entry key="ext_dict"></entry>
	 <!--用户可以在这里配置自己的扩展停止词字典-->
        <!-- <entry key="ext_stopwords">stopword/keys.dir</entry> -->
    <entry key="ext_stopwords"></entry>
	<!--用户可以在这里配置远程扩展字典 -->
	<!-- <entry key="remote_ext_dict"></entry> -->
    <entry key="remote_ext_dict">http://localhost/customize.dic</entry>
	<!--用户可以在这里配置远程扩展停止词字典-->
	<entry key="remote_ext_stopwords">http://localhost/stopword.dic</entry>
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

五. 导入数据库数据

一. 重启ES

二. 将提供的 mysql-news.conf 这个文件放到 LOGSTASH_HOME/config 目录下(依情况作一些修改)

三. 将mysql的驱动包放到 LOGSTASH_HOME/logstash-core/lib/jars 目录下。

四.进入 LOGSTASH_HOME/bin 目录下打开dos命令行,执行如下命令:

logstash  -f ../config/mysql-news.conf

六. 书写测试DSL

前缀搜索的DSL

GET news/_search
{
  "_source": false, 
  "suggest": {
    "news_tag_suggest": {
      // jnl 就是需要前缀提示的内容
      "prefix": "jnl",   
      "completion": {
        "field": "tags",
        "size": 10,
        "skip_duplicates":true
      }
    }
  }
}

高亮搜索的 DSL

GET news/_search
{
  "_source": ["id", "title", "content", "url"], 
  "query": {
    "multi_match": {
      // 用户在搜索框中输入的内容,然后进行搜索
      "query": "梅长苏",
      "fields": ["title", "content"]
    }
  },
  "highlight": {
    "pre_tags": "<span class='hl'>", 
    "post_tags": "</span>", 
    "fields": {
      "title": {},
      "content": {}
    }
  }
}

七. 服务端代码的实现

@RestController
@RequestMapping("/news")
public class NewsController {

    private RestHighLevelClient restHighLevelClient;

    /*
     * @Qualifier("elasticsearchClient") 主要是针对容器中有多个 bean, 可以通过 @Qualifier 将那个bean注入进来
     * @param elasticsearchClient
     */
    public NewsController(@Qualifier("elasticsearchClient") RestHighLevelClient restHighLevelClient) {
        this.restHighLevelClient = restHighLevelClient;
    }

    @GetMapping("/tips")
    public List<String> prefixTip(String tips) throws IOException {
        Request request  = new Request("GET", "news/_search");

        String suggestJson = String.format("{" +
                "  \"_source\": false, " +
                "  \"suggest\": {" +
                "    \"news_tag_suggest\": {" +
                "      \"prefix\": \"%s\"," +
                "      \"completion\": {" +
                "        \"field\": \"tags\"," +
                "        \"size\": 10," +
                "        \"skip_duplicates\":true" +
                "      }" +
                "    }" +
                "  }" +
                "}", tips);

        request.setJsonEntity(suggestJson);

        // 发送请求, 返回结果
        Response response = restHighLevelClient.getLowLevelClient().performRequest(request);

        // 返回的json字符串, 就是于 kibana中得到的json数据
        String responseJson = EntityUtils.toString(response.getEntity());

        JSONObject jsonObject = JSONObject.parseObject(responseJson);
        // 获取 kibana 中的 "suggest" 对应的json对象
        JSONObject suggest = jsonObject.getJSONObject("suggest");

        // 就是搜索的信息与结构
        JSONObject resultInfo = suggest.getJSONArray("news_tag_suggest").getJSONObject(0);

        JSONArray options = resultInfo.getJSONArray("options");

        // 给定长度, 是一种优化的策略
        List<String> results = new ArrayList<>(options.size());

        for(int i = 0; i < options.size(); i++) {
            // opt 就是我们需要的数据
            JSONObject opt = options.getJSONObject(i);

            results.add(opt.getString("text"));
        }
        return results;
    }

    @GetMapping("/search")
    public List<NewsModel> search(String keyword) throws IOException {
        Request request  = new Request("GET", "news/_search");

        String suggestJson = String.format("{" +
                "  \"_source\": [\"title\", \"content\", \"url\", \"id\"], " +
                "  \"query\": {" +
                "    \"multi_match\": {" +
                "      \"query\": \"%s\"," +
                "      \"fields\": [\"title\", \"content\"]" +
                "    }" +
                "  }," +
                "  \"highlight\": {" +
                "    \"pre_tags\": \"<span class='hl'>\", " +
                "    \"post_tags\": \"</span>\", " +
                "    \"fields\": {" +
                "      \"title\": {}," +
                "      \"content\": {}" +
                "    }" +
                "  }" +
                "}", keyword);

        request.setJsonEntity(suggestJson);

        // 发送请求, 返回结果
        Response response = restHighLevelClient.getLowLevelClient().performRequest(request);

        // 返回的json字符串, 就是于 kibana中得到的json数据
        String responseJson = EntityUtils.toString(response.getEntity());

        JSONObject jsonObject = JSONObject.parseObject(responseJson);

        JSONArray hits = jsonObject.getJSONObject("hits").getJSONArray("hits");

        List<NewsModel> results = new ArrayList<>();

        if(hits.size() > 0) {
            for(int i = 0; i < hits.size(); i++) {
                // 这是最终查询的每一条数据
                JSONObject data = hits.getJSONObject(i);
                NewsModel nm = new NewsModel();

                // 原始数据
                JSONObject originData = data.getJSONObject("_source");

                nm.setId(originData.getInteger("id"));
                nm.setUrl(originData.getString("url"));

                // 高亮数据
                JSONObject highLightData = data.getJSONObject("highlight");

                // 获取title中高亮的文本碎片, 返回的是一个数组
                JSONArray titles = highLightData.getJSONArray("title");

                // 如果title中没有查询的高亮文本, 就从原始数据中取
                if(null == titles) {
                    nm.setTitle(originData.getString("title"));
                }else {
                    StringBuffer titleSb = new StringBuffer();
                    for(int j = 0; j < titles.size(); j++) {
                        titleSb.append(titles.getString(j));
                    }
                    nm.setTitle(titleSb.toString());
                }

                JSONArray contents = highLightData.getJSONArray("content");

                // 如果title中没有查询的高亮文本, 就从原始数据中取
                if(null == contents) {
                    nm.setContent(originData.getString("content"));
                }else {
                    StringBuffer contentSb = new StringBuffer();
                    for(int j = 0; j < contents.size(); j++) {
                        contentSb.append(contents.getString(j));
                    }
                    nm.setContent(contentSb.toString());
                }

                results.add(nm);
            }
            return results;
        }else {
            return results;
        }
    }
}

八. 前端页面的实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/jquery-ui.min.css">
    <script src="js/jquery-2.1.1.min.js"></script>
    <script src="js/jquery-ui.min.js"></script>
    <script src="js/bootstrap.min.js"></script>
    <script src="js/vue.js"></script>
    <script src="js/axios.js"></script>
    <style>
        .hl {
            color: orangered;
        }
    </style>
</head>
<body>
    <div class="container-fluid">
        <div class="row mt-3 pb-3 mb-3" style="border-bottom: 1px solid #e2e3e5;">
            <div class="col-10">
                <form class="form-inline" onsubmit="javascript: return false;">
                    <div class="form-group col-6">
                        <input class="form-control col" id="search-text" >
                    </div>
                    <button type="submit" class="btn btn-primary col-1" onclick="beginSearch()">搜索一下</button>
                </form>
            </div>
        </div>
        <div id="app">
            <div class="row mb-3" v-for="nr in newsResults" :key="nr.id">
                <div class="col-10">
                    <h4><a target="_blank" :href="nr.url" v-html="nr.title"></a></h4>
                    <p v-html="nr.content"></p>
                </div>
            </div>
        </div>
    </div>
    <script>
        const vm = new Vue({
            el:'#app',
            data() {
                return {
                    newsResults: []
                }
            }
        })

        $('#search-text').autocomplete({
            delay: 300,  // 延迟查询,意思是当在输入框中多输入了一个词,多久往服务器发送请求
            max: 20,  // 指的是下拉列表中最多出现多少个
            source: function(request, cb) {
                // console.log(request.term)
                $.ajax({
                    url: '/news/tips?tips=' + request.term,
                    type: 'get',
                    dataType: 'json',
                    success: function(_data) {
                        // let ds = [];
                        // for(let i = 0; i < _data.length; i++) {
                        //     ds.push(_data[i]);
                        // }
                        // 只需要调用 cb() 方法, 会自动将数组的值展示在输入框的下面
                        cb(_data);
                    }
                })
            },
            // minlength: 1   // 最低输入多少个字母就往服务器端发送请求
        })

        $('#search-text').on('keyup', e => {
            console.log(e)
            // 当用户点击回车的时候
            if(e.keycode == 13) {
                beginSearch()
            }
        })

        // 当用户在输入框或者点击搜索按钮的时候,开始执行搜索
        function beginSearch() {
            let searchText = $('#search-text').val().trim() // 获取输入框的值
            if(searchText) {  // 有值才搜索
                vm.$data.newsResults = []
                $.ajax({
                    url: '/news/search?keyword=' + searchText,
                    type: 'get',
                    dataType: 'json'
                }).then(res => {
                    // 给Vue中的 data() {} 中的 newsResults
                    for(let i = 0; i < res.length; i++) {
                        vm.$data.newsResults.push(res[i])
                    }
                })
            }
        }
    </script>
</body>
</html>
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐