Index: main/plugin-web/src/org/ametys/web/frontoffice/AbstractSearchGenerator.java =================================================================== --- main/plugin-web/src/org/ametys/web/frontoffice/AbstractSearchGenerator.java (revision 34031) +++ main/plugin-web/src/org/ametys/web/frontoffice/AbstractSearchGenerator.java (working copy) @@ -93,7 +93,6 @@ /** * Abstract class for lucene search - * */ public abstract class AbstractSearchGenerator extends ServiceableGenerator implements Contextualizable, FieldNames { @@ -137,7 +136,18 @@ { _context = (org.apache.cocoon.environment.Context) context.get(org.apache.cocoon.Constants.CONTEXT_ENVIRONMENT_CONTEXT); } - + + /** + * Template methods to disable/enable the processing of the facets during the search. + * @return true to enable facets + */ + protected boolean useFacets() + { + // facets are disabled by default, to avoid unnecessary processing when + // facets are not used. + return false; + } + @Override public void generate() throws IOException, SAXException, ProcessingException { @@ -206,21 +216,22 @@ Searcher searcher = null; try { - BitSet queryBitSet = null; - - boolean submit = request.getParameter("submit-form") != null; searcher = getSearchIndex(request, siteNames, lang); + FacetData facetData = useFacets() ? new FacetData(searcher, currentSiteName, lang) : null; + + boolean submit = request.getParameter("submit-form") != null; + if (searcher != null && submit && isInputValid()) { - queryBitSet = search(request, searcher, pageIndex, start, offset); + search(request, searcher, facetData, pageIndex, start, offset); } else { - queryBitSet = search(request, searcher, pageIndex, start, offset, false); + search(request, searcher, facetData, pageIndex, start, offset, false); } - + // SAX the form criteria and values - saxFormParameters(request, start, offset, queryBitSet, currentSiteName, lang); + saxFormParameters(request, facetData, start, offset, currentSiteName, lang); } catch (IllegalArgumentException e) { @@ -249,37 +260,37 @@ * Search * @param request the request * @param searcher the lucene search index + * @param facetData * @param pageIndex the page index * @param start * @param offset - * @return the BitSet representing the search results. * @throws IllegalArgumentException * @throws ParseException * @throws SAXException * @throws IOException */ - protected BitSet search (Request request, Searcher searcher, int pageIndex, int start, int offset) throws IllegalArgumentException, ParseException, SAXException, IOException + protected void search(Request request, Searcher searcher, FacetData facetData, int pageIndex, int start, int offset) throws IllegalArgumentException, ParseException, SAXException, IOException { - return search(request, searcher, pageIndex, start, offset, true); + search(request, searcher, facetData, pageIndex, start, offset, true); } /** * Search * @param request the request * @param searcher the lucene search index + * @param facetData * @param pageIndex the page index * @param start * @param offset * @param saxResults false to not sax results - * @return the BitSet representing the search results. * @throws IllegalArgumentException * @throws ParseException * @throws SAXException * @throws IOException */ - protected BitSet search (Request request, Searcher searcher, int pageIndex, int start, int offset, boolean saxResults) throws IllegalArgumentException, ParseException, SAXException, IOException + protected void search(Request request, Searcher searcher, FacetData facetData, int pageIndex, int start, int offset, boolean saxResults) throws IllegalArgumentException, ParseException, SAXException, IOException { - Query query = getQuery(request); + Query query = getQuery(request, facetData); getLogger().info(query.toString()); List sortFields = new ArrayList(); @@ -306,12 +317,10 @@ XMLUtils.startElement(contentHandler, "hits", atts); saxHits(searcher, rs, start, offset); XMLUtils.endElement(contentHandler, "hits"); - + // SAX pagination saxPagination(rs.totalHits, start, offset); } - - return collectorWrapper.getBitSet(); } /** @@ -332,14 +341,15 @@ * Search * @param request the request * @param searcher the lucene search index + * @param facetData * @return the BitSet representing the search results. * @throws IllegalArgumentException * @throws ParseException * @throws IOException */ - protected BitSet getDocuments(Request request, Searcher searcher) throws IllegalArgumentException, ParseException, IOException + protected BitSet getDocuments(Request request, Searcher searcher, FacetData facetData) throws IllegalArgumentException, ParseException, IOException { - Query query = getQuery(request); + Query query = getQuery(request, facetData); getLogger().info(query.toString()); BitSetCollector collector = new BitSetCollector(); @@ -352,14 +362,14 @@ /** * SAX the search parameters from the request parameters * @param request The request + * @param facetData * @param start The start index * @param offset The number of results - * @param queryBitSet * @param siteName The current site name * @param lang The current language * @throws SAXException If an error occurs while SAXing */ - protected void saxFormParameters (Request request, int start, int offset, BitSet queryBitSet, String siteName, String lang) throws SAXException + protected void saxFormParameters (Request request, FacetData facetData, int start, int offset, String siteName, String lang) throws SAXException { Map criteria = getCriteria(request, siteName, lang); @@ -367,7 +377,7 @@ XMLUtils.startElement(contentHandler, "fields"); saxFormFields(request, siteName, lang); - saxCriteria(criteria, request, queryBitSet, siteName, lang, true); + saxCriteria(criteria, request, facetData, siteName, lang, true); XMLUtils.endElement(contentHandler, "fields"); boolean submit = request.getParameter("submit-form") != null; @@ -401,36 +411,26 @@ * SAX criteria. * @param criteria * @param request - * @param queryBitSet + * @param facetData * @param siteName * @param lang * @param withHitCount * @throws SAXException */ - protected void saxCriteria(Map criteria, Request request, BitSet queryBitSet, String siteName, String lang, boolean withHitCount) throws SAXException + protected void saxCriteria(Map criteria, Request request, FacetData facetData, String siteName, String lang, boolean withHitCount) throws SAXException { try { - Map criteriaValues = getCriteriaValues(criteria, request); - BitSet criteriaValuesBitSet = getCriteriaValuesBitSet(criteriaValues, siteName, lang); - - if (queryBitSet != null) + if (facetData != null) { - if (criteriaValuesBitSet == null) - { - criteriaValuesBitSet = queryBitSet; - } - else - { - criteriaValuesBitSet.and(queryBitSet); - } + Map> criteriaValues = getCriteriaValues(criteria, request); + facetData.setCriteriaValues(criteriaValues); } for (String criterionName : criteria.keySet()) { Criterion criterion = criteria.get(criterionName); - - saxCriterion(criterion, siteName, lang, criteriaValuesBitSet); + saxCriterion(criterion, facetData, siteName, lang); } } catch (IOException e) @@ -444,19 +444,19 @@ /** * SAX a single criterion. * @param criterion the criterion. + * @param facetData * @param siteName the site name. * @param lang the language. - * @param criteriaValuesBitSet the result BitSet of the currently selected criteria values. * @throws SAXException * @throws IOException */ - protected void saxCriterion(Criterion criterion, String siteName, String lang, BitSet criteriaValuesBitSet) throws SAXException, IOException + protected void saxCriterion(Criterion criterion, FacetData facetData, String siteName, String lang) throws SAXException, IOException { AttributesImpl criterionAttrs = new AttributesImpl(); - if (criteriaValuesBitSet != null) + Integer count = facetData != null ? facetData.getCount(criterion.getLuceneTermName()) : null; + if (count != null) { - int criterionCount = criteriaValuesBitSet.cardinality(); - criterionAttrs.addCDATAAttribute("count", Integer.toString(criterionCount)); + criterionAttrs.addCDATAAttribute("count", Integer.toString(count)); } XMLUtils.startElement(contentHandler, criterion.getXmlGroupName(), criterionAttrs); @@ -464,12 +464,15 @@ for (String value : criterion.getValues().keySet()) { I18nizableText label = criterion.getValues().get(value); - - int count = getCount(criteriaValuesBitSet, criterion, value, siteName, lang); - + AttributesImpl valueAttrs = new AttributesImpl(); valueAttrs.addCDATAAttribute("value", value); - valueAttrs.addCDATAAttribute("count", Integer.toString(count)); + + count = facetData != null ? facetData.getCount(criterion.getLuceneTermName(), value) : null; + if (count != null) + { + valueAttrs.addCDATAAttribute("count", Integer.toString(count)); + } XMLUtils.startElement(contentHandler, criterion.getXmlElementName(), valueAttrs); label.toSAX(contentHandler); @@ -491,11 +494,13 @@ /** * Get the query from request parameters * @param request The request + * @param facetData * @return The query * @throws ParseException If an error occurs * @throws IllegalArgumentException If the search field is invalid + * @throws IOException */ - protected abstract Query getQuery (Request request) throws ParseException, IllegalArgumentException; + protected abstract Query getQuery (Request request, FacetData facetData) throws ParseException, IllegalArgumentException, IOException; /** * SAX the result hits @@ -529,77 +534,37 @@ * @param request the request object. * @return the criteria user values. */ - protected Map getCriteriaValues(Map criteria, Request request) + protected Map> getCriteriaValues(Map criteria, Request request) { - Map values = new HashMap(); - + Map> values = new HashMap>(); + for (String name : criteria.keySet()) { Criterion criterion = criteria.get(name); - String value = request.getParameter(criterion.getFieldName()); - - if (StringUtils.isNotEmpty(value)) + String[] criterionValues = request.getParameterValues(criterion.getFieldName()); + + if (ArrayUtils.isNotEmpty(criterionValues)) { - values.put(criterion.getLuceneTermName(), value); + for (String criterionValue : criterionValues) + { + if (StringUtils.isNotEmpty(criterionValue)) + { + List criterionValueList = values.get(criterion.getLuceneTermName()); + if (criterionValueList == null) + { + criterionValueList = new ArrayList(); + values.put(criterion.getLuceneTermName(), criterionValueList); + } + + criterionValueList.add(criterionValue); + } + } } } return values; } - - /** - * Get a BitSet corresponding to the documents returned by the currently selected criteria values. - * @param criteriaValues the currently selected criteria values. - * @param siteName the site name. - * @param lang the language. - * @return a BitSet corresponding to the currently selected documents. - * @throws IOException - */ - protected BitSet getCriteriaValuesBitSet(Map criteriaValues, String siteName, String lang) throws IOException - { - BitSet bitSet = null; - - for (String name : criteriaValues.keySet()) - { - String value = criteriaValues.get(name); - BitSet currentBitSet = _indexTermCache.getBitSet(siteName, lang, new Term(name, value)); - - if (bitSet == null) - { - bitSet = currentBitSet; - } - else - { - bitSet.and(currentBitSet); - } - } - - return bitSet; - } - - /** - * Get the result count for a criterion value, given the currently selected values. - * @param valuesBitSet a BitSet corresponding to the currently selected values. - * @param criterion the criterion being evaluated. - * @param currentValue the criterion value. - * @param siteName the site name. - * @param lang the language. - * @return the result count. - * @throws IOException - */ - protected int getCount(BitSet valuesBitSet, Criterion criterion, String currentValue, String siteName, String lang) throws IOException - { - Term term = new Term(criterion.getLuceneTermName(), currentValue); - BitSet bitset = _indexTermCache.getBitSet(siteName, lang, term); - - if (valuesBitSet != null) - { - bitset.and(valuesBitSet); - } - - return bitset.cardinality(); - } - + /** * SAX a hit of type {@link FieldNames}.TYPE_PAGE * @param rs The search result @@ -862,7 +827,7 @@ * @param siteNames The list of sites to search in. Empty to search on all sites * @param lang The current language * @return The index searcher - * @throws IOException If an error occurred while opening indexes + * @throws IOException If an error occurs while opening indexes */ protected Searcher getSearchIndex (Request request, List siteNames, String lang) throws IOException { @@ -1407,4 +1372,241 @@ this._values = values; } } + + /** + * Facet data + * Receives and holds the necessary data needed to calculate the facets for each criteria. + * {@link #setBaseQuery(Query)} and {@link #setCriteriaValues(Map)} should be properly called in order to setup the facet data. + * After this, the facet could be retrieved by using the methods {@link #getCount(String)} and {@link #getCount(String, String)} + */ + protected class FacetData + { + /** The lucene search used for the current search */ + private Searcher _searcher; + /** The current site name */ + private String _siteName; + /** The current lang */ + private String _lang; + + /** The bitset of the documents returned by the base query */ + private BitSet _baseQueryBitSet; + + /** The bitset of the documents returned by the full query (ie. base query + each enumerated criterion query) */ + private BitSet _fullQueryBitSet; + + /** The list of current values for each criterion in a map, where keys are the lucene term name of the criterion */ + private Map> _criterionValues; + + /** + * The bit sets corresponding to the documents returned by the current + * criterion values in a map. Keys are the lucene term name of the + * criterion + */ + private Map _criterionValuesBitSets; + + /** + * The final (computed) bit set for each criterion, which will be used + * to extract to criterion facet values. These bitsets are calculated by + * using the other bitsets described above + */ + private Map _computedCriterionBitSets; + + /** + * Facet data constructor + * @param searcher + * @param siteName + * @param lang + */ + public FacetData(Searcher searcher, String siteName, String lang) + { + _searcher = searcher; + _siteName = siteName; + _lang = lang; + } + + /** + * Receives the base query and extracts necessary data accordingly. + * @param baseQuery The base query is the full search query without the parts that involves the faceted criterion + * @throws IOException If an error occurs while opening indexes + */ + public void setBaseQuery(Query baseQuery) throws IOException + { + BitSetCollector bitSetCollector = new BitSetCollector(); + _searcher.search(baseQuery, null, bitSetCollector); + + _baseQueryBitSet = bitSetCollector.getBitSet(); + } + + /** + * Set the criterion values. Keys of the map must be the lucene term names of the criterion. + * This method must be called in order to get expected facet values. + * @param criteriaValues + */ + public void setCriteriaValues(Map> criteriaValues) + { + _criterionValues = criteriaValues; + } + + /** + * Get the facet value (count) for a criterion + * @param luceneTermName The lucene term name of the criterion + * @return The facet value or null if not applicable + * @throws IOException If an error occurs while opening indexes + */ + public Integer getCount(String luceneTermName) throws IOException + { + BitSet criterionBitSet = _getComputedCriterionBitSet(luceneTermName); + return criterionBitSet != null ? criterionBitSet.cardinality() : null; + } + + /** + * Get the facet value (count) for a criterion and a given value + * @param luceneTermName The lucene term name of the criterion + * @param value The criterion value for which the facet must be retrieved + * @return The facet value or null if not applicable + * @throws IOException If an error occurs while opening indexes + */ + public Integer getCount(String luceneTermName, String value) throws IOException + { + BitSet criterionBitSet = _getComputedCriterionBitSet(luceneTermName); + BitSet criterionValueBitSet = _indexTermCache.getBitSet(_siteName, _lang, new Term(luceneTermName, value)); + + if (criterionBitSet != null) + { + criterionValueBitSet.and(criterionBitSet); + } + + return criterionValueBitSet.cardinality(); + } + + /** + * Retrieves the computed bitset for each criterion. + * @param luceneTermName + * @return The computed bitset + * @throws IOException If an error occurs while opening indexes + */ + protected BitSet _getComputedCriterionBitSet(String luceneTermName) throws IOException + { + if (_computedCriterionBitSets == null) + { + _computedCriterionBitSets = new HashMap(); + } + + if (!_computedCriterionBitSets.containsKey(luceneTermName)) + { + _computeCriterionBitSet(luceneTermName); + } + + return _computedCriterionBitSets.get(luceneTermName); + } + + private void _computeCriterionBitSet(String luceneTermName) throws IOException + { + Map criterionValuesBitSets = _getCriterionValuesBitSets(); + BitSet criterionBitSet = null; + + if (criterionValuesBitSets.containsKey(luceneTermName)) + { + criterionBitSet = _baseQueryBitSet != null ? (BitSet) _baseQueryBitSet.clone() : null; + + for (String name : criterionValuesBitSets.keySet()) + { + if (!name.equals(luceneTermName)) + { + BitSet criterionValuesBitSet = criterionValuesBitSets.get(name); + if (criterionBitSet == null) + { + criterionBitSet = criterionValuesBitSet; + } + else + { + criterionBitSet.and(criterionValuesBitSet); + } + } + } + } + else + { + criterionBitSet = _getFullQueryBitSet(); + } + + _computedCriterionBitSets.put(luceneTermName, criterionBitSet); + } + + /** + * Retrieves the map of bitset for the current values of each criterion + * @return the criterion values bitsets + * @throws IOException If an error occurs while opening indexes + */ + protected Map _getCriterionValuesBitSets() throws IOException + { + if (_criterionValuesBitSets == null) + { + _computeCriterionValuesBitSets(); + } + + return _criterionValuesBitSets; + } + + private void _computeCriterionValuesBitSets() throws IOException + { + _criterionValuesBitSets = new HashMap(); + + for (String name : _criterionValues.keySet()) + { + List values = _criterionValues.get(name); + BitSet criterionValuesBitSet = null; + + for (String value : values) + { + BitSet valueBitSet = _indexTermCache.getBitSet(_siteName, _lang, new Term(name, value)); + if (criterionValuesBitSet == null) + { + criterionValuesBitSet = valueBitSet; + } + else + { + criterionValuesBitSet.or(valueBitSet); + } + } + + _criterionValuesBitSets.put(name, criterionValuesBitSet); + } + } + + /** + * Retrieves the bitset for the full query + * @return The full query bitset + * @throws IOException If an error occurs while opening indexes + */ + protected BitSet _getFullQueryBitSet() throws IOException + { + if (_fullQueryBitSet == null) + { + _computeFullQueryBitSet(); + } + + return _fullQueryBitSet; + } + + private void _computeFullQueryBitSet() throws IOException + { + _fullQueryBitSet = _baseQueryBitSet != null ? (BitSet) _baseQueryBitSet.clone() : null; + + Map criterionValuesBitSets = _getCriterionValuesBitSets(); + + for (String name : criterionValuesBitSets.keySet()) + { + BitSet criterionValuesBitSet = criterionValuesBitSets.get(name); + if (_fullQueryBitSet == null) + { + _fullQueryBitSet = criterionValuesBitSet; + } + else + { + _fullQueryBitSet.and(criterionValuesBitSet); + } + } + } + } } Index: main/plugin-web/src/org/ametys/web/frontoffice/SearchGenerator.java =================================================================== --- main/plugin-web/src/org/ametys/web/frontoffice/SearchGenerator.java (revision 34031) +++ main/plugin-web/src/org/ametys/web/frontoffice/SearchGenerator.java (working copy) @@ -82,7 +82,7 @@ protected static final String CONTENT_TYPE_CHOICE_NONE = "none"; @Override - protected BitSet search(Request request, Searcher searcher, int pageIndex, int start, int offset) throws IllegalArgumentException, ParseException, SAXException, IOException + protected void search(Request request, Searcher searcher, FacetData facetData, int pageIndex, int start, int offset) throws IllegalArgumentException, ParseException, SAXException, IOException { String searchCTypeType = parameters.getParameter("search-by-content-types-choice", CONTENT_TYPE_CHOICE_NONE); @@ -170,17 +170,15 @@ } } XMLUtils.endElement(contentHandler, "content-types"); - - return bitSet; } else { - return super.search(request, searcher, pageIndex, start, offset); + super.search(request, searcher, facetData, pageIndex, start, offset); } } @Override - protected Query getQuery (Request request) throws ParseException, IllegalArgumentException + protected Query getQuery (Request request, FacetData facetData) throws ParseException, IllegalArgumentException { BooleanQuery query = new BooleanQuery(); Index: main/plugin-web/src/org/ametys/web/frontoffice/SearchResourcesGenerator.java =================================================================== --- main/plugin-web/src/org/ametys/web/frontoffice/SearchResourcesGenerator.java (revision 34031) +++ main/plugin-web/src/org/ametys/web/frontoffice/SearchResourcesGenerator.java (working copy) @@ -69,7 +69,7 @@ protected static final String[] SPREADSHEET_MIMETYPE = new String [] {"vnd.oasis.opendocument.spreadsheet", "application/vnd.ms-excel"}; @Override - protected Query getQuery (Request request) throws ParseException, IllegalArgumentException + protected Query getQuery (Request request, FacetData facetData) throws ParseException, IllegalArgumentException { BooleanQuery query = new BooleanQuery();