elasticsearch 实现联想输入搜索

2019-03-11 09:29|来源: 网络

通常,在项目中需要联想输入(即输入关键字,提示相关词条,类似百度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强大啊,但是毋庸置疑的是,功能确实强大了。

对这类话题感兴趣?欢迎发送邮件至 donlianli@126.com    
关于我:邯郸人,擅长Java,Javascript,Extjs,oracle sql。    
更多我之前的文章,可以访问  我的空间    

 
转自:http://donlianli.iteye.com/blog/1923017

相关问答

更多
  • 不需要每次集合更新时都不需要运行它。 根据config中的刷新间隔刷新索引,或者通过调用“_refresh”手动刷新索引 是的,支持分页,使用字段“从”,“大小”,“排序”排序查询请参阅 ElasticSearch分页和排序 是的,你可以在任何领域搜索,请参阅http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query/ no you need not run that every time a collection ge ...
  • 我用Django elasticsearch,但它不工作。 根据你的干草堆设置,你不使用Elasticsearch。 您正在使用SimpleEngine ,它根本不是真正的搜索引擎。 要使用Elasticsearch,您的设置必须包含以下内容: HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', ...
  • 他们不一样。 msearch可以向ES发送多个搜索请求,每个请求可以在一个索引或多个索引或类型中搜索。 在msearch中,您可以使用完全不同的查询来提交多个搜索请求。 在Mindex中,只能查询一个查询 They are not the same. msearch give you the ability to send multiple search request to ES, each request can be search in one index or in multiple indexes ...
  • 查询时间提升允许您给予一个查询比另一个查询更多的权重。 例如,假设您正在查询“Quick Brown Fox”的title和body字段,可以将其写为: { "query": { "bool": { "should": [ { "match": { "title": "Quick Brown Fox" } }, { "match": { ...
  • 这是一个很好的用例。 当然Elasticsearch可以执行这样的任务,但它更具手动性。 你必须编写自己的脚本。 因此,例如,如果要汇总数据,可以使用ElasticSearch聚合,并获取结果(以JSON格式提供)并将其存储回保存摘要数据的索引中。 这样,即使您删除原始数据,您的摘要数据仍会存在。 Elasticsearch带有不同的客户端。 我喜欢使用Python Elasticsearch DSL库。 This is a great use case. Of course Elasticsearch c ...
  • 你有什么问题? 1:单独运行ES还是作为应用程序的一部分? 单独运行Elasticsearch会更好。 每次启动应用程序时都不会启动ES。 启动ES可能需要一些时间(启动,同步;如果有大量数据需要更长时间)。 2:ES是MongoDB的一个很好的替代品吗? 取决于你的要求。 Elasticsearch用于搜索。 不要用作(主)数据库。 Elasticsearch不是事务性的! (有更多的理由你不会这样做) What's your question? 1: Run ES standalone or as pa ...
  • 对于文件安全存储的温暖和舒适的感觉? 如果需要,您可以将所有内容存储在ES中,某些数据集不关心耐久性,但ES具有副本集和其他耐久性措施,因此持久性不是一个大问题。 我从使用ES中可以看出的主要问题是它没有一个日志,这可能会造成问题,但它是一个直接的磁盘技术。 因此,“温暖和舒适”的感觉完全适用于ES。 这几乎是主题的长度......我知道这不太有趣。 for that warm and cozy feeling that documents are being safely stored? You can ...
  • 您正在寻找的是分页。 您可以通过查询固定大小并设置from参数来实现目标。 由于您希望以250个结果的批量设置显示,因此可以设置size = 250并且对于每个连续查询,将from的值增加250 。 GET /_search?size=250 ---- return first 250 results GET /_search?size=250&from=250 ---- next 250 results GET /_search?size=2 ...
  • 根据AngularJs的最佳实践和文档,控制器不应该有您的业务逻辑。 我认为模块化您的应用程序更好。 如你所知: 使用控制器: 设置$ scope对象的初始状态。 向$ scope对象添加行为。 不要使用控制器: 操纵DOM - 控制器应仅包含业务逻辑。 将任何表示逻辑放入控制器会显着影响其可测试性。 Angular对大多数情况和指令进行数据绑定以封装手动DOM操作。 格式输入 - 改为使用角度形式控件。 滤镜输出 - 改为使用角度滤镜。 跨控制器共享代码或状态 - 改为使用角度服务。 管理其他组件的生命周 ...
  • 您似乎知道如何使用关系数据库,但是当您考虑NoSQL时,您从关系角度来看它。 NoSQL世界很大,不同的数据库提供不同的优点和缺点。 例如:使用solr / elasticsearch / mongo可以让您存储数据并相当有效地查询它,而其他数据库,例如键值大数据数据库(cassandra和Hbase,仅举几例)将提供更好的性能,但您将被限制为“完全匹配”查询。 就个人而言,我坚信使用关系数据库,只要存储的数据量不是太多就无法处理。 关系数据库非常成熟和强大。 (另外,如果你打算使用关系数据库,我可以建议使 ...