Skip to content

Commit

Permalink
feat(search): switch to like search & remove enableWordSearch configu…
Browse files Browse the repository at this point in the history
…ration (#2811)

* feat(search): Switch to like search & remove enableWordSearch configuration

* enableWordSearch is now enabled by default.
* Word search uses now a "like" algorithm, not a "start with" algorithm

Closes [BPM-43](https://bonitasoft.atlassian.net/browse/BPM-43)
Covers [BPM-44](https://bonitasoft.atlassian.net/browse/BPM-44)
  • Loading branch information
danila-m authored Dec 12, 2023
1 parent 95ebb72 commit 0abb78f
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1282,43 +1282,43 @@ public void searchPendingTasksManagedBy() throws Exception {
final ProcessDefinitionBuilder processBuilder = new ProcessDefinitionBuilder().createNewInstance(PROCESS_NAME,
PROCESS_VERSION);
processBuilder.addActor(ACTOR_NAME).addDescription("Famous French actor");
final DesignProcessDefinition designProcessDefinition = processBuilder.addUserTask("userTask1", ACTOR_NAME)
.addUserTask("userTask2", ACTOR_NAME)
.addUserTask("userTask3", ACTOR_NAME).addUserTask("task4", ACTOR_NAME)
.addUserTask("userTask5", ACTOR_NAME)
.addUserTask("userTask6", ACTOR_NAME).getProcess();
final DesignProcessDefinition designProcessDefinition = processBuilder.addUserTask("usertask1", ACTOR_NAME)
.addUserTask("usertask2", ACTOR_NAME)
.addUserTask("usertask3", ACTOR_NAME).addUserTask("task4", ACTOR_NAME)
.addUserTask("usertask5", ACTOR_NAME)
.addUserTask("usertask6", ACTOR_NAME).getProcess();
final ProcessDefinition processDefinition = deployAndEnableProcessWithActor(designProcessDefinition, ACTOR_NAME,
john);
final ProcessInstance pi0 = getProcessAPI().startProcess(processDefinition.getId());
waitForUserTask(pi0, "userTask1");
waitForUserTask(pi0, "userTask2");
waitForUserTask(pi0, "userTask3");
waitForUserTask(pi0, "usertask1");
waitForUserTask(pi0, "usertask2");
waitForUserTask(pi0, "usertask3");
waitForUserTask(pi0, "task4");
waitForUserTask(pi0, "userTask5");
waitForUserTask(pi0, "userTask6");
waitForUserTask(pi0, "usertask5");
waitForUserTask(pi0, "usertask6");

// filter all *userTask*, managedBy jack:
SearchOptionsBuilder builder = new SearchOptionsBuilder(0, 10);
builder.searchTerm("userTask");
builder.searchTerm("usertask");
builder.sort(HumanTaskInstanceSearchDescriptor.NAME, Order.ASC);
SearchResult<HumanTaskInstance> aHumanTasksRes = getProcessAPI().searchPendingTasksManagedBy(jack.getId(),
builder.done());
assertEquals(5, aHumanTasksRes.getCount());
List<HumanTaskInstance> tasks = aHumanTasksRes.getResult();
HumanTaskInstance humanTaskInstance = tasks.get(0);
assertEquals("userTask1", humanTaskInstance.getName());
assertEquals("usertask1", humanTaskInstance.getName());
humanTaskInstance = tasks.get(1);
assertEquals("userTask2", humanTaskInstance.getName());
assertEquals("usertask2", humanTaskInstance.getName());
humanTaskInstance = tasks.get(2);
assertEquals("userTask3", humanTaskInstance.getName());
assertEquals("usertask3", humanTaskInstance.getName());
humanTaskInstance = tasks.get(3);
assertEquals("userTask5", humanTaskInstance.getName());
assertEquals("usertask5", humanTaskInstance.getName());
humanTaskInstance = tasks.get(4);
assertEquals("userTask6", humanTaskInstance.getName());
assertEquals("usertask6", humanTaskInstance.getName());

// filter all *userTask*, managedBy jules:
builder = new SearchOptionsBuilder(0, 10);
builder.searchTerm("userTask");
builder.searchTerm("usertask");
builder.sort(HumanTaskInstanceSearchDescriptor.NAME, Order.ASC);
aHumanTasksRes = getProcessAPI().searchPendingTasksManagedBy(jules.getId(), builder.done());
assertEquals(0, aHumanTasksRes.getCount());
Expand All @@ -1329,20 +1329,12 @@ public void searchPendingTasksManagedBy() throws Exception {
builder.searchTerm("task");
builder.sort(HumanTaskInstanceSearchDescriptor.NAME, Order.DESC);
aHumanTasksRes = getProcessAPI().searchPendingTasksManagedBy(jack.getId(), builder.done());
assertEquals(1, aHumanTasksRes.getCount());
tasks = aHumanTasksRes.getResult();
humanTaskInstance = tasks.get(0);
assertEquals("task4", humanTaskInstance.getName());
assertEquals(6, aHumanTasksRes.getCount());

disableAndDeleteProcess(processDefinition);
deleteUsers(john, jack, jules);
}

public void searchPendingTasksWithApostrophe() throws Exception {
searchPendingTasks("userTask'1", ACTOR_NAME);
searchPendingTasks("userTask1", "ACTOR'NAME");
}

@Test
public void searchPendingTasksWithMultipleWords() throws Exception {
final ProcessDefinitionBuilder processBuilder = new ProcessDefinitionBuilder().createNewInstance(PROCESS_NAME,
Expand Down Expand Up @@ -1514,9 +1506,9 @@ public void searchPendingTasksWithLikeWildcardsCharacters() throws Exception {
builder.searchTerm("step#");
final SearchResult<HumanTaskInstance> searchHumanTaskInstancesWithEscapeCharacter = getProcessAPI()
.searchHumanTaskInstances(builder.done());
assertEquals(3, searchHumanTaskInstancesWithEscapeCharacter.getCount());
assertEquals(5, searchHumanTaskInstancesWithEscapeCharacter.getCount());
List<HumanTaskInstance> tasks = searchHumanTaskInstancesWithEscapeCharacter.getResult();
assertThat(tasks).extracting("name").containsOnly("step#1_b", "step#1_c", "step#1a");
assertThat(tasks).extracting("name").containsOnly("step#1_b", "step#1_c", "step#1a", "%step#2", "%step#4_a");

builder = new SearchOptionsBuilder(0, 10);
builder.sort(HumanTaskInstanceSearchDescriptor.NAME, Order.ASC);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,6 @@
<bean factory-bean="hbmConfigurationProvider" factory-method="getClassAliasMappings" />
</constructor-arg>
<constructor-arg name="likeEscapeCharacter" value="${bonita.platform.persistence.tenant.likeEscapeCharacter}" />
<constructor-arg name="enableWordSearch" value="${bonita.platform.persistence.tenant.enableWordSearch}" />
<constructor-arg name="wordSearchExclusionMappings" ref="tenantWordSearchExclusionMappings" />
</bean>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,9 @@ bonita.platform.sequence.10080=20000

# Platform persistence service
bonita.platform.persistence.platform.likeEscapeCharacter=#
bonita.platform.persistence.platform.enableWordSearch=false

# Tenant persistence service
bonita.platform.persistence.tenant.likeEscapeCharacter=#
bonita.platform.persistence.tenant.enableWordSearch=false

# Hibernate persistence configuration:
bonita.platform.persistence.generate_statistics=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@ public class HQLQueryBuilder<T> extends QueryBuilder<T> {
HQLQueryBuilder(Session session, Query baseQuery, OrderByBuilder orderByBuilder,
Map<String, String> classAliasMappings,
char likeEscapeCharacter,
boolean wordSearchEnabled,
OrderByCheckingMode orderByCheckingMode,
SelectListDescriptor<T> selectDescriptor) {
super(session, baseQuery, orderByBuilder, classAliasMappings, likeEscapeCharacter, wordSearchEnabled,
super(session, baseQuery, orderByBuilder, classAliasMappings, likeEscapeCharacter,
orderByCheckingMode, selectDescriptor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
abstract class QueryBuilder<T> {

private final Query baseQuery;
private final boolean wordSearchEnabled;
private final OrderByCheckingMode orderByCheckingMode;
private final AbstractSelectDescriptor<T> selectDescriptor;
private final QueryGeneratorForFilters queryGeneratorForFilters;
Expand All @@ -42,13 +41,12 @@ abstract class QueryBuilder<T> {

QueryBuilder(Session session, Query baseQuery, OrderByBuilder orderByBuilder,
Map<String, String> classAliasMappings,
char likeEscapeCharacter, boolean wordSearchEnabled, OrderByCheckingMode orderByCheckingMode,
char likeEscapeCharacter, OrderByCheckingMode orderByCheckingMode,
AbstractSelectDescriptor<T> selectDescriptor) {
this.session = session;
this.classAliasMappings = classAliasMappings;
stringQueryBuilder = new StringBuilder(baseQuery.getQueryString());
this.baseQuery = baseQuery;
this.wordSearchEnabled = wordSearchEnabled;
this.orderByCheckingMode = orderByCheckingMode;
this.selectDescriptor = selectDescriptor;
this.queryGeneratorForFilters = new QueryGeneratorForFilters(classAliasMappings,
Expand All @@ -62,7 +60,7 @@ public String getQuery() {
return stringQueryBuilder.toString();
}

void appendFilters(List<FilterOption> filters, SearchFields multipleFilter, boolean enableWordSearch) {
void appendFilters(List<FilterOption> filters, SearchFields multipleFilter) {
Set<String> specificFilters = emptySet();
if (!filters.isEmpty()) {
if (!hasWHEREInRootQuery(stringQueryBuilder.toString())) {
Expand All @@ -77,7 +75,7 @@ void appendFilters(List<FilterOption> filters, SearchFields multipleFilter, bool
parameters.putAll(whereClause.getParameters());
}
if (multipleFilter != null && multipleFilter.getTerms() != null && !multipleFilter.getTerms().isEmpty()) {
handleMultipleFilters(stringQueryBuilder, multipleFilter, specificFilters, enableWordSearch);
handleMultipleFilters(stringQueryBuilder, multipleFilter, specificFilters);
}
}

Expand Down Expand Up @@ -108,8 +106,7 @@ private static String removeAllParenthesisBlocks(String q) {
}

private void handleMultipleFilters(final StringBuilder builder, final SearchFields multipleFilter,
final Set<String> specificFilters,
final boolean enableWordSearch) {
final Set<String> specificFilters) {
final Map<Class<? extends PersistentObject>, Set<String>> allTextFields = multipleFilter.getFields();
final Set<String> fields = new HashSet<>();
for (final Map.Entry<Class<? extends PersistentObject>, Set<String>> entry : allTextFields.entrySet()) {
Expand All @@ -122,12 +119,12 @@ private void handleMultipleFilters(final StringBuilder builder, final SearchFiel

if (!fields.isEmpty()) {
final List<String> terms = multipleFilter.getTerms();
applyFiltersOnQuery(builder, fields, terms, enableWordSearch);
applyFiltersOnQuery(builder, fields, terms);
}
}

private void applyFiltersOnQuery(final StringBuilder queryBuilder, final Set<String> fields,
final List<String> terms, final boolean enableWordSearch) {
final List<String> terms) {
if (!hasWHEREInRootQuery(queryBuilder.toString())) {
queryBuilder.append(" WHERE ");
} else {
Expand All @@ -136,7 +133,7 @@ private void applyFiltersOnQuery(final StringBuilder queryBuilder, final Set<Str
queryBuilder.append("(");

QueryGeneratorForSearchTerm.QueryGeneratedSearchTerms result = queryGeneratorForSearchTerm.generate(fields,
terms, enableWordSearch);
terms);
queryBuilder.append(result.getSearch());

queryBuilder.append(")");
Expand All @@ -155,12 +152,11 @@ boolean hasChanged() {

abstract Query rebuildQuery(AbstractSelectDescriptor<T> selectDescriptor, Session session, Query query);

void manageFiltersAndParameters(AbstractSelectDescriptor<T> selectDescriptor, boolean enableWordSearch)
void manageFiltersAndParameters(AbstractSelectDescriptor<T> selectDescriptor)
throws SBonitaReadException {
if (selectDescriptor.hasAFilter()) {
final QueryOptions queryOptions = selectDescriptor.getQueryOptions();
appendFilters(queryOptions.getFilters(), queryOptions.getMultipleFilter(),
enableWordSearch);
appendFilters(queryOptions.getFilters(), queryOptions.getMultipleFilter());
}
if (selectDescriptor.hasOrderByParameters()) {
appendOrderByClause(selectDescriptor.getQueryOptions().getOrderByOptions(),
Expand All @@ -185,7 +181,7 @@ private void setParameters(final Query query, final Map<String, Object> inputPar
}

public Query build() throws SBonitaReadException {
manageFiltersAndParameters(selectDescriptor, wordSearchEnabled);
manageFiltersAndParameters(selectDescriptor);
Query query = baseQuery;
if (hasChanged()) {
query = rebuildQuery(selectDescriptor, session, baseQuery);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,19 @@ public class QueryBuilderFactory {
private Map<String, String> classAliasMappings;
private char likeEscapeCharacter;
private final Set<Class<? extends PersistentObject>> wordSearchExclusionMappings = new HashSet<>();
private final boolean enableWordSearch;

public QueryBuilderFactory(OrderByCheckingMode orderByCheckingMode, Map<String, String> classAliasMappings,
char likeEscapeCharacter, boolean enableWordSearch, Set<String> wordSearchExclusionMappings)
char likeEscapeCharacter, Set<String> wordSearchExclusionMappings)
throws Exception {
this.orderByCheckingMode = orderByCheckingMode;
this.classAliasMappings = classAliasMappings;
this.likeEscapeCharacter = likeEscapeCharacter;
this.enableWordSearch = enableWordSearch;
initializeWordSearchExclusions(enableWordSearch, wordSearchExclusionMappings);
initializeWordSearchExclusions(wordSearchExclusionMappings);
}

private void initializeWordSearchExclusions(boolean enableWordSearch, Set<String> wordSearchExclusionMappings)
private void initializeWordSearchExclusions(Set<String> wordSearchExclusionMappings)
throws Exception {
if (enableWordSearch) {
log.warn("The word based search feature is experimental, using it in production may impact performances.");
}
if (wordSearchExclusionMappings != null && !wordSearchExclusionMappings.isEmpty()) {
if (!enableWordSearch) {
log.info("You defined an exclusion mapping for the word based search feature, but it is not enabled.");
}
for (final String wordSearchExclusionMapping : wordSearchExclusionMappings) {
final Class<?> clazz = Class.forName(wordSearchExclusionMapping);
if (!PersistentObject.class.isAssignableFrom(clazz)) {
Expand All @@ -70,14 +62,13 @@ private void initializeWordSearchExclusions(boolean enableWordSearch, Set<String
public <T> QueryBuilder createQueryBuilderFor(Session session,
SelectListDescriptor<T> selectDescriptor) {
Query query = session.getNamedQuery(selectDescriptor.getQueryName());
boolean wordSearchEnabled = isWordSearchEnabled(selectDescriptor.getEntityType());
if (query instanceof NativeQuery) {
return new SQLQueryBuilder<>(session, query, orderByBuilder, classAliasMappings,
likeEscapeCharacter,
wordSearchEnabled, orderByCheckingMode, selectDescriptor);
orderByCheckingMode, selectDescriptor);
} else {
return new HQLQueryBuilder<>(session, query, orderByBuilder, classAliasMappings, likeEscapeCharacter,
wordSearchEnabled, orderByCheckingMode, selectDescriptor);
orderByCheckingMode, selectDescriptor);
}
}

Expand All @@ -86,7 +77,7 @@ public void setOrderByBuilder(OrderByBuilder orderByBuilder) {
}

protected boolean isWordSearchEnabled(final Class<? extends PersistentObject> entityClass) {
if (!enableWordSearch || entityClass == null) {
if (entityClass == null) {
return false;
}
for (final Class<? extends PersistentObject> exclusion : wordSearchExclusionMappings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,40 +49,31 @@ private String buildLikeEscapeClause(String term) {
}

void buildLikeClauseForOneFieldOneTerm(final StringBuilder queryBuilder, final String currentField,
final String currentTerm,
final boolean enableWordSearch) {
// Search if a sentence starts with the term
final String currentTerm) {
// We do not want to search for %currentTerm% to ensure we can use Lucene-like library.
queryBuilder.append(currentField)
.append(buildLikeEscapeClause(escapeTerm(currentTerm, likeEscapeCharacter) + "%"));

if (enableWordSearch) {
// Search also if a word starts with the term
// We do not want to search for %currentTerm% to ensure we can use Lucene-like library.
queryBuilder.append(" OR ").append(currentField)
.append(buildLikeEscapeClause("% " + escapeTerm(currentTerm, likeEscapeCharacter) + "%"));
}
.append(buildLikeEscapeClause("%" + escapeTerm(currentTerm, likeEscapeCharacter) + "%"));
}

private void buildLikeClauseForOneFieldMultipleTerms(final StringBuilder queryBuilder, final String currentField,
final List<String> terms,
final boolean enableWordSearch) {
final List<String> terms) {
final Iterator<String> termIterator = terms.iterator();
while (termIterator.hasNext()) {
final String currentTerm = termIterator.next();

buildLikeClauseForOneFieldOneTerm(queryBuilder, currentField, currentTerm, enableWordSearch);
buildLikeClauseForOneFieldOneTerm(queryBuilder, currentField, currentTerm);

if (termIterator.hasNext()) {
queryBuilder.append(" OR ");
}
}
}

QueryGeneratedSearchTerms generate(Set<String> fields, List<String> terms, boolean enableWordSearch) {
QueryGeneratedSearchTerms generate(Set<String> fields, List<String> terms) {
StringBuilder stringBuilder = new StringBuilder();
final Iterator<String> fieldIterator = fields.iterator();
while (fieldIterator.hasNext()) {
buildLikeClauseForOneFieldMultipleTerms(stringBuilder, fieldIterator.next(), terms, enableWordSearch);
buildLikeClauseForOneFieldMultipleTerms(stringBuilder, fieldIterator.next(), terms);
if (fieldIterator.hasNext()) {
stringBuilder.append(" OR ");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ public class SQLQueryBuilder<T> extends QueryBuilder<T> {
SQLQueryBuilder(Session session, Query baseQuery,
OrderByBuilder orderByBuilder,
Map<String, String> classAliasMappings, char likeEscapeCharacter,
boolean wordSearchEnabled,
OrderByCheckingMode orderByCheckingMode,
SelectListDescriptor<T> selectDescriptor) {
super(session, baseQuery, orderByBuilder, classAliasMappings, likeEscapeCharacter, wordSearchEnabled,
super(session, baseQuery, orderByBuilder, classAliasMappings, likeEscapeCharacter,
orderByCheckingMode,
selectDescriptor);
}
Expand Down
Loading

0 comments on commit 0abb78f

Please sign in to comment.