/* * Copyright (c) 2011 LinkedIn, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.flaptor.indextank.blender; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import com.flaptor.indextank.index.DocId; import com.flaptor.indextank.index.QueryMatcher; import com.flaptor.indextank.index.ScoredMatch; import com.flaptor.indextank.index.TopMatches; import com.flaptor.indextank.index.results.SimpleScoredDocIds; import com.flaptor.indextank.index.scorer.FacetingManager; import com.flaptor.indextank.query.Query; import com.flaptor.util.CollectionsUtil; import com.flaptor.util.Execute; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Multiset; import com.google.common.collect.Sets; /** * A BlendingQueryMatcher is created based on two internal matchers. One of * them should be static and provide older results and the other one should * be fresher and its results will be considered true over the old ones. * <br><br> * This matcher will merge results from both searchers and will prioritize * the current results. Any result with changes in the newer matcher will * be ignored in the older matcher. * * @author Santiago Perez (santip) */ public class BlendingQueryMatcher implements QueryMatcher { private static final Logger logger = Logger.getLogger(Execute.whoAmI()); private final QueryMatcher historySearcher; private final QueryMatcher currentSearcher; /** * Constructor. */ public BlendingQueryMatcher(QueryMatcher history, QueryMatcher current) { this.historySearcher = history; this.currentSearcher = current; } @Override public TopMatches findMatches(Query query, Predicate<DocId> docFilter, int limit, int scoringFunctionIndex) throws InterruptedException { Predicate<DocId> historyFilter = notModified(currentSearcher, docFilter); /* instrumentation */ long historyStart = System.currentTimeMillis(); TopMatches history = historySearcher.findMatches(query, historyFilter, limit, scoringFunctionIndex); int historyMatches = history.getTotalMatches(); /* instrumentation */ long currentStart = System.currentTimeMillis(); TopMatches current = docFilter == null ? currentSearcher.findMatches(query, limit, scoringFunctionIndex) : currentSearcher.findMatches(query, docFilter, limit, scoringFunctionIndex); int currentMatches = current.getTotalMatches(); /* instrumentation */ long endSearch = System.currentTimeMillis(); List<ScoredMatch> currentResults = Lists.newArrayList(current); Iterable<ScoredMatch> historyResults = filterIds(history, getIds(currentResults)); List<ScoredMatch> results = mergeIntoList(currentResults, historyResults, limit); int matches = Math.abs(currentMatches) + Math.abs(historyMatches); if (results.size() < limit) { matches = results.size(); } if (currentMatches < 0 || historyMatches < 0) { matches = -matches; } /* instrumentation */ long end = System.currentTimeMillis(); logger.debug("(Search) historic searcher took: " + (currentStart - historyStart) + " ms., current searcher took: " + + (endSearch - currentStart) + " ms., merge took: " + (end - endSearch) + " ms."); Map<String, Multiset<String>> facets = FacetingManager.mergeFacets(history.getFacetingResults(), current.getFacetingResults()); return new SimpleScoredDocIds(results, limit, matches, facets); } public TopMatches findMatches(Query query, int limit, int scoringFunctionIndex) throws InterruptedException { return findMatches(query, null, limit, scoringFunctionIndex); } @Override public boolean hasChanges(DocId docid) throws InterruptedException { return historySearcher.hasChanges(docid) || currentSearcher.hasChanges(docid); } private static Set<DocId> getIds(List<ScoredMatch> currentResults) { return Sets.newHashSet(Iterables.transform(currentResults, ScoredMatch.DOC_ID_FUNCTION)); } private static List<ScoredMatch> mergeIntoList(Iterable<ScoredMatch> a, Iterable<ScoredMatch> b, int limit) { Iterable<ScoredMatch> merged = CollectionsUtil.mergeIterables(ImmutableList.of(a, b)); return Lists.newArrayList(merged); } private static Iterable<ScoredMatch> filterIds(Iterable<ScoredMatch> results, final Set<DocId> ids) { return Iterables.filter(results, new Predicate<ScoredMatch>() { @Override public boolean apply(ScoredMatch r) { return !ids.contains(r.getDocId()); } }); } private static Predicate<DocId> notModified(final QueryMatcher s, final Predicate<DocId> andFilter) { return new Predicate<DocId>() { @Override public boolean apply(DocId docid) { try { return !s.hasChanges(docid) && (andFilter == null || andFilter.apply(docid)) ; } catch (InterruptedException e) { return false; } } }; } @Override public int countMatches(Query query) throws InterruptedException { return countMatches(query, null); } @Override public int countMatches(Query query, Predicate<DocId> idFilter) throws InterruptedException { Predicate<DocId> historyFilter = notModified(currentSearcher, idFilter); int count = historySearcher.countMatches(query, historyFilter); if (idFilter == null) { count += currentSearcher.countMatches(query); } else { count += currentSearcher.countMatches(query, idFilter); } return count; } }