elasticsearch 实现联想输入搜索

通常,在项目中需要联想输入(即输入关键字,提示相关词条,类似百度google的搜索)的需求,可能大家都是用的数据库的like '%关键字%‘来实现。但是这样实现有几个问题。

第一、这样的搜索无论是oracle还是mysql,都是无法使用索引的。在oracle中可能有全文检索可以使用,但是个人感觉效果不是很好。

第二、输入的关键字有like的通病,就是只有保含关键字的词条才会被命中。如果中间加个空格之类的,db就无能为力了。

第三、如果要想对命中结果进行相关度排序,这个在常规数据库是无法做到的。虽然,可以按照命中词条的长度进行升序排序,但是加上排序,性能不是很好。

下面介绍一下使用elasticsearch实现联想输入的搜索,因为是搜索引擎,天生就不具备上面的3个问题。

在具体介绍使用方法之前,我们先找个搜索数据。我找的是ICD(就是疾病名称的国标),谁让咱一生都在跟他做斗争。这个在网上一搜一堆。

有了数据,我们先要简单描述一下我们要达到的一个目的。一般的搜索都支持汉字 和拼音两种检索方法。我们的这个检索也满足这个需求。

搜索需求描述:

1、支持汉字和简拼两种搜索方法。

2、输入“高血压”时,按照相关度,将带“高血压”名称的疾病名称按照相关度降序排序。

3、输入“老年 高血压”,时,将带“老年”和“高血压”名称的疾病名称按照相关度降序排序。

4、输入拼音'gxy‘时,将拼音中带有gxy相关的疾病按照相关度降序排序。

....

类似测试用例的需求,到此打住。

那么,我们一步一步实现这种需求。

首先,我们定义了一个ICD的类,算作我们的模型,其实没有模型也可以,只要存入到es且知道各个field的名称就行。这个里面我们只需要关注疾病名称diseaseName及简拼pinyin字段即可,这个字段默认是字符串,ES默认会帮我们分词。

Java代码
  1. import java.io.Serializable;  

  2. import java.math.BigDecimal;  

  3. /**

  4. * ICD抽象对象

  5. * @author donlianli@126.com

  6. */  

  7. public class ICD implements Serializable{  

  8.    private static final long serialVersionUID = 6934803011248581109L;  

  9.    //疾病ID  

  10.    private int id;  

  11.    //疾病编码  

  12.    private String code;  

  13.    //疾病名称  

  14.    private String diseaseName;  

  15.    //疾病加拼音  

  16.    private String mergeName;  

  17.    //汉语拼音简拼  

  18.    private String pinyin;  

  19.    //是否恶心肿瘤  

  20.    private boolean isTherioma;  

  21.    //是否住院特殊病种  

  22.    private boolean isSpecialDisease;  

  23.      

  24.    public ICD(BigDecimal id, String diseaseName, String code,  

  25.            String pinyin, String isTherioma, String isSpecialDisease) {  

  26.        this.id = id.intValue();  

  27.        this.diseaseName = diseaseName;  

  28.        this.code = code;  

  29.        this.pinyin = pinyin;  

  30.        if("是".equals(isTherioma)){  

  31.            this.isTherioma = true;  

  32.        }  

  33.        else {  

  34.            this.isTherioma = false;  

  35.        }  

  36.          

  37.        if("是".equals(isSpecialDisease)){  

  38.            this.isSpecialDisease = true;  

  39.        }  

  40.        else {  

  41.            this.isSpecialDisease = false;  

  42.        }  

  43.        this.mergeName = diseaseName + "," + pinyin;  

  44.    }  

  45.    //set,get ......  

  46.      

  47. }  


第二步,将数据存储到elasticsearch里面,我们取个名称叫code,起个type名称叫icd。ICD大概2w条数据,我使用默认的bulkIndex,存到es大概用了3秒。

我这里是把数据从oracle导入到elasticsearch。

Java代码
  1. import java.math.BigDecimal;  

  2. import java.sql.Connection;  

  3. import java.sql.PreparedStatement;  

  4. import java.sql.ResultSet;  

  5. import java.util.ArrayList;  

  6. import java.util.List;  

  7.  

  8. import org.elasticsearch.action.bulk.BulkRequestBuilder;  

  9. import org.elasticsearch.action.bulk.BulkResponse;  

  10. import org.elasticsearch.action.index.IndexRequestBuilder;  

  11. import org.elasticsearch.client.Client;  

  12.  

  13. import com.donlianli.es.ESUtils;  

  14. import com.donlianli.es.db.DatabaseUtils;  

  15.  

  16. public class ICDManager {  

  17.      

  18.    public static void main(String[] argvs){  

  19.        ICDManager manager = new ICDManager();  

  20.        manager.indexDataDirect();  

  21.    }  

  22.    /**

  23.     * 直接将数据初始化到ES中

  24.     * 不创建mapping

  25.     */  

  26.    private void indexDataDirect() {  

  27.        List<ICD> icdList = getIcdListFromDB();    

  28.        System.out.println(" get icd from db finish,size:" + icdList.size());  

  29.        bulkIndex(icdList);  

  30.    }  

  31.      

  32.    private void bulkIndex(List<ICD> icdList) {  

  33.        Client client = ESUtils.getCodeClient();  

  34.        BulkRequestBuilder bulkRequest = client.prepareBulk();  

  35.        long b = System.currentTimeMillis();  

  36.        for(int i=0,l=icdList.size();i<l;i++){  

  37.            //业务对象  

  38.            ICD icd = icdList.get(i);  

  39.            String json = ESUtils.toJson(icd);  

  40.            IndexRequestBuilder indexRequest = client.prepareIndex("code","icd")  

  41.            .setSource(json).setId(String.valueOf(icd.getId()));  

  42.            //添加到builder中  

  43.            bulkRequest.add(indexRequest);  

  44.        }  

  45.        BulkResponse bulkResponse = bulkRequest.execute().actionGet();  

  46.        if (bulkResponse.hasFailures()) {  

  47.            System.out.println(bulkResponse.buildFailureMessage());  

  48.        }  

  49.        long useTime = System.currentTimeMillis()-b;  

  50.        System.out.println("useTime:" + useTime);  

  51.    }  

  52.    private List<ICD> getIcdListFromDB() {  

  53.        Connection conn = DatabaseUtils.getOracleConnection();  

  54.        String sql = "select * from icd_11";  

  55.        PreparedStatement st = null;  

  56.        ResultSet rs = null;  

  57.        List<ICD> list = new ArrayList<ICD>();  

  58.        try{  

  59.            st = conn.prepareStatement(sql);  

  60.            rs = st.executeQuery();  

  61.            while(rs.next()){  

  62.                BigDecimal id = rs.getBigDecimal("ID");  

  63.                String diseaseName = rs.getString("DISEASE_NAME");  

  64.                String code = rs.getString("CODE");  

  65.                String pinyin = rs.getString("PINYIN");  

  66.                String isTherioma = rs.getString("THERIOMA_FLAG");  

  67.                String isSpecialDisease = rs.getString("OTHER_FLAG");  

  68.                  

  69.                list.add(new ICD(id,diseaseName,code,pinyin,isTherioma,isSpecialDisease));  

  70.            }  

  71.              

  72.            return list;  

  73.        }  

  74.        catch(Exception e){  

  75.            e.printStackTrace();  

  76.        }  

  77.        finally{  

  78.            try{  

  79.            if(rs!= null){  

  80.                rs.close();  

  81.            }  

  82.            if(st!= null){  

  83.                st.close();  

  84.            }  

  85.            conn.close();  

  86.            }  

  87.            catch(Exception e){  

  88.                e.printStackTrace();  

  89.            }  

  90.        }  

  91.        return null;  

  92.    }  

  93. }  


第三步,搜索接口,跑测试用例。

Java代码
  1. import org.elasticsearch.action.search.SearchResponse;  

  2. import org.elasticsearch.client.Client;  

  3. import org.elasticsearch.index.query.MultiMatchQueryBuilder;  

  4. import org.elasticsearch.index.query.QueryBuilders;  

  5. import org.elasticsearch.search.SearchHit;  

  6. import org.elasticsearch.search.SearchHits;  

  7.  

  8. import com.donlianli.es.ESUtils;  

  9.  

  10. public class PinyinSearchTest {  

  11.    public static void main(String[] args) {  

  12.        Client client = ESUtils.getCodeClient();  

  13.        String keyWord = "高血压";  

  14. //      String keyWord = "老年 高血压";  

  15. //      String keyWord = "gxy";  

  16.        //多个字段匹配  

  17.        MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyWord, "diseaseName","pinyin");  

  18.          

  19.        long b = System.currentTimeMillis();  

  20.        SearchResponse response = client.prepareSearch("code").setTypes("icd")  

  21.                .setQuery(query)  

  22.                .setFrom(0)  

  23.                //前20个  

  24.                .setSize(20)  

  25.                .execute().actionGet();  

  26.        long useTime = System.currentTimeMillis()-b;  

  27.        System.out.println("search use time:" + useTime + " ms");  

  28.          

  29.        SearchHits shs = response.getHits();  

  30.        for (SearchHit hit : shs) {  

  31.            System.out.println("分数:"  

  32.                    + hit.getScore()  

  33.                    + ",ID:"  

  34.                    + hit.getId()  

  35.                    + ", 疾病名称:"  

  36.                    + hit.getSource().get("diseaseName")  

  37.                    + ",拼音:" + hit.getSource().get("pinyin"));  

  38.        }  

  39.        client.close();  

  40.    }  

  41. }  

3.1,关键字:'高血压'

search use time:174 ms
分数:2.3859928,ID:6904, 疾病名称:高血压病,拼音:gxyb
分数:2.136423,ID:6907, 疾病名称:高血压I期,拼音:gxyyq
分数:2.12253,ID:6908, 疾病名称:高血压Ⅱ期,拼音:gxyeq
分数:2.12253,ID:6910, 疾病名称:高血压危象,拼音:gxywx
分数:2.0906634,ID:6917, 疾病名称:肾性高血压,拼音:sxgxy
分数:2.0877438,ID:6909, 疾病名称:高血压Ⅲ期,拼音:gxysq
分数:2.0821526,ID:18767, 疾病名称:高原性高血压,拼音:gyxgxy
分数:1.9905697,ID:6906, 疾病名称:恶性高血压,拼音:exgxy
分数:1.9510978,ID:7260, 疾病名称:高血压脑出血,拼音:gxyncx
分数:1.9078629,ID:6923, 疾病名称:肾血管性高血压,拼音:sxgxgxy
分数:1.8312198,ID:6914, 疾病名称:高血压性肾病,拼音:gxyxsb
分数:1.8193114,ID:7367, 疾病名称:高血压性脑病,拼音:gxyxnb
分数:1.8193114,ID:13470, 疾病名称:妊娠引起高血压,拼音:rsyqgxy
分数:1.7919972,ID:6905, 疾病名称:临界性高血压,拼音:ljxgxy
分数:1.7919972,ID:6912, 疾病名称:高血压性心脏病,拼音:gxyxxzb
分数:1.7894946,ID:6928, 疾病名称:继发性高血压,拼音:jfxgxy
分数:1.7062025,ID:6913, 疾病名称:高血压性肾衰竭,拼音:gxyxssj
分数:1.7062025,ID:13485, 疾病名称:孕产妇高血压,拼音:ycfgxy
分数:1.7062025,ID:14534, 疾病名称:新生儿高血压,拼音:xsegxy
分数:1.7062025,ID:16181, 疾病名称:应激性高血压,拼音:yjxgxy

3.2关键字:'老年 高血压'

search use time:144 ms
分数:1.1089094,ID:6904, 疾病名称:高血压病,拼音:gxyb
分数:0.99291986,ID:6907, 疾病名称:高血压I期,拼音:gxyyq
分数:0.9864628,ID:6908, 疾病名称:高血压Ⅱ期,拼音:gxyeq
分数:0.9864628,ID:6910, 疾病名称:高血压危象,拼音:gxywx
分数:0.9716526,ID:6917, 疾病名称:肾性高血压,拼音:sxgxy
分数:0.97029567,ID:6909, 疾病名称:高血压Ⅲ期,拼音:gxysq
分数:0.96769714,ID:18767, 疾病名称:高原性高血压,拼音:gyxgxy
分数:0.9251333,ID:6906, 疾病名称:恶性高血压,拼音:exgxy
分数:0.9067884,ID:7260, 疾病名称:高血压脑出血,拼音:gxyncx
分数:0.8866946,ID:6923, 疾病名称:肾血管性高血压,拼音:sxgxgxy
分数:0.8510741,ID:6914, 疾病名称:高血压性肾病,拼音:gxyxsb
分数:0.8455395,ID:7367, 疾病名称:高血压性脑病,拼音:gxyxnb
分数:0.8455395,ID:13470, 疾病名称:妊娠引起高血压,拼音:rsyqgxy
分数:0.8328451,ID:6905, 疾病名称:临界性高血压,拼音:ljxgxy
分数:0.8328451,ID:6912, 疾病名称:高血压性心脏病,拼音:gxyxxzb
分数:0.831682,ID:6928, 疾病名称:继发性高血压,拼音:jfxgxy
分数:0.8074301,ID:6820, 疾病名称:老年耳聋,拼音:lnel
分数:0.80348647,ID:7612, 疾病名称:老年痣,拼音:lnz
分数:0.7929714,ID:6913, 疾病名称:高血压性肾衰竭,拼音:gxyxssj
分数:0.7929714,ID:13485, 疾病名称:孕产妇高血压,拼音:ycfgxy

高血压和老年的相关并都出来了。只可惜老年高血压,没有列入ICD.

3.3拼音:'gxy'

呃?怎么没有出来?

这个问题折腾了我一天。一开始我以为是被es列入了禁用词。后来,找到是因为没有设置analyzer导致,在设analyzer的过程中竟然还犯了好几个低级错误,导致我非常怀疑设置analyzer是否管用。

这个问题涉及到分词,而分词我还没有好好研究过。总之,在创建索引及mapping的时候,指定一个analyzer就可以解决这个问题。

创建index及mapping的代码如下:

Java代码
  1. import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;  

  2.  

  3. import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;  

  4. import org.elasticsearch.client.Client;  

  5. import org.elasticsearch.common.settings.ImmutableSettings;  

  6. import org.elasticsearch.common.settings.ImmutableSettings.Builder;  

  7. import org.elasticsearch.common.xcontent.XContentBuilder;  

  8.  

  9. import com.donlianli.es.ESUtils;  

  10. /**

  11. * 创建code的mapping

  12. * @author donlianli@126.com

  13. */  

  14. public class CodeMappingTest {  

  15.    static final String INDEX_NAME="code";  

  16.    static final String TYPE_NAME="icd";  

  17.      

  18.    public static void  main(String[] argv) throws Exception{  

  19.        Client client = ESUtils.getCodeClient();  

  20.        Builder settings = ImmutableSettings.settingsBuilder()  

  21.                .loadFromSource(getAnalysisSettings());  

  22.        //首先创建索引库  

  23.        CreateIndexResponse  indexresponse = client.admin().indices()  

  24.        //这个索引库的名称还必须不包含大写字母  

  25.        .prepareCreate(INDEX_NAME).setSettings(settings)  

  26.        //这里直接添加type的mapping  

  27.        .addMapping(TYPE_NAME, getMapping())  

  28.        .execute().actionGet();  

  29.          

  30.        System.out.println("success:"+indexresponse.isAcknowledged());  

  31.    }  

  32.    private static String getAnalysisSettings() throws Exception {  

  33.        XContentBuilder mapping = jsonBuilder()    

  34.                   .startObject()    

  35.                   //主分片数量  

  36.                   .field("number_of_shards",5)  

  37.                   .field("number_of_replicas",0)  

  38.                     .startObject("analysis")    

  39.                        .startObject("filter")  

  40.                            //创建分词过滤器  

  41.                            .startObject("pynGram")  

  42.                                .field("type","nGram")  

  43.                                //从1开始  

  44.                                .field("min_gram",1)  

  45.                                .field("max_gram",15)  

  46.                            .endObject()  

  47.                        .endObject()      

  48.                          

  49.                        .startObject("analyzer")  

  50.                                //拼音analyszer  

  51.                                .startObject("pyAnalyzer")  

  52.                                .field("type","custom")  

  53.                                .field("tokenizer","standard")  

  54.                                .field("filter"new String[]{"lowercase","pynGram"})  

  55.                                .endObject()  

  56.                        .endObject()      

  57.                    .endObject()    

  58.                  .endObject();    

  59.        System.out.println(mapping.string());  

  60.        return mapping.string();  

  61.    }  

  62.    /**

  63.     * mapping 一旦定义,之后就不能修改。

  64.     * @return

  65.     * @throws Exception

  66.     */  

  67.    private static XContentBuilder getMapping() throws Exception{  

  68.        XContentBuilder mapping = jsonBuilder()    

  69.                   .startObject()    

  70.                     .startObject("icd")    

  71.                     //指定分词器  

  72.                     .field("index_analyzer","pyAnalyzer")  

  73.                     .startObject("properties")          

  74.                       .startObject("id")  

  75.                            .field("type""long")  

  76.                            .field("store""yes")  

  77.                        .endObject()      

  78.                          

  79.                       .startObject("code")  

  80.                            .field("type""string")  

  81.                            .field("store""yes")  

  82.                            .field("index""analyzed")  

  83.                        .endObject()    

  84.                          

  85.                         .startObject("diseaseName")  

  86.                            .field("type""string")  

  87.                            .field("store""yes")  

  88.                            .field("index""analyzed")  

  89.                        .endObject()    

  90.                          

  91.                         .startObject("mergeName")  

  92.                            .field("type""string")  

  93.                            .field("store""yes")  

  94.                            .field("index""analyzed")  

  95.                        .endObject()  

  96.                          

  97.                        .startObject("pinyin")  

  98.                            .field("type""string")  

  99.                            .field("store""yes")  

  100.                            .field("index""analyzed")  

  101.                        .endObject()    

  102.                          

  103.                       .startObject("isTherioma")  

  104.                            .field("type""boolean")  

  105.                            .field("store""yes")  

  106.                       .endObject()    

  107.                        

  108.                        .startObject("isSpecialDisease")  

  109.                            .field("type""boolean")  

  110.                            .field("store""yes")  

  111.                       .endObject()    

  112.                        

  113.                     .endObject()    

  114.                    .endObject()    

  115.                  .endObject();    

  116.        return mapping;  

  117.    }  



(PS:其实还有一种简单的方法,不用创建analyzer,在搜索的时候,使用'*gxy*'进行搜索也可以)

最后,我还把这个检索跟oracle的like进行了比较。结果发现oracle只用20ms就能算出结果,而es却用了将近100ms。可见这种吹捧的nosql,性能不见得比oracle强大啊,但是毋庸置疑的是,功能确实强大了。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值