Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3723d73
Small mapping update and response parsing fix.
ivanmrsulja Jun 10, 2024
951508b
Added query parser to transform Solr queries to ES JSON-like queries.
ivanmrsulja Jun 11, 2024
987fa5c
Updated mapping to support aggregations.
ivanmrsulja Jun 12, 2024
22a85e6
Added small ES query optimizations.
ivanmrsulja Jun 18, 2024
ee3fbb4
Completed implementation of missing ES engine methods. Improved docum…
ivanmrsulja Jun 24, 2024
b74091c
Switched to one search engine configuration URL. Implemented a server…
ivanmrsulja Jun 26, 2024
d46a80c
Added SSL and Basic auth support. Added OpenSearch support.
ivanmrsulja Jul 5, 2024
84a822c
Added fallback configuration property for legacy Solr configurations.
ivanmrsulja Aug 28, 2024
a6b288a
Refactored code so that common property fallback resolution is in it'…
ivanmrsulja Sep 3, 2024
789a23e
Fixed fetch count bug. Fixed advanced search filter bug where filter …
ivanmrsulja Oct 10, 2024
1a4b269
Fixed facets query bug. Fixed delete by query bug.
ivanmrsulja Oct 11, 2024
db39bcc
Fixed UTF-8 parsing bug while indexing.
ivanmrsulja Oct 15, 2024
991d06c
Added support for *_drsim fields. Fixed pagination and statistics bug.
ivanmrsulja Oct 28, 2024
162a03e
Fixed sort by relevance bug.
ivanmrsulja Oct 31, 2024
b6fbaf2
Updated documentation.
ivanmrsulja Nov 14, 2024
a1fd76c
Updated example.runtime.properties
ivanmrsulja Nov 19, 2024
829a0e6
Fixed inaccurate count retrieved for large indexes when using search …
ivanmrsulja Dec 3, 2024
b5b7383
Added field mappings which were missing for autocomplete search.
ivanmrsulja Dec 9, 2024
03d74b9
Small bugfix and code refactor.
ivanmrsulja Dec 13, 2024
6c0cd8b
Fixed sorting issue.
ivanmrsulja Dec 26, 2024
f95843d
Fixed match all query bug.
ivanmrsulja Mar 28, 2025
d346d22
Added support for aggregation (facet) search.
ivanmrsulja May 21, 2025
d11b311
Added support for facet browsing functionality. Added support for reg…
ivanmrsulja Jun 11, 2025
023e53f
Fixed date range slider issue.
ivanmrsulja Jun 13, 2025
7e01a57
Implemented a linear pass experimental fix to parsing problems.
ivanmrsulja Jun 18, 2025
ba5387a
Fixed all query parsing issues.
ivanmrsulja Jun 25, 2025
b008651
Updated lucene dependencies to latest supported version.
ivanmrsulja Jun 30, 2025
8859e76
Added search controller flag for treating input as simple text. Fixed…
ivanmrsulja Aug 15, 2025
c323406
Small cleanup.
ivanmrsulja Aug 19, 2025
7d8c893
Merge branch 'main' into feature/elasticsearch-integration
ivanmrsulja Aug 25, 2025
deee6c0
Faceted filter fix.
ivanmrsulja Aug 25, 2025
41098af
Fixed empty field edgecase.
ivanmrsulja Aug 27, 2025
70b6df8
Small fix to apply edgecase in situations where controller is called …
ivanmrsulja Sep 16, 2025
43211ad
Added tests. Cleaned up code.
ivanmrsulja Sep 17, 2025
f64040f
Renamed few getters and setters to align with naming convention.
ivanmrsulja Oct 3, 2025
15d2098
Small optimisation and variable renaming to align to naming convention.
ivanmrsulja Oct 8, 2025
4dacb92
Enchanced HTTP/HTTPS ES integration infrastructure with optional basi…
ivanmrsulja Oct 22, 2025
00e1210
Certificate trust level bugfix.
ivanmrsulja Nov 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@
<artifactId>guava</artifactId>
<version>32.0.0-jre</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>fluent-hc</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,4 +161,8 @@ public enum Order {
* @return defines whether the text of a facet field should be compared case insensitively.
*/
boolean isFacetTextCompareCaseInsensitive();

void setSimpleQuery(boolean value);

boolean isSimpleQuery();
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public static ResponseValues process(VitroRequest vreq, Map<String, List<String>
int hitsPerPage = getHitsPerPage(vreq);
int documentsToReturn = hitsPerPage;
if (!wasHtmlRequested) {
documentsToReturn = getDocumentsNumber(vreq);
documentsToReturn = getDocumentsNumber(vreq);
}
String queryText = getQueryText(vreq);
log.debug("Query text is \"" + queryText + "\"");
Expand Down Expand Up @@ -203,6 +203,8 @@ public static ResponseValues process(VitroRequest vreq, Map<String, List<String>
SearchResponse response = null;

try {
query.setSimpleQuery(!query.getQuery().trim().equals("*:*"));

response = search.query(query);
} catch (Exception ex) {
String msg = makeBadSearchMessage(queryText, ex.getMessage(), vreq);
Expand Down Expand Up @@ -259,7 +261,7 @@ public static ResponseValues process(VitroRequest vreq, Map<String, List<String>
/* Compile the data for the templates */

Map<String, Object> body = new HashMap<String, Object>();

/* Add ClassGroup and type refinement links to body */
if (wasHtmlRequested) {
if (log.isDebugEnabled()) {
Expand Down Expand Up @@ -415,7 +417,7 @@ private static int getHitsPerPage(VitroRequest vreq) {
log.debug("hitsPerPage is " + hitsPerPage);
return hitsPerPage;
}

private static int getDocumentsNumber(VitroRequest vreq) {
int documentsNumber = DEFAULT_DOCUMENTS_NUMBER;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class BaseSearchQuery implements SearchQuery {
private int facetMinCount = -1;
private boolean facetTextToCompareIgnoreCase;
private String facetContainsText;
private boolean simpleQuery = false;

@Override
public SearchQuery setQuery(String query) {
Expand Down Expand Up @@ -171,4 +172,13 @@ public boolean isFacetTextCompareCaseInsensitive() {
return facetTextToCompareIgnoreCase;
}

@Override
public void setSimpleQuery(boolean value) {
this.simpleQuery = value;
}

@Override
public boolean isSimpleQuery() {
return this.simpleQuery;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package edu.cornell.mannlib.vitro.webapp.searchengine.base;

import java.util.Objects;

import javax.annotation.Nullable;

import edu.cornell.mannlib.vitro.webapp.config.ConfigurationProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SearchEngineUtil {

private static final Log log = LogFactory.getLog(SearchEngineUtil.class);

@Nullable
public static String getSearchEngineURLProperty() {
ConfigurationProperties config = ConfigurationProperties.getInstance();
if (Objects.isNull(config)) {
return null;
}

if (config.getProperty("vitro.local.searchengine.url", "").isEmpty()) {
Comment thread
tamu-sad-iii marked this conversation as resolved.
return tryFetchLegacySolrConfiguration(config);
}

return config.getProperty("vitro.local.searchengine.url", "");
}

private static String tryFetchLegacySolrConfiguration(ConfigurationProperties config) {
String legacyConfigValue = config.getProperty("vitro.local.solr.url", "");
if (!legacyConfigValue.isEmpty()) {
log.warn(
"vitro.local.solr.url is deprecated, switch to using" +
" vitro.local.searchengine.url as soon as possible.");
}

return legacyConfigValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package edu.cornell.mannlib.vitro.webapp.searchengine.elasticsearch;

import co.elastic.clients.elasticsearch._types.query_dsl.ExistsQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.FuzzyQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchPhraseQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.PrefixQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.RegexpQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.WildcardQuery;

public class CustomQueryBuilder {

private static final String MAX_FUZZY_EDITS = "2";

private CustomQueryBuilder() {
}


public static Query buildQuery(SearchType queryType, String field, String value) {
Comment thread
ivanmrsulja marked this conversation as resolved.
validateInput(field, value);

switch (queryType) {
case MATCH:
return MatchQuery.of(m -> m
.field(field)
.query(value)
)._toQuery();
case FUZZY:
return FuzzyQuery.of(m -> m
.field(field)
.value(value.replace("~", ""))
.fuzziness(MAX_FUZZY_EDITS)
)._toQuery();
case PREFIX:
return PrefixQuery.of(m -> m
.field(field)
.value(value)
)._toQuery();
case RANGE:
String[] values = value.split("TO");
return RangeQuery.of(m -> m
.field(field)
.from(values[0].replace("[", "").replace("(", "").trim())
.to(values[1].replace("]", "").replace(")", "").trim())
)._toQuery();
case EXISTS:
return ExistsQuery.of(m -> m
.field(field)
)._toQuery();
case MATCH_ALL:
return MatchAllQuery.of(m -> m)._toQuery();
case REGEXP:
String regexpValue;

boolean isSolrRegexpSpecification = value.startsWith("/") && value.endsWith("/") && value.length() > 1;
if (isSolrRegexpSpecification) {
regexpValue = value.substring(1, value.length() - 1);
} else {
regexpValue = value;
}

return RegexpQuery.of(m -> m
.field(field)
.value(regexpValue)
)._toQuery();
case WILDCARD:
if (field.trim().equals("*")) {
return MatchAllQuery.of(m -> m)._toQuery();
}

return WildcardQuery.of(m -> m
.field(field)
.value(value.replace(".*", "*"))
)._toQuery();
default:
return MatchPhraseQuery.of(m -> m
.field(field)
.query(value.length() > 1 ? value.substring(1, value.length() - 1) : value)
// Remove leading and trailing '"' character
)._toQuery();
}
}

private static void validateInput(String field, String value) {
if (field == null || field.isEmpty()) {
throw new IllegalArgumentException("Field not specified");
}
if (value == null) {
throw new IllegalArgumentException("Value not specified");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.apache.http.entity.ContentType;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.databind.ObjectMapper;

import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngineException;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchInputDocument;
import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchInputField;
import edu.cornell.mannlib.vitro.webapp.search.VitroSearchTermNames;
import edu.cornell.mannlib.vitro.webapp.utils.http.ESHttpBasicClientFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;

/**
* The nuts and bolts of adding a document to the Elasticsearch index
Expand All @@ -44,7 +48,16 @@ private void addDocument(SearchInputDocument doc)
throws SearchEngineException {
try {
Map<String, List<Object>> map = convertDocToMap(doc);

if (map.containsKey(VitroSearchTermNames.NAME_RAW)) {
map.putIfAbsent(VitroSearchTermNames.AC_NAME_STEMMED, map.get(VitroSearchTermNames.NAME_RAW));
map.putIfAbsent(VitroSearchTermNames.AC_NAME_UNTOKENIZED, map.get(VitroSearchTermNames.NAME_RAW));
}

String json = new ObjectMapper().writeValueAsString(map);
if (json.contains("_drsim")) {
json = reformatDRSIMFields(json);
}
log.debug("Adding document for '" + doc.getField("DocId") + "': "
+ json);

Expand Down Expand Up @@ -75,15 +88,46 @@ private Map<String, List<Object>> convertDocToMap(SearchInputDocument doc) {
return map;
}

private String reformatDRSIMFields(String json) {
String patternString = "\\[(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z) TO (\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z)]";
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(json);

StringBuffer result = new StringBuffer();

while (matcher.find()) {
String dateStart = matcher.group(1);
String dateEnd = matcher.group(2);

String replacement = String.format("{\"gte\": \"%s\", \"lte\": \"%s\"}", dateStart, dateEnd)
.replace("{", "\\{")
.replace("}", "\\}");

matcher.appendReplacement(result, replacement);
}

matcher.appendTail(result);
return result.toString().replace("[\"{", "{").replace("}\"]", "}");
}

private void putToElastic(String json, String docId)
throws SearchEngineException {
try {
String url = baseUrl + "/_doc/"
+ URLEncoder.encode(docId, "UTF8");
Response response = Request.Put(url)
.bodyString(json, ContentType.APPLICATION_JSON).execute();
log.debug("Response from Elasticsearch: "
+ response.returnContent().asString());
HttpClient httpClient = ESHttpBasicClientFactory.getHttpClient(baseUrl);

HttpPut request = new HttpPut(url);
request.addHeader("Content-Type", "application/json");
request.setEntity(new StringEntity(json, "UTF-8"));
HttpResponse response = httpClient.execute(request);
if (response.getStatusLine().getStatusCode() >= 400) {
log.warn("Response from Elasticsearch: "
+ EntityUtils.toString(response.getEntity()));
} else {
log.debug("Response from Elasticsearch: "
+ EntityUtils.toString(response.getEntity()));
}
} catch (Exception e) {
throw new SearchEngineException("Failed to put to Elasticsearch",
e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
import java.util.HashMap;
import java.util.Map;

import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;

import com.fasterxml.jackson.databind.ObjectMapper;

import edu.cornell.mannlib.vitro.webapp.modules.searchEngine.SearchEngineException;
import edu.cornell.mannlib.vitro.webapp.utils.http.ESHttpBasicClientFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.util.EntityUtils;

/**
* The nuts and bolts of getting the number of documents in the Elasticsearch
Expand All @@ -25,9 +26,10 @@ public ESCounter(String baseUrl) {

public int count() throws SearchEngineException {
try {
String url = baseUrl + "/_doc/_count";
Response response = Request.Get(url).execute();
String json = response.returnContent().asString();
String url = baseUrl + "/_count";
HttpClient httpClient = ESHttpBasicClientFactory.getHttpClient(baseUrl);
HttpResponse response = httpClient.execute(new HttpGet(url));
String json = EntityUtils.toString(response.getEntity());

@SuppressWarnings("unchecked")
Map<String, Object> map = new ObjectMapper().readValue(json,
Expand Down
Loading