LUCENE-732 - DateTools support for QueryParser, Resolution can be set on a per field basis

git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@494184 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Chris M. Hostetter 2007-01-08 20:02:51 +00:00
parent dc20634a97
commit ceaa0c3091
4 changed files with 284 additions and 31 deletions

View File

@ -170,6 +170,11 @@ API Changes
on an older format index will create a single .nrm file for the new
segment. (Doron Cohen via Yonik Seeley)
14. LUCENE-732: DateTools support has been added to QueryParser, with
setters for both the default Resolution, and per-field Resolution.
For backwards compatibility, DateField is still used if no Resolutions
are specified. (Michael Busch via Chris Hostetter)
Bug fixes
1. Fixed the web application demo (built with "ant war-demo") which

View File

@ -45,14 +45,31 @@ import org.apache.lucene.util.Parameter;
* documentation</a>.
* </p>
*
* <p>In {@link RangeQuery}s, QueryParser tries to detect date values, e.g. <tt>date:[6/1/2005 TO 6/4/2005]</tt>
* produces a range query that searches for "date" fields between 2005-06-01 and 2005-06-04. Note
* that the format of the accpeted input depends on {@link #setLocale(Locale) the locale}. This
* feature also assumes that your index uses the {@link DateField} class to store dates.
* If you use a different format (e.g. {@link DateTools}) and you still want QueryParser
* to turn local dates in range queries into valid queries you need to create your own
* <p>
* In {@link RangeQuery}s, QueryParser tries to detect date values, e.g.
* <tt>date:[6/1/2005 TO 6/4/2005]</tt> produces a range query that searches
* for "date" fields between 2005-06-01 and 2005-06-04. Note that the format
* of the accepted input depends on {@link #setLocale(Locale) the locale}.
* By default a date is converted into a search term using the deprecated
* {@link DateField} for compatibility reasons.
* To use the new {@link DateTools} to convert dates, a
* {@link DateTools.Resolution} has to be set.
* </p>
* <p>
* The date resolution that shall be used for RangeQueries can be set
* using {@link #setDateResolution(DateTools.Resolution)}
* or {@link #setDateResolution(String, DateTools.Resolution)}. The former
* sets the default date resolution for all fields, whereas the latter can
* be used to set field specific date resolutions. Field specific date
* resolutions take, if set, precedence over the default date resolution.
* </p>
* <p>
* If you use neither {@link DateField} nor {@link DateTools} in your
* index, you can create your own
* query parser that inherits QueryParser and overwrites
* {@link #getRangeQuery(String, String, String, boolean)}.</p>
* {@link #getRangeQuery(String, String, String, boolean)} to
* use a different method for date conversion.
* </p>
*
* <p>Note that QueryParser is <em>not</em> thread-safe.</p>
*
@ -60,7 +77,6 @@ import org.apache.lucene.util.Parameter;
* @author Peter Halacsy
* @author Tatu Saloranta
*/
public class QueryParser implements QueryParserConstants {
private static final int CONJ_NONE = 0;
@ -92,6 +108,11 @@ public class QueryParser implements QueryParserConstants {
int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength;
Locale locale = Locale.getDefault();
// the default date resolution
DateTools.Resolution dateResolution = null;
// maps field names to date resolutions
Map fieldToDateResolution = null;
/** The default operator for parsing queries.
* Use {@link QueryParser#setDefaultOperator} to change it.
*/
@ -286,6 +307,61 @@ public class QueryParser implements QueryParserConstants {
return locale;
}
/**
* Sets the default date resolution used by RangeQueries for fields for which no
* specific date resolutions has been set. Field specific resolutions can be set
* with {@link #setDateResolution(String, DateTools.Resolution)}.
*
* @param dateResolution the default date resolution to set
*/
public void setDateResolution(DateTools.Resolution dateResolution) {
this.dateResolution = dateResolution;
}
/**
* Sets the date resolution used by RangeQueries for a specific field.
*
* @param field field for which the date resolution is to be set
* @param dateResolution date resolution to set
*/
public void setDateResolution(String fieldName, DateTools.Resolution dateResolution) {
if (fieldName == null) {
throw new IllegalArgumentException("Field cannot be null.");
}
if (fieldToDateResolution == null) {
// lazily initialize HashMap
fieldToDateResolution = new HashMap();
}
fieldToDateResolution.put(fieldName, dateResolution);
}
/**
* Returns the date resolution that is used by RangeQueries for the given field.
* Returns null, if no default or field specific date resolution has been set
* for the given field.
*
*/
public DateTools.Resolution getDateResolution(String fieldName) {
if (fieldName == null) {
throw new IllegalArgumentException("Field cannot be null.");
}
if (fieldToDateResolution == null) {
// no field specific date resolutions set; return default date resolution instead
return this.dateResolution;
}
DateTools.Resolution resolution = (DateTools.Resolution) fieldToDateResolution.get(fieldName);
if (resolution == null) {
// no date resolutions set for the given field; return default date resolution instead
resolution = this.dateResolution;
}
return resolution;
}
protected void addClause(Vector clauses, int conj, int mods, Query q) {
boolean required, prohibited;
@ -472,8 +548,17 @@ public class QueryParser implements QueryParserConstants {
cal.set(Calendar.MILLISECOND, 999);
d2 = cal.getTime();
}
part1 = DateField.dateToString(d1);
part2 = DateField.dateToString(d2);
DateTools.Resolution resolution = getDateResolution(field);
if (resolution == null) {
// no default or field specific date resolution has been set,
// use deprecated DateField to maintain compatibilty with
// pre-1.9 Lucene versions.
part1 = DateField.dateToString(d1);
part2 = DateField.dateToString(d2);
} else {
part1 = DateTools.dateToString(d1, resolution);
part2 = DateTools.dateToString(d2, resolution);
}
}
catch (Exception e) { }
@ -1158,6 +1243,12 @@ public class QueryParser implements QueryParserConstants {
finally { jj_save(0, xla); }
}
final private boolean jj_3R_3() {
if (jj_scan_token(STAR)) return true;
if (jj_scan_token(COLON)) return true;
return false;
}
final private boolean jj_3R_2() {
if (jj_scan_token(TERM)) return true;
if (jj_scan_token(COLON)) return true;
@ -1174,12 +1265,6 @@ public class QueryParser implements QueryParserConstants {
return false;
}
final private boolean jj_3R_3() {
if (jj_scan_token(STAR)) return true;
if (jj_scan_token(COLON)) return true;
return false;
}
public QueryParserTokenManager token_source;
public Token token, jj_nt;
private int jj_ntk;

View File

@ -69,14 +69,31 @@ import org.apache.lucene.util.Parameter;
* documentation</a>.
* </p>
*
* <p>In {@link RangeQuery}s, QueryParser tries to detect date values, e.g. <tt>date:[6/1/2005 TO 6/4/2005]</tt>
* produces a range query that searches for "date" fields between 2005-06-01 and 2005-06-04. Note
* that the format of the accpeted input depends on {@link #setLocale(Locale) the locale}. This
* feature also assumes that your index uses the {@link DateField} class to store dates.
* If you use a different format (e.g. {@link DateTools}) and you still want QueryParser
* to turn local dates in range queries into valid queries you need to create your own
* <p>
* In {@link RangeQuery}s, QueryParser tries to detect date values, e.g.
* <tt>date:[6/1/2005 TO 6/4/2005]</tt> produces a range query that searches
* for "date" fields between 2005-06-01 and 2005-06-04. Note that the format
* of the accepted input depends on {@link #setLocale(Locale) the locale}.
* By default a date is converted into a search term using the deprecated
* {@link DateField} for compatibility reasons.
* To use the new {@link DateTools} to convert dates, a
* {@link DateTools.Resolution} has to be set.
* </p>
* <p>
* The date resolution that shall be used for RangeQueries can be set
* using {@link #setDateResolution(DateTools.Resolution)}
* or {@link #setDateResolution(String, DateTools.Resolution)}. The former
* sets the default date resolution for all fields, whereas the latter can
* be used to set field specific date resolutions. Field specific date
* resolutions take, if set, precedence over the default date resolution.
* </p>
* <p>
* If you use neither {@link DateField} nor {@link DateTools} in your
* index, you can create your own
* query parser that inherits QueryParser and overwrites
* {@link #getRangeQuery(String, String, String, boolean)}.</p>
* {@link #getRangeQuery(String, String, String, boolean)} to
* use a different method for date conversion.
* </p>
*
* <p>Note that QueryParser is <em>not</em> thread-safe.</p>
*
@ -84,7 +101,6 @@ import org.apache.lucene.util.Parameter;
* @author Peter Halacsy
* @author Tatu Saloranta
*/
public class QueryParser {
private static final int CONJ_NONE = 0;
@ -116,6 +132,11 @@ public class QueryParser {
int fuzzyPrefixLength = FuzzyQuery.defaultPrefixLength;
Locale locale = Locale.getDefault();
// the default date resolution
DateTools.Resolution dateResolution = null;
// maps field names to date resolutions
Map fieldToDateResolution = null;
/** The default operator for parsing queries.
* Use {@link QueryParser#setDefaultOperator} to change it.
*/
@ -310,6 +331,61 @@ public class QueryParser {
return locale;
}
/**
* Sets the default date resolution used by RangeQueries for fields for which no
* specific date resolutions has been set. Field specific resolutions can be set
* with {@link #setDateResolution(String, DateTools.Resolution)}.
*
* @param dateResolution the default date resolution to set
*/
public void setDateResolution(DateTools.Resolution dateResolution) {
this.dateResolution = dateResolution;
}
/**
* Sets the date resolution used by RangeQueries for a specific field.
*
* @param field field for which the date resolution is to be set
* @param dateResolution date resolution to set
*/
public void setDateResolution(String fieldName, DateTools.Resolution dateResolution) {
if (fieldName == null) {
throw new IllegalArgumentException("Field cannot be null.");
}
if (fieldToDateResolution == null) {
// lazily initialize HashMap
fieldToDateResolution = new HashMap();
}
fieldToDateResolution.put(fieldName, dateResolution);
}
/**
* Returns the date resolution that is used by RangeQueries for the given field.
* Returns null, if no default or field specific date resolution has been set
* for the given field.
*
*/
public DateTools.Resolution getDateResolution(String fieldName) {
if (fieldName == null) {
throw new IllegalArgumentException("Field cannot be null.");
}
if (fieldToDateResolution == null) {
// no field specific date resolutions set; return default date resolution instead
return this.dateResolution;
}
DateTools.Resolution resolution = (DateTools.Resolution) fieldToDateResolution.get(fieldName);
if (resolution == null) {
// no date resolutions set for the given field; return default date resolution instead
resolution = this.dateResolution;
}
return resolution;
}
protected void addClause(Vector clauses, int conj, int mods, Query q) {
boolean required, prohibited;
@ -496,8 +572,17 @@ public class QueryParser {
cal.set(Calendar.MILLISECOND, 999);
d2 = cal.getTime();
}
part1 = DateField.dateToString(d1);
part2 = DateField.dateToString(d2);
DateTools.Resolution resolution = getDateResolution(field);
if (resolution == null) {
// no default or field specific date resolution has been set,
// use deprecated DateField to maintain compatibilty with
// pre-1.9 Lucene versions.
part1 = DateField.dateToString(d1);
part2 = DateField.dateToString(d2);
} else {
part1 = DateTools.dateToString(d1, resolution);
part2 = DateTools.dateToString(d2, resolution);
}
}
catch (Exception e) { }

View File

@ -27,6 +27,7 @@ import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.WhitespaceAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.DateField;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexWriter;
@ -38,6 +39,7 @@ import java.io.IOException;
import java.io.Reader;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
/**
@ -127,6 +129,16 @@ public class TestQueryParser extends TestCase {
}
}
public void assertQueryEquals(QueryParser qp, String field, String query, String result)
throws Exception {
Query q = qp.parse(query);
String s = q.toString(field);
if (!s.equals(result)) {
fail("Query /" + query + "/ yielded /" + s
+ "/, expecting /" + result + "/");
}
}
public void assertEscapedQueryEquals(String query, Analyzer a, String result)
throws Exception {
String escapedQuery = QueryParser.escape(query);
@ -378,12 +390,28 @@ public class TestQueryParser extends TestCase {
assertQueryEquals("( bar blar { a TO z}) ", null, "bar blar {a TO z}");
assertQueryEquals("gack ( bar blar { a TO z}) ", null, "gack (bar blar {a TO z})");
}
private String getDate(String s) throws Exception {
/** for testing legacy DateField support */
private String getLegacyDate(String s) throws Exception {
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
return DateField.dateToString(df.parse(s));
}
/** for testing DateTools support */
private String getDate(String s, DateTools.Resolution resolution) throws Exception {
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
return getDate(df.parse(s), resolution);
}
/** for testing DateTools support */
private String getDate(Date d, DateTools.Resolution resolution) throws Exception {
if (resolution == null) {
return DateField.dateToString(d);
} else {
return DateTools.dateToString(d, resolution);
}
}
private String getLocalizedDate(int year, int month, int day, boolean extendLastDate) {
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
Calendar calendar = Calendar.getInstance();
@ -397,16 +425,66 @@ public class TestQueryParser extends TestCase {
return df.format(calendar.getTime());
}
public void testDateRange() throws Exception {
/** for testing legacy DateField support */
public void testLegacyDateRange() throws Exception {
String startDate = getLocalizedDate(2002, 1, 1, false);
String endDate = getLocalizedDate(2002, 1, 4, false);
Calendar endDateExpected = Calendar.getInstance();
endDateExpected.set(2002, 1, 4, 23, 59, 59);
endDateExpected.set(Calendar.MILLISECOND, 999);
assertQueryEquals("[ " + startDate + " TO " + endDate + "]", null,
"[" + getDate(startDate) + " TO " + DateField.dateToString(endDateExpected.getTime()) + "]");
"[" + getLegacyDate(startDate) + " TO " + DateField.dateToString(endDateExpected.getTime()) + "]");
assertQueryEquals("{ " + startDate + " " + endDate + " }", null,
"{" + getDate(startDate) + " TO " + getDate(endDate) + "}");
"{" + getLegacyDate(startDate) + " TO " + getLegacyDate(endDate) + "}");
}
public void testDateRange() throws Exception {
String startDate = getLocalizedDate(2002, 1, 1, false);
String endDate = getLocalizedDate(2002, 1, 4, false);
Calendar endDateExpected = Calendar.getInstance();
endDateExpected.set(2002, 1, 4, 23, 59, 59);
endDateExpected.set(Calendar.MILLISECOND, 999);
final String defaultField = "default";
final String monthField = "month";
final String hourField = "hour";
QueryParser qp = new QueryParser("field", new SimpleAnalyzer());
// Don't set any date resolution and verify if DateField is used
assertDateRangeQueryEquals(qp, defaultField, startDate, endDate,
endDateExpected.getTime(), null);
// set a field specific date resolution
qp.setDateResolution(monthField, DateTools.Resolution.MONTH);
// DateField should still be used for defaultField
assertDateRangeQueryEquals(qp, defaultField, startDate, endDate,
endDateExpected.getTime(), null);
// set default date resolution to MILLISECOND
qp.setDateResolution(DateTools.Resolution.MILLISECOND);
// set second field specific date resolution
qp.setDateResolution(hourField, DateTools.Resolution.HOUR);
// for this field no field specific date resolution has been set,
// so verify if the default resolution is used
assertDateRangeQueryEquals(qp, defaultField, startDate, endDate,
endDateExpected.getTime(), DateTools.Resolution.MILLISECOND);
// verify if field specific date resolutions are used for these two fields
assertDateRangeQueryEquals(qp, monthField, startDate, endDate,
endDateExpected.getTime(), DateTools.Resolution.MONTH);
assertDateRangeQueryEquals(qp, hourField, startDate, endDate,
endDateExpected.getTime(), DateTools.Resolution.HOUR);
}
public void assertDateRangeQueryEquals(QueryParser qp, String field, String startDate, String endDate,
Date endDateInclusive, DateTools.Resolution resolution) throws Exception {
assertQueryEquals(qp, field, field + ":[" + startDate + " TO " + endDate + "]",
"[" + getDate(startDate, resolution) + " TO " + getDate(endDateInclusive, resolution) + "]");
assertQueryEquals(qp, field, field + ":{" + startDate + " TO " + endDate + "}",
"{" + getDate(startDate, resolution) + " TO " + getDate(endDate, resolution) + "}");
}
public void testEscaped() throws Exception {