View Javadoc
1   /*
2    * Copyright 2012-2017 CodeLibs Project and the Others.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13   * either express or implied. See the License for the specific language
14   * governing permissions and limitations under the License.
15   */
16  package org.codelibs.fess.app.service;
17  
18  import java.text.NumberFormat;
19  import java.util.Enumeration;
20  import java.util.HashSet;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.concurrent.ExecutionException;
26  import java.util.function.Consumer;
27  
28  import javax.annotation.Resource;
29  import javax.servlet.http.HttpServletRequest;
30  
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.fess.Constants;
33  import org.codelibs.fess.entity.QueryContext;
34  import org.codelibs.fess.entity.SearchRenderData;
35  import org.codelibs.fess.entity.SearchRequestParams;
36  import org.codelibs.fess.entity.SearchRequestParams.SearchRequestType;
37  import org.codelibs.fess.es.client.FessEsClient;
38  import org.codelibs.fess.es.client.FessEsClient.SearchConditionBuilder;
39  import org.codelibs.fess.es.client.FessEsClientException;
40  import org.codelibs.fess.helper.QueryHelper;
41  import org.codelibs.fess.helper.SystemHelper;
42  import org.codelibs.fess.mylasta.action.FessUserBean;
43  import org.codelibs.fess.mylasta.direction.FessConfig;
44  import org.codelibs.fess.util.ComponentUtil;
45  import org.codelibs.fess.util.QueryResponseList;
46  import org.codelibs.fess.util.QueryStringBuilder;
47  import org.dbflute.optional.OptionalEntity;
48  import org.dbflute.optional.OptionalThing;
49  import org.dbflute.util.DfTypeUtil;
50  import org.elasticsearch.ElasticsearchException;
51  import org.elasticsearch.action.DocWriteResponse.Result;
52  import org.elasticsearch.action.bulk.BulkRequestBuilder;
53  import org.elasticsearch.action.bulk.BulkResponse;
54  import org.elasticsearch.action.update.UpdateRequestBuilder;
55  import org.elasticsearch.action.update.UpdateResponse;
56  import org.elasticsearch.index.query.BoolQueryBuilder;
57  import org.elasticsearch.index.query.QueryBuilders;
58  import org.lastaflute.taglib.function.LaFunctions;
59  import org.lastaflute.web.util.LaRequestUtil;
60  
61  public class SearchService {
62  
63      // ===================================================================================
64      //                                                                            Constant
65      //
66  
67      // ===================================================================================
68      //                                                                           Attribute
69      //
70  
71      @Resource
72      protected FessEsClient fessEsClient;
73  
74      @Resource
75      protected SystemHelper systemHelper;
76  
77      @Resource
78      protected FessConfig fessConfig;
79  
80      @Resource
81      protected QueryHelper queryHelper;
82  
83      // ===================================================================================
84      //                                                                              Method
85      //                                                                      ==============
86  
87      public void search(final SearchRequestParams params, final SearchRenderData data, final OptionalThing<FessUserBean> userBean) {
88          final long requestedTime = systemHelper.getCurrentTimeAsLong();
89          final long startTime = System.currentTimeMillis();
90  
91          LaRequestUtil.getOptionalRequest().ifPresent(request -> {
92              request.setAttribute(Constants.REQUEST_LANGUAGES, params.getLanguages());
93              request.setAttribute(Constants.REQUEST_QUERIES, params.getQuery());
94          });
95  
96          final String query =
97                  QueryStringBuilder.query(params.getQuery()).extraQueries(params.getExtraQueries()).fields(params.getFields()).build();
98  
99          final int pageStart = params.getStartPosition();
100         final int pageSize = params.getPageSize();
101         final String sortField = params.getSort();
102         final List<Map<String, Object>> documentItems =
103                 fessEsClient.search(
104                         fessConfig.getIndexDocumentSearchIndex(),
105                         fessConfig.getIndexDocumentType(),
106                         searchRequestBuilder -> {
107                             fessConfig.processSearchPreference(searchRequestBuilder, userBean);
108                             return SearchConditionBuilder.builder(searchRequestBuilder)
109                                     .query(StringUtil.isBlank(sortField) ? query : query + " sort:" + sortField).offset(pageStart)
110                                     .size(pageSize).facetInfo(params.getFacetInfo()).geoInfo(params.getGeoInfo())
111                                     .similarDocHash(params.getSimilarDocHash()).responseFields(queryHelper.getResponseFields())
112                                     .searchRequestType(params.getType()).build();
113                         }, (searchRequestBuilder, execTime, searchResponse) -> {
114                             final QueryResponseList queryResponseList = ComponentUtil.getQueryResponseList();
115                             queryResponseList.init(searchResponse, pageStart, pageSize);
116                             return queryResponseList;
117                         });
118         data.setDocumentItems(documentItems);
119 
120         // search
121         final QueryResponseList queryResponseList = (QueryResponseList) documentItems;
122         data.setFacetResponse(queryResponseList.getFacetResponse());
123 
124         @SuppressWarnings("unchecked")
125         final Set<String> highlightQueries = (Set<String>) params.getAttribute(Constants.HIGHLIGHT_QUERIES);
126         if (highlightQueries != null) {
127             final StringBuilder buf = new StringBuilder(100);
128             highlightQueries.stream().forEach(q -> {
129                 buf.append("&hq=").append(LaFunctions.u(q));
130             });
131             data.setAppendHighlightParams(buf.toString());
132         }
133 
134         queryResponseList.setExecTime(System.currentTimeMillis() - startTime);
135         final NumberFormat nf = NumberFormat.getInstance(params.getLocale());
136         nf.setMaximumIntegerDigits(2);
137         nf.setMaximumFractionDigits(2);
138         String execTime;
139         try {
140             execTime = nf.format((double) queryResponseList.getExecTime() / 1000);
141         } catch (final Exception e) {
142             execTime = StringUtil.EMPTY;
143         }
144         data.setExecTime(execTime);
145 
146         final String queryId = queryHelper.generateId();
147 
148         data.setPageSize(queryResponseList.getPageSize());
149         data.setCurrentPageNumber(queryResponseList.getCurrentPageNumber());
150         data.setAllRecordCount(queryResponseList.getAllRecordCount());
151         data.setAllPageCount(queryResponseList.getAllPageCount());
152         data.setExistNextPage(queryResponseList.isExistNextPage());
153         data.setExistPrevPage(queryResponseList.isExistPrevPage());
154         data.setCurrentStartRecordNumber(queryResponseList.getCurrentStartRecordNumber());
155         data.setCurrentEndRecordNumber(queryResponseList.getCurrentEndRecordNumber());
156         data.setPageNumberList(queryResponseList.getPageNumberList());
157         data.setPartialResults(queryResponseList.isPartialResults());
158         data.setQueryTime(queryResponseList.getQueryTime());
159         data.setSearchQuery(query);
160         data.setRequestedTime(requestedTime);
161         data.setQueryId(queryId);
162 
163         // search log
164         if (fessConfig.isSearchLog()) {
165             ComponentUtil.getSearchLogHelper().addSearchLog(params, DfTypeUtil.toLocalDateTime(requestedTime), queryId, query, pageStart,
166                     pageSize, queryResponseList);
167         }
168 
169         // favorite
170         if (fessConfig.isUserFavorite()) {
171             ComponentUtil.getUserInfoHelper().storeQueryId(queryId, documentItems);
172         }
173 
174     }
175 
176     public int deleteByQuery(final HttpServletRequest request, final SearchRequestParams params) {
177 
178         final String query =
179                 QueryStringBuilder.query(params.getQuery()).extraQueries(params.getExtraQueries()).fields(params.getFields()).build();
180 
181         final QueryContext queryContext = queryHelper.build(params.getType(), query, context -> {
182             context.skipRoleQuery();
183         });
184         return fessEsClient.deleteByQuery(fessConfig.getIndexDocumentUpdateIndex(), fessConfig.getIndexDocumentType(),
185                 queryContext.getQueryBuilder());
186     }
187 
188     public String[] getLanguages(final HttpServletRequest request, final SearchRequestParams params) {
189         if (params.getLanguages() != null) {
190             final Set<String> langSet = new HashSet<>();
191             for (final String lang : params.getLanguages()) {
192                 if (StringUtil.isNotBlank(lang) && lang.length() < 1000) {
193                     if (Constants.ALL_LANGUAGES.equalsIgnoreCase(lang)) {
194                         langSet.add(Constants.ALL_LANGUAGES);
195                     } else {
196                         final String normalizeLang = systemHelper.normalizeLang(lang);
197                         if (normalizeLang != null) {
198                             langSet.add(normalizeLang);
199                         }
200                     }
201                 }
202             }
203             if (langSet.size() > 1 && langSet.contains(Constants.ALL_LANGUAGES)) {
204                 return new String[] { Constants.ALL_LANGUAGES };
205             } else {
206                 langSet.remove(Constants.ALL_LANGUAGES);
207             }
208             return langSet.toArray(new String[langSet.size()]);
209         } else if (fessConfig.isBrowserLocaleForSearchUsed()) {
210             final Set<String> langSet = new HashSet<>();
211             final Enumeration<Locale> locales = request.getLocales();
212             if (locales != null) {
213                 while (locales.hasMoreElements()) {
214                     final Locale locale = locales.nextElement();
215                     final String normalizeLang = systemHelper.normalizeLang(locale.toString());
216                     if (normalizeLang != null) {
217                         langSet.add(normalizeLang);
218                     }
219                 }
220                 if (!langSet.isEmpty()) {
221                     return langSet.toArray(new String[langSet.size()]);
222                 }
223             }
224         }
225         return StringUtil.EMPTY_STRINGS;
226     }
227 
228     public OptionalEntity<Map<String, Object>> getDocumentByDocId(final String docId, final String[] fields,
229             final OptionalThing<FessUserBean> userBean) {
230         return fessEsClient.getDocument(
231                 fessConfig.getIndexDocumentSearchIndex(),
232                 fessConfig.getIndexDocumentType(),
233                 builder -> {
234                     final BoolQueryBuilder boolQuery =
235                             QueryBuilders.boolQuery().must(QueryBuilders.termQuery(fessConfig.getIndexFieldDocId(), docId));
236                     final Set<String> roleSet = ComponentUtil.getRoleQueryHelper().build(SearchRequestType.JSON); // TODO SearchRequestType?
237                     if (!roleSet.isEmpty()) {
238                         final BoolQueryBuilder roleQuery = QueryBuilders.boolQuery();
239                         roleSet.stream().forEach(name -> {
240                             roleQuery.should(QueryBuilders.termQuery(fessConfig.getIndexFieldRole(), name));
241                         });
242                         boolQuery.filter(roleQuery);
243                     }
244                     builder.setQuery(boolQuery);
245                     builder.setFetchSource(fields, null);
246                     fessConfig.processSearchPreference(builder, userBean);
247                     return true;
248                 });
249 
250     }
251 
252     public List<Map<String, Object>> getDocumentListByDocIds(final String[] docIds, final String[] fields,
253             final OptionalThing<FessUserBean> userBean, final SearchRequestType searchRequestType) {
254         return fessEsClient.getDocumentList(
255                 fessConfig.getIndexDocumentSearchIndex(),
256                 fessConfig.getIndexDocumentType(),
257                 builder -> {
258                     final BoolQueryBuilder boolQuery =
259                             QueryBuilders.boolQuery().must(QueryBuilders.termsQuery(fessConfig.getIndexFieldDocId(), docIds));
260                     if (searchRequestType != SearchRequestType.ADMIN_SEARCH) {
261                         final Set<String> roleSet = ComponentUtil.getRoleQueryHelper().build(searchRequestType);
262                         if (!roleSet.isEmpty()) {
263                             final BoolQueryBuilder roleQuery = QueryBuilders.boolQuery();
264                             roleSet.stream().forEach(name -> {
265                                 roleQuery.should(QueryBuilders.termQuery(fessConfig.getIndexFieldRole(), name));
266                             });
267                             boolQuery.filter(roleQuery);
268                         }
269                     }
270                     builder.setQuery(boolQuery);
271                     builder.setSize(fessConfig.getPagingSearchPageMaxSizeAsInteger().intValue());
272                     builder.setFetchSource(fields, null);
273                     fessConfig.processSearchPreference(builder, userBean);
274                     return true;
275                 });
276     }
277 
278     public boolean update(final String id, final String field, final Object value) {
279         return fessEsClient.update(fessConfig.getIndexDocumentUpdateIndex(), fessConfig.getIndexDocumentType(), id, field, value);
280     }
281 
282     public boolean update(final String id, final Consumer<UpdateRequestBuilder> builderLambda) {
283         try {
284             final UpdateRequestBuilder builder =
285                     fessEsClient.prepareUpdate(fessConfig.getIndexDocumentUpdateIndex(), fessConfig.getIndexDocumentType(), id);
286             builderLambda.accept(builder);
287             final UpdateResponse response = builder.execute().actionGet(fessConfig.getIndexIndexTimeout());
288             return response.getResult() == Result.CREATED || response.getResult() == Result.UPDATED;
289         } catch (final ElasticsearchException e) {
290             throw new FessEsClientException("Failed to update doc  " + id, e);
291         }
292     }
293 
294     public boolean bulkUpdate(final Consumer<BulkRequestBuilder> consumer) {
295         final BulkRequestBuilder builder = fessEsClient.prepareBulk();
296         consumer.accept(builder);
297         try {
298             final BulkResponse response = builder.execute().get();
299             if (response.hasFailures()) {
300                 throw new FessEsClientException(response.buildFailureMessage());
301             } else {
302                 return true;
303             }
304         } catch (InterruptedException | ExecutionException e) {
305             throw new FessEsClientException("Failed to update bulk data.", e);
306         }
307     }
308 }