Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -950,22 +950,36 @@ else if( fieldbcontentname.startsWith("date") ){

fieldValueStr = fieldValueStr.replaceAll(specialCharsToEscape, "\\\\$1");

if(fieldName.equals("languageId") || fieldValueStr.contains("-")){
if("catchall".equals(fieldName)) {
// Mandatory gate: match either a catchall token PREFIX (fast, existing
// behavior) OR the raw title via wildcard. Unlike catchall (which
// aggregates every field of the document), title_dotraw is scoped to a
// single field, so this alternative recovers mid-token and
// exact-full-value matches -- e.g. a file named "IMG_0004.jpeg"
// tokenizes to "img_0004"+"jpeg", so "0004" or a full-name search never
// satisfies a catchall-only prefix gate -- without reintroducing an
// unscoped, whole-document wildcard like the old broad catchall:*value*.
// Boosts are deliberately asymmetric: catchall (a real token-prefix hit)
// outranks title_dotraw (a raw substring hit that can land anywhere,
// including mid-word). Mirrors GlobalSearchAttributeStrategy, which is
// the equivalent gate for the new Content Search / Content Drive path.
luceneQuery.append("+(catchall:" + fieldValueStr + "*^10 OR title_dotraw:*"
+ fieldValueStr + "*^2) ");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how did you choose the ^10 and ^2?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Carried over from GlobalSearchAttributeStrategy (#36793) so both paths rank the same. Happy to change the spread if necessary, but I think it should be done in both places

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add some more context:

The values are added in what that class already had:
^10 for a token prefix
^2 for the raw substring, which can land mid word and must never outrank a real match

That ordering is not just an assumption, GlobalSearchAttributeStrategyMatchingTest asserts it, so the numbers are arbitrary but the ranking they produce is tested, of course

} else if(fieldName.equals("languageId") || fieldValueStr.contains("-")){
luceneQuery.append("+" + fieldName +":" + fieldValueStr + " ");
}else{
luceneQuery.append("+" + fieldName +":" + fieldValueStr + "* ");
}

if("catchall".equals(fieldName)) {

luceneQuery.append(" title:'" + fieldValueStr + "'^15 ");
final String[] titleSplit = fieldValueStr.split("[,|\\s+]");
if (titleSplit.length > 1) {
for (final String term : titleSplit) {
luceneQuery.append(" title:" + term + "^5 ");
}
}
luceneQuery.append(" title_dotraw:*" + fieldValueStr + "*^5 ");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.dotcms.datagen.ContentTypeDataGen;
import com.dotcms.datagen.ContentletDataGen;
import com.dotcms.datagen.FieldDataGen;
import com.dotcms.datagen.FileAssetDataGen;
import com.dotcms.datagen.FolderDataGen;
import com.dotcms.datagen.LanguageDataGen;
import com.dotcms.languagevariable.business.LanguageVariableAPI;
import com.dotcms.repackage.org.directwebremoting.WebContext;
Expand All @@ -33,6 +35,7 @@
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.portlets.contentlet.model.IndexPolicy;
import com.dotmarketing.portlets.folders.business.FolderAPI;
import com.dotmarketing.portlets.folders.model.Folder;
import com.dotmarketing.portlets.languagesmanager.model.Language;
import com.dotmarketing.portlets.structure.model.Relationship;
import com.dotmarketing.portlets.structure.model.Structure;
Expand All @@ -42,6 +45,7 @@
import com.google.common.collect.ImmutableList;
import com.liferay.portal.model.User;
import com.liferay.portal.util.WebKeys;
import com.liferay.util.FileUtil;
import com.liferay.util.StringPool;
import com.liferay.util.servlet.SessionMessages;
import com.tngtech.java.junit.dataprovider.DataProvider;
Expand All @@ -56,6 +60,8 @@

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
Expand Down Expand Up @@ -535,4 +541,73 @@ public void test_searchContentletsByUser_shouldReturnContentWithMultiLang() thro

}

/**
* <b>Method to Test:</b> {@link ContentletAjax#searchContentletsByUser(List, String, List, List,
* boolean, boolean, boolean, boolean, int, String, int, User, HttpSession, String, String)}<p>
* <b>When:</b> the legacy Relationships search dialog performs a global ({@code catchall})
* search for a term that lands mid-token, or that spans a tokenizer boundary such as the exact
* full file name<p>
* <b>Should:</b> Find the content anyway, while a term present in no field still returns
* nothing — i.e. the broad, unscoped {@code catchall:*value*} is not reintroduced. See issue
* #37052.
*/
@Test
public void test_searchContentletsByUser_globalSearch_matchesMidTokenAndExactFullName()
throws Exception {

final String uniqueToken = "qa" + System.currentTimeMillis();
// Tokenizes to "img_<uniqueToken>_0004" + "jpeg", so <uniqueToken> sits mid-token and the
// full name spans the "." boundary
final String fileName = "IMG_" + uniqueToken + "_0004.jpeg";

final Host host = APILocator.getHostAPI().findDefaultHost(systemUser, false);
final Folder folder = new FolderDataGen().site(host).nextPersisted();

final File tempDir = Files.createTempDirectory(uniqueToken).toFile();
final File file = new File(tempDir, fileName);
FileUtil.write(file, "helloworld");

final Contentlet fileAsset = new FileAssetDataGen(folder, file).nextPersisted();
final String fileAssetTypeInode = contentTypeAPI.find("FileAsset").inode();

try {
// A genuine token prefix — worked before the fix and must keep working
assertGlobalSearchFindsOnly(fileAssetTypeInode, "IMG_" + uniqueToken, fileAsset);

// Mid-token term — the catchall prefix gate alone never matched this
assertGlobalSearchFindsOnly(fileAssetTypeInode, uniqueToken, fileAsset);

// Exact full name — spans the "." tokenizer boundary
assertGlobalSearchFindsOnly(fileAssetTypeInode, fileName, fileAsset);

// Negative control: a term in no field must still return nothing, otherwise the broad
// whole-document wildcard is back
final List results = new ContentletAjax().searchContentletsByUser(
ImmutableList.of(BaseContentType.ANY), fileAssetTypeInode,
CollectionsUtils.list("catchall", "zz" + uniqueToken),
Collections.emptyList(), false, false, false,
false, 0, "moddate", 0, systemUser, null, null, null);

assertEquals(0, Integer.parseInt(((Map) results.get(0)).get("total").toString()));
} finally {
contentletAPI.destroy(fileAsset, systemUser, false);
FileUtil.deltree(tempDir);
}
}

private void assertGlobalSearchFindsOnly(final String contentTypeInode, final String searchTerm,
final Contentlet expected) throws DotDataException, DotSecurityException {

final List results = new ContentletAjax().searchContentletsByUser(
ImmutableList.of(BaseContentType.ANY), contentTypeInode,
CollectionsUtils.list("catchall", searchTerm), Collections.emptyList(),
false, false, false, false, 0, "moddate", 0, systemUser, null, null, null);

assertNotNull(results);
assertEquals("Searching for '" + searchTerm + "' must return exactly one match",
1, Integer.parseInt(((Map) results.get(0)).get("total").toString()));
assertEquals("Searching for '" + searchTerm + "' returned the wrong content",
expected.getIdentifier(), ((Map) results.get(3)).get("identifier"));
}

}
Loading