欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

elasticsearch 关键词搜索 & #

程序员文章站 2022-07-09 12:40:34
...

原文链接:https://www.hexianwei.com/2019/04/21/关键字搜索/

项目背景:需要提供关键词搜索功能(elasticSearch)。其中 & 表示逻辑并, # 表示逻辑或。并且逻辑从左往右。& 和 # 没有优先级之分。但是,可以使用小括号,代表优先级。

例子:

万科#房地产  =>  万科 | 房地产
万科&房地产  =>  万科 & 房地产
万科#房地产&科技  =>  万科&科技 | 房地产&科技
万科&房地产#科技  =>  万科&房地产|科技 
万科&(房地产#科技)  => 万科&房地产| 万科&科技 

乍一看貌似很简单,但是 # 和 & 之间没有优先级之分,所以就不能套用四则运算的逻辑来做了。

ps: 大约花了大半天的时间来实现总体功能,使功能可用,

update 2019年05月06日22:02:53 : 更新了括号的逻辑 bug

技术点:正则,stack, 分拆,递归

解决

因为最后的结果是要使用 es 去搜索。看到上面的解析结果。都拆分为 | 的逻辑,

譬如 万科#房地产 结果就是 万科 | 房地产,es 语句就是多个 ** bool should**,

如果是 万科#房地产&科技,解析的结果是:万科&科技 | 房地产&科技,使用 es 就是两个 bool should,并且 每个 should 里面是两个 must

所以,解决的方式就是将用户输入的参数拆分为 List<List> 的结果。最外面的list表示 | , 里面表示 & 。

//万科#房地产&科技
[[万科, 科技], [房地产, 科技]]

//万科&房地产#科技
[[万科, 房地产], [科技]]

//万科&(房地产#科技)
[[万科,房地产],[万科,科技]]

拆分问题为三步:

  • 全是 & 关键字的
  • 包含括号的
  • 不包含括号的

全是 & 关键字的

如果都是 & 关键字组合的,则关键字都是 & 的逻辑,直接按照 & 分割返回就好了。

String[] strings = params.split("#");
//如果都是 & 关键字,则按照 # 分割之后的结果大小是 1。
if (strings.length == 1) {
    //去掉脏数据,都是 & ,括号没意义
    params.replaceAll("(\\()|(\\))", "");

    strings = params.split("&");
    List list = new ArrayList();
    for (String string : strings) {
        list.add(string);
    }
    resultList.add(list);
    return resultList;
}

case:

万科&(房地产&科技)

//解析结果
[[万科, 房地产, 科技]]

//es语句
{
    "bool": {
        "should": [
            {
                "bool": {
                    "must": [
                        {
                            "multi_match": {
                                "query": "万科",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        },
                        {
                            "multi_match": {
                                "query": "房地产",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        },
                        {
                            "multi_match": {
                                "query": "科技",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        }
                    ]
                }
            }
        ]
    }
}

不包含( 的

如果都是 # 的很好处理了。但是如果是 # 和 & 的混合,则就有点麻烦了。没有单独分出一个逻辑是 全是 # 的原因是:他和 # & 混合的情况是一个情况,都是 有多个 should 的。也就是说 List<List> ,最外面的 list 的size > 1。

以 万科#房地产&科技 为例子。
最终的结果是 [[万科,房地产],[万科,科技]]。

处理逻辑是 还是先按照 # 分割为 list.

所以结果是:[[万科],[房地产&科技]]
里面的 list 之间都是 | 的关系。因为优先级是从左往右。
所以,开始从左往右解析,如果元素包含了 & ,则 按照 & 分割,然后✖️前面的集合元素。也就是:[[万科],[房地产&科技]] => [[万科,房地产],[万科,科技]]

public List<List<String>> noParentheses(String para
    String[] strings = params.split("#");
    List<List<String>> resultList = new ArrayList<>
    for (String s : strings) {
        //用户可能输入多个 ## 在一起
        if (s.isEmpty()) {
            continue;
        }
        List temp = new ArrayList();
        temp.add(s);
        resultList.add(temp);
    }

    //处理分割之后,包含& 的,相当于 ✖️的逻辑
    for (int i = 0; i < resultList.size(); i++) {
        List<String> tempList = resultList.get(i);
        if (tempList.get(0).contains("&")) {
            String[] strs = tempList.get(0).split("
            for (int j = 0; j < i; j++) {
                List<String> temp = resultList.get(
                temp.add(strs[1]);
                resultList.set(j, temp);
            }
            tempList.clear();
            tempList.add(strs[0]);
            tempList.add(strs[1]);
        }
    }
    return resultList;
}
万科#房地产&科技

//解析结果
[[万科, 科技)], [房地产, 科技)]]


//es 结果
{
    "bool": {
        "should": [
            {
                "bool": {
                    "must": [
                        {
                            "multi_match": {
                                "query": "万科",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        },
                        {
                            "multi_match": {
                                "query": "科技)",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        }
                    ]
                }
            },
            {
                "bool": {
                    "must": [
                        {
                            "multi_match": {
                                "query": "房地产",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        },
                        {
                            "multi_match": {
                                "query": "科技)",
                                "minimum_should_match": "100%",
                                "fields": [
                                    "company_name_cn",
                                    "company_name_en",
                                    "company_shortname_cn",
                                    "company_shortname_en",
                                    "company_description_cn",
                                    "product_name"
                                ],
                                "type": "best_fields"
                            }
                        }
                    ]
                }
            }
        ]
    }
}

包含 () 的逻辑

如果包含 (),则 第一步先校验 合法性。即括号是不是成对出现的。

  • 校验括号合法性的逻辑

很简单的逻辑,使用栈。如果遇到左括号,则 push,如果是右括号则 pop。其他的不操作。最后验证 栈是不是空的。如果是空的,则表示括号是成对出现的。

public boolean isValidKeyWords(String keys) {
    char[] chars = keys.toCharArray();
    Stack stack = new Stack();
    for (char aChar : chars) {
        switch (aChar) {
            case '(':
                stack.push(aChar);
                break;
            case ')':
                if (stack.empty() || !stack.peek().equals('(')) {
                    return false;
                } else {
                    stack.pop();
                }
                break;
            default:
                break;
        }
    }
    return stack.isEmpty();
}
  • 解析

括号代表了优先级。

譬如:

万科&房地产#科技 => 万科 房地产 | 科技

万科&(房地产#科技) 万科 房地产 | 万科 科技

核心代码:如果遇到 ( ,则将()里面的内容取出来,如果包含 ( ,则递归调用 formatParams,否则调用 noParentheses

public List<List<String>> hasParentheses(String params) {
    List<List<String>> resultList = new ArrayList<>();
    char[] chars = params.toCharArray();
    //存储括号里面的关键词
    List<String> subList = new LinkedList();
    int index = 0;
    //当前关键字在括号里面的逻辑
    Boolean flag = Boolean.FALSE;
    //括号结束的标志
    int flagEnd = 0;
    for (int i = 0; i < chars.length; i++) {
        switch (chars[i]) {
            case '#':
                if (!flag) {
                    if (index == i) {
                        String subParams = String.valueOf(Arrays.copyOfRange(chars, index + 1, chars.length));
                        resultList.addAll(formatParams(subParams));
                    } else {
                        List<String> tempList = new ArrayList<>();
                        String subParams = String.valueOf(Arrays.copyOfRange(chars, index, i));
                        tempList.add(subParams);
                        resultList.add(tempList);
                        index = i;
                    }
                } else {
                    subList.add(String.valueOf(chars[i]));
                }
                break;
            case '(':
                flagEnd++;
                if (flag) {
                    subList.add(String.valueOf(chars[i]));
                } else {
                    flag = Boolean.TRUE;
                    index = i;
                }
                break;
            case ')':
                flagEnd--;
                if (flag && flagEnd == 0) {
                    flag = Boolean.FALSE;
                    if (!subList.isEmpty()) {
                        String localKey = String.join("", subList);
                        if (localKey.contains("(")) {
                            resultList.addAll(formatParams(localKey));
                        } else {
                            resultList.addAll(noParentheses(localKey));
                        }
                        subList.clear();
                        index = i + 1;
                    }
                }else{
                    subList.add(String.valueOf(chars[i]));
                }
                break;
            default:
                if (flag) {
                    subList.add(String.valueOf(chars[i]));
                }
                break;
        }
    }
    return resultList;
}

使用两个带括号的例子:

//(万科#(房地产&科技))#信息
[[万科], [房地产, 科技], [信息]]
//万科#(房地产&科技)
[[万科], [房地产, 科技]]

全部代码

import java.util.*;

/**
 * @author beer
 * @date 2019-04-21 20:23
 * @description: es 关键字搜索
 */
public class EsKeyWords {

    public static void main(String[] args) {
        EsKeyWords esKeyWords = new EsKeyWords();
        System.out.println(esKeyWords.formatParams("(万科#(房地产&科技))#信息"));
        System.out.println(esKeyWords.formatParams("万科#(房地产&科技)"));
    }

    public List<List<String>> formatParams(String params) {
        String[] strings = params.split("#");
        List<List<String>> resultList = new ArrayList<>();
        //如果都是 &
        if (strings.length == 1) {
            params = params.replaceAll("(\\()|(\\))", "");
            strings = params.split("&");
            List list = new ArrayList();
            for (String string : strings) {
                list.add(string);
            }
            resultList.add(list);
            return resultList;
        }
        if (params.contains("(")) {
            //校验 "(" ")" 的合法性,是否是成对出现的
            Boolean isValid = isValidKeyWords(params);
            if (!isValid) {
                throw new IllegalArgumentException("illegal keywords : " + params);
            }
            resultList = hasParentheses(params);
        } else {
            resultList = noParentheses(params);
        }
        return resultList;
    }

    public List<List<String>> hasParentheses(String params) {
        List<List<String>> resultList = new ArrayList<>();
        char[] chars = params.toCharArray();
        //存储括号里面的关键词
        List<String> subList = new LinkedList();
        int index = 0;
        //当前关键字在括号里面的逻辑
        Boolean flag = Boolean.FALSE;
        //括号结束的标志
        int flagEnd = 0;
        for (int i = 0; i < chars.length; i++) {
            switch (chars[i]) {
                case '#':
                    if (!flag) {
                        if (index == i) {
                            String subParams = String.valueOf(Arrays.copyOfRange(chars, index + 1, chars.length));
                            resultList.addAll(formatParams(subParams));
                        } else {
                            List<String> tempList = new ArrayList<>();
                            String subParams = String.valueOf(Arrays.copyOfRange(chars, index, i));
                            tempList.add(subParams);
                            resultList.add(tempList);
                            index = i;
                        }
                    } else {
                        subList.add(String.valueOf(chars[i]));
                    }
                    break;
                case '(':
                    flagEnd++;
                    if (flag) {
                        subList.add(String.valueOf(chars[i]));
                    } else {
                        flag = Boolean.TRUE;
                        index = i;
                    }
                    break;
                case ')':
                    flagEnd--;
                    if (flag && flagEnd == 0) {
                        flag = Boolean.FALSE;
                        if (!subList.isEmpty()) {
                            String localKey = String.join("", subList);
                            if (localKey.contains("(")) {
                                resultList.addAll(formatParams(localKey));
                            } else {
                                resultList.addAll(noParentheses(localKey));
                            }
                            subList.clear();
                            index = i + 1;
                        }
                    }else{
                        subList.add(String.valueOf(chars[i]));
                    }
                    break;
                default:
                    if (flag) {
                        subList.add(String.valueOf(chars[i]));
                    }
                    break;
            }
        }
        return resultList;
    }


    public List<List<String>> noParentheses(String params) {
        String[] strings = params.split("#");
        List<List<String>> resultList = new ArrayList<>();
        for (String s : strings) {
            //用户可能输入多个 ## 在一起
            if (s.isEmpty()) {
                continue;
            }
            List temp = new ArrayList();
            temp.add(s);
            resultList.add(temp);
        }
        for (int i = 0; i < resultList.size(); i++) {
            List<String> tempList = resultList.get(i);
            if (tempList.get(0).contains("&")) {
                String[] strs = tempList.get(0).split("&");
                for (int j = 0; j < i; j++) {
                    List<String> temp = resultList.get(j);
                    temp.add(strs[1]);
                    resultList.set(j, temp);
                }
                tempList.clear();
                tempList.add(strs[0]);
                tempList.add(strs[1]);
            }
        }
        return resultList;
    }

    public boolean isValidKeyWords(String keys) {
        char[] chars = keys.toCharArray();
        Stack stack = new Stack();
        for (char aChar : chars) {
            switch (aChar) {
                case '(':
                    stack.push(aChar);
                    break;
                case ')':
                    if (stack.empty() || !stack.peek().equals('(')) {
                        return false;
                    } else {
                        stack.pop();
                    }
                    break;
                default:
                    break;
            }
        }
        return stack.isEmpty();
    }
}