通常,在項目中需要聯想輸入(即輸入關鍵字,提示相關詞條,類似百度google的搜尋)的需求,可能大家都是用的資料庫的like '%關鍵字%‘來實作。但是這樣實作有幾個問題。
第一、這樣的搜尋無論是oracle還是mysql,都是無法使用索引的。在oracle中可能有全文檢索可以使用,但是個人感覺效果不是很好。
第二、輸入的關鍵字有like的通病,就是隻有保含關鍵字的詞條才會被命中。如果中間加個空格之類的,db就無能為力了。
第三、如果要想對命中結果進行相關度排序,這個在正常資料庫是無法做到的。雖然,可以按照命中詞條的長度進行升序排序,但是加上排序,性能不是很好。
下面介紹一下使用elasticsearch實作聯想輸入的搜尋,因為是搜尋引擎,天生就不具備上面的3個問題。
在具體介紹使用方法之前,我們先找個搜尋資料。我找的是ICD(就是疾病名稱的國标),誰讓咱一生都在跟他做鬥争。這個在網上一搜一堆。
有了資料,我們先要簡單描述一下我們要達到的一個目的。一般的搜尋都支援漢字 和拼音兩種檢索方法。我們的這個檢索也滿足這個需求。
搜尋需求描述:
1、支援漢字和簡拼兩種搜尋方法。
2、輸入“高血壓”時,按照相關度,将帶“高血壓”名稱的疾病名稱按照相關度降序排序。
3、輸入“老年 高血壓”,時,将帶“老年”和“高血壓”名稱的疾病名稱按照相關度降序排序。
4、輸入拼音'gxy‘時,将拼音中帶有gxy相關的疾病按照相關度降序排序。
....
類似測試用例的需求,到此打住。
那麼,我們一步一步實作這種需求。
首先,我們定義了一個ICD的類,算作我們的模型,其實沒有模型也可以,隻要存入到es且知道各個field的名稱就行。這個裡面我們隻需要關注疾病名稱diseaseName及簡拼pinyin字段即可,這個字段預設是字元串,ES預設會幫我們分詞。
Java代碼
- import java.io.Serializable;
- import java.math.BigDecimal;
- public class ICD implements Serializable{
- private static final long serialVersionUID = 6934803011248581109L;
- //疾病ID
- private int id;
- //疾病編碼
- private String code;
- //疾病名稱
- private String diseaseName;
- //疾病加拼音
- private String mergeName;
- //漢語拼音簡拼
- private String pinyin;
- //是否惡心惡性良性腫瘤
- private boolean isTherioma;
- //是否住院特殊病種
- private boolean isSpecialDisease;
- public ICD(BigDecimal id, String diseaseName, String code,
- String pinyin, String isTherioma, String isSpecialDisease) {
- this.id = id.intValue();
- this.diseaseName = diseaseName;
- this.code = code;
- this.pinyin = pinyin;
- if("是".equals(isTherioma)){
- this.isTherioma = true;
- }
- else {
- this.isTherioma = false;
- }
- if("是".equals(isSpecialDisease)){
- this.isSpecialDisease = true;
- }
- else {
- this.isSpecialDisease = false;
- }
- this.mergeName = diseaseName + "," + pinyin;
- }
- //set,get ......
- }
第二步,将資料存儲到elasticsearch裡面,我們取個名稱叫code,起個type名稱叫icd。ICD大概2w條資料,我使用預設的bulkIndex,存到es大概用了3秒。
我這裡是把資料從oracle導入到elasticsearch。
Java代碼
- import java.math.BigDecimal;
- import java.sql.Connection;
- import java.sql.PreparedStatement;
- import java.sql.ResultSet;
- import java.util.ArrayList;
- import java.util.List;
- import org.elasticsearch.action.bulk.BulkRequestBuilder;
- import org.elasticsearch.action.bulk.BulkResponse;
- import org.elasticsearch.action.index.IndexRequestBuilder;
- import org.elasticsearch.client.Client;
- import com.donlianli.es.ESUtils;
- import com.donlianli.es.db.DatabaseUtils;
- public class ICDManager {
- public static void main(String[] argvs){
- ICDManager manager = new ICDManager();
- manager.indexDataDirect();
- }
- private void indexDataDirect() {
- List<ICD> icdList = getIcdListFromDB();
- System.out.println(" get icd from db finish,size:" + icdList.size());
- bulkIndex(icdList);
- }
- private void bulkIndex(List<ICD> icdList) {
- Client client = ESUtils.getCodeClient();
- BulkRequestBuilder bulkRequest = client.prepareBulk();
- long b = System.currentTimeMillis();
- for(int i=0,l=icdList.size();i<l;i++){
- //業務對象
- ICD icd = icdList.get(i);
- String json = ESUtils.toJson(icd);
- IndexRequestBuilder indexRequest = client.prepareIndex("code","icd")
- .setSource(json).setId(String.valueOf(icd.getId()));
- //添加到builder中
- bulkRequest.add(indexRequest);
- }
- BulkResponse bulkResponse = bulkRequest.execute().actionGet();
- if (bulkResponse.hasFailures()) {
- System.out.println(bulkResponse.buildFailureMessage());
- }
- long useTime = System.currentTimeMillis()-b;
- System.out.println("useTime:" + useTime);
- }
- private List<ICD> getIcdListFromDB() {
- Connection conn = DatabaseUtils.getOracleConnection();
- String sql = "select * from icd_11";
- PreparedStatement st = null;
- ResultSet rs = null;
- List<ICD> list = new ArrayList<ICD>();
- try{
- st = conn.prepareStatement(sql);
- rs = st.executeQuery();
- while(rs.next()){
- BigDecimal id = rs.getBigDecimal("ID");
- String diseaseName = rs.getString("DISEASE_NAME");
- String code = rs.getString("CODE");
- String pinyin = rs.getString("PINYIN");
- String isTherioma = rs.getString("THERIOMA_FLAG");
- String isSpecialDisease = rs.getString("OTHER_FLAG");
- list.add(new ICD(id,diseaseName,code,pinyin,isTherioma,isSpecialDisease));
- }
- return list;
- }
- catch(Exception e){
- e.printStackTrace();
- }
- finally{
- try{
- if(rs!= null){
- rs.close();
- }
- if(st!= null){
- st.close();
- }
- conn.close();
- }
- catch(Exception e){
- e.printStackTrace();
- }
- }
- return null;
- }
- }
第三步,搜尋接口,跑測試用例。
Java代碼
- import org.elasticsearch.action.search.SearchResponse;
- import org.elasticsearch.client.Client;
- import org.elasticsearch.index.query.MultiMatchQueryBuilder;
- import org.elasticsearch.index.query.QueryBuilders;
- import org.elasticsearch.search.SearchHit;
- import org.elasticsearch.search.SearchHits;
- import com.donlianli.es.ESUtils;
- public class PinyinSearchTest {
- public static void main(String[] args) {
- Client client = ESUtils.getCodeClient();
- String keyWord = "高血壓";
- // String keyWord = "老年 高血壓";
- // String keyWord = "gxy";
- //多個字段比對
- MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyWord, "diseaseName","pinyin");
- long b = System.currentTimeMillis();
- SearchResponse response = client.prepareSearch("code").setTypes("icd")
- .setQuery(query)
- .setFrom(0)
- //前20個
- .setSize(20)
- .execute().actionGet();
- long useTime = System.currentTimeMillis()-b;
- System.out.println("search use time:" + useTime + " ms");
- SearchHits shs = response.getHits();
- for (SearchHit hit : shs) {
- System.out.println("分數:"
- + hit.getScore()
- + ",ID:"
- + hit.getId()
- + ", 疾病名稱:"
- + hit.getSource().get("diseaseName")
- + ",拼音:" + hit.getSource().get("pinyin"));
- }
- client.close();
- }
- }
3.1,關鍵字:'高血壓'
[java] view plain copy
- 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關鍵字:'老年 高血壓'
[java] view plain copy
- 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代碼
- import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
- import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
- import org.elasticsearch.client.Client;
- import org.elasticsearch.common.settings.ImmutableSettings;
- import org.elasticsearch.common.settings.ImmutableSettings.Builder;
- import org.elasticsearch.common.xcontent.XContentBuilder;
- import com.donlianli.es.ESUtils;
- public class CodeMappingTest {
- static final String INDEX_NAME="code";
- static final String TYPE_NAME="icd";
- public static void main(String[] argv) throws Exception{
- Client client = ESUtils.getCodeClient();
- Builder settings = ImmutableSettings.settingsBuilder()
- .loadFromSource(getAnalysisSettings());
- //首先建立索引庫
- CreateIndexResponse indexresponse = client.admin().indices()
- //這個索引庫的名稱還必須不包含大寫字母
- .prepareCreate(INDEX_NAME).setSettings(settings)
- //這裡直接添加type的mapping
- .addMapping(TYPE_NAME, getMapping())
- .execute().actionGet();
- System.out.println("success:"+indexresponse.isAcknowledged());
- }
- private static String getAnalysisSettings() throws Exception {
- XContentBuilder mapping = jsonBuilder()
- .startObject()
- //主分片數量
- .field("number_of_shards",5)
- .field("number_of_replicas",0)
- .startObject("analysis")
- .startObject("filter")
- //建立分詞過濾器
- .startObject("pynGram")
- .field("type","nGram")
- //從1開始
- .field("min_gram",1)
- .field("max_gram",15)
- .endObject()
- .endObject()
- .startObject("analyzer")
- //拼音analyszer
- .startObject("pyAnalyzer")
- .field("type","custom")
- .field("tokenizer","standard")
- .field("filter", new String[]{"lowercase","pynGram"})
- .endObject()
- .endObject()
- .endObject()
- .endObject();
- System.out.println(mapping.string());
- return mapping.string();
- }
- private static XContentBuilder getMapping() throws Exception{
- XContentBuilder mapping = jsonBuilder()
- .startObject()
- .startObject("icd")
- //指定分詞器
- .field("index_analyzer","pyAnalyzer")
- .startObject("properties")
- .startObject("id")
- .field("type", "long")
- .field("store", "yes")
- .endObject()
- .startObject("code")
- .field("type", "string")
- .field("store", "yes")
- .field("index", "analyzed")
- .endObject()
- .startObject("diseaseName")
- .field("type", "string")
- .field("store", "yes")
- .field("index", "analyzed")
- .endObject()
- .startObject("mergeName")
- .field("type", "string")
- .field("store", "yes")
- .field("index", "analyzed")
- .endObject()
- .startObject("pinyin")
- .field("type", "string")
- .field("store", "yes")
- .field("index", "analyzed")
- .endObject()
- .startObject("isTherioma")
- .field("type", "boolean")
- .field("store", "yes")
- .endObject()
- .startObject("isSpecialDisease")
- .field("type", "boolean")
- .field("store", "yes")
- .endObject()
- .endObject()
- .endObject()
- .endObject();
- return mapping;
- }
(PS:其實還有一種簡單的方法,不用建立analyzer,在搜尋的時候,使用'*gxy*'進行搜尋也可以)
最後,我還把這個檢索跟oracle的like進行了比較。結果發現oracle隻用20ms就能算出結果,而es卻用了将近100ms。可見這種吹捧的nosql,性能不見得比oracle強大啊,但是毋庸置疑的是,功能确實強大了。