mirror of https://github.com/apache/lucene.git
SOLR-1149 -- Made QParserPlugin and related classes extendible as an experimental API
git-svn-id: https://svn.apache.org/repos/asf/lucene/solr/trunk@777656 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
7d80857e70
commit
b1447effbe
|
@ -467,6 +467,9 @@ Other Changes
|
|||
|
||||
32. Upgraded to Lucene 2.9-dev r776177 (shalin)
|
||||
|
||||
33. SOLR-1149: Made QParserPlugin and related classes extendible as an experimental API.
|
||||
(Kaktu Chakarabati via shalin)
|
||||
|
||||
Build
|
||||
----------------------
|
||||
1. SOLR-776: Added in ability to sign artifacts via Ant for releases (gsingers)
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
/**
|
||||
* 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.search;
|
||||
|
||||
import org.apache.lucene.queryParser.ParseException;
|
||||
import org.apache.lucene.queryParser.QueryParser;
|
||||
import org.apache.lucene.search.BooleanClause;
|
||||
import org.apache.lucene.search.BooleanQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.solr.common.SolrException;
|
||||
import org.apache.solr.common.params.DefaultSolrParams;
|
||||
import org.apache.solr.common.params.DisMaxParams;
|
||||
import org.apache.solr.common.params.SolrParams;
|
||||
import org.apache.solr.common.util.NamedList;
|
||||
import org.apache.solr.request.SolrQueryRequest;
|
||||
import org.apache.solr.schema.IndexSchema;
|
||||
import org.apache.solr.util.SolrPluginUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Query parser for dismax queries
|
||||
* <p/>
|
||||
* <b>Note: This API is experimental and may change in non backward-compatible ways in the future</b>
|
||||
*
|
||||
* @version $Id$
|
||||
*/
|
||||
public class DisMaxQParser extends QParser {
|
||||
|
||||
/**
|
||||
* A field we can't ever find in any schema, so we can safely tell DisjunctionMaxQueryParser to use it as our
|
||||
* defaultField, and map aliases from it to any field in our schema.
|
||||
*/
|
||||
private static String IMPOSSIBLE_FIELD_NAME = "\uFFFC\uFFFC\uFFFC";
|
||||
|
||||
|
||||
public DisMaxQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
|
||||
super(qstr, localParams, params, req);
|
||||
}
|
||||
|
||||
protected Map<String, Float> queryFields;
|
||||
protected Query parsedUserQuery;
|
||||
|
||||
|
||||
protected String[] boostParams;
|
||||
protected List<Query> boostQueries;
|
||||
protected Query altUserQuery;
|
||||
protected QParser altQParser;
|
||||
|
||||
|
||||
public Query parse() throws ParseException {
|
||||
SolrParams solrParams = localParams == null ? params : new DefaultSolrParams(localParams, params);
|
||||
|
||||
IndexSchema schema = req.getSchema();
|
||||
|
||||
queryFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.QF));
|
||||
Map<String, Float> phraseFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.PF));
|
||||
|
||||
float tiebreaker = solrParams.getFloat(DisMaxParams.TIE, 0.0f);
|
||||
|
||||
int pslop = solrParams.getInt(DisMaxParams.PS, 0);
|
||||
int qslop = solrParams.getInt(DisMaxParams.QS, 0);
|
||||
|
||||
/* a generic parser for parsing regular lucene queries */
|
||||
QueryParser p = schema.getSolrQueryParser(null);
|
||||
|
||||
/* a parser for dealing with user input, which will convert
|
||||
* things to DisjunctionMaxQueries
|
||||
*/
|
||||
SolrPluginUtils.DisjunctionMaxQueryParser up =
|
||||
new SolrPluginUtils.DisjunctionMaxQueryParser(schema, IMPOSSIBLE_FIELD_NAME);
|
||||
up.addAlias(IMPOSSIBLE_FIELD_NAME,
|
||||
tiebreaker, queryFields);
|
||||
up.setPhraseSlop(qslop);
|
||||
|
||||
/* for parsing sloppy phrases using DisjunctionMaxQueries */
|
||||
SolrPluginUtils.DisjunctionMaxQueryParser pp =
|
||||
new SolrPluginUtils.DisjunctionMaxQueryParser(schema, IMPOSSIBLE_FIELD_NAME);
|
||||
pp.addAlias(IMPOSSIBLE_FIELD_NAME,
|
||||
tiebreaker, phraseFields);
|
||||
pp.setPhraseSlop(pslop);
|
||||
|
||||
|
||||
/* the main query we will execute. we disable the coord because
|
||||
* this query is an artificial construct
|
||||
*/
|
||||
BooleanQuery query = new BooleanQuery(true);
|
||||
|
||||
/* * * Main User Query * * */
|
||||
parsedUserQuery = null;
|
||||
String userQuery = getString();
|
||||
altUserQuery = null;
|
||||
if (userQuery == null || userQuery.trim().length() < 1) {
|
||||
// If no query is specified, we may have an alternate
|
||||
String altQ = solrParams.get(DisMaxParams.ALTQ);
|
||||
if (altQ != null) {
|
||||
altQParser = subQuery(altQ, null);
|
||||
altUserQuery = altQParser.parse();
|
||||
query.add(altUserQuery, BooleanClause.Occur.MUST);
|
||||
} else {
|
||||
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "missing query string");
|
||||
}
|
||||
} else {
|
||||
// There is a valid query string
|
||||
userQuery = SolrPluginUtils.partialEscape(SolrPluginUtils.stripUnbalancedQuotes(userQuery)).toString();
|
||||
userQuery = SolrPluginUtils.stripIllegalOperators(userQuery).toString();
|
||||
|
||||
String minShouldMatch = solrParams.get(DisMaxParams.MM, "100%");
|
||||
Query dis = up.parse(userQuery);
|
||||
parsedUserQuery = dis;
|
||||
|
||||
if (dis instanceof BooleanQuery) {
|
||||
BooleanQuery t = new BooleanQuery();
|
||||
SolrPluginUtils.flattenBooleanQuery(t, (BooleanQuery) dis);
|
||||
SolrPluginUtils.setMinShouldMatch(t, minShouldMatch);
|
||||
parsedUserQuery = t;
|
||||
}
|
||||
query.add(parsedUserQuery, BooleanClause.Occur.MUST);
|
||||
|
||||
|
||||
/* * * Add on Phrases for the Query * * */
|
||||
|
||||
/* build up phrase boosting queries */
|
||||
|
||||
/* if the userQuery already has some quotes, strip them out.
|
||||
* we've already done the phrases they asked for in the main
|
||||
* part of the query, this is to boost docs that may not have
|
||||
* matched those phrases but do match looser phrases.
|
||||
*/
|
||||
String userPhraseQuery = userQuery.replace("\"", "");
|
||||
Query phrase = pp.parse("\"" + userPhraseQuery + "\"");
|
||||
if (null != phrase) {
|
||||
query.add(phrase, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* * * Boosting Query * * */
|
||||
boostParams = solrParams.getParams(DisMaxParams.BQ);
|
||||
//List<Query> boostQueries = SolrPluginUtils.parseQueryStrings(req, boostParams);
|
||||
boostQueries = null;
|
||||
if (boostParams != null && boostParams.length > 0) {
|
||||
boostQueries = new ArrayList<Query>();
|
||||
for (String qs : boostParams) {
|
||||
if (qs.trim().length() == 0) continue;
|
||||
Query q = subQuery(qs, null).parse();
|
||||
boostQueries.add(q);
|
||||
}
|
||||
}
|
||||
if (null != boostQueries) {
|
||||
if (1 == boostQueries.size() && 1 == boostParams.length) {
|
||||
/* legacy logic */
|
||||
Query f = boostQueries.get(0);
|
||||
if (1.0f == f.getBoost() && f instanceof BooleanQuery) {
|
||||
/* if the default boost was used, and we've got a BooleanQuery
|
||||
* extract the subqueries out and use them directly
|
||||
*/
|
||||
for (Object c : ((BooleanQuery) f).clauses()) {
|
||||
query.add((BooleanClause) c);
|
||||
}
|
||||
} else {
|
||||
query.add(f, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
} else {
|
||||
for (Query f : boostQueries) {
|
||||
query.add(f, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* * * Boosting Functions * * */
|
||||
|
||||
String[] boostFuncs = solrParams.getParams(DisMaxParams.BF);
|
||||
if (null != boostFuncs && 0 != boostFuncs.length) {
|
||||
for (String boostFunc : boostFuncs) {
|
||||
if (null == boostFunc || "".equals(boostFunc)) continue;
|
||||
Map<String, Float> ff = SolrPluginUtils.parseFieldBoosts(boostFunc);
|
||||
for (String f : ff.keySet()) {
|
||||
Query fq = subQuery(f, FunctionQParserPlugin.NAME).parse();
|
||||
Float b = ff.get(f);
|
||||
if (null != b) {
|
||||
fq.setBoost(b);
|
||||
}
|
||||
query.add(fq, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultHighlightFields() {
|
||||
return queryFields.keySet().toArray(new String[queryFields.keySet().size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query getHighlightQuery() throws ParseException {
|
||||
return parsedUserQuery;
|
||||
}
|
||||
|
||||
public void addDebugInfo(NamedList<Object> debugInfo) {
|
||||
super.addDebugInfo(debugInfo);
|
||||
debugInfo.add("altquerystring", altUserQuery);
|
||||
if (null != boostQueries) {
|
||||
debugInfo.add("boost_queries", boostParams);
|
||||
debugInfo.add("parsed_boost_queries",
|
||||
QueryParsing.toString(boostQueries, req.getSchema()));
|
||||
}
|
||||
debugInfo.add("boostfuncs", req.getParams().getParams(DisMaxParams.BF));
|
||||
}
|
||||
}
|
|
@ -16,22 +16,10 @@
|
|||
*/
|
||||
package org.apache.solr.search;
|
||||
|
||||
import org.apache.lucene.queryParser.ParseException;
|
||||
import org.apache.lucene.search.BooleanClause;
|
||||
import org.apache.lucene.search.BooleanQuery;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.solr.common.SolrException;
|
||||
import org.apache.solr.common.params.DefaultSolrParams;
|
||||
import org.apache.solr.common.params.DisMaxParams;
|
||||
import org.apache.solr.common.params.SolrParams;
|
||||
import org.apache.solr.common.util.NamedList;
|
||||
import org.apache.solr.request.SolrQueryRequest;
|
||||
import org.apache.solr.schema.IndexSchema;
|
||||
import org.apache.solr.util.SolrPluginUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
/**
|
||||
* Create a dismax query from the input value.
|
||||
* <br>Other parameters: all main query related parameters from the {@link org.apache.solr.handler.DisMaxRequestHandler} are supported.
|
||||
|
@ -46,193 +34,6 @@ public class DisMaxQParserPlugin extends QParserPlugin {
|
|||
}
|
||||
|
||||
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
|
||||
return new DismaxQParser(qstr, localParams, params, req);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class DismaxQParser extends QParser {
|
||||
|
||||
/**
|
||||
* A field we can't ever find in any schema, so we can safely tell
|
||||
* DisjunctionMaxQueryParser to use it as our defaultField, and
|
||||
* map aliases from it to any field in our schema.
|
||||
*/
|
||||
private static String IMPOSSIBLE_FIELD_NAME = "\uFFFC\uFFFC\uFFFC";
|
||||
|
||||
|
||||
public DismaxQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
|
||||
super(qstr, localParams, params, req);
|
||||
}
|
||||
|
||||
Map<String,Float> queryFields;
|
||||
Query parsedUserQuery;
|
||||
|
||||
|
||||
private String[] boostParams;
|
||||
private List<Query> boostQueries;
|
||||
private Query altUserQuery;
|
||||
private QParser altQParser;
|
||||
|
||||
|
||||
public Query parse() throws ParseException {
|
||||
SolrParams solrParams = localParams == null ? params : new DefaultSolrParams(localParams, params);
|
||||
|
||||
IndexSchema schema = req.getSchema();
|
||||
|
||||
queryFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.QF));
|
||||
Map<String,Float> phraseFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.PF));
|
||||
|
||||
float tiebreaker = solrParams.getFloat(DisMaxParams.TIE, 0.0f);
|
||||
|
||||
int pslop = solrParams.getInt(DisMaxParams.PS, 0);
|
||||
int qslop = solrParams.getInt(DisMaxParams.QS, 0);
|
||||
|
||||
/* a parser for dealing with user input, which will convert
|
||||
* things to DisjunctionMaxQueries
|
||||
*/
|
||||
SolrPluginUtils.DisjunctionMaxQueryParser up =
|
||||
new SolrPluginUtils.DisjunctionMaxQueryParser(schema, IMPOSSIBLE_FIELD_NAME);
|
||||
up.addAlias(IMPOSSIBLE_FIELD_NAME,
|
||||
tiebreaker, queryFields);
|
||||
up.setPhraseSlop(qslop);
|
||||
|
||||
/* for parsing sloppy phrases using DisjunctionMaxQueries */
|
||||
SolrPluginUtils.DisjunctionMaxQueryParser pp =
|
||||
new SolrPluginUtils.DisjunctionMaxQueryParser(schema, IMPOSSIBLE_FIELD_NAME);
|
||||
pp.addAlias(IMPOSSIBLE_FIELD_NAME,
|
||||
tiebreaker, phraseFields);
|
||||
pp.setPhraseSlop(pslop);
|
||||
|
||||
|
||||
/* the main query we will execute. we disable the coord because
|
||||
* this query is an artificial construct
|
||||
*/
|
||||
BooleanQuery query = new BooleanQuery(true);
|
||||
|
||||
/* * * Main User Query * * */
|
||||
parsedUserQuery = null;
|
||||
String userQuery = getString();
|
||||
altUserQuery = null;
|
||||
if( userQuery == null || userQuery.trim().length() < 1 ) {
|
||||
// If no query is specified, we may have an alternate
|
||||
String altQ = solrParams.get( DisMaxParams.ALTQ );
|
||||
if (altQ != null) {
|
||||
altQParser = subQuery(altQ, null);
|
||||
altUserQuery = altQParser.parse();
|
||||
query.add( altUserQuery , BooleanClause.Occur.MUST );
|
||||
} else {
|
||||
throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "missing query string" );
|
||||
}
|
||||
}
|
||||
else {
|
||||
// There is a valid query string
|
||||
userQuery = SolrPluginUtils.partialEscape(SolrPluginUtils.stripUnbalancedQuotes(userQuery)).toString();
|
||||
userQuery = SolrPluginUtils.stripIllegalOperators(userQuery).toString();
|
||||
|
||||
String minShouldMatch = solrParams.get(DisMaxParams.MM, "100%");
|
||||
Query dis = up.parse(userQuery);
|
||||
parsedUserQuery = dis;
|
||||
|
||||
if (dis instanceof BooleanQuery) {
|
||||
BooleanQuery t = new BooleanQuery();
|
||||
SolrPluginUtils.flattenBooleanQuery(t, (BooleanQuery)dis);
|
||||
SolrPluginUtils.setMinShouldMatch(t, minShouldMatch);
|
||||
parsedUserQuery = t;
|
||||
}
|
||||
query.add(parsedUserQuery, BooleanClause.Occur.MUST);
|
||||
|
||||
|
||||
/* * * Add on Phrases for the Query * * */
|
||||
|
||||
/* build up phrase boosting queries */
|
||||
|
||||
/* if the userQuery already has some quotes, strip them out.
|
||||
* we've already done the phrases they asked for in the main
|
||||
* part of the query, this is to boost docs that may not have
|
||||
* matched those phrases but do match looser phrases.
|
||||
*/
|
||||
String userPhraseQuery = userQuery.replace("\"","");
|
||||
Query phrase = pp.parse("\"" + userPhraseQuery + "\"");
|
||||
if (null != phrase) {
|
||||
query.add(phrase, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* * * Boosting Query * * */
|
||||
boostParams = solrParams.getParams(DisMaxParams.BQ);
|
||||
//List<Query> boostQueries = SolrPluginUtils.parseQueryStrings(req, boostParams);
|
||||
boostQueries=null;
|
||||
if (boostParams!=null && boostParams.length>0) {
|
||||
boostQueries = new ArrayList<Query>();
|
||||
for (String qs : boostParams) {
|
||||
if (qs.trim().length()==0) continue;
|
||||
Query q = subQuery(qs, null).parse();
|
||||
boostQueries.add(q);
|
||||
}
|
||||
}
|
||||
if (null != boostQueries) {
|
||||
if(1 == boostQueries.size() && 1 == boostParams.length) {
|
||||
/* legacy logic */
|
||||
Query f = boostQueries.get(0);
|
||||
if (1.0f == f.getBoost() && f instanceof BooleanQuery) {
|
||||
/* if the default boost was used, and we've got a BooleanQuery
|
||||
* extract the subqueries out and use them directly
|
||||
*/
|
||||
for (Object c : ((BooleanQuery)f).clauses()) {
|
||||
query.add((BooleanClause)c);
|
||||
}
|
||||
} else {
|
||||
query.add(f, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
} else {
|
||||
for(Query f : boostQueries) {
|
||||
query.add(f, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* * * Boosting Functions * * */
|
||||
|
||||
String[] boostFuncs = solrParams.getParams(DisMaxParams.BF);
|
||||
if (null != boostFuncs && 0 != boostFuncs.length) {
|
||||
for (String boostFunc : boostFuncs) {
|
||||
if(null == boostFunc || "".equals(boostFunc)) continue;
|
||||
Map<String,Float> ff = SolrPluginUtils.parseFieldBoosts(boostFunc);
|
||||
for (String f : ff.keySet()) {
|
||||
Query fq = subQuery(f, FunctionQParserPlugin.NAME).parse();
|
||||
Float b = ff.get(f);
|
||||
if (null != b) {
|
||||
fq.setBoost(b);
|
||||
}
|
||||
query.add(fq, BooleanClause.Occur.SHOULD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultHighlightFields() {
|
||||
String[] highFields = queryFields.keySet().toArray(new String[0]);
|
||||
return highFields;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query getHighlightQuery() throws ParseException {
|
||||
return parsedUserQuery;
|
||||
}
|
||||
|
||||
public void addDebugInfo(NamedList<Object> debugInfo) {
|
||||
super.addDebugInfo(debugInfo);
|
||||
debugInfo.add("altquerystring", altUserQuery);
|
||||
if (null != boostQueries) {
|
||||
debugInfo.add("boost_queries", boostParams);
|
||||
debugInfo.add("parsed_boost_queries",
|
||||
QueryParsing.toString(boostQueries, req.getSchema()));
|
||||
}
|
||||
debugInfo.add("boostfuncs", req.getParams().getParams(DisMaxParams.BF));
|
||||
return new DisMaxQParser(qstr, localParams, params, req);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,14 +27,19 @@ import org.apache.solr.request.SolrQueryRequest;
|
|||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* <b>Note: This API is experimental and may change in non backward-compatible ways in the future</b>
|
||||
*
|
||||
* @version $Id$
|
||||
*/
|
||||
public abstract class QParser {
|
||||
String qstr;
|
||||
SolrParams params;
|
||||
SolrParams localParams;
|
||||
SolrQueryRequest req;
|
||||
int recurseCount;
|
||||
protected String qstr;
|
||||
protected SolrParams params;
|
||||
protected SolrParams localParams;
|
||||
protected SolrQueryRequest req;
|
||||
protected int recurseCount;
|
||||
|
||||
Query query;
|
||||
protected Query query;
|
||||
|
||||
|
||||
public QParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
|
||||
|
|
|
@ -470,20 +470,20 @@ public class QueryParsing {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// simple class to help with parsing a string
|
||||
static class StrParser {
|
||||
/**
|
||||
* Simple class to help with parsing a string
|
||||
* <b>Note: This API is experimental and may change in non backward-compatible ways in the future</b>
|
||||
*/
|
||||
public static class StrParser {
|
||||
String val;
|
||||
int pos;
|
||||
int end;
|
||||
|
||||
StrParser(String val) {
|
||||
public StrParser(String val) {
|
||||
this(val,0,val.length());
|
||||
}
|
||||
|
||||
StrParser(String val, int start, int end) {
|
||||
public StrParser(String val, int start, int end) {
|
||||
this.val = val;
|
||||
this.pos = start;
|
||||
this.end = end;
|
||||
|
|
|
@ -27,6 +27,8 @@ import java.util.Set;
|
|||
* Returns a score for each document based on a ValueSource,
|
||||
* often some function of the value of a field.
|
||||
*
|
||||
* <b>Note: This API is experimental and may change in non backward-compatible ways in the future</b>
|
||||
*
|
||||
* @version $Id$
|
||||
*/
|
||||
public class FunctionQuery extends Query {
|
||||
|
@ -51,9 +53,9 @@ public class FunctionQuery extends Query {
|
|||
public void extractTerms(Set terms) {}
|
||||
|
||||
protected class FunctionWeight implements Weight {
|
||||
Searcher searcher;
|
||||
float queryNorm;
|
||||
float queryWeight;
|
||||
protected Searcher searcher;
|
||||
protected float queryNorm;
|
||||
protected float queryWeight;
|
||||
|
||||
public FunctionWeight(Searcher searcher) {
|
||||
this.searcher = searcher;
|
||||
|
|
Loading…
Reference in New Issue