Search Queries and Filters
To get sensible results from the search engine, you must provide a sensible query. Liferay’s query APIs are in the portal-search-api
module and are used to construct both queries and filters.
Filters ask a yes or no question for every search document and do not calculate relevance. A filter might ask is the status field equal to staging or live?
Queries ask the same yes or no question compute how well a document matches the specified criteria. This is the concept of relevance scoring. A query might ask Does the document’s content field contain the words “Liferay”, “Content”, or “Management”, and how relevant is the content of the document to the searched keywords?
To use queries and filters in Liferay, construct the query and then add it to the request as a query or filter:
-
Add a query to the request with
SearchRequestBuilder.query(fooQuery)
. -
To filter instead of querying, add it to the search request with
SearchRequestBuilder.postFilterQuery(fooQuery)
.
Here you can deploy, test, and inspect a Gogo Shell command that queries the company index.
While developing, it can be helpful to inspect the query as generated by the search engine. See Inspecting any Liferay-Generated Elasticsearch Query for information.
Deploy A Gogo Shell Command with Custom Search Queries
Start a new Liferay instance by running
docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.120-ga120
Sign in to Liferay at http://localhost:8080. Use the email address test@liferay.com and the password test. When prompted, change the password to learn.
Then, download the project and complete some prerequisites:
-
Download and unzip the
liferay-b9f3
project:curl https://resources.learn.liferay.com/dxp/latest/en/using-search/developer-guide/liferay-b9f3.zip -O
unzip liferay-b9f3.zip
-
From the module root, build and deploy.
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
NoteThis command is the same as copying the deployed jars to
/opt/liferay/osgi/modules
on the Docker container. -
Confirm the deployment in the Liferay Docker container console.
STARTED com.acme.b9f3.impl_1.0.0 [1775]
-
The B9F3 sample includes a shell script that loads documents and content into Liferay. To run it, first retrieve the default site’s ID and the global site’s ID from the running Liferay instance.
In the default site, open Site Menu () → Configuration → Site Settings.
-
Go to Site Configuration and record the Site ID.
-
Go to the Global site. Click Site Selector () → Available Sites → Global.
-
Find and record the Site ID for the Global site.
-
To run the script, go to
liferay-b9f3/curl
in the terminal. -
Run the shell script with the site IDs as arguments:
./populate_b9f3.sh [default site ID] [global site ID]
For example,
./populate_b9f3.sh 20116 20119
The script adds these resources:
-
Web Content:
- Able Content
- Able Content Folder
- Able Content
-
Document and Media:
- Able Document
- Able Document Folder
- Able Document
-
Test the B9F3 Search Queries
-
On the locally running Liferay’s home page, enter able into the search bar. Six results appear.
-
Open the Global Menu (), navigate to Control Panel → Gogo Shell.
-
Enter b9f3:search able and answer the CAPTCHA challenge.
-
Click Execute.
-
Four results appear, all of them root content. Anything nested inside the created folders does not have a
folderId
of0
, and is not returned. However, the folders themselves are returned, because they are contained in the root folder and given afolderId
of0
.
The root folder in the Documents and Media and Web Content applications have a folderId
of 0
. You can check the indexed folder ID of each search result by enabling Display Results in Document Form in the Search Results widget. See Inspecting Search Engine Documents for more information.
Understanding the B9F3 Search Queries
First, initialize a SearchRequestBuilder
. You can use this object to construct the search request.
SearchRequestBuilder searchRequestBuilder =
_searchRequestBuilderFactory.builder();
Next, populate the SearchContext
within the request. This sets the entry class names to search and the company ID of the instance to be searched. It also sets the keywords to search. These keywords are entered by the user at search time.
You must either set keywords into the search context or enable empty search in the request builder with searchRequestBuilder.emptySearchEnabled(true);
.
searchRequestBuilder.withSearchContext(
searchContext -> {
searchContext.setCompanyId(_portal.getDefaultCompanyId());
searchContext.setEntryClassNames(
new String[] {
"com.liferay.document.library.kernel.model.DLFileEntry",
"com.liferay.document.library.kernel.model.DLFolder",
"com.liferay.journal.model.JournalArticle",
"com.liferay.journal.model.JournalFolder"
});
searchContext.setKeywords(keywords);
});
Now fashion the query clauses. This example nests two MUST query clauses inside a Boolean query: one clause is a term query for matching the folderId
field to the value 0
, and the other is for performing a full text match query of the user’s search keywords to the localized title field.
BooleanQuery booleanQuery = _queries.booleanQuery();
booleanQuery.addMustQueryClauses(
_queries.term(Field.FOLDER_ID, "0"),
_queries.match(
StringBundler.concat(
"localized_", Field.TITLE, StringPool.UNDERLINE,
LocaleUtil.US),
keywords));
Add the Boolean query with its nested clauses to the request, execute the request, then process the response as needed. This prints the document’s uid
field and its score.
SearchRequest searchRequest = searchRequestBuilder.query(
booleanQuery
).build();
SearchResponse searchResponse = _searcher.search(searchRequest);
SearchHits searchHits = searchResponse.getSearchHits();
for (SearchHit searchHit : searchHits.getSearchHits()) {
Document document = searchHit.getDocument();
String uid = document.getString(Field.UID);
System.out.println(
StringBundler.concat(
"Document ", uid, " has a score of ",
searchHit.getScore()));
}
These Liferay services are referenced in the B9F3 code:
@Reference
private Portal _portal;
@Reference
private Queries _queries;
@Reference
private RoleLocalService _roleLocalService;
@Reference
private Searcher _searcher;
@Reference
private SearchRequestBuilderFactory _searchRequestBuilderFactory;
@Reference
private UserLocalService _userLocalService;
Implementing Nested Queries
To create queries for object fields, web content structure fields, or document metadata sets, you must query the field according to its nested structure using a nested query. Inside the query, specify the path (e.g., ddmFieldArray
for web content and document metadata sets, nestedFieldArray
for objects) and create a Boolean query with two clauses that use dot notation: one clause matches the field name, and the other matches the value (e.g., the user’s keywords).
Querying Web Content Structure Fields
Web content structures and documents and media metadata sets are indexed similarly. A web content structure field is indexed like this:
{
ddmFieldName=ddm__text__35174__Text25689566_en_US,
ddmFieldValueText_en_US_String_sortable=able text,
ddmValueFieldName=ddmFieldValueText_en_US,
ddmFieldValueText_en_US=Able text
}
In Elasticsearch’s query syntax you might create a query like this for the field:
{
"nested": {
"path": "ddmFieldArray",
"query": {
"bool": {
"must": [
{
"match": {
"ddmFieldArray.ddmFieldName": "ddm__text__35174__Text25689566_en_US"
}
},
{
"match": {
"ddmFieldArray.ddmFieldValueText_en_US": "${keywords}"
}
}
]
}
}
}
}
You can create the same query in Liferay’s Java search API:
BooleanQuery booleanQuery = queries.booleanQuery();
MatchQuery fieldNameQuery = queries.match("ddmFieldArray.ddmFieldName", "ddm__text__35174__Text25689566_en_US");
MatchQuery fieldValueQuery = queries.match("ddmFieldArray.ddmFieldValueKeyword_en_US", keywords);
booleanQuery.addMustQueryClauses(fieldNameQuery, fieldValueQuery);
NestedQuery nestedQuery = queries.nested("ddmFieldArray", booleanQuery);
Querying Object Fields
An object’s text field is indexed like this:
[
{
fieldName=fooText,
value_en_US=Able Text,
valueFieldName=value_en_US
},
{
fieldName=fooText,
value_keyword_lowercase=Able Text,
valueFieldName=value_keyword_lowercase
}
]
In Elasticsearch’s query syntax you might create a query like this for the field:
{
"nested": {
"path": "nestedFieldArray",
"query": {
"bool": {
"must": [
{
"match": {
"nestedFieldArray.fieldName": "fooText"
}
},
{
"match": {
"nestedFieldArray.value_en_US": "${keywords}"
}
}
]
}
}
}
}
You can create the same query in Liferay’s Java search API:
BooleanQuery booleanQuery = queries.booleanQuery();
MatchQuery fieldNameQuery = queries.match("nestedFieldArray.fieldName", "fooText");
MatchQuery fieldValueQuery = queries.match("nestedFieldArray.value_en_US", keywords);
booleanQuery.addMustQueryClauses(fieldNameQuery, fieldValueQuery);
NestedQuery nestedQuery = queries.nested("nestedFieldArray", booleanQuery);
Filtering
There’s no separate API for filtering. Construct the query as usual and add it to the search request, specifying it as a filter using the SearchRequestBuilder.postFilterQuery(fooQuery)
method.
For example, you can change the B9F3 code to first filter the documents with the folderId
of 0
and then perform the match query on the keywords and title field:
TermQuery termQuery = _queries.term(Field.FOLDER_ID, "0");
searchRequestBuiler.postFilterQuery(termQuery);
MatchQuery matchQuery = _queries.match(
StringBundler.concat(
"localized_", Field.TITLE, StringPool.UNDERLINE,
LocaleUtil.US), keywords);
searchRequestBuilder.query(matchQuery);
SearchRequest searchRequest = searchRequestBuilder.build();