Indicizzazione e Ricerca di Custom Entity su Liferay 7.4

Scopri come indicizzare e integrare la ricerca di custom entity su Liferay 7.4 con Service Builder, sfruttando Elasticsearch e Opensearch per ottenere ricerche avanzate e performanti su dati personalizzati

Miriade utilizza con successo Liferay da diversi anni, in vari scenari e per diversi clienti, sia come soluzione di portale istituzionale sia come strumento di lavoro interno.

Liferay è una piattaforma software open source basata su Java, progettata per la creazione di portali web aziendali e applicazioni digitali personalizzabili. È utilizzata principalmente per sviluppare intranet, extranet, siti pubblici e soluzioni di digital experience.

Originariamente noto come un portal framework, permette di integrare contenuti, servizi e applicazioni in un unico ambiente centralizzato. La sua architettura modulare consente di aggiungere funzionalità tramite plugin o moduli OSGi.

Le funzionalità principali includono gestione dei contenuti (CMS), gestione utenti e permessi avanzati, workflow, motore di ricerca interno, supporto per multilingua e responsive design. Liferay supporta anche l’integrazione con sistemi esterni (CRM, ERP, LDAP) e fornisce API REST e GraphQL per lo sviluppo headless.

Viene distribuito in due versioni:

  • Liferay CE (Community Edition): gratuita e open source
  • Liferay DXP (Digital Experience Platform): a pagamento, con supporto commerciale e funzionalità enterprise avanzate.
     

Il Contesto

Uno dei punti di forza di Liferay è la disponibilità di una SDK che permette di:

  • sviluppare moduli custom, sia di backend sia di frontend;
  • sviluppare personalizzazioni di moduli o servizi core;
  • sviluppare estensioni per alcuni dei suoi framework, come ad esempio:
    • modalità e protocolli di autenticazione
    • modalità e protocolli di pagamento (per la parte commerce)
    • tipologie di repository documentali
    • altro
  • Sviluppare temi e layout grafici custom.

Una delle principali tipologie di modulo custom utilizzato è chiamato “Service Builder”, uno strumento di generazione automatica del codice che semplifica la creazione del livello di accesso ai dati e dei servizi business logic all’interno della piattaforma.

Di fatto è un framework, integrato in Liferay, (basato su Java e OSGi) che permette di definire modelli di entità (Entity) in un file XML (service.xml) e di generare automaticamente il codice Java necessario per:

  • la persistenza dei dati (DAO e utilità per l’accesso al database);
  • la creazione di servizi locali e remoti;
  • la gestione dei metodi CRUD;
  • il supporto per transazioni, sicurezza, e scalabilità.

Le caratteristiche principali del Service Builder sono:

Generazione automatica del codice
Da una semplice definizione XML, vengono generati i layer DAO, service e utility con codice strutturato e manutenibile.

Supporto per servizi locali e remoti

  • LocalService (invocabile solo all'interno della stessa JVM)
  • RemoteService (esposto tramite API SOAP/JSON)

Integrazione con il database
Crea automaticamente le tabelle nel database alla prima esecuzione del deployment, e permette anche di gestire il versionamento della struttura del database.

Gestione avanzata delle transazioni
Consente di definire il comportamento transazionale per ciascun metodo.

Modularità OSGi
I servizi generati sono registrati dinamicamente come componenti OSGi, rendendo il codice più modulare e componibile.

Estendibilità
I servizi generati possono essere personalizzati e arricchiti tramite override o aggiunta di metodi custom.

Integrazione con Liferay Permissions
Supporta l'integrazione con il sistema di permessi di Liferay per applicare controlli su risorse e azioni.

In sintesi, il Service Builder è una componente chiave per sviluppare applicazioni robuste su Liferay, riducendo il boilerplate e migliorando la coerenza dell’architettura.

Il Problema

In questo contesto, in alcuni scenari potrebbe essere utile per motivi di business implementare ricerche avanzate su queste entity custom, per le quali eventuali query su database potrebbero risultare complesse o poco performanti. In questo caso Liferay permette di sfruttare l’integrazione con il suo sistema di indicizzazione e ricerca.

Ricordiamo che Liferay 7.4 utilizza come motore di ricerca principale Elasticsearch, e che recentemente è uscito anche il plugin per il supporto ad Opensearch.

Questi due motori sono sufficientemente simili in termini di utilizzo da permettere a Liferay di utilizzare un unico framework che astrae l’implementazione e permette allo sviluppatore di utilizzare il motore di ricerca.

Per le principali entity “core” di Liferay, il portale già implementa tutte queste funzioni. Ma come posso fare, come sviluppatore, ad integrare le mie entity ed i miei sviluppi con questo framework?

Gli Obiettivi

L’integrazione riguarda tre aspetti principali.

  1. Aggiunta all’indice delle entity custom sviluppate tramite il service builder.
  2. Integrare le custom entity nello strumento di ricerca “generica” di Liferay.
  3. Eseguire ricerche custom ed elaborare i risultati.

NOTA

Per questo articolo useremo come esempio un servizio fatto in un nostro recente progetto, il cui service builder si occupa di salvare a database alcuni dati di prodotto che vogliamo indicizzare, che corrispondono ad una entity chiamata PageEntry.


Parte della logica è ovviamente dettata dalle necessità di contesto, e può essere più semplice in altri casi.
 

La Soluzione

Visto che gli obiettivi sono 3, andiamo per ordine.

1. Aggiunta all’indice di entity custom

La prima cosa da fare in questo caso è andare a fare un override dei principali metodi CRUD all’interno del -LocalService: cioè:

  • addPageEntry(PageEntry entity)
  • updatePageEntry(PageEntry entity)
  • deletePageEntry(PageEntry entity)
  • deletePageEntry(long pageEntryId)

L’override è necessario per poter aggiungere alla firma del metodo l’annotation Indexable, nelle sue due versioni:

JAVA

@Indexable(type = IndexableType.REINDEX)
// per gli add e gli update

@Indexable(type = IndexableType.DELETE)
// per i delete

Attenzione che queste annotazioni dovranno essere messe anche su eventuali metodi custom che facciano direttamente scritture senza passare attraverso uno dei metodi standard.

Una volta fatto questo è necessario specificare al framework quali campi e con quali caratteristiche andare a scrivere nell’indice, e questo può essere fatto in due modalità:

  • Quella più tradizionale, andando ad implementare una classe che estende BaseIndexer<Entity>
  • Quella più nuova, andando ad implementare due classi:
    • Un ModelIndexerWriterContributor<Entity>
    • Un ModelDocumentContributor<Entity>

Nella seconda soluzione i metodi da implementare nelle due classi corrispondono quasi del tutto alla somma dei metodi della prima, per cui al momento le due opzioni sono quasi equivalenti.

In entrambi i casi il metodo principale sarà doGetDocument(Entity entity) nel primo caso e contribute(Document document, Entity entity) nel secondo. Lo scopo di entrambi è quello di aggiungere campi al documento che rappresenta la vostra ricerca, al netto di quelli base già popolati da liferay.

Lascio qui di seguito un esempio del secondo caso


Questo permetterà, dopo il reindex o dopo l’aggiunta di nuovi dati, di popolare l’indice. Tuttavia non abbiamo al momento ancora modo di leggere questi dati.

2. Integrare le custom entity nello strumento di ricerca di Liferay

Uno degli obiettivi potrebbe essere quello di leggere i dati indicizzati e di presentarli nella pagina di ricerca “standard” di Liferay, come se fosse un normale oggetto core.

NOTE 

Si ricorda è possibile modificare e personalizzare la pagina di ricerca di Liferay, utilizzando varie portlet collegate, per cui prima di gridare “non funziona!” verificate di non aver attivato qualche filtro, magari sulla tipologia di dato ricercato, che vi blocca la visualizzazione dei vostri oggetti.

NOTA 2

Nel codice che segue verrà oscurato il nome completo dei package, lasciando solamente la parte finale. È inteso che il nome del package deve sempre essere esplicitato per intero.

 

L’integrazione nella ricerca “nativa” di Liferay è probabilmente la parte più complessa, poiché serve implementare una serie di classi.

Base Searcher

Serve ad abilitare la ricerca per la nostra entity. Si noti che viene chiamato il metodo setPermissionAware(boolean) che indica a Liferay che i risultati tornati dovranno subire il check del layer permissions prima di poter essere visualizzati dall’utente finale.

JAVA

@Component(
    property = "model.class.name=*****.page.search.model.PageEntry",
    service = BaseSearcher.class
)
public class PageEntrySearcher extends BaseSearcher {

    public PageEntrySearcher() {
        setDefaultSelectedFieldNames(
            Field.ASSET_TAG_NAMES, Field.ASSET_CATEGORY_IDS, Field.COMPANY_ID, Field.ENTRY_CLASS_NAME, 
            Field.ENTRY_CLASS_PK, Field.CONTENT, Field.MODIFIED_DATE, Field.SUBTITLE,
            Field.SCOPE_GROUP_ID, Field.GROUP_ID, Field.TITLE, Field.UID);
        setFilterSearch(true);
        setPermissionAware(true);
    }

    @Override
    public String getClassName() {
        return _CLASS_NAME;
    }

    private static final String _CLASS_NAME = PageEntry.class.getName();

}

ModelSearchConfigurator

Serve ad indicare a Liferay quali altri servizi devono essere utilizzati per le varie funzionalità.

JAVA

@Component(service = ModelSearchConfigurator.class)
public class PageEntryModelSearchConfigurator
    implements ModelSearchConfigurator<PageEntry> {

    @Override
    public String getClassName() {
        return PageEntry.class.getName();
    }

    @Override
    public String[] getDefaultSelectedFieldNames() {
        return new String[] {
            Field.ASSET_TAG_NAMES, Field.ASSET_CATEGORY_IDS, Field.COMPANY_ID, 
            Field.ENTRY_CLASS_NAME, Field.ENTRY_CLASS_PK, Field.MODIFIED_DATE, 
            Field.SCOPE_GROUP_ID, Field.GROUP_ID, Field.UID
        };
    }

    @Override
    public String[] getDefaultSelectedLocalizedFieldNames() {
        return new String[] {};
    }

    @Override
    public ModelIndexerWriterContributor<PageEntry> getModelIndexerWriterContributor() {
        return modelIndexWriterContributor;
    }

    @Override
    public ModelSummaryContributor getModelSummaryContributor() {
        return modelSummaryContributor;
    }

    @Override
    public ModelVisibilityContributor getModelVisibilityContributor() {
        return _modelVisibilityContributor;
    }
    
    @Override
    public boolean isPermissionAware() {
        return true;
    }

    @Reference
    private PageEntryLocalService _pageEntryLocalService;
    
    @Reference
    private DynamicQueryBatchIndexingActionableFactory
        _dynamicQueryBatchIndexingActionableFactory;

    @Reference
    private Localization _localization;

    @Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
    private ModelIndexerWriterContributor<PageEntry> modelIndexWriterContributor;

    @Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
    private ModelSummaryContributor modelSummaryContributor;
    
    @Reference(target = "(indexer.class.name=*****.page.search.model.PageEntry)")
    private ModelVisibilityContributor _modelVisibilityContributor;

}
 

In particolare a noi interessano i tre campi finali, poi usati nei rispettivi getter.
Il primo, cioè il ModelIndexerWriterContributor è lo stesso utilizzato al punto 1 di questa soluzione.
 

ModelVisibilityContributor

Serve a determinare se un dato oggetto è visibile nella ricerca, in base al suo stato (se rilevante)

JAVA

@Component(
    property = "indexer.class.name=*****.page.search.model.PageEntry",
    service = ModelVisibilityContributor.class
)
public class PageEntryModelVisibilityContributor
    implements ModelVisibilityContributor {


    @Override
    public boolean isVisible(long classPK, int status) {
        try {
            PageEntry entry = _pageEntryLocalService.fetchPageEntry(classPK);

            return isVisible(entry.getStatus(), status);
        }
        catch (Throwable portalException) {
            if (_log.isWarnEnabled()) {
                _log.warn(
                    "Unable to check visibility for page entry ",
                    portalException);
            }

            return false;
        }
    }

    private static final Log _log = LogFactoryUtil.getLog(
        PageEntryModelVisibilityContributor.class);

    @Reference
    private PageEntryLocalService _pageEntryLocalService;

}
 

ModelSummaryContributor

Serve a creare il “summary” della nostra entity, cioè il testo che verrà mostrato nella nostra ricerca per ogni oggetto. Si compone di due campi: titolo e descrizione. In questo caso l’implementazione è abbastanza semplice, viene scelto di mostrare due campi localizzati e di impostare una lunghezza massima della descrizione a 200 caratteri (per motivi legati al design).

JAVA

@Component(
    property = "indexer.class.name=**.page.search.model.PageEntry",
    service = ModelSummaryContributor.class
)
public class PageEntryModelSummaryContributor
    implements ModelSummaryContributor {
    
    @Override
    public Summary getSummary(
        Document document, Locale locale, String snippet) {

        String languageId = LocaleUtil.toLanguageId(locale);

        return _createSummary(
            document, _localization.getLocalizedName(Field.SUBTITLE, languageId),
            _localization.getLocalizedName(Field.TITLE, languageId));
    }

    private Summary _createSummary(
        Document document, String contentField, String titleField) {

        Summary summary = new Summary(
            document.get(titleField, titleField),
            document.get(contentField, contentField));

        summary.setMaxContentLength(200);
        return summary;
    }

    @Reference
    private Localization _localization;

}
 

KeywordQueryContributor

L’ultima classe della nostra serie, che serve per specificare a Liferay come costruire la query per i nostri oggetti, in base alla ricerca effettuata dall’utente. Per farla breve, in quali campi cercare.

@Component(
    property = "indexer.class.name=*****.page.search.model.PageEntry",
    service = KeywordQueryContributor.class
)
public class PageEntryKeywordQueryContributor
    implements KeywordQueryContributor {

    @Override
    public void contribute(
        String keywords, BooleanQuery booleanQuery,
        KeywordQueryContributorHelper keywordQueryContributorHelper) {

        SearchContext searchContext =
            keywordQueryContributorHelper.getSearchContext();

        _queryHelper.addSearchLocalizedTerm(
            booleanQuery, searchContext, Field.CONTENT, false);
        _queryHelper.addSearchLocalizedTerm(
            booleanQuery, searchContext, Field.SUBTITLE, false);
        _queryHelper.addSearchLocalizedTerm(
            booleanQuery, searchContext, Field.TITLE, false);

        QueryConfig queryConfig = searchContext.getQueryConfig();

        queryConfig.addHighlightFieldNames(
            _searchLocalizationHelper.getLocalizedFieldNames(
                new String[] {Field.SUBTITLE,  Field.TITLE}, searchContext));
    }

    @Reference
    private QueryHelper _queryHelper;

    @Reference
    private SearchLocalizationHelper _searchLocalizationHelper;

}

Come anticipato, non è proprio una implementazione da 2 minuti di lavoro, ma ovviamente il framework è molto articolato, e supporta l’implementazione di più motori di ricerca a runtime, per cui deve necessariamente essere in grado di gestire tutti gli aspetti della ricerca.

Tutto apposto?
No, mi dispiace per voi ma mancano ancora due piccoli passaggi. Finora abbiamo implementato il salvataggio dei dati nell’indice e la loro lettura, manca ovviamente la parte di presentation, cioè come effettivamente mostrare i risultati della nostra ricerca.

BaseAssetRendererFactory

È una classe di utility, gestita da OSGI, che ha il compito essenzialmente di istanziare un AssetRenderer per ogni nostro oggetto e passargli i servizi che servono.

JAVA

@Component(
        immediate = true,
        property = {"javax.portlet.name=" + PageEntryWebKeys.PageEntryKey},
        service = AssetRendererFactory.class)
public class PageEntryAssetRendererFactory
    extends BaseAssetRendererFactory<PageEntry> {

    public static final String TYPE = "pageEntry";

    public PageEntryAssetRendererFactory() {
        setClassName(PageEntry.class.getName());
        setLinkable(true);
        setPortletId(PageEntryWebKeys.PageEntryKey);
        setSearchable(true);
    }

    @Override
    public AssetRenderer<PageEntry> getAssetRenderer(long classPK, int type)
        throws PortalException {

        PageEntryAssetRenderer pageEntryAssetRenderer =
            new PageEntryAssetRenderer(
                _pageEntryLocalService.getPageEntry(classPK));

        pageEntryAssetRenderer.setAssetRendererType(type);
        pageEntryAssetRenderer.setServletContext(_servletContext);
        pageEntryAssetRenderer.setLayoutLocalService(_layoutLocalService);

        return pageEntryAssetRenderer;
    }

    @Override
    public String getType() {
        return TYPE;
    }

    @Override
    public String getIconCssClass() {
        return "web-content";
    }
    
    @Override
    public PortletURL getURLView(
        LiferayPortletResponse liferayPortletResponse,
        WindowState windowState) {

        LiferayPortletURL liferayPortletURL =
            liferayPortletResponse.createLiferayPortletURL(
                PageEntryWebKeys.PageEntryKey, PortletRequest.RENDER_PHASE);

        try {
            liferayPortletURL.setWindowState(windowState);
        }
        catch (WindowStateException wse) {
        }

        return liferayPortletURL;
    }

    @Reference
    private PageEntryLocalService _pageEntryLocalService;
    
    @Reference
    private LayoutLocalService _layoutLocalService;
    
    @Reference(target = "(osgi.web.symbolicname=***.page.search.web)", unbind = "-")
    private ServletContext _servletContext;
    
}
 

BaseJSPAssetRenderer

Questa classe è quella che si occupa effettivamente di definire come il nostro oggetto sarà visualizzato nella ricerca di Liferay, ed in particolare il link al risultato, l’immagine, titolo, summary, status ed eventuali permissions. Come vedete si tratta di un oggetto plain, non gestito da OSGI.

JAVA

public class PageEntryAssetRenderer extends BaseJSPAssetRenderer<PageEntry> {

    public PageEntryAssetRenderer(PageEntry pageEntry) {
        _pageEntry = pageEntry;
    }

    @Override
    public PageEntry getAssetObject() {
        return _pageEntry;
    }

    @Override
    public String getClassName() {
        return PageEntry.class.getName();
    }

    @Override
    public long getClassPK() {
        return _pageEntry.getPageId();
    }

    @Override
    public long getGroupId() {
        return _pageEntry.getGroupId();
    }

    @Override
    public String getJspPath(
        HttpServletRequest httpServletRequest, String template) {
        return null;
    }
    
    @Override
    public String getThumbnailPath(PortletRequest portletRequest) throws Exception {
        try {
            Layout layout = _layoutLocalService.fetchLayout(_pageEntry.getPlid());
            if (layout != null && layout.getIconImage()) {
                return "/image/logo?img_id="+layout.getIconImageId();
            }
        } catch (Throwable e) {
            _log.error(e.getMessage());
            return super.getThumbnailPath(portletRequest);
        }
        return super.getThumbnailPath(portletRequest);
    }
    
    @Override
    public boolean include(
            HttpServletRequest request, HttpServletResponse response,
            String template)
        throws Exception {
        request.setAttribute(PageEntryWebKeys.PageEntry, _pageEntry);

        return super.include(request, response, template);
    }

    @Override
    public String getSummary(
        PortletRequest portletRequest, PortletResponse portletResponse) {
        return _pageEntry.getSummary(getLocale(portletRequest)) + " " + _pageEntry.getPlid();
    }

    @Override
    public String getTitle(Locale locale) {
        return _pageEntry.getTitle(locale);
    }
    
    @Override
    public int getStatus() {
        return _pageEntry.getStatus();
    }
    
    @Override
    public String getURLViewInContext(
            LiferayPortletRequest liferayPortletRequest,
            LiferayPortletResponse liferayPortletResponse,
            String noSuchEntryRedirect)
        throws PortalException {

        ThemeDisplay themeDisplay =
            (ThemeDisplay)liferayPortletRequest.getAttribute(
                WebKeys.THEME_DISPLAY);

        return getURLViewInContext(themeDisplay, noSuchEntryRedirect);
    }

    public String getURLViewInContext(
            ThemeDisplay themeDisplay, String noSuchEntryRedirect)
        throws PortalException {
        try {
            Layout layout = _layoutLocalService.fetchLayout(_pageEntry.getPlid());
            StringBuilder urlBuilder = new StringBuilder();
            urlBuilder.append(themeDisplay.getScopeGroup().getPathFriendlyURL(layout.isPrivateLayout(), themeDisplay));
            urlBuilder.append(themeDisplay.getScopeGroup().getFriendlyURL());
            urlBuilder.append(layout.getFriendlyURL(themeDisplay.getLocale()));
            return urlBuilder.toString();
        } catch (Throwable e) {
            _log.error(e.getMessage());
            return "/";
        }
    }

    @Override
    public long getUserId() {
        return _pageEntry.getUserId();
    }

    @Override
    public String getUserName() {
        return _pageEntry.getUserName();
    }
    
    @Override
    public String getUuid() {
        return _pageEntry.getUuid();
    }
    
    public boolean hasDeletePermission(PermissionChecker permissionChecker) {
        return false;
    }

    @Override
    public boolean hasEditPermission(PermissionChecker permissionChecker) {
        return false;
    }

    @Override
    public boolean hasViewPermission(PermissionChecker permissionChecker) {
        try {
            return LayoutPermissionUtil.contains(permissionChecker, _pageEntry.getPlid(), ActionKeys.VIEW);
        } catch (PortalException e) {
            _log.error(e.getMessage());
            if (_log.isDebugEnabled())
                _log.debug(e.getMessage(), e);
        }
        return false;
    }
    
    public void setLayoutLocalService(LayoutLocalService layoutLocalService) {
        _layoutLocalService = layoutLocalService;
    }
    
    private static final Log _log = LogFactoryUtil.getLog(PageEntryAssetRenderer.class);

    private PageEntry _pageEntry;
    private LayoutLocalService _layoutLocalService;
}
 

3. Eseguire ricerche custom ed elaborare i risultati

Questo terzo obiettivo esula dal secondo. Ci proponiamo cioè di effettuare query direttamente verso l’indice e di mostrare i risultati esternamente alla ricerca di Liferay, per esempio attraverso una portlet custom o esposti attraverso un servizio. Lo step necessario resta il punto 1 di questa soluzione, cioè la scrittura dei dati nell’indice.

Mi scuserete ma per questo terzo caso il codice sarà leggermente diverso, sostanzialmente perché riferito ad una diversa entity di un diverso progetto.
La logica resta invariata, in ogni caso.

L’idea alla base di questa soluzione è di utilizzare servizi che Liferay mette già a disposizione nel package com.liferay.portal.search per costruire query che a tutti gli effetti assomigliano alla trasposizione Java di una query su Elasticsearch/Opensearch.

Vediamo di quali oggetti parliamo, e poi qualche esempio.

JAVA

@Reference
protected Queries queries;

@Reference
protected Searcher searcher;

@Reference
protected SearchRequestBuilderFactory searchRequestBuilderFactory;
    
@Reference
protected HighlightBuilderFactory highlightBuilderFactory;

@Reference
protected FieldConfigBuilderFactory fieldConfigBuilderFactory;

@Reference
private Sorts _sorts;

Come già detto, sono tutti oggetti dello stesso package e sono tutti iniettabili tramite OSGI.

Un esempio di ricerca

JAVA

public SearchResultDto doQuery(long userId, Integer page, Integer pageSize, String keywords, boolean enabledContacts, boolean searchAnd, SearchContactDto searchFilter) {
        
//build base search
    SearchRequestBuilder searchRequestBuilder = searchRequestBuilderFactory.builder();
        searchRequestBuilder.withSearchContext(
                searchContext -> {                    searchContext.setCompanyId(PortalUtil.getDefaultCompanyId());
searchContext.setEntryClassNames(new String[] {Contatto.class.getName()});
                });
        
    BooleanQuery query = queries.booleanQuery();

    //add dates filter
    query = addValidityFilters(query, enabledContacts);
    //add keywords filter
    if (Validator.isNotNull(keywords))
        query = addFullTextFilter(query, keywords, searchAnd, permissions);
    
    //add columns specific filters
    if (searchFilter != null && searchFilter.getSearchFilter() != null && searchFilter.getSearchFilter().getColumnFilters() != null) {
        for (ColumnFilterDto colFilter : searchFilter.getSearchFilter().getColumnFilters()) {
            query = addNumberFilter(query, colFilter, false, false);
        }
    }
    
    //add highlights to evidence match to users
    Highlight highlight = highlightBuilderFactory.builder()        .addFieldConfig(fieldConfigBuilderFactory.builder(RubricaSearchTerms.UFFICIO).fragmentSize(80).numFragments(3).build())
                .preTags(StringPool.BLANK)
                .postTags(StringPool.BLANK)
                .requireFieldMatch(true)
                .build();
        
    //build search request, with paging and sorting
    SearchRequest searchRequest = searchRequestBuilder
            .query(query)
            .from(page * pageSize)
            .size(pageSize)
            .emptySearchEnabled(true)
            .highlightEnabled(true)
            .highlight(highlight)
            .sorts(
                    _sorts.score(),
                        _sorts.field(Field.NAME+StringPool.UNDERLINE+Field.SORTABLE_FIELD_SUFFIX, SortOrder.ASC),
                        _sorts.field(RubricaSearchTerms.UFFICIO+StringPool.UNDERLINE+Field.SORTABLE_FIELD_SUFFIX, SortOrder.ASC)
                        )
                .build();
        
    // execute actual search
    SearchResponse searchResponse = searcher.search(searchRequest);
        
    List<ContattoDto> result = new ArrayList<>();
    
    // extract results and convert to usefull dto
    for (SearchHit hit : searchResponse.getSearchHits().getSearchHits()) {
        
        if (_log.isDebugEnabled())
            _log.debug(hit.getDocument());
            
        result.add(contactMapper.mapToDto(hit));
    }

    return new SearchResultDto(result, Long.valueOf(searchResponse.getSearchHits().getTotalHits()).intValue());
    }
 

I metodi correlati ritornano sempre delle BooleanQuery, ad esempio

JAVA

protected BooleanQuery addValidityFilters(BooleanQuery query, boolean enabled) {
    Date now = new Date();
        
    if (enabled) {
        BooleanQuery dateAfterQuery = queries.booleanQuery();
dateAfterQuery.addShouldQueryClauses(queries.booleanQuery().addMustNotQueryClauses(queries.exists(RubricaSearchTerms.UNTIL)));
            dateAfterQuery.addShouldQueryClauses(queries.rangeTerm(RubricaSearchTerms.FROM, true, false, now, null));
            
            BooleanQuery dateBeforeQuery = queries.booleanQuery();
            dateBeforeQuery.addShouldQueryClauses(queries.booleanQuery().addMustNotQueryClauses(queries.exists(RubricaSearchTerms.FROM)));
            dateBeforeQuery.addShouldQueryClauses(queries.rangeTerm(RubricaSearchTerms.UNTIL, false, true, null, now));
            query = query.addFilterQueryClauses(dateAfterQuery).addFilterQueryClauses(dateBeforeQuery);
        }         
        return query;
    }
 

Alcuni metodi interessanti dell’oggetto BooleanQuery

  • addMustQueryClauses → aggiunge un filtro “and”
  • addMustNotQueryClauses → aggiunge un filtro “and not”
  • addShouldQueryClauses → aggiunge il filtro “or”
  • booleanQuery → aggiunge una sottoquery
  • terms → aggiunge un termine alla query

La combinazione di questi metodi e di sottoquery permette di mappare la complessità delle query.
 

I Risultati

Il risultato dei punti 2 e 3, entrambi unitamente al punto 1, permettono di visualizzare i dati di nostre entity custom in due modalità: tramite la pagina di ricerca di Liferay e tramite una portlet custom che esegua direttamente query sul motore di indicizzazione.

Le Conclusioni

La combinazione di queste tre implementazione permette di implementare la ricerca a tutti i livelli di entity custom sviluppate ad hoc. Con una logica simile è perfino possibile modificare le logiche di ricerca di oggetti core o di rimappare gli oggetti core in wrapper e indicizzarli a parte rispetto ai rispettivi core.

Nell’esempio ai punti 1 e 2, si trattava infatti di rimappare le pagine di Liferay, con logiche diverse ed aggiungendo dati alla ricerca, tratti da campi expando. Si può infine implementare una ricerca che esuli da quella standard di liferay e venga visualizzata tramite servizi e portlet custom.
 

 

Ti è piaciuto quanto hai letto? Iscriviti a MISPECIAL, la nostra newsletter, per ricevere altri interessanti contenuti.

Iscriviti a MISPECIAL
Contenuti simili
DIGITAL ENTERPRISE
Custom entity su Liferay 7.4 con Service Builder, sfruttando Elasticsearch e Opensearch
giu 23, 2025

Custom entity su Liferay 7.4 con Service Builder, sfruttando Elasticsearch e Opensearch per ottenere ricerche avanzate e performanti su dati personalizzati

DIGITAL ENTERPRISE
mar 09, 2022

La continuous integration è un metodo di sviluppo software in cui gli sviluppatori aggiungono regolarmente modifiche al codice in un repository centralizzato, con la creazione di build e i test eseguiti automaticamente con lo scopo di individuare e risolvere i bug con maggiore tempestività, migliorare la qualità del software e ridurre il tempo richiesto per convalidare e pubblicare nuovi aggiornamenti.