通常,在項(xiàng)目中需要聯(lián)想輸入(即輸入關(guān)鍵字,提示相關(guān)詞條,類似百度google的搜索)的需求,可能大家都是用的數(shù)據(jù)庫(kù)的like '%關(guān)鍵字%‘來(lái)實(shí)現(xiàn)。但是這樣實(shí)現(xiàn)有幾個(gè)問(wèn)題。
第一、這樣的搜索無(wú)論是Oracle還是MySQL,都是無(wú)法使用索引的。在oracle中可能有全文檢索可以使用,但是個(gè)人感覺(jué)效果不是很好。
第二、輸入的關(guān)鍵字有l(wèi)ike的通病,就是只有保含關(guān)鍵字的詞條才會(huì)被命中。如果中間加個(gè)空格之類的,db就無(wú)能為力了。
第三、如果要想對(duì)命中結(jié)果進(jìn)行相關(guān)度排序,這個(gè)在常規(guī)數(shù)據(jù)庫(kù)是無(wú)法做到的。雖然,可以按照命中詞條的長(zhǎng)度進(jìn)行升序排序,但是加上排序,性能不是很好。
下面介紹一下使用elasticsearch實(shí)現(xiàn)聯(lián)想輸入的搜索,因?yàn)槭撬阉饕妫焐筒痪邆渖厦娴?個(gè)問(wèn)題。
在具體介紹使用方法之前,我們先找個(gè)搜索數(shù)據(jù)。我找的是ICD(就是疾病名稱的國(guó)標(biāo)),誰(shuí)讓咱一生都在跟他做斗爭(zhēng)。這個(gè)在網(wǎng)上一搜一堆。
有了數(shù)據(jù),我們先要簡(jiǎn)單描述一下我們要達(dá)到的一個(gè)目的。一般的搜索都支持漢字 和拼音兩種檢索方法。我們的這個(gè)檢索也滿足這個(gè)需求。
搜索需求描述:
1、支持漢字和簡(jiǎn)拼兩種搜索方法。
2、輸入“高血壓”時(shí),按照相關(guān)度,將帶“高血壓”名稱的疾病名稱按照相關(guān)度降序排序。
3、輸入“老年 高血壓”,時(shí),將帶“老年”和“高血壓”名稱的疾病名稱按照相關(guān)度降序排序。
4、輸入拼音'gxy‘時(shí),將拼音中帶有g(shù)xy相關(guān)的疾病按照相關(guān)度降序排序。
....
類似測(cè)試用例的需求,到此打住。
那么,我們一步一步實(shí)現(xiàn)這種需求。
首先,我們定義了一個(gè)ICD的類,算作我們的模型,其實(shí)沒(méi)有模型也可以,只要存入到es且知道各個(gè)field的名稱就行。這個(gè)里面我們只需要關(guān)注疾病名稱diseaseName及簡(jiǎn)拼pinyin字段即可,這個(gè)字段默認(rèn)是字符串,ES默認(rèn)會(huì)幫我們分詞。
java代碼import java.io.Serializable;
import java.math.BigDecimal;
/**
* ICD抽象對(duì)象
* @author donlianli@126.com
*/
public class ICD implements Serializable{
PRivate static final long serialVersionUID = 6934803011248581109L;
//疾病ID
private int id;
//疾病編碼
private String code;
//疾病名稱
private String diseaseName;
//疾病加拼音
private String mergeName;
//漢語(yǔ)拼音簡(jiǎn)拼
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 ......
}
第二步,將數(shù)據(jù)存儲(chǔ)到elasticsearch里面,我們?nèi)€(gè)名稱叫code,起個(gè)type名稱叫icd。ICD大概2w條數(shù)據(jù),我使用默認(rèn)的bulkIndex,存到es大概用了3秒。
我這里是把數(shù)據(jù)從oracle導(dǎo)入到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();
}
/**
* 直接將數(shù)據(jù)初始化到ES中
* 不創(chuàng)建mapping
*/
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++){
//業(yè)務(wù)對(duì)象
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;
}
}
第三步,搜索接口,跑測(cè)試用例。
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";
//多個(gè)字段匹配
MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyWord, "diseaseName","pinyin");
long b = System.currentTimeMillis();
SearchResponse response = client.prepareSearch("code").setTypes("icd")
.setQuery(query)
.setFrom(0)
//前20個(gè)
.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("分?jǐn)?shù):"
+ hit.getScore()
+ ",ID:"
+ hit.getId()
+ ", 疾病名稱:"
+ hit.getSource().get("diseaseName")
+ ",拼音:" + hit.getSource().get("pinyin"));
}
client.close();
}
}
3.1,關(guān)鍵字:'高血壓'
search use time:174 ms分?jǐn)?shù):2.3859928,ID:6904, 疾病名稱:高血壓病,拼音:gxyb分?jǐn)?shù):2.136423,ID:6907, 疾病名稱:高血壓I期,拼音:gxyyq分?jǐn)?shù):2.12253,ID:6908, 疾病名稱:高血壓Ⅱ期,拼音:gxyeq分?jǐn)?shù):2.12253,ID:6910, 疾病名稱:高血壓危象,拼音:gxywx分?jǐn)?shù):2.0906634,ID:6917, 疾病名稱:腎性高血壓,拼音:sxgxy分?jǐn)?shù):2.0877438,ID:6909, 疾病名稱:高血壓Ⅲ期,拼音:gxysq分?jǐn)?shù):2.0821526,ID:18767, 疾病名稱:高原性高血壓,拼音:gyxgxy分?jǐn)?shù):1.9905697,ID:6906, 疾病名稱:惡性高血壓,拼音:exgxy分?jǐn)?shù):1.9510978,ID:7260, 疾病名稱:高血壓腦出血,拼音:gxyncx分?jǐn)?shù):1.9078629,ID:6923, 疾病名稱:腎血管性高血壓,拼音:sxgxgxy分?jǐn)?shù):1.8312198,ID:6914, 疾病名稱:高血壓性腎病,拼音:gxyxsb分?jǐn)?shù):1.8193114,ID:7367, 疾病名稱:高血壓性腦病,拼音:gxyxnb分?jǐn)?shù):1.8193114,ID:13470, 疾病名稱:妊娠引起高血壓,拼音:rsyqgxy分?jǐn)?shù):1.7919972,ID:6905, 疾病名稱:臨界性高血壓,拼音:ljxgxy分?jǐn)?shù):1.7919972,ID:6912, 疾病名稱:高血壓性心臟病,拼音:gxyxxzb分?jǐn)?shù):1.7894946,ID:6928, 疾病名稱:繼發(fā)性高血壓,拼音:jfxgxy分?jǐn)?shù):1.7062025,ID:6913, 疾病名稱:高血壓性腎衰竭,拼音:gxyxssj分?jǐn)?shù):1.7062025,ID:13485, 疾病名稱:孕產(chǎn)婦高血壓,拼音:ycfgxy分?jǐn)?shù):1.7062025,ID:14534, 疾病名稱:新生兒高血壓,拼音:xsegxy分?jǐn)?shù):1.7062025,ID:16181, 疾病名稱:應(yīng)激性高血壓,拼音:yjxgxy3.2關(guān)鍵字:'老年 高血壓'
search use time:144 ms分?jǐn)?shù):1.1089094,ID:6904, 疾病名稱:高血壓病,拼音:gxyb分?jǐn)?shù):0.99291986,ID:6907, 疾病名稱:高血壓I期,拼音:gxyyq分?jǐn)?shù):0.9864628,ID:6908, 疾病名稱:高血壓Ⅱ期,拼音:gxyeq分?jǐn)?shù):0.9864628,ID:6910, 疾病名稱:高血壓危象,拼音:gxywx分?jǐn)?shù):0.9716526,ID:6917, 疾病名稱:腎性高血壓,拼音:sxgxy分?jǐn)?shù):0.97029567,ID:6909, 疾病名稱:高血壓Ⅲ期,拼音:gxysq分?jǐn)?shù):0.96769714,ID:18767, 疾病名稱:高原性高血壓,拼音:gyxgxy分?jǐn)?shù):0.9251333,ID:6906, 疾病名稱:惡性高血壓,拼音:exgxy分?jǐn)?shù):0.9067884,ID:7260, 疾病名稱:高血壓腦出血,拼音:gxyncx分?jǐn)?shù):0.8866946,ID:6923, 疾病名稱:腎血管性高血壓,拼音:sxgxgxy分?jǐn)?shù):0.8510741,ID:6914, 疾病名稱:高血壓性腎病,拼音:gxyxsb分?jǐn)?shù):0.8455395,ID:7367, 疾病名稱:高血壓性腦病,拼音:gxyxnb分?jǐn)?shù):0.8455395,ID:13470, 疾病名稱:妊娠引起高血壓,拼音:rsyqgxy分?jǐn)?shù):0.8328451,ID:6905, 疾病名稱:臨界性高血壓,拼音:ljxgxy分?jǐn)?shù):0.8328451,ID:6912, 疾病名稱:高血壓性心臟病,拼音:gxyxxzb分?jǐn)?shù):0.831682,ID:6928, 疾病名稱:繼發(fā)性高血壓,拼音:jfxgxy分?jǐn)?shù):0.8074301,ID:6820, 疾病名稱:老年耳聾,拼音:lnel分?jǐn)?shù):0.80348647,ID:7612, 疾病名稱:老年痣,拼音:lnz分?jǐn)?shù):0.7929714,ID:6913, 疾病名稱:高血壓性腎衰竭,拼音:gxyxssj分?jǐn)?shù):0.7929714,ID:13485, 疾病名稱:孕產(chǎn)婦高血壓,拼音:ycfgxy高血壓和老年的相關(guān)并都出來(lái)了。只可惜老年高血壓,沒(méi)有列入ICD.
3.3拼音:'gxy'
呃?怎么沒(méi)有出來(lái)?
這個(gè)問(wèn)題折騰了我一天。一開(kāi)始我以為是被es列入了禁用詞。后來(lái),找到是因?yàn)闆](méi)有設(shè)置analyzer導(dǎo)致,在設(shè)analyzer的過(guò)程中竟然還犯了好幾個(gè)低級(jí)錯(cuò)誤,導(dǎo)致我非常懷疑設(shè)置analyzer是否管用。
這個(gè)問(wèn)題涉及到分詞,而分詞我還沒(méi)有好好研究過(guò)。總之,在創(chuàng)建索引及mapping的時(shí)候,指定一個(gè)analyzer就可以解決這個(gè)問(wèn)題。
創(chuàng)建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;
/**
* 創(chuàng)建code的mapping
* @author donlianli@126.com
*/
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());
//首先創(chuàng)建索引庫(kù)
CreateIndexResponse indexresponse = client.admin().indices()
//這個(gè)索引庫(kù)的名稱還必須不包含大寫字母
.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()
//主分片數(shù)量
.field("number_of_shards",5)
.field("number_of_replicas",0)
.startObject("analysis")
.startObject("filter")
//創(chuàng)建分詞過(guò)濾器
.startObject("pynGram")
.field("type","nGram")
//從1開(kāi)始
.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();
}
/**
* mapping 一旦定義,之后就不能修改。
* @return
* @throws Exception
*/
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:其實(shí)還有一種簡(jiǎn)單的方法,不用創(chuàng)建analyzer,在搜索的時(shí)候,使用'*gxy*'進(jìn)行搜索也可以)
最后,我還把這個(gè)檢索跟oracle的like進(jìn)行了比較。結(jié)果發(fā)現(xiàn)oracle只用20ms就能算出結(jié)果,而es卻用了將近100ms。可見(jiàn)這種吹捧的nosql,性能不見(jiàn)得比oracle強(qiáng)大啊,但是毋庸置疑的是,功能確實(shí)強(qiáng)大了。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注