網(wǎng)絡(luò)營(yíng)銷案例分析200字關(guān)鍵詞seo如何優(yōu)化
如何實(shí)現(xiàn) Es 全文檢索、高亮文本略縮處理
- 前言
- 技術(shù)選型
- JAVA 常用語法說明
- 全文檢索開發(fā)
- 高亮開發(fā)
- Es Map 轉(zhuǎn)對(duì)象使用
- 核心代碼 Trans 接口(支持父類屬性的復(fù)雜映射)
- Trans 接口的不足
- 真實(shí)項(xiàng)目落地效果
前言
最近手上在做 Es 全文檢索的需求,類似于百度那種,根據(jù)關(guān)鍵字檢索出對(duì)應(yīng)的文章,然后高亮顯示,特此記錄一下,其實(shí)主要就是處理 Es 數(shù)據(jù)那塊復(fù)雜,涉及到高亮文本替換以及高亮字段截取,還有要考慮到代碼的復(fù)用性,是否可以將轉(zhuǎn)換代碼抽離出來,提供給不同結(jié)構(gòu)的索引來使用。
技術(shù)選型
像市面上有的 Spring Data,碼云上面的 GVP 項(xiàng)目 (EasyEs)等其他封裝框架。使用起來確實(shí)很方便,但是考慮到由于開源項(xiàng)目的不穩(wěn)定性且 Es 不同版本間語法差異比較大,決定使用原生的 Api。也就是使用 RestHighLevelClient。
JAVA 常用語法說明
查時(shí)間范圍內(nèi)的數(shù)據(jù) BoolQuery 里面嵌套一個(gè) RangeQuery 即可在RangeQuery 里面指定時(shí)間范圍。BoolQuery.must() 各位理解為 Mybatis 中的 eq 方法即可,必須包含的意思。
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery(articleRequest.getSortType());if (StringUtils.isNotEmpty(articleRequest.getBeginTime())) {rangeQuery.gte(articleRequest.getBeginTime());}if (StringUtils.isNotEmpty(articleRequest.getEndTime())) {rangeQuery.lte(articleRequest.getEndTime());}boolQuery.must(rangeQuery);
BoolQuery.should() 方法可以理解為 OR 可包含可不包含,多字段全文檢索時(shí)應(yīng)用 shoud。
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));
termsQuery 字符精確匹配
QueryBuilders.termsQuery()
字符短句匹配,字符不會(huì)進(jìn)行分詞
QueryBuilders.matchPhraseQuery()
分詞匹配
QueryBuilders.multiMatchQuery()
全文檢索開發(fā)
核心代碼如下
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();if (StringUtils.isNotEmpty(articleRequest.getKeyword())) {for (int i = 0; i < articleRequest.getKeys().length; i++) {//根據(jù)短句匹配boolQuery.should(QueryBuilders.matchPhraseQuery(articleRequest.getKeys()[i], articleRequest.getKeyword()));}}
高亮開發(fā)
里面可以指定高亮的字段,以及高亮前綴,尾綴,API的調(diào)用,直接 copy 就行
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().highlighter(new HighlightBuilder().requireFieldMatch(false).field("author").field("title").field("body").field("attachments.filename").preTags(EsConstant.HIGHT_PREFIX).postTags(EsConstant.HIGHT_END).fragmentSize(800000)//下面這兩項(xiàng),如果你要高亮如文字內(nèi)容等有很多字的字段,必須配置,不然會(huì)導(dǎo)致高亮不全,文章內(nèi)容缺失等;.numOfFragments(0)).query(boolQuery).from(articleRequest.getPage() - 1).size(articleRequest.getSize())
Es Map 轉(zhuǎn)對(duì)象使用
由于索引結(jié)構(gòu)是已 ArticleResponse 格式存儲(chǔ)的,查詢的時(shí)候也需將的得到 SourceAsMap 轉(zhuǎn)換成 ArticleResponse 格式,核心邏輯我都封裝到 Trans 接口了。利用反射實(shí)現(xiàn)的,當(dāng)然也可以用其他技術(shù)實(shí)現(xiàn),例如 MapStruct 在編譯期間就自動(dòng)生成對(duì)應(yīng)的 get、set 方法,比反射效率高點(diǎn),畢竟反射是運(yùn)行期間的屬性映射!!!!
SearchHits hits = restHighLevelClient.search(new SearchRequest().indices(indexname).source(searchSourceBuilder)).getHits();for (SearchHit hit : hits) {result.add(new ArticleResponse().trans(hit.getSourceAsMap(),hit.getHighlightFields(),Collections.singletonList("attachments.filename")));
使用的話只需讓 ArticleResponse 類實(shí)現(xiàn) Trans 接口,即可調(diào)用里面的 trans 方法。
核心代碼 Trans 接口(支持父類屬性的復(fù)雜映射)
主要邏輯就是挨個(gè)拿到本身、然后遞歸獲取父類的所有字段名稱、字段類型放到一個(gè) Map(nameTypeMap) 中,然后遍歷 SourceAsMap 挨個(gè)進(jìn)行字段類型匹配校驗(yàn),如果是 String 類型直接進(jìn)行反射填充屬性。
非 String 類型,進(jìn)行類型轉(zhuǎn)換然后再進(jìn)行屬性填充。
以及高亮字段文本略縮的處理,主要就是用了下 Jsoup 中去除 Html 標(biāo)簽的 Api,本來想著讓前端自己去找插件看能不能處理下的,無奈說處理不了,想了個(gè)取巧的方法,高亮標(biāo)簽我用特殊字符,然后去除所有的 html 標(biāo)簽后,我的特殊字符還存在,之后將特殊字符再次替換回高亮 Html 標(biāo)簽,這樣就得到了只存在我自定義高亮 Html 標(biāo)簽的一段文本了,同時(shí)高亮標(biāo)簽里面我塞了一個(gè) id,之后根據(jù)高亮標(biāo)簽中的 id 截取字符即可,即可實(shí)現(xiàn)文本略縮的效果,同事直呼秒啊哈哈哈哈
/*** map 轉(zhuǎn)對(duì)象* author:zzh*/
public interface Trans<T> {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");Class getTargetClass();/*** 邏輯寫的太多了,可以搞幾個(gè)抽象類抽分功能* @param SourceAsMap 原始數(shù)據(jù)* @param highlightFieldsSource 高亮數(shù)據(jù)* @param highLightFields 高亮字段*/default Object trans(Map<String, Object> SourceAsMap, Map<String, HighlightField> highlightFieldsSource, List<String> highLightFields) throws IntrospectionException, InstantiationException, IllegalAccessException {Object o = getTargetClass().newInstance();Class tclass = getTargetClass();HashMap<String, Class> nameTypeMap = new HashMap<>();//找到父類的所有字段do {Arrays.stream(tclass.getDeclaredFields()).forEach(field -> {field.setAccessible(true);//key:字段名稱,value:字段類型nameTypeMap.put(field.getName(), field.getType());});tclass = tclass.getSuperclass();} while (!tclass.equals(Object.class));PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(o.getClass()).getPropertyDescriptors();Arrays.stream(propertyDescriptors).forEach(propertyDescriptor -> {if (!"targetClass".equals(propertyDescriptor.getName()) && !Objects.isNull(SourceAsMap.get(propertyDescriptor.getName()))) {try {Method writeMethod = propertyDescriptor.getWriteMethod();if (null != writeMethod) {if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}Object sourceValue = SourceAsMap.get(propertyDescriptor.getName());//父類以及自己所有字段類型Class aClass = nameTypeMap.get(propertyDescriptor.getName());//String 類型以及高亮直接賦值if (sourceValue.getClass().equals(aClass)) {HighlightField highlightObject = highlightFieldsSource.get(propertyDescriptor.getName());//如果高亮字段是 body,為了避免高亮文本處于文章末尾搜索頁顯示不到的問題,因此采用截取字符串將高亮字段偏移至前面if ("body".equals(propertyDescriptor.getName()) && null != highlightObject) {String highlightString = highlightObject.getFragments()[0].toString();//去除所有 html 標(biāo)簽,并將自定義高亮前綴替換 span 標(biāo)簽,這樣就實(shí)現(xiàn)了只保留高亮標(biāo)簽的目的了highlightString = Jsoup.parse(highlightString).body().text().replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML).replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML);//高亮字段前 50 個(gè)字到文章末尾highlightString = highlightString.substring((highlightString.indexOf(EsConstant.HIGHT_HTML_ID) - EsConstant.HIGHT_SIZE) < 0? 0 : (highlightString.indexOf(EsConstant.HIGHT_HTML_ID) - EsConstant.HIGHT_SIZE));writeMethod.invoke(o, highlightObject != null ? highlightString : SourceAsMap.get(propertyDescriptor.getName()));} else {//非 body 的其他高亮字段正常替換高亮文本writeMethod.invoke(o, highlightObject != null ? highlightObject.getFragments()[0].toString().replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML).replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML) : SourceAsMap.get(propertyDescriptor.getName()));}}/*** 類型不一致強(qiáng)轉(zhuǎn),這里可以搞個(gè)策略模式優(yōu)化優(yōu)化*/else {if (aClass.equals(Date.class)) {Date parse = simpleDateFormat.parse(String.valueOf(SourceAsMap.get(propertyDescriptor.getName())));writeMethod.invoke(o, parse);}if (aClass.equals(Integer.class)) {writeMethod.invoke(o, Integer.valueOf(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));}if (aClass.equals(Long.class)) {writeMethod.invoke(o, Long.valueOf(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));}if (aClass.equals(List.class)) {ArrayList<Map<String, Object>> oraginSources = (ArrayList<Map<String, Object>>) SourceAsMap.get(propertyDescriptor.getName());//復(fù)雜對(duì)象高亮字段映射if (null != oraginSources && 0 != highlightFieldsSource.size()) {for (int i = 0; i < oraginSources.size(); i++) {for (int j = 0; j < highLightFields.size(); j++) {try {if (highlightFieldsSource.containsKey(highLightFields.get(j))) {oraginSources.get(i).put(highLightFields.get(j).split("\\.")[1],highlightFieldsSource.get(highLightFields.get(j)).getFragments()[j].toString().replaceAll(EsConstant.HIGHT_PREFIX, EsConstant.HIGHT_PREFIX_HTML).replaceAll(EsConstant.HIGHT_END, EsConstant.HIGHT_END_HTML));}} catch (Exception e) {e.printStackTrace();}}}}writeMethod.invoke(o, oraginSources);}if (aClass.equals(int.class)) {writeMethod.invoke(o, Integer.parseInt(String.valueOf(SourceAsMap.get(propertyDescriptor.getName()))));}}} else throw new RuntimeException(propertyDescriptor.getName() + "~ writeMethod is null!!!!!");} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();} catch (Exception e) {e.printStackTrace();}}});return o;}}
Trans 接口的不足
追求極至代碼解耦的人,里面的類型轉(zhuǎn)換的代碼可以搞個(gè)策略模式優(yōu)化優(yōu)化。Trans 接口定義一個(gè)就好,可以搞幾個(gè)抽象類,譬如專門處理高亮文本的抽象類、不涉及到嵌套對(duì)象的抽象類轉(zhuǎn)換、以及通用轉(zhuǎn)換抽象類出來,寫多了感覺自己再寫源碼了,那些搞開源項(xiàng)目的還有一些主流框架的源碼不都是這么干的,但是需要花費(fèi)一定的精力去寫,筆者比較懶,下完班只想回家美美的打打游戲,追追劇,就點(diǎn)到為止了。
真實(shí)項(xiàng)目落地效果
復(fù)雜對(duì)象高亮字段替換效果