1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.exoplatform.social.core.jpa.search;
18
19 import java.text.Normalizer;
20 import java.util.ArrayList;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Map;
24
25 import org.exoplatform.commons.search.es.ElasticSearchException;
26 import org.exoplatform.commons.search.es.client.ElasticSearchingClient;
27 import org.json.simple.JSONArray;
28 import org.json.simple.JSONObject;
29 import org.json.simple.parser.JSONParser;
30 import org.json.simple.parser.ParseException;
31 import org.apache.commons.lang.StringUtils;
32
33 import org.exoplatform.container.xml.InitParams;
34 import org.exoplatform.container.xml.PropertiesParam;
35 import org.exoplatform.services.log.ExoLogger;
36 import org.exoplatform.services.log.Log;
37 import org.exoplatform.social.core.identity.model.Identity;
38 import org.exoplatform.social.core.identity.model.Profile;
39 import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider;
40 import org.exoplatform.social.core.profile.ProfileFilter;
41 import org.exoplatform.social.core.relationship.model.Relationship.Type;
42 import org.exoplatform.social.core.search.Sorting;
43 import org.exoplatform.social.core.search.Sorting.SortBy;
44 import org.exoplatform.social.core.service.LinkProvider;
45 import org.exoplatform.social.core.storage.impl.StorageUtils;
46
47
48
49
50
51
52
53 public class ProfileSearchConnector {
54 private static final Log LOG = ExoLogger.getLogger(ProfileSearchConnector.class);
55 private final ElasticSearchingClient client;
56 private String index;
57 private String searchType;
58
59 public ProfileSearchConnector(InitParams initParams, ElasticSearchingClient client) {
60 PropertiesParam param = initParams.getPropertiesParam("constructor.params");
61 this.index = param.getProperty("index");
62 this.searchType = param.getProperty("searchType");
63 this.client = client;
64 }
65
66 public List<Identity> search(Identity identity,
67 ProfileFilter filter,
68 Type type,
69 long offset,
70 long limit) {
71 if(identity == null && filter.getViewerIdentity() != null) {
72 identity = filter.getViewerIdentity();
73 }
74 String esQuery = buildQueryStatement(identity, filter, type, offset, limit);
75 String jsonResponse = this.client.sendRequest(esQuery, this.index, this.searchType);
76 return buildResult(jsonResponse);
77 }
78
79
80
81
82
83
84
85
86
87 public int count(Identity identity,
88 ProfileFilter filter,
89 Type type) {
90 String esQuery = buildQueryStatement(identity, filter, type, 0, 1);
91 String jsonResponse = this.client.sendRequest(esQuery, this.index, this.searchType);
92 return getCount(jsonResponse);
93 }
94
95 private int getCount(String jsonResponse) {
96
97 LOG.debug("Search Query response from ES : {} ", jsonResponse);
98 JSONParser parser = new JSONParser();
99
100 Map<?, ?> json = null;
101 try {
102 json = (Map<?, ?>)parser.parse(jsonResponse);
103 } catch (ParseException e) {
104 throw new ElasticSearchException("Unable to parse JSON response", e);
105 }
106
107 JSONObject jsonResult = (JSONObject) json.get("hits");
108 if (jsonResult == null) return 0;
109
110 int count = Integer.parseInt(jsonResult.get("total").toString());
111 return count;
112 }
113
114 private List<Identity> buildResult(String jsonResponse) {
115
116 LOG.debug("Search Query response from ES : {} ", jsonResponse);
117
118 List<Identity> results = new ArrayList<Identity>();
119 JSONParser parser = new JSONParser();
120
121 Map json = null;
122 try {
123 json = (Map)parser.parse(jsonResponse);
124 } catch (ParseException e) {
125 throw new ElasticSearchException("Unable to parse JSON response", e);
126 }
127
128 JSONObject jsonResult = (JSONObject) json.get("hits");
129 if (jsonResult == null) return results;
130
131
132 JSONArray jsonHits = (JSONArray) jsonResult.get("hits");
133 Identity identity = null;
134 Profile p;
135 for(Object jsonHit : jsonHits) {
136 JSONObject hitSource = (JSONObject) ((JSONObject) jsonHit).get("_source");
137 String position = (String) hitSource.get("position");
138 String name = (String) hitSource.get("name");
139 String userName = (String) hitSource.get("userName");
140 String firstName = (String) hitSource.get("firstName");
141 String lastName = (String) hitSource.get("lastName");
142 String avatarUrl = (String) hitSource.get("avatarUrl");
143 String email = (String) hitSource.get("email");
144 String identityId = (String) ((JSONObject) jsonHit).get("_id");
145 identity = new Identity(OrganizationIdentityProvider.NAME, userName);
146 identity.setId(identityId);
147 p = new Profile(identity);
148 p.setId(identityId);
149 p.setAvatarUrl(avatarUrl);
150 p.setUrl(LinkProvider.getProfileUri(userName));
151 p.setProperty(Profile.FULL_NAME, name);
152 p.setProperty(Profile.FIRST_NAME, firstName);
153 p.setProperty(Profile.LAST_NAME, lastName);
154 p.setProperty(Profile.POSITION, position);
155 p.setProperty(Profile.EMAIL, email);
156 p.setProperty(Profile.USERNAME, userName);
157 identity.setProfile(p);
158 results.add(identity);
159 }
160 return results;
161 }
162
163 private String buildQueryStatement(Identity identity, ProfileFilter filter, Type type, long offset, long limit) {
164 String expEs = buildExpression(filter);
165 StringBuilder esQuery = new StringBuilder();
166 esQuery.append("{\n");
167 esQuery.append(" \"from\" : " + offset + ", \"size\" : " + limit + ",\n");
168 Sorting sorting = filter.getSorting();
169 if (sorting != null && SortBy.DATE.equals(sorting.sortBy)) {
170 esQuery.append(" \"sort\": {\"lastUpdatedDate\": {\"order\": \""
171 + (sorting.orderBy == null ? "desc" : sorting.orderBy.name()) + "\"}}\n");
172 } else {
173 esQuery.append(" \"sort\": {\"name.raw\": {\"order\": \"asc\"}}\n");
174 }
175 StringBuilder esSubQuery = new StringBuilder();
176 esSubQuery.append(" ,\n");
177 esSubQuery.append("\"query\" : {\n");
178 esSubQuery.append(" \"constant_score\" : {\n");
179 esSubQuery.append(" \"filter\" : {\n");
180 esSubQuery.append(" \"bool\" :{\n");
181 boolean subQueryEmpty = true;
182 if (filter.getRemoteIds() != null && !filter.getRemoteIds().isEmpty()) {
183 subQueryEmpty = false;
184 StringBuilder remoteIds = new StringBuilder();
185 for (String remoteId : filter.getRemoteIds()) {
186 if (remoteIds.length() > 0) {
187 remoteIds.append(",");
188 }
189 remoteIds.append("\"").append(remoteId).append("\"");
190 }
191 esSubQuery.append(" \"must\" : {\n");
192 esSubQuery.append(" \"terms\" :{\n");
193 esSubQuery.append(" \"userName\" : [" + remoteIds.toString() + "]\n");
194 esSubQuery.append(" } \n");
195 esSubQuery.append(" },\n");
196 }
197 if (identity != null && type != null) {
198 subQueryEmpty = false;
199 esSubQuery.append(" \"must\" : {\n");
200 esSubQuery.append(" \"query_string\" : {\n");
201 esSubQuery.append(" \"query\" : \""+ identity.getId() +"\",\n");
202 esSubQuery.append(" \"fields\" : [\"" + buildTypeEx(type) + "\"]\n");
203 esSubQuery.append(" }\n");
204 esSubQuery.append(" }\n");
205 } else if (filter.getExcludedIdentityList() != null && filter.getExcludedIdentityList().size() > 0) {
206 subQueryEmpty = false;
207 esSubQuery.append(" \"must_not\": [\n");
208 esSubQuery.append(" {\n");
209 esSubQuery.append(" \"ids\" : {\n");
210 esSubQuery.append(" \"values\" : [" + buildExcludedIdentities(filter) + "]\n");
211 esSubQuery.append(" }\n");
212 esSubQuery.append(" }\n");
213 esSubQuery.append(" ]\n");
214 }
215
216 if (expEs != null && expEs.length() > 0) {
217 if(!subQueryEmpty) {
218 esSubQuery.append(" ,\n");
219 }
220 subQueryEmpty = false;
221 esSubQuery.append(" \"filter\": [\n");
222 esSubQuery.append(" {");
223 esSubQuery.append(" \"query_string\": {\n");
224 esSubQuery.append(" \"query\": \"" + expEs + "\"\n");
225 esSubQuery.append(" }\n");
226 esSubQuery.append(" }\n");
227 esSubQuery.append(" ]\n");
228 }
229 esSubQuery.append(" } \n");
230 esSubQuery.append(" } \n");
231 esSubQuery.append(" }\n");
232 esSubQuery.append(" }\n");
233 if(!subQueryEmpty) {
234 esQuery.append(esSubQuery);
235 }
236
237 esQuery.append("}\n");
238 LOG.debug("Search Query request to ES : {} ", esQuery);
239
240 return esQuery.toString();
241 }
242
243
244
245
246
247
248 private String buildExcludedIdentities(ProfileFilter filter) {
249 StringBuilder typeExp = new StringBuilder();
250 if (filter.getExcludedIdentityList() != null && filter.getExcludedIdentityList().size() > 0) {
251
252 Iterator<Identity> iter = filter.getExcludedIdentityList().iterator();
253 Identity first = iter.next();
254 typeExp.append("\"").append(first.getId()).append("\"");
255
256 if (!iter.hasNext()) {
257 return typeExp.toString();
258 }
259 Identity next;
260 while (iter.hasNext()) {
261 next = iter.next();
262 typeExp.append(",\"").append(next.getId()).append("\"");
263 }
264 }
265 return typeExp.toString();
266 }
267
268
269
270
271
272
273 private String buildTypeEx(Type type) {
274 String result;
275 switch(type) {
276 case CONFIRMED:
277 result = "connections";
278 break;
279 case INCOMING:
280
281
282 result = "outgoings";
283 break;
284 case OUTGOING:
285
286
287 result = "incomings";
288 break;
289 default:
290 throw new IllegalArgumentException("Type ["+type+"] not supported");
291 }
292 return result;
293 }
294
295 private String buildExpression(ProfileFilter filter) {
296 StringBuilder esExp = new StringBuilder();
297 char firstChar = filter.getFirstCharacterOfName();
298
299 if (firstChar != '\u0000') {
300 char lowerCase = Character.toLowerCase(firstChar);
301 char upperCase = Character.toUpperCase(firstChar);;
302 esExp.append("lastName.whitespace:").append("(").append(upperCase).append(StorageUtils.ASTERISK_STR).append(" OR ").append(lowerCase).append(StorageUtils.ASTERISK_STR).append(")");
303 return esExp.toString();
304 }
305
306
307 String inputName = StringUtils.isBlank(filter.getName()) ? null : filter.getName().replace(StorageUtils.ASTERISK_STR, StorageUtils.EMPTY_STR);
308 if (StringUtils.isNotBlank(inputName)) {
309
310 String[] keys = inputName.split(" ");
311 if (keys.length > 1) {
312
313 if (filter.isSearchEmail()) {
314 esExp.append("(");
315 }
316 esExp.append("(");
317 for (int i = 0; i < keys.length; i++) {
318 if (i != 0 ) {
319 esExp.append(" AND ") ;
320 }
321 esExp.append(" name.whitespace:").append(StorageUtils.ASTERISK_STR).append(removeAccents(keys[i])).append(StorageUtils.ASTERISK_STR);
322 }
323 esExp.append(")");
324 if (filter.isSearchEmail()) {
325 esExp.append(" OR ( ");
326 for (int i = 0; i < keys.length; i++) {
327 if (i != 0 ) {
328 esExp.append(" AND ") ;
329 }
330 esExp.append(" email:").append(StorageUtils.ASTERISK_STR).append(removeAccents(keys[i])).append(StorageUtils.ASTERISK_STR);
331 }
332 esExp.append(") )");
333 }
334 } else {
335 esExp.append("( name.whitespace:").append(StorageUtils.ASTERISK_STR).append(removeAccents(inputName)).append(StorageUtils.ASTERISK_STR);
336 if (filter.isSearchEmail()) {
337 esExp.append(" OR email:").append(StorageUtils.ASTERISK_STR).append(removeAccents(inputName)).append(StorageUtils.ASTERISK_STR);
338 }
339 esExp.append(" OR userName:").append(StorageUtils.ASTERISK_STR).append(removeAccents(inputName)).append(StorageUtils.ASTERISK_STR).append(")");
340 }
341 }
342
343
344 String skills = StringUtils.isBlank(filter.getSkills()) ? null : filter.getSkills().replace(StorageUtils.ASTERISK_STR, StorageUtils.EMPTY_STR);
345 if (StringUtils.isNotBlank(skills)) {
346 if (esExp.length() > 0) {
347 esExp.append(" AND ");
348 }
349
350 esExp.append("skills:").append(StorageUtils.ASTERISK_STR).append(removeAccents(skills)).append(StorageUtils.ASTERISK_STR);
351 }
352
353
354 String position = StringUtils.isBlank(filter.getPosition()) ? null : filter.getPosition().replace(StorageUtils.ASTERISK_STR, StorageUtils.EMPTY_STR);
355 if (StringUtils.isNotBlank(position)) {
356 if (esExp.length() > 0) {
357 esExp.append(" AND ");
358 }
359 esExp.append("position:").append(StorageUtils.ASTERISK_STR).append(removeAccents(position)).append(StorageUtils.ASTERISK_STR);
360 }
361 return esExp.toString();
362 }
363
364 private static String removeAccents(String string) {
365 string = Normalizer.normalize(string, Normalizer.Form.NFD);
366 string = string.replaceAll("[\\p{InCombiningDiacriticalMarks}]", "");
367 return string;
368 }
369 }