All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.response.transform.SubQueryAugmenterFactory Maven / Gradle / Ivy

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.solr.response.transform;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TotalHits;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.ResultContext;
import org.apache.solr.search.DocList;
import org.apache.solr.search.DocSlice;
import org.apache.solr.search.JoinQParserPlugin;
import org.apache.solr.search.ReturnFields;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.search.SolrReturnFields;
import org.apache.solr.search.TermsQParserPlugin;

/**
 *
 * This transformer executes subquery per every result document. It must be given an unique name. 
 * There might be a few of them, eg fl=*,foo:[subquery],bar:[subquery]. 
 * Every [subquery] occurrence adds a field into a result document with the given name, 
 * the value of this field is a document list, which is a result of executing subquery using 
 * document fields as an input.
 * 
 * 

Subquery Parameters Shift

* if subquery is declared as fl=*,foo:[subquery], subquery parameters * are prefixed with the given name and period. eg
* q=*:*&fl=*,foo:[subquery]&foo.q=to be continued&foo.rows=10&foo.sort=id desc * *

Document Field As An Input For Subquery Parameters

* * It's necessary to pass some document field value as a parameter for subquery. It's supported via * implicit row.fieldname parameters, and can be (but might not only) referred via * Local Parameters syntax.
* q=name:john&fl=name,id,depts:[subquery]&depts.q={!terms f=id v=$row.dept_id}&depts.rows=10 * Here departments are retrieved per every employee in search result. We can say that it's like SQL * join ON emp.dept_id=dept.id
* Note, when document field has multiple values they are concatenated with comma by default, it can be changed by * foo:[subquery separator=' '] local parameter, this mimics {@link TermsQParserPlugin} to work smoothly with. * *

Cores And Collections In SolrCloud

* use foo:[subquery fromIndex=departments] invoke subquery on another core on the same node, it's like * {@link JoinQParserPlugin} for non SolrCloud mode. But for SolrCloud just (and only) explicitly specify * its' native parameters like collection, shards for subquery, eg
* q=*:*&fl=*,foo:[subquery]&foo.q=cloud&foo.collection=departments * *

When used in Real Time Get

*

* When used in the context of a Real Time Get, the values from each document that are used * in the subquery are the "real time" values (possibly from the transaction log), but the query * itself is still executed against the currently open searcher. Note that this means if a * document is updated but not yet committed, an RTG request for that document that uses * [subquery] could include the older (committed) version of that document, * with different field values, in the subquery results. *

*/ public class SubQueryAugmenterFactory extends TransformerFactory{ @Override public DocTransformer create(String field, SolrParams params, SolrQueryRequest req) { if (field.contains("[") || field.contains("]")) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "please give an explicit name for [subquery] column ie fl=relation:[subquery ..]"); } checkThereIsNoDupe(field, req.getContext()); String fromIndex = params.get("fromIndex"); final SolrClient solrClient; solrClient = new EmbeddedSolrServer(req.getCore()); SolrParams subParams = retainAndShiftPrefix(req.getParams(), field+"."); return new SubQueryAugmenter(solrClient, fromIndex, field, field, subParams, params.get(TermsQParserPlugin.SEPARATOR, ",")); } @SuppressWarnings("unchecked") private void checkThereIsNoDupe(String field, Map context) { // find a map @SuppressWarnings({"rawtypes"}) final Map conflictMap; final String conflictMapKey = getClass().getSimpleName(); if (context.containsKey(conflictMapKey)) { conflictMap = (Map) context.get(conflictMapKey); } else { conflictMap = new HashMap<>(); context.put(conflictMapKey, conflictMap); } // check entry absence if (conflictMap.containsKey(field)) { throw new SolrException(ErrorCode.BAD_REQUEST, "[subquery] name "+field+" is duplicated"); } else { conflictMap.put(field, true); } } private SolrParams retainAndShiftPrefix(SolrParams params, String subPrefix) { ModifiableSolrParams out = new ModifiableSolrParams(); Iterator baseKeyIt = params.getParameterNamesIterator(); while (baseKeyIt.hasNext()) { String key = baseKeyIt.next(); if (key.startsWith(subPrefix)) { out.set(key.substring(subPrefix.length()), params.getParams(key)); } } return out; } } class SubQueryAugmenter extends DocTransformer { private static final class Result extends ResultContext { private final SolrDocumentList docList; final SolrReturnFields justWantAllFields = new SolrReturnFields(); private Result(SolrDocumentList docList) { this.docList = docList; } @Override public ReturnFields getReturnFields() { return justWantAllFields; } @Override public Iterator getProcessedDocuments(){ return docList.iterator(); } @Override public boolean wantsScores() { return justWantAllFields.wantsScore(); } @Override public DocList getDocList() { return new DocSlice((int)docList.getStart(), docList.size(), new int[0], new float[docList.size()], (int) docList.getNumFound(), docList.getMaxScore() == null ? Float.NaN : docList.getMaxScore(), docList.getNumFoundExact() ? TotalHits.Relation.EQUAL_TO : TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO); } @Override public SolrIndexSearcher getSearcher() { return null; } @Override public SolrQueryRequest getRequest() { return null; } @Override public Query getQuery() { return null; } } /** project document values to prefixed parameters * multivalues are joined with a separator, it always return single value */ static final class DocRowParams extends SolrParams { final private SolrDocument doc; final private String prefixDotRowDot; final private String separator; public DocRowParams(SolrDocument doc, String prefix, String separator ) { this.doc = doc; this.prefixDotRowDot = "row.";//prefix+ ".row."; this.separator = separator; } @Override public String[] getParams(String param) { final Collection vals = mapToDocField(param); if (vals != null) { StringBuilder rez = new StringBuilder(); for (@SuppressWarnings({"rawtypes"})Iterator iterator = vals.iterator(); iterator.hasNext();) { Object object = iterator.next(); rez.append(convertFieldValue(object)); if (iterator.hasNext()) { rez.append(separator); } } return new String[]{rez.toString()}; } return null; } @Override public String get(String param) { final String[] aVal = this.getParams(param); if (aVal != null) { assert aVal.length == 1 : "that's how getParams is written" ; return aVal[0]; } return null; } /** @return null if prefix doesn't match, field is absent or empty */ protected Collection mapToDocField(String param) { if (param.startsWith(prefixDotRowDot)) { final String docFieldName = param.substring(prefixDotRowDot.length()); final Collection vals = doc.getFieldValues(docFieldName); if (vals == null || vals.isEmpty()) { return null; } else { return vals; } } return null; } protected String convertFieldValue(Object val) { if (val instanceof IndexableField) { IndexableField f = (IndexableField)val; return f.stringValue(); } return val.toString(); } @Override public Iterator getParameterNamesIterator() { final Iterator fieldNames = doc.getFieldNames().iterator(); return new Iterator() { @Override public boolean hasNext() { return fieldNames.hasNext(); } @Override public String next() { final String fieldName = fieldNames.next(); return prefixDotRowDot + fieldName; } }; } } final private String name; final private SolrParams baseSubParams; final private String prefix; final private String separator; final private SolrClient server; final private String coreName; public SubQueryAugmenter(SolrClient server, String coreName, String name,String prefix, SolrParams baseSubParams, String separator) { this.name = name; this.prefix = prefix; this.baseSubParams = baseSubParams; this.separator = separator; this.server = server; this.coreName = coreName; } @Override public String getName() { return name; } /** * Returns false -- this transformer does use an IndexSearcher, but it does not (necessarily) need * the searcher from the ResultContext of the document being returned. Instead we use the current * "live" searcher for the specified core. */ @Override public boolean needsSolrIndexSearcher() { return false; } @Override public void transform(SolrDocument doc, int docid) { final SolrParams docWithDeprefixed = SolrParams.wrapDefaults( new DocRowParams(doc, prefix, separator), baseSubParams); try { QueryResponse rsp = server.query(coreName, docWithDeprefixed); SolrDocumentList docList = rsp.getResults(); doc.setField(getName(), new Result(docList)); } catch (Exception e) { String docString = doc.toString(); throw new SolrException(ErrorCode.BAD_REQUEST, "while invoking " + name + ":[subquery"+ (coreName!=null ? "fromIndex="+coreName : "") +"] on doc=" + docString.substring(0, Math.min(100, docString.length())), e.getCause()); } } }