Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Panel
borderColor#0081C0
titleColor#0081C0

This topic applies to XperienCentral versions 10.12.0 and higher.

 


XperienCentral allows you to make fields of custom content item facets searchable via the Advanced Search UI. This topic explains how the mechanism works and what its restrictions and possibilities are. Note that the API is not specifically about creating custom facets, but rather about how to add custom fields to the index. The ability to turn a field into a facet is just one of the possible options.

...

The JCR Property annotation is supported as well, but because it is not as powerful and merely exists for backwards compatibility purposes, we recommend you do not explicitly use it to index data.

In

...

This Topic

Table of Contents
maxLevel2

 


...

Tutorial

The API is explained on the basis of a simple example. The following shows the custom media item definition: 


Code Block
themeEclipse
public interface RecipeVersion extends MediaItemArticleVersion {
 
    public String getName();
 
    public String getDescription();
 
    public Locale getLanguage();
 
    public Country getCountry();
 
    public List<Ingredient> getIngredients();
}

public interface Country {
 
    public String getName();
 
    public String getCapital();
 
    public String getMotto();
}

public interface Ingredient {

    public String getName();
}

 


The implementation will return the following:

...

Now we expand our interface.

 

Code Block
themeEclipse
@Indexable
public interface RecipeVersion extends MediaItemArticleVersion {

    @Field
    public String getName();

    @Field
    public String getDescription();
     public Locale getLanguage();
     public Country getCountry();
     public List<Ingredient> getIngredients();
}

 

When indexing a recipe version with above definition, the method getName() will be invoked and its result will be indexed. The name of the recipe will be stored and indexed, but we cannot search on it yet. Furthermore, since it is some sort of title, we want its value to weigh more during a search. getDescription() will also be stored and indexed, but it is unlikely the description should be stored; only being able to search on its value should be enough. We can achieve this using the following parameters (the value after '=' is the default value):

...

What we actually want is: 

 

Code Block
themeEclipse
@Indexable
public interface RecipeVersion extends MediaItemArticleVersion {

    @Field(heading = true, boost = 1)
    public String getName();
    
    @Field(stored = false, body = true)

    public String getDescription();
 
    public Locale getLanguage();
 
    public Country getCountry(); 
     public List<Ingredient> getIngredients();
}

 

In this case, the name will be stored and copied to the heading and a match on the name will cause the document to appear higher up in the Advanced Search. The description will not get its own field but will instead be added to the body.

...

The adapter interface looks like this:

 

Code Block
themeEclipse
/**
 * Class that takes specific values and transforms them into (possibly language specific) other values.
 *
 * @see Field
 */
public interface FieldAdapter<T> {

    /**
     * Whether the adapted value can depend on the language.
     *
     * @return true or false
     */
    boolean isLanguageSpecific();

    /**
     * Adapts a T to return the value in the specified language. The specified language may be null,
     * in which case no specific language may be used. An implementation may return null, which is
     * equivalent to "do not index this field for this language". The returned value is the one that
     * is stored and is thus what is returned and displayed in Advanced Search. If this adapter is
     * language specific, its adapt methods will be called for each language and for a null language
     * to index the value in a language agnostic way (if possible). If it is not language specific,
     * only the latter is the case. This method is only invoked if this adapter's field is stored.
     * The returned value should match one of the registered {@link FieldTypeDefinition SOLR types}.
     *
     * @param value the value to adapt
     * @param forLocale the target locale to represent the value in
     * @return the adapted value, or null if no value should be stored
     * @see FieldAdapter#adapt(Object, Locale)
     * @see Field#stored()
     */
    Object adapt(T value, Locale forLocale);
}

 

so, the implementation for a locale looks like this:

 

Code Block
themeEclipse
public class LocaleFieldAdapter implements FieldAdapter<Locale> {

    @Override
    public boolean isLanguageSpecific() {
        return true;
    }

    @Override
    public Object adapt(Locale value, Locale forLocale) {
        if (value == null) { // If the language is missing, we can't do anything
            return null;
        }

        if (forLocale == null) { // If there is no target language, just return the language tag
            return value.getLanguage();
        }

        return value.getDisplayLanguage(forLocale); // Else, return the display language
    }
}

 

Now we update our recipe to index the language as well:

 

Code Block
themeEclipse
@Indexable
public interface RecipeVersion extends MediaItemArticleVersion {

    @Field(heading = true, boost = 1)
    public String getName();
    
    @Field(stored = false, body = true)
    public String getDescription();
    
    @Field(adapter = LocaleFieldAdapter.class)
    public Locale getLanguage();
    
    public Country getCountry();

    public List<Ingredient> getIngredients();
}

 

At this point, the following custom data would be indexed:

...

Now that we have indexed a few trivial properties, we want to index some more complex properties: Country and List<Ingredient>. We could index this using an adapter, just like we did for Locale, but since we have access to the Country and Ingredient classes, it is much nicer to make use of @Document and annotate the referred classes. This annotation tells the indexer to not index the value, but instead to treat the value as an object that we should parse and scan for annotations as well. This looks like this.

 

Code Block
themeEclipse
@Indexable
public interface RecipeVersion extends MediaItemArticleVersion {

    @Field(heading = true, boost = 1)
    public String getName();
    
    @Field(stored = false, body = true)
    public String getDescription();
    
    @Field(adapter = LocaleFieldAdapter.class)
    public Locale getLanguage();
    
    @Document
    public Country getCountry();

    @Document
    public List<Ingredient> getIngredients();
}

@Indexable // Don't forget to use this annotation here!
public interface Country {

    @ReferField
    public String getName();
    
    public String getCapital();
    
    public String getMotto();
}

@Indexable // Don't forget to use this annotation here!
public interface Ingredient {

    @ReferField(body = true)
    public String getName();
}

 

This is straight-forward; two things should be noted:

  1. getIngredients() returns a list, but you do not need to take care of this explicitly because XperienCentral iterates it into lists for you and considers all Ingredients in the list instead of the list itself.
  2. Instead of @Field we use @ReferField in the referred classes. We do this to indicate that this is a field that should be included if the document is included in an other document, rather than being a document in itself. This makes it possible to store a reference to a type rather than the complete type, which is redundant. Say we want to index countries as well, we would basically annotate country as follows:

    Code Block
    themeEclipse
    @Indexable
    public interface Country {
    
        @Field( ... )
        @ReferField
        public String getName();
        
        @Field( ... )
        public String getCapital();
        
        @Field( ... )
        public String getMotto();
    }


    Thanks to ReferField, only the name of the country will be included in other documents, and the name, capital, and motto will be included if the document is the root document (that is, if countries themselves are indexed). For the same reason we have a ReferDocument as well.

The document annotations have an adapter parameter as well so that you can change the returned value before it is scanned for annotations. A document adapter behaves in exactly the same way as a field adapter. You are allowed to return any object or primitive, but remember that only annotated fields in the resulting class will be indexed, so returning a string does not index the string value. Instead, it scans String.class for annotations (which it won't find) and in the end does not index anything. FieldReferFieldDocument, and ReferDocument may all be used at the same time.

...

So what we would do is create a language getter and add the parameter.

 

Code Block
themeEclipse
public class RecipeLanguageGetter implements LanguageGetter<RecipeVersion> {

    @Override
    public Locale getLanguage(RecipeVersion document) {
        return document.getLanguage();
    }
}

@Indexable(languageGetter = RecipeLanguageGetter.class)
public interface RecipeVersion extends MediaItemArticleVersion {
    ...
}

 

Now we will index the following:

...

Code Block
themeEclipse
/**
 * Contains information that can be used to uniquely identify a facet. The facet type is the class
 * its values have. The owners are the types the facet belongs to. The path consists of the derived
 * names of the methods annotated that eventually lead to the facet. <i>An example:</i> we annotated
 * {@link PageVersion#getWebsite()}, which leads to {@link Website#getTitle()}, which returns a
 * {@link String}. This gives us the following results: <code>type: String.class</code>, <code>
 * owner: PageVersion.class</code>, <code>path: ["website", "title"]</code>. The owning type is always
 * the type in which the annotation is present.
 */
public interface SearchFacetDescriptor {

    /**
     * Returns the owning type of the facet.
     *
     * @return the owner
     */
    public Class<?> getOwnerType();

    /**
     * Returns the facet values' type.
     *
     * @return the type of the values
     */
    public Class<?> getValuesType();

    /**
     * Returns the facet path.
     *
     * @return the method path to the facet
     */
    public List<String> getPath();
}

...


Our implementation of the SearchFacetComponentDefinition looks like this: 


Code Block
themeEclipse
public class RecipeFacetComponentDefinition extends ComponentDefinitionImpl implements SearchFacetComponentDefinition {

    private final List<String> myPath;
    private final Map<Locale, String> myTitles;
    private final int myPosition;

    public RecipeFacetComponentDefinition(List<String> path, Map<Locale, String> titles, int position) {
        super(false);

        myPath = path;
        myTitles = titles;
        myPosition = position;
    }

    @Override
    public String getWidgetId() {
        return "stringFacetWidget";
    }

    @Override
    public String getTitle(Locale forLocale) {
        return myTitles.get(forLocale);
    }

    @Override
    public int getPosition() {
        return myPosition;
    }

    @Override
    public Map<String, String> getFacetProperties() {
        return null;
    }

    @Override
    public int accept(SearchFacetDescriptor facet) {
        if (facet.getOwnerType().getName().equals(RecipeVersion.class.getName()) 
		  && Collections.indexOfSubList(facet.getPath(), myPath) != -1) {
            return 10;
        }

        return -1;
    }
}

 


And to actually create the facet definitions, we do the following:

 


Code Block
themeEclipse
public class Activator extends ComponentBundleActivatorBase {

    ...
    
    private ComponentDefinition[] getComponentDefinitions() {
        return new ComponentDefinition[] {
            createFacetComponentDefinition("recipelanguage", Arrays.asList("language"), titleMap("Recipe Language", "Recepttaal"), 1),
            createFacetComponentDefinition("recipecountry", Arrays.asList("country", "name"), titleMap("Country", "Keuken"), 2),
            createFacetComponentDefinition("recipeingredient", Arrays.asList("ingredients", "name"), titleMap("Ingredient", "Ingrediënt"), 3)
        }
    }
    
    private Map<Locale, String> titleMap(final String english, final String dutch) {
        return new HashMap<Locale, String>(2, 1f) {{
            put(Locale.forLanguageTag("en"), english);
            put(Locale.forLanguageTag("nl"), dutch);
        }};
    }
  
    private static RecipeFacetComponentDefinition createFacetComponentDefinition(String facet, List<String> path, Map<Locale, String> titles, int position) {
      RecipeFacetComponentDefinition definition = new RecipeFacetComponentDefinition(path, titles, position);
      definition.setId(WCBConstants.BUNDLE_ID + "-" + facet);
      definition.setName(WCBConstants.BUNDLE_NAME + "-" + facet);
      definition.setDescription(WCBConstants.BUNDLE_DESCRIPTION + " " + facet);
      definition.setTypeId(SearchFacetComponentType.class.getName());
      definition.setProperties(new Hashtable<String, String>());
      definition.setInterfaceClassNames(new String[]{SearchFacetComponent.class.getName()});
      definition.setImplementationClassName(SimpleSearchFacetComponent.class.getName());
      definition.setDependencies(new ComponentDependency[]{});
      definition.setWrapperClassNames(new String[]{});

      return definition;
  }
}

 


Now we're finished. This is what we see after opening the Advanced Search after our recipe is indexed (we titled it "My recipe"). You will find that if you open the search dialog in Dutch, the facet titles will be the ones we added via the facet definition. Furthermore, "Recepttaal" will have the option "Engels" instead of "English".

 

Image Removed

 

The  The complete source and deployable jar can be found in the attachments.

...

  • One parameter not described is the extension parameter:
    • extension=null - By default, an annotated class is, except for the referred classes, the only class that is scanned for annotations. In most cases this is fine, but in some cases it is not sufficient. For example, if one needs to invoke an external service that is not accessible from within the object or should not be a public/API method. To address, it is possible to define an extension of a class; a class with the annotated class as constructor parameter. This extension is scanned in addition to the class and comes therefore with more flexibility. It is possible to define an extension per type in the hierarchy level.
      • Extensions are treated like any other class, so if an extension is not annotated with @Indexable, it is not scanned.
      • The owner of a field originating from an extension is not the extension class, but rather the class requiring the extension.
      • An extension must be a concrete class with a single-argument constructor of the type the extension is for. For example: public RecipeVersionExtension(RecipeVersion recipe).
  • It is possible to index result sets from a query as well. To do so, you should create a custom media item that invokes the SQL command that returns the ResultSet you want to index. Then, you should create a Map based on this result set and return this - the returned values will then be indexed with their key as part of the field name. You can use an extension for this. For example:

    Code Block
    themeEclipse
    @Field(body = true)
    public Map<String, String> getFields() {
        Map<String, String> fields = new HashMap<>();
    
        DataSource dataSource = ...;
        String selectQuery = ...;
    
        try(Connection conn = dataSource.getConnection();
            PreparedStatement stmt = conn.prepareStatement(selectQuery)) {
            ResultSet rs = stmt.executeQuery();
                
            for (int i = 0; rs.next(); ++i) {
                fields.put(rs.getMetaData().getColumnName(i), rs.getString(i));
            }
        } catch (SQLException e) {
            LOG.log(Level.WARNING, "Could not (fully) index resultset", e);
        }
    
        return fields;
    }



  • Both interfaces and classes (including implementations) can be indexed, so implementation-specific data is allowed as well.
  • P(ackage-p)rivate/protected methods can be indexed but will be made accessible.
  • Annotated methods may not have parameters and may not return void.
  • If a class that is not exported through the Export-Package directive in the pom.xml adds a facet, this facet's SearchFacetDescriptor will have owner void.class.

 


Back to top

 


...

Overview

Packages

  • nl.gx.webmanager.services.contentindex.annotation
  • nl.gx.webmanager.services.contentindex.adapter

...

  • This annotation is inherited and may be overridden.
  • Use @Document to annotate methods that should be taken into account of if the owner class itself is indexed, and @ReferDocument for when the declaring class will be part of the indexed document.
  • They have the following parameters:
    • inheritLanguage=false - If this is set to true, the language of the target document will be set/overridden to the language of the current document. This may either be the language explicitly defined for the object declaring the annotation or the language inherited from a parent document. This setting is useful if referred documents should be indexed with the same language as the root document. For example, elements on pages.
    • adapter=null - A DocumentAdapter that changes the object whose class will be scanned for indexable properties.
      • An adapter may be
        • a concrete class, in which case it should have a zero-arguments constructor;
        • an interface of which an implementation is exposed via OSGi (whiteboard pattern).
      • An adapter may decide to adapt a value to nothing (that is, return null), causing no value to be scanned. This is also what happens if an adapt method terminates exceptionally, although in this case a warning is logged as well.
      • If an annotated method returns null, this value is given to the adapter anyway in order to give it the chance to make something out of it.

...

  • If your facet does not appear in the Advanced Search, ensure that:
    • you have created one of your annotated content items;
    • the value you expect to see is actually used and does not return null;
    • the content item is indexed;
    • your path is fully lowercased.
  • If a class that is not exported through the Export-Package directive in the pom.xml adds a facet, this facet's SearchFacetDescriptor will have owner void.class.
  • Many things are logged at log level FINE. This should give you detailed information about what values are indexed for a specific method, and why.

 


Back to top

 

...


...

Search Facet Group Methods


Panel
borderColor#0081C0
titleColor#0081C0

The following applies to XperienCentral versions R41.0 and higher.


When a SearchFacetComponentDefinition is created, a SearchFacetGroup can be provided as well. Providing a SearchFacetGroup will make sure that the custom facet is also added to the Configure Columns panel within the R41 list view in Advanced Search. In XperienCentral versions R41 and higher, the following methods can be used to add and remove search facet groups:


Code Block
themeEclipse
/**
 * Add a search facet group to an existing search facet group definition
 * @param searchFacetGroup the search facet group to add.
 */
void addSearchFacetGroup(SearchFacetGroup searchFacetGroup);

/**
 * Remove the SearchFacetGroup with the specified identifier from the set of search facets groups
 * @param identifier the identifier of the the search facet group that needs to be removed.
 */
void removeSearchFacetGroup(String identifier); 


Back to top