How to search different itemtypes with one `SolrFacetSearchConfig`

Adding Category itemtype for indexing doesn’t require any specific actions, except adding corresponding changes to solr.impex files. But in case of two different itemtypes in one SolrFacetSearchConfig it is not enough just to change impexes you also need to adopt java source code.

Keep in mind that different SolrIndexType in one SolrFacetSearchConfig would end up with having two different solr cores on solr server, and you would need to adjust OOTB code to properly select, for which SolrIndexType (solr core) search is executed. This is useful, when it is required to reuse solr configurations SolrFacetSearchConfig between solr indexes without duplicating configurations.

For example, simple impex, which creates in one solr index two index types Product and Category with indexing fields - itemtype, code and name:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$productCatalog = blogProductCatalog
$indexCatalogVersion = Online
$catalogVersions = catalogVersions(catalog(id), version);
$serverConfigName = blogSolrServerConfig
$indexConfigName = blogSolrIndexConfig
$searchConfigName = blogPageSize
$productFacetSearchConfigName = blogIndex
$categoryFacetSearchConfigName = blogCategoryIndex
$facetSearchConfigDescription = blog Solr Index
$searchIndexNamePrefix = blog
$solrProductIndexedType = blogProductType
$solrCategoryIndexedType = blogCategoryType
$indexBaseSite = basesite
$indexLanguages = en
$indexCurrencies = GBP


INSERT_UPDATE SolrIndexedType; identifier[unique = true]; type(code); variant; sorts(&sortRefID)
                             ; $solrProductIndexedType  ; Product   ; false  ; sortRef1, sortRef2, sortRef3, sortRef4, sortRef5, sortRef6, sortRef7
                             ; $solrCategoryIndexedType ; Category  ; false  ; sortRefC6, sortRefC7, sortRefC8, sortRefC9

INSERT_UPDATE SolrFacetSearchConfig; name[unique = true]           ; description                   ; indexNamePrefix        ; languages(isocode); currencies(isocode); solrServerConfig(name); solrSearchConfig(description); solrIndexConfig(name); solrIndexedTypes(identifier)                      ; enabledLanguageFallbackMechanism; $catalogVersions
                                   ; $productFacetSearchConfigName ; $facetSearchConfigDescription ; $searchIndexNamePrefix ; $indexLanguages   ; $indexCurrencies   ; $serverConfigName     ; $searchConfigName            ; $indexConfigName     ; $solrProductIndexedType, $solrCategoryIndexedType ; true                            ; $productCatalog:$indexCatalogVersion

INSERT_UPDATE SolrIndexedProperty; solrIndexedType(identifier)[unique = true]; name[unique = true]; type(code); sortableType(code); currency[default = false]; localized[default = false]; multiValue[default = false]; fieldValueProvider; valueProviderParameter
                                 ; $solrProductIndexedType                   ; itemtype           ; string    ;                   ;                          ;                           ;                            ;
                                 ; $solrProductIndexedType                   ; code               ; string    ; int               ;                          ;                           ;                            ;
                                 ; $solrProductIndexedType                   ; name               ; text      ; sortabletext      ;                          ; true                      ;                            ;

INSERT_UPDATE SolrIndexedProperty; solrIndexedType(identifier)[unique = true]; name[unique = true]; type(code); sortableType(code); currency[default = false]; localized[default = false]; multiValue[default = false]; useForSpellchecking[default = false]; useForAutocomplete[default = false]; fieldValueProvider; valueProviderParameter
                                 ; $solrCategoryIndexedType                  ; itemtype           ; string    ;                   ;                          ;                           ;                            ;                                     ;                                    ;
                                 ; $solrCategoryIndexedType                  ; code               ; string    ;                   ;                          ;                           ;                            ; false                               ; false                              ;
                                 ; $solrCategoryIndexedType                  ; name               ; string    ;                   ;                          ; true                      ;                            ; false                               ; false                              ;

OOTB hybris can’t search into one index different itemtypes, for example Product and Category. Root of the problem is in getIndexedType method of SearchSolrQueryPopulator, which takes the first available index itemtype from solr search config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    protected IndexedType getIndexedType(final FacetSearchConfig config) {
    final IndexConfig indexConfig = config.getIndexConfig();

    // Strategy for working out which of the available indexed types to use
    final Collection<IndexedType> indexedTypes = indexConfig.getIndexedTypes().values();
    if (indexedTypes != null && !indexedTypes.isEmpty()) {
        // When there are multiple - select the first
        return indexedTypes.iterator().next();
    }

    // No indexed types
    return null;
}

To fix such behaviour must be extend SolrSearchQueryData with indexTypeIdentifier attribute and must be override populate method of SearchSolrQueryPopulator.

Extending data object:

1
2
3
4
5
6

<bean class="de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchQueryData">
    <!-- Property to define index type for type aware search. Default is used product. -->
    <property name="indexTypeIdentifier" type="String"/>

</bean>

To override populate method new BlogTypeAwareSearchSolrQueryPopulator class would be created and commerceSearchSolrQueryPopulator bean would be overriden with instance of new class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.blog.core.search.solrfacetsearch.populators;

import com.blog.core.search.solrfacetsearch.services.BlogIndexTypeService;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SearchQueryPageableData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchQueryData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchRequest;
import de.hybris.platform.commerceservices.search.solrfacetsearch.populators.SearchSolrQueryPopulator;
import de.hybris.platform.solrfacetsearch.config.FacetSearchConfig;
import de.hybris.platform.solrfacetsearch.config.IndexedType;
import de.hybris.platform.solrfacetsearch.search.SearchQuery;
import org.apache.commons.lang.StringUtils;

import javax.annotation.Resource;

import static com.blog.core.constants.BlogCoreConstants.PRODUCT_INDEX_TYPE_QUALIFIER;

public class BlogTypeAwareSearchSolrQueryPopulator<INDEXED_PROPERTY_TYPE, INDEXED_TYPE_SORT_TYPE> extends SearchSolrQueryPopulator<INDEXED_PROPERTY_TYPE, INDEXED_TYPE_SORT_TYPE> {

    @Resource
    private BlogIndexTypeService blogIndexTypeService;

    @Override
    public void populate(
            final SearchQueryPageableData<SolrSearchQueryData> source,
            final SolrSearchRequest<FacetSearchConfig, IndexedType, INDEXED_PROPERTY_TYPE, SearchQuery, INDEXED_TYPE_SORT_TYPE> target) {
        super.populate(source, target);

        setTypeAwareIndexType(source, target);
    }

    private void setTypeAwareIndexType(SearchQueryPageableData<SolrSearchQueryData> source, SolrSearchRequest<FacetSearchConfig, IndexedType, INDEXED_PROPERTY_TYPE, SearchQuery, INDEXED_TYPE_SORT_TYPE> target) {
        String indexTypeQualifier = PRODUCT_INDEX_TYPE_QUALIFIER;

        // Set index type qualifier from passed data
        if (StringUtils.isNotBlank(source.getSearchQueryData().getIndexTypeIdentifier())) {
            indexTypeQualifier = source.getSearchQueryData().getIndexTypeIdentifier();
        }

        // Reset index type
        if (target.getIndexedType() != null && !isProperIndexType(target, indexTypeQualifier)) {
            IndexedType properIndexType = blogIndexTypeService.getIndexedType(target.getFacetSearchConfig(), indexTypeQualifier);
            if (properIndexType != null) {
                // We can only search one core so select the indexed type
                target.setIndexedType(properIndexType);

                // Create the solr search query for the config and type (this sets-up the default page size and sort order)
                final SearchQuery searchQuery = createSearchQuery(target.getFacetSearchConfig(), target.getIndexedType(), source.getSearchQueryData().getSearchQueryContext(), source
                        .getSearchQueryData().getFreeTextSearch());
                searchQuery.setCatalogVersions(target.getCatalogVersions());
                searchQuery.setCurrency(getCommonI18NService().getCurrentCurrency().getIsocode());
                searchQuery.setLanguage(getCommonI18NService().getCurrentLanguage().getIsocode());

                target.setSearchQuery(searchQuery);
            }
        }
    }

    private boolean isProperIndexType(SolrSearchRequest<FacetSearchConfig, IndexedType, INDEXED_PROPERTY_TYPE, SearchQuery, INDEXED_TYPE_SORT_TYPE> target, String indexTypeQualifier) {
        return target.getIndexedType().getIdentifier().toLowerCase().contains(indexTypeQualifier);
    }

}
1
2
3
4
5

<alias name="blogTypeAwareSearchSolrQueryPopulator" alias="commerceSearchSolrQueryPopulator"/>
<bean id="blogTypeAwareSearchSolrQueryPopulator"
      class="com.blog.core.search.solrfacetsearch.populators.BlogTypeAwareSearchSolrQueryPopulator"
      parent="defaultCommerceSearchSolrQueryPopulator"/>

Methods for resolving current index would be put in separate BlogIndexTypeService class, which would be reused for facades and services.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.blog.core.search.solrfacetsearch.services;

import de.hybris.platform.solrfacetsearch.config.FacetSearchConfig;
import de.hybris.platform.solrfacetsearch.config.IndexConfig;
import de.hybris.platform.solrfacetsearch.config.IndexedType;
import de.hybris.platform.solrfacetsearch.model.config.SolrFacetSearchConfigModel;
import de.hybris.platform.solrfacetsearch.model.config.SolrIndexedTypeModel;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.List;

import static com.blog.core.constants.BlogCoreConstants.PRODUCT_INDEX_TYPE_QUALIFIER;

public class BlogIndexTypeService {

    // FOR JALO LAYER
    @Nullable
    public IndexedType getDefaultIndexedType(FacetSearchConfig config) {
        return getIndexedType(config, PRODUCT_INDEX_TYPE_QUALIFIER);
    }


    @Nullable
    public IndexedType getIndexedType(FacetSearchConfig config, String indexTypeIdentifier) {
        final IndexConfig indexConfig = config.getIndexConfig();

        // Strategy for working out which of the available indexed types to use
        final Collection<IndexedType> indexedTypes = indexConfig.getIndexedTypes().values();
        for (IndexedType indexedType : indexedTypes) {
            if (isProperIndexType(indexedType.getIdentifier(), indexTypeIdentifier)) {
                return indexedType;
            }
        }

        // No indexed types
        return null;
    }


    // FOR SERVICE LAYER
    @Nullable
    public SolrIndexedTypeModel getDefaultIndexedType(SolrFacetSearchConfigModel config) {
        return getIndexedType(config, PRODUCT_INDEX_TYPE_QUALIFIER);
    }


    @Nullable
    public SolrIndexedTypeModel getIndexedType(SolrFacetSearchConfigModel config, String indexTypeIdentifier) {

        List<SolrIndexedTypeModel> indexedTypes = config.getSolrIndexedTypes();

        // Strategy for working out which of the available indexed types to use
        for (SolrIndexedTypeModel indexedType : indexedTypes) {
            if (isProperIndexType(indexedType.getIdentifier(), indexTypeIdentifier)) {
                return indexedType;
            }
        }


        // No indexed types
        return null;
    }

    // Check method
    private boolean isProperIndexType(String indexIdentifier, String properIndexType) {
        return indexIdentifier.toLowerCase().contains(properIndexType);

    }
}

OOTB hybris provides default mechanizm to search only Product itemtypes. Such possibility is provided by DefaultSolrProductSearchService and DefaultSolrProductSearchFacade. After implementing of type aware solr index search, could be implemented services and facades for solr searhc of Category itemtype.

For basic usage it will be enough to implement textSearch and searchAgain methods:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.blog.core.search.solrfacetsearch.solrcategorysearch;

import de.hybris.platform.commerceservices.search.facetdata.CategorySearchPageData;
import de.hybris.platform.commerceservices.search.pagedata.PageableData;


public interface BlogSolrCategorySearchService<STATE, ITEM, RESULT extends CategorySearchPageData<STATE, ITEM>> {

    RESULT textSearch(String text, PageableData pageableData);

    RESULT searchAgain(STATE searchQueryData, PageableData pageableData);
}

Implementation of this methods are similar to DefaultSolrProductSearchService the difference is in explicit set of Category index type into SolrSearchQueryData:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.blog.core.search.solrfacetsearch.solrcategorysearch.impl;

import com.blog.core.search.solrfacetsearch.solrcategorysearch.BlogSolrCategorySearchService;
import de.hybris.platform.commerceservices.search.facetdata.CategorySearchPageData;
import de.hybris.platform.commerceservices.search.pagedata.PageableData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SearchQueryPageableData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchQueryData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchQueryTermData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchRequest;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchResponse;
import org.springframework.core.convert.converter.Converter;

import java.util.Collections;

import static com.blog.core.constants.BlogCoreConstants.CATEGORY_INDEX_TYPE_QUALIFIER;
import static de.hybris.platform.servicelayer.util.ServicesUtil.validateParameterNotNull;

/**
 * Created by clutcher on 16.01.17.
 * Solr category search service
 */
public class BlogSolrCategorySearchServiceImpl<ITEM> implements BlogSolrCategorySearchService<SolrSearchQueryData, ITEM, CategorySearchPageData<SolrSearchQueryData, ITEM>> {

    private Converter<SearchQueryPageableData<SolrSearchQueryData>, SolrSearchRequest> searchQueryPageableConverter;
    private Converter<SolrSearchRequest, SolrSearchResponse> searchRequestConverter;
    private Converter<SolrSearchResponse, CategorySearchPageData<SolrSearchQueryData, ITEM>> searchResponseConverter;

    @Override
    public CategorySearchPageData<SolrSearchQueryData, ITEM> textSearch(String text, PageableData pageableData) {
        final SolrSearchQueryData searchQueryData = new SolrSearchQueryData();
        searchQueryData.setFreeTextSearch(text);
        searchQueryData.setFilterTerms(Collections.<SolrSearchQueryTermData>emptyList());

        return doSearch(searchQueryData, pageableData);
    }

    @Override
    public CategorySearchPageData<SolrSearchQueryData, ITEM> searchAgain(SolrSearchQueryData searchQueryData, PageableData pageableData) {
        return doSearch(searchQueryData, pageableData);
    }


    private CategorySearchPageData<SolrSearchQueryData, ITEM> doSearch(
            final SolrSearchQueryData searchQueryData, final PageableData pageableData) {
        validateParameterNotNull(searchQueryData, "SearchQueryData cannot be null");

        searchQueryData.setIndexTypeIdentifier(CATEGORY_INDEX_TYPE_QUALIFIER);
        // Create the SearchQueryPageableData that contains our parameters
        final SearchQueryPageableData<SolrSearchQueryData> searchQueryPageableData = buildSearchQueryPageableData(searchQueryData,
                pageableData);

        // Build up the search request
        final SolrSearchRequest solrSearchRequest = getSearchQueryPageableConverter().convert(searchQueryPageableData);

        // Execute the search
        final SolrSearchResponse solrSearchResponse = getSearchRequestConverter().convert(solrSearchRequest);

        // Convert the response
        return getSearchResponseConverter().convert(solrSearchResponse);
    }

    private SearchQueryPageableData<SolrSearchQueryData> buildSearchQueryPageableData(final SolrSearchQueryData searchQueryData,
                                                                                      final PageableData pageableData) {
        final SearchQueryPageableData<SolrSearchQueryData> searchQueryPageableData = new SearchQueryPageableData<>();
        searchQueryPageableData.setSearchQueryData(searchQueryData);
        searchQueryPageableData.setPageableData(pageableData);
        return searchQueryPageableData;
    }

    public Converter<SearchQueryPageableData<SolrSearchQueryData>, SolrSearchRequest> getSearchQueryPageableConverter() {
        return searchQueryPageableConverter;
    }

    public void setSearchQueryPageableConverter(Converter<SearchQueryPageableData<SolrSearchQueryData>, SolrSearchRequest> searchQueryPageableConverter) {
        this.searchQueryPageableConverter = searchQueryPageableConverter;
    }

    public Converter<SolrSearchRequest, SolrSearchResponse> getSearchRequestConverter() {
        return searchRequestConverter;
    }

    public void setSearchRequestConverter(Converter<SolrSearchRequest, SolrSearchResponse> searchRequestConverter) {
        this.searchRequestConverter = searchRequestConverter;
    }

    public Converter<SolrSearchResponse, CategorySearchPageData<SolrSearchQueryData, ITEM>> getSearchResponseConverter() {
        return searchResponseConverter;
    }

    public void setSearchResponseConverter(Converter<SolrSearchResponse, CategorySearchPageData<SolrSearchQueryData, ITEM>> searchResponseConverter) {
        this.searchResponseConverter = searchResponseConverter;
    }
}

And finally facades could be implemented and used on storefront:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

package com.blog.facades.search.solrcategorysearch;

import de.hybris.platform.commercefacades.product.data.CategoryData;
import de.hybris.platform.commercefacades.search.data.SearchStateData;
import de.hybris.platform.commerceservices.search.facetdata.CategorySearchPageData;
import de.hybris.platform.commerceservices.search.pagedata.PageableData;


public interface BlogSolrCategorySearchFacade<ITEM extends CategoryData> {

    CategorySearchPageData<SearchStateData, ITEM> textSearch(String text);

    CategorySearchPageData<SearchStateData, ITEM> textSearch(SearchStateData searchState, PageableData pageableData);

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.blog.facades.search.solrcategorysearch.impl;

import com.blog.core.search.solrfacetsearch.solrcategorysearch.BlogSolrCategorySearchService;
import com.blog.facades.search.solrcategorysearch.BlogSolrCategorySearchFacade;
import de.hybris.platform.commercefacades.product.data.CategoryData;
import de.hybris.platform.commercefacades.search.data.SearchQueryData;
import de.hybris.platform.commercefacades.search.data.SearchStateData;
import de.hybris.platform.commerceservices.search.facetdata.CategorySearchPageData;
import de.hybris.platform.commerceservices.search.pagedata.PageableData;
import de.hybris.platform.commerceservices.search.resultdata.SearchResultValueData;
import de.hybris.platform.commerceservices.search.solrfacetsearch.data.SolrSearchQueryData;
import de.hybris.platform.commerceservices.threadcontext.ThreadContextService;
import de.hybris.platform.servicelayer.dto.converter.Converter;
import org.springframework.util.Assert;

import javax.annotation.Resource;

public class BlogSolrCategorySearchFacadeImpl<ITEM extends CategoryData> implements BlogSolrCategorySearchFacade<ITEM> {

    @Resource
    private ThreadContextService threadContextService;

    @Resource
    private BlogSolrCategorySearchService<SolrSearchQueryData, SearchResultValueData, CategorySearchPageData<SolrSearchQueryData, SearchResultValueData>> categorySearchService;

    @Resource
    private Converter<CategorySearchPageData<SolrSearchQueryData, SearchResultValueData>, CategorySearchPageData<SearchStateData, ITEM>> categorySearchPageConverter;

    @Resource(name = "solrSearchQueryDecoder")
    private Converter<SearchQueryData, SolrSearchQueryData> searchQueryDecoder;


    @Override
    public CategorySearchPageData<SearchStateData, ITEM> textSearch(String text) {


        return threadContextService.executeInContext(
                new ThreadContextService.Executor<CategorySearchPageData<SearchStateData, ITEM>, ThreadContextService.Nothing>() {
                    @Override
                    public CategorySearchPageData<SearchStateData, ITEM> execute() {
                        return categorySearchPageConverter.convert(categorySearchService.textSearch(text, null));
                    }
                });
    }

    @Override
    public CategorySearchPageData<SearchStateData, ITEM> textSearch(SearchStateData searchState, PageableData pageableData) {
        Assert.notNull(searchState, "SearchStateData must not be null.");

        return threadContextService.executeInContext(
                new ThreadContextService.Executor<CategorySearchPageData<SearchStateData, ITEM>, ThreadContextService.Nothing>() {
                    @Override
                    public CategorySearchPageData<SearchStateData, ITEM> execute() {
                        return categorySearchPageConverter.convert(
                                categorySearchService.searchAgain(decodeState(searchState), pageableData));
                    }
                });
    }

    private SolrSearchQueryData decodeState(final SearchStateData searchState) {
        return searchQueryDecoder.convert(searchState.getQuery());
    }
}

Happy searching!

comments powered by Disqus