Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors PrefixQuery #12032

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public QueryValidationException validate() {

@Override
protected final int doHashCode() {
return Objects.hash(getClass(), fieldName, value);
return Objects.hash(fieldName, value);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@

package org.elasticsearch.index.query;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.support.QueryParsers;

import java.io.IOException;
import java.util.Objects;

/**
* A Query that matches documents containing terms with a specified prefix.
Expand All @@ -30,35 +41,47 @@ public class PrefixQueryBuilder extends AbstractQueryBuilder<PrefixQueryBuilder>

public static final String NAME = "prefix";

private final String name;

private final String prefix;

private final String fieldName;
private final String value;
private String rewrite;

static final PrefixQueryBuilder PROTOTYPE = new PrefixQueryBuilder(null, null);

/**
* A Query that matches documents containing terms with a specified prefix.
*
* @param name The name of the field
* @param prefix The prefix query
* @param fieldName The name of the field
* @param value The prefix query
*/
public PrefixQueryBuilder(String name, String prefix) {
this.name = name;
this.prefix = prefix;
public PrefixQueryBuilder(String fieldName, String value) {
this.fieldName = fieldName;
this.value = value;
}

public String fieldName() {
return this.fieldName;
}

public String value() {
return this.value;
}

public PrefixQueryBuilder rewrite(String rewrite) {
this.rewrite = rewrite;
return this;
}

public String rewrite() {
return this.rewrite;
}

@Override
public void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
builder.startObject(name);
builder.field("prefix", prefix);
builder.startObject(fieldName);
builder.field("prefix", this.value);
if (rewrite != null) {
builder.field("rewrite", rewrite);
}
Expand All @@ -71,4 +94,62 @@ public void doXContent(XContentBuilder builder, Params params) throws IOExceptio
public String getName() {
return NAME;
}

@Override
protected Query doToQuery(QueryParseContext parseContext) throws IOException {
MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(parseContext.parseFieldMatcher(), rewrite, null);

Query query = null;
MappedFieldType fieldType = parseContext.fieldMapper(fieldName);
if (fieldType != null) {
query = fieldType.prefixQuery(value, method, parseContext);
}
if (query == null) {
PrefixQuery prefixQuery = new PrefixQuery(new Term(fieldName, BytesRefs.toBytesRef(value)));
if (method != null) {
prefixQuery.setRewriteMethod(method);
}
query = prefixQuery;
}

return query;
}

@Override
public QueryValidationException validate() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also reuse the BaseTermQueryBuilder#validate here? I realize we agreed on always overwriting this for the AbstractQueryBuilder, but since this just repeats the super types implementation, maybe we should call it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely! +1

QueryValidationException validationException = null;
if (Strings.isEmpty(this.fieldName)) {
validationException = addValidationError("field name cannot be null or empty.", validationException);
}
if (this.value == null) {
validationException = addValidationError("query text cannot be null", validationException);
}
return validationException;
}

@Override
protected PrefixQueryBuilder doReadFrom(StreamInput in) throws IOException {
PrefixQueryBuilder prefixQueryBuilder = new PrefixQueryBuilder(in.readString(), in.readString());
prefixQueryBuilder.rewrite = in.readOptionalString();
return prefixQueryBuilder;
}

@Override
protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeString(value);
out.writeOptionalString(rewrite);
}

@Override
protected final int doHashCode() {
return Objects.hash(fieldName, value, rewrite);
}

@Override
protected boolean doEquals(PrefixQueryBuilder other) {
return Objects.equals(fieldName, other.fieldName) &&
Objects.equals(value, other.value) &&
Objects.equals(rewrite, other.rewrite);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,15 @@

package org.elasticsearch.index.query;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.support.QueryParsers;

import java.io.IOException;

/**
*
*/
public class PrefixQueryParser extends BaseQueryParserTemp {
public class PrefixQueryParser extends BaseQueryParser {

@Inject
public PrefixQueryParser() {
Expand All @@ -46,14 +39,14 @@ public String[] names() {
}

@Override
public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException {
public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException {
XContentParser parser = parseContext.parser();

String fieldName = parser.currentName();
String rewriteMethod = null;
String queryName = null;

String value = null;
String rewrite = null;

String queryName = null;
float boost = AbstractQueryBuilder.DEFAULT_BOOST;
String currentFieldName = null;
XContentParser.Token token;
Expand All @@ -75,7 +68,7 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
} else if ("boost".equals(currentFieldName)) {
boost = parser.floatValue();
} else if ("rewrite".equals(currentFieldName)) {
rewriteMethod = parser.textOrNull();
rewrite = parser.textOrNull();
} else {
throw new QueryParsingException(parseContext, "[regexp] query does not support [" + currentFieldName + "]");
}
Expand All @@ -94,26 +87,10 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars
if (value == null) {
throw new QueryParsingException(parseContext, "No value specified for prefix query");
}

MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(parseContext.parseFieldMatcher(), rewriteMethod, null);

Query query = null;
MappedFieldType fieldType = parseContext.fieldMapper(fieldName);
if (fieldType != null) {
query = fieldType.prefixQuery(value, method, parseContext);
}
if (query == null) {
PrefixQuery prefixQuery = new PrefixQuery(new Term(fieldName, BytesRefs.toBytesRef(value)));
if (method != null) {
prefixQuery.setRewriteMethod(method);
}
query = prefixQuery;
}
query.setBoost(boost);
if (queryName != null) {
parseContext.addNamedQuery(queryName, query);
}
return query;
return new PrefixQueryBuilder(fieldName, value)
.rewrite(rewrite)
.boost(boost)
.queryName(queryName);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.index.query;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.search.PrefixQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.query.support.QueryParsers;
import org.junit.Test;

import java.io.IOException;

import static org.hamcrest.Matchers.is;

public class PrefixQueryBuilderTest extends BaseQueryTestCase<PrefixQueryBuilder> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's possible to also extend the generic BaseTermQueryTestCase here, since a lot of the setup looks similar. That one is also testing Boolean as a value type while this test adds Date there. Maybe the creation of the expected query gets a bit too complicated, but the two existing Test classes (SpanTermQueryBuilderTest, TermQueryBuilderTest) are quiet small that way. Might be worth looking into merging, unless you already tried and found it to hard to read afterwards.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patially revoking my last comment, I would only try this and add the inheritance level in the test if in the end you decide to have PrefixQueryBuilder extend BaseTermQueryBuilder. Thinking about it a bit more, I'm not sure if this is maybe growing to complex at this point and a bit of code duplication should be traded for ease of understanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for using BaseTermQueryTestCase

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if PrefixQueryBuilder won't extend BaseTermQueryBuilder anymore I think we can't extend BaseTermQueryTestCase either here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be possible, but I would try to avoid it in this case. I would go for either using both BaseTerm classes or none.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK then we go for none? I am ok either way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave it as-is, it needs to extend BaseQueryTestCase


@Override
protected PrefixQueryBuilder doCreateTestQueryBuilder() {
String fieldName = randomBoolean() ? STRING_FIELD_NAME : randomAsciiOfLengthBetween(1, 10);
String value = randomAsciiOfLengthBetween(1, 10);
PrefixQueryBuilder query = new PrefixQueryBuilder(fieldName, value);

if (randomBoolean()) {
query.rewrite(getRandomRewriteMethod());
}
return query;
}

@Override
protected Query doCreateExpectedQuery(PrefixQueryBuilder queryBuilder, QueryParseContext context) throws IOException {
//norelease fix to be removed to avoid NPE on unmapped fields (Dtests.seed=BF5D7566DECBC5B1)
context.parseFieldMatcher(randomBoolean() ? ParseFieldMatcher.EMPTY : ParseFieldMatcher.STRICT);

MultiTermQuery.RewriteMethod method = QueryParsers.parseRewriteMethod(context.parseFieldMatcher(), queryBuilder.rewrite(), null);

Query query = null;
MappedFieldType fieldType = context.fieldMapper(queryBuilder.fieldName());
if (fieldType != null) {
query = fieldType.prefixQuery(queryBuilder.value(), method, context);
}
if (query == null) {
PrefixQuery prefixQuery = new PrefixQuery(new Term(queryBuilder.fieldName(), BytesRefs.toBytesRef(queryBuilder.value())));
if (method != null) {
prefixQuery.setRewriteMethod(method);
}
query = prefixQuery;
}

return query;
}

@Test
public void testValidate() {
PrefixQueryBuilder prefixQueryBuilder = new PrefixQueryBuilder("", "prefix");
assertThat(prefixQueryBuilder.validate().validationErrors().size(), is(1));

prefixQueryBuilder = new PrefixQueryBuilder("field", null);
assertThat(prefixQueryBuilder.validate().validationErrors().size(), is(1));

prefixQueryBuilder = new PrefixQueryBuilder("field", "prefix");
assertNull(prefixQueryBuilder.validate());

prefixQueryBuilder = new PrefixQueryBuilder(null, null);
assertThat(prefixQueryBuilder.validate().validationErrors().size(), is(2));
}
}