diff --git a/docs/index.md b/docs/index.md index 4962ab8f9..a762b583d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ fluent API. | [SQL (JDBC)]({{ site.baseurl }}/tutorials/sql) | `{{ site.group_id }}:querydsl-sql` | | [R2DBC (Reactive SQL)]({{ site.baseurl }}/tutorials/r2dbc) | `{{ site.group_id }}:querydsl-r2dbc` | | [MongoDB]({{ site.baseurl }}/tutorials/mongodb) | `{{ site.group_id }}:querydsl-mongodb` | +| [Lucene]({{ site.baseurl }}/tutorials/lucene) | `{{ site.group_id }}:querydsl-lucene9` / `querydsl-lucene10` | | [Collections]({{ site.baseurl }}/tutorials/collections) | `{{ site.group_id }}:querydsl-collections` | | [Spatial]({{ site.baseurl }}/tutorials/spatial) | `{{ site.group_id }}:querydsl-sql-spatial` | | [Kotlin Extensions]({{ site.baseurl }}/tutorials/kotlin) | `{{ site.group_id }}:querydsl-kotlin` | diff --git a/docs/introduction.md b/docs/introduction.md index ece1714cf..2cae7c053 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -19,7 +19,7 @@ construction faster and safer. HQL for Hibernate was the first target language for Querydsl. Today the framework supports **JPA**, **SQL (JDBC)**, **R2DBC**, **MongoDB**, -**Collections**, **Spatial**, **Kotlin**, and **Scala** as backends. +**Lucene**, **Collections**, **Spatial**, **Kotlin**, and **Scala** as backends. If you are new to database access in Java, [this guide](https://www.marcobehler.com/guides/a-guide-to-accessing-databases-in-java) diff --git a/docs/migration.md b/docs/migration.md index 4226447aa..1ea3e8624 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -158,9 +158,9 @@ The following modules have been removed from this fork: | Module | Reason | Alternative | |:-------|:-------|:------------| | `querydsl-jdo` | JDO usage has declined significantly | Use JPA instead | -| `querydsl-lucene3` | Lucene 3 is EOL | Use Lucene/Elasticsearch client directly | -| `querydsl-lucene4` | Lucene 4 is EOL | Use Lucene/Elasticsearch client directly | -| `querydsl-lucene5` | Lucene integration is rarely used | Use Lucene/Elasticsearch client directly | +| `querydsl-lucene3` | Lucene 3 is EOL | Use `querydsl-lucene9` or `querydsl-lucene10` | +| `querydsl-lucene4` | Lucene 4 is EOL | Use `querydsl-lucene9` or `querydsl-lucene10` | +| `querydsl-lucene5` | Lucene 5 is EOL | Use `querydsl-lucene9` or `querydsl-lucene10` | | `querydsl-hibernate-search` | Hibernate Search has its own query DSL | Use Hibernate Search API directly | If you depend on any of these modules, you have two options: @@ -174,6 +174,8 @@ If you depend on any of these modules, you have two options: | Module | Description | |:-------|:------------| | [`querydsl-r2dbc`]({{ site.baseurl }}/tutorials/r2dbc) | Reactive, non-blocking database access via R2DBC and Project Reactor | +| [`querydsl-lucene9`]({{ site.baseurl }}/tutorials/lucene) | Lucene 9 integration (Java 17+), replacing the old lucene3/4/5 modules | +| [`querydsl-lucene10`]({{ site.baseurl }}/tutorials/lucene) | Lucene 10 integration (Java 21+) | | [`querydsl-kotlin`]({{ site.baseurl }}/tutorials/kotlin) | Kotlin extension functions — use `+`, `-`, `*`, `/`, `%` operators on expressions | ## Step-by-Step Migration diff --git a/docs/tutorials/lucene.md b/docs/tutorials/lucene.md new file mode 100644 index 000000000..b26fafa70 --- /dev/null +++ b/docs/tutorials/lucene.md @@ -0,0 +1,212 @@ +--- +layout: default +title: Querying Lucene +parent: Tutorials +nav_order: 6 +--- + +# Querying Lucene + +This chapter describes the querying functionality of the Lucene modules. + +## Maven Integration + +Two modules are available depending on your Lucene version: + +**Lucene 9** (Java 17+): + +```xml + + {{ site.group_id }} + querydsl-lucene9 + {{ site.querydsl_version }} + +``` + +**Lucene 10** (Java 21+): + +```xml + + {{ site.group_id }} + querydsl-lucene10 + {{ site.querydsl_version }} + +``` + +Both modules provide the same API. The examples below use the `com.querydsl.lucene9` +package — replace with `com.querydsl.lucene10` if you are on Lucene 10. + +## Creating the Query Types + +Since Lucene has no schema, query types are created manually. For a document +with `year` and `title` fields the query type looks like this: + +```java +public class QDocument extends EntityPathBase { + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final StringPath year = createString("year"); + + public final StringPath title = createString("title"); +} +``` + +`QDocument` represents a Lucene document with the fields `year` and `title`. + +Code generation is not available for Lucene since no schema data is available. + +## Querying + +Querying with Querydsl Lucene is straightforward: + +```java +QDocument doc = new QDocument("doc"); + +IndexSearcher searcher = new IndexSearcher(index); +LuceneQuery query = new LuceneQuery(searcher); +List documents = query + .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle"))) + .fetch(); +``` + +This is transformed into the following Lucene query: + +``` ++year:[1800 TO 2000] +title:huckle* +``` + +### Custom Serializer + +The default `LuceneSerializer` does not lowercase terms and splits on +whitespace. To customize this behavior, pass a serializer to the query: + +```java +LuceneSerializer serializer = new LuceneSerializer(true, true); +LuceneQuery query = new LuceneQuery(serializer, searcher); +``` + +The constructor parameters are: + +| Parameter | Description | +|:----------|:------------| +| `lowerCase` | Convert search terms to lowercase | +| `splitTerms` | Split terms by whitespace into multi-term queries | + +## Typed Queries + +Use `TypedQuery` to transform Lucene documents into custom types: + +```java +TypedQuery query = new TypedQuery<>(searcher, doc -> { + Person person = new Person(); + person.setName(doc.get("name")); + person.setAge(Integer.parseInt(doc.get("age"))); + return person; +}); +List results = query.where(doc.title.eq("Engineer")).fetch(); +``` + +## General Usage + +Use the cascading methods of the `LuceneQuery` class: + +**where:** Add query filters, either in varargs form separated via commas or +cascaded via the and-operator. Supported operations include equality, inequality, +range queries, string matching (`like`, `startsWith`, `endsWith`, `contains`), +and collection operations (`in`, `notIn`). + +**orderBy:** Add ordering of the result as a varargs array of order +expressions. Use `asc()` and `desc()` on numeric, string, and other comparable +expressions to access the `OrderSpecifier` instances. + +**limit, offset, restrict:** Set the paging of the result. `limit` for max +results, `offset` for skipping rows, and `restrict` for defining both in one +call. + +**load:** Select specific fields to load from the index instead of loading +the entire document. + +## Ordering + +```java +query + .where(doc.title.like("*")) + .orderBy(doc.title.asc(), doc.year.desc()) + .fetch(); +``` + +This is equivalent to the Lucene query `title:*` with results sorted ascending +by title and descending by year. + +Alternatively, use a `Sort` instance directly: + +```java +Sort sort = ...; +query + .where(doc.title.like("*")) + .sort(sort) + .fetch(); +``` + +## Limit + +```java +query + .where(doc.title.like("*")) + .limit(10) + .fetch(); +``` + +## Offset + +```java +query + .where(doc.title.like("*")) + .offset(3) + .fetch(); +``` + +## Field Selection + +Load only specific fields from the index: + +```java +query + .where(doc.title.ne("")) + .load(doc.title) + .fetch(); +``` + +## Fuzzy Searches + +Fuzzy searches can be expressed via `fuzzyLike` methods in the +`LuceneExpressions` class: + +```java +query + .where(LuceneExpressions.fuzzyLike(doc.title, "Hello")) + .fetch(); +``` + +You can also control the maximum edit distance and prefix length: + +```java +query + .where(LuceneExpressions.fuzzyLike(doc.title, "Hello", 2, 0)) + .fetch(); +``` + +## Applying Lucene Filters + +Apply a native Lucene `Query` as a filter: + +```java +query + .where(doc.title.like("*")) + .filter(IntPoint.newExactQuery("year", 1990)) + .fetch(); +``` diff --git a/pom.xml b/pom.xml index 098883cb2..b74d4f3d7 100644 --- a/pom.xml +++ b/pom.xml @@ -878,6 +878,14 @@ Lucene 5 com.querydsl.lucene5* + + Lucene 9 + com.querydsl.lucene9* + + + Lucene 10 + com.querydsl.lucene10* + Hibernate Search com.querydsl.hibernate.search* diff --git a/querydsl-libraries/pom.xml b/querydsl-libraries/pom.xml index 73b166c66..b8f927bcb 100644 --- a/querydsl-libraries/pom.xml +++ b/querydsl-libraries/pom.xml @@ -36,6 +36,10 @@ querydsl-scala querydsl-kotlin + + + querydsl-lucene9 + querydsl-lucene10 diff --git a/querydsl-libraries/querydsl-lucene10/README.md b/querydsl-libraries/querydsl-lucene10/README.md new file mode 100644 index 000000000..bdeb11981 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/README.md @@ -0,0 +1,57 @@ +## Querydsl Lucene 10 + +The Lucene module provides integration with the Lucene 10 indexing library. + +**Maven integration** + + Add the following dependencies to your Maven project : +```XML + + io.github.openfeign.querydsl + querydsl-lucene10 + ${querydsl.version} + +``` + +**Creating the query types** + +With fields year and title a manually created query type could look something like this: + +```JAVA +public class QDocument extends EntityPathBase{ + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final StringPath year = createString("year"); + + public final StringPath title = createString("title"); +} +``` + +QDocument represents a Lucene document with the fields year and title. + +Code generation is not available for Lucene, since no schema data is available. + +**Querying** + +Querying with Querydsl Lucene is as simple as this: + +```JAVA +QDocument doc = new QDocument("doc"); + +IndexSearcher searcher = new IndexSearcher(index); +LuceneQuery query = new LuceneQuery(true, searcher); +List documents = query + .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle")) + .fetch(); +``` + +which is transformed into the following Lucene query : +``` ++year:[1800 TO 2000] +title:huckle* +``` + +For more information on the Querydsl Lucene module visit the reference documentation http://www.querydsl.com/static/querydsl/latest/reference/html/ch02s05.html diff --git a/querydsl-libraries/querydsl-lucene10/pom.xml b/querydsl-libraries/querydsl-lucene10/pom.xml new file mode 100644 index 000000000..2db58c233 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + io.github.openfeign.querydsl + querydsl-libraries + 7.2-SNAPSHOT + + + querydsl-lucene10 + Querydsl - Lucene 10 support + Lucene 10 support for Querydsl + + + 10.3.2 + 21 + org.apache.lucene.*;version="[10.0.0,11)", + ${osgi.import.package.root} + + + + + org.jetbrains + annotations + provided + + + org.apache.lucene + lucene-core + ${lucene.version} + provided + + + org.apache.lucene + lucene-analysis-common + ${lucene.version} + provided + + + org.apache.lucene + lucene-queryparser + ${lucene.version} + provided + + + io.github.openfeign.querydsl + querydsl-core + ${project.version} + + + + + io.github.openfeign.querydsl + querydsl-core + ${project.version} + test-jar + test + + + + io.github.openfeign.querydsl + querydsl-apt + ${project.version} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.querydsl.lucene10 + + + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + com.querydsl.apt.QuerydslAnnotationProcessor + + + + + + diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/AbstractLuceneQuery.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/AbstractLuceneQuery.java new file mode 100644 index 000000000..c321326eb --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/AbstractLuceneQuery.java @@ -0,0 +1,352 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.CloseableIterator; +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.Fetchable; +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.QueryException; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.SimpleQuery; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TotalHitCountCollectorManager; +import org.jetbrains.annotations.Nullable; + +/** + * AbstractLuceneQuery is an abstract super class for Lucene query implementations + * + * @author tiwe + * @param projection type + * @param concrete subtype of querydsl + */ +public abstract class AbstractLuceneQuery> + implements SimpleQuery, Fetchable { + + private static final String JAVA_ISO_CONTROL = "[\\p{Cntrl}&&[^\r\n\t]]"; + + private final QueryMixin queryMixin; + + private final IndexSearcher searcher; + + private final LuceneSerializer serializer; + + private final Function transformer; + + @Nullable private Set fieldsToLoad; + + private List filters = Collections.emptyList(); + + @Nullable private Query filter; + + @Nullable private Sort querySort; + + @SuppressWarnings("unchecked") + public AbstractLuceneQuery( + LuceneSerializer serializer, IndexSearcher searcher, Function transformer) { + queryMixin = new QueryMixin((Q) this, new DefaultQueryMetadata()); + this.serializer = serializer; + this.searcher = searcher; + this.transformer = transformer; + } + + public AbstractLuceneQuery(IndexSearcher searcher, Function transformer) { + this(LuceneSerializer.DEFAULT, searcher, transformer); + } + + private long innerCount() { + try { + final int maxDoc = searcher.getIndexReader().maxDoc(); + if (maxDoc == 0) { + return 0; + } + return searcher.search( + createQuery(), new TotalHitCountCollectorManager(searcher.getSlices())); + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + } + + @Override + public long fetchCount() { + return innerCount(); + } + + protected Query createQuery() { + Query originalQuery; + if (queryMixin.getMetadata().getWhere() == null) { + originalQuery = new MatchAllDocsQuery(); + } else { + originalQuery = + serializer.toQuery(queryMixin.getMetadata().getWhere(), queryMixin.getMetadata()); + } + Query filter = getFilter(); + if (filter != null) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(originalQuery, Occur.MUST); + builder.add(filter, Occur.FILTER); + return builder.build(); + } + return originalQuery; + } + + @Override + public Q distinct() { + throw new UnsupportedOperationException("use distinct(path) instead"); + } + + /** + * Apply the given Lucene Query as a filter to the search results + * + * @param filter filter query + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q filter(Query filter) { + if (filters.isEmpty()) { + this.filter = filter; + filters = Collections.singletonList(filter); + } else { + this.filter = null; + if (filters.size() == 1) { + filters = new ArrayList<>(); + } + filters.add(filter); + } + return (Q) this; + } + + private Query getFilter() { + if (filter == null && !filters.isEmpty()) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (Query f : filters) { + builder.add(f, Occur.SHOULD); + } + filter = builder.build(); + } + return filter; + } + + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + @Override + public CloseableIterator iterate() { + final QueryMetadata metadata = queryMixin.getMetadata(); + final List> orderBys = metadata.getOrderBy(); + final Integer queryLimit = metadata.getModifiers().getLimitAsInteger(); + final Integer queryOffset = metadata.getModifiers().getOffsetAsInteger(); + Sort sort = querySort; + int limit; + final int offset = queryOffset != null ? queryOffset : 0; + try { + limit = maxDoc(); + if (limit == 0) { + return CloseableIterator.of(Collections.emptyIterator()); + } + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + if (queryLimit != null && queryLimit < limit) { + limit = queryLimit; + } + if (sort == null && !orderBys.isEmpty()) { + sort = serializer.toSort(orderBys); + } + + try { + ScoreDoc[] scoreDocs; + int sumOfLimitAndOffset = limit + offset; + if (sumOfLimitAndOffset < 1) { + throw new QueryException( + "The given limit (" + + limit + + ") and offset (" + + offset + + ") cause an integer overflow."); + } + if (sort != null) { + scoreDocs = searcher.search(createQuery(), sumOfLimitAndOffset, sort).scoreDocs; + } else { + scoreDocs = searcher.search(createQuery(), sumOfLimitAndOffset, Sort.INDEXORDER).scoreDocs; + } + if (offset < scoreDocs.length) { + return new ResultIterator(scoreDocs, offset, searcher, fieldsToLoad, transformer); + } + return CloseableIterator.of(Collections.emptyIterator()); + } catch (final IOException e) { + throw new QueryException(e); + } + } + + private List innerList() { + return CloseableIterator.asList(iterate()); + } + + @Override + public List fetch() { + return innerList(); + } + + /** + * Set the given fields to load + * + * @param fieldsToLoad fields to load + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q load(Set fieldsToLoad) { + this.fieldsToLoad = fieldsToLoad; + return (Q) this; + } + + /** + * Load only the fields of the given paths + * + * @param paths fields to load + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q load(Path... paths) { + Set fields = new HashSet(); + for (Path path : paths) { + fields.add(serializer.toField(path)); + } + this.fieldsToLoad = fields; + return (Q) this; + } + + @Override + public QueryResults fetchResults() { + List documents = innerList(); + return new QueryResults(documents, queryMixin.getMetadata().getModifiers(), innerCount()); + } + + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + public Q orderBy(OrderSpecifier o) { + return queryMixin.orderBy(o); + } + + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + @Override + public

Q set(ParamExpression

param, P value) { + return queryMixin.set(param, value); + } + + @SuppressWarnings("unchecked") + public Q sort(Sort sort) { + this.querySort = sort; + return (Q) this; + } + + @Nullable + private T oneResult(boolean unique) { + try { + int maxDoc = maxDoc(); + if (maxDoc == 0) { + return null; + } + final ScoreDoc[] scoreDocs = + searcher.search(createQuery(), maxDoc, Sort.INDEXORDER).scoreDocs; + int index = 0; + QueryModifiers modifiers = queryMixin.getMetadata().getModifiers(); + Long offset = modifiers.getOffset(); + if (offset != null) { + index = offset.intValue(); + } + Long limit = modifiers.getLimit(); + if (unique + && (limit == null ? scoreDocs.length - index > 1 : limit > 1 && scoreDocs.length > 1)) { + throw new NonUniqueResultException( + "Unique result requested, but " + scoreDocs.length + " found."); + } else if (scoreDocs.length > index) { + Document document; + if (fieldsToLoad != null) { + document = searcher.storedFields().document(scoreDocs[index].doc, fieldsToLoad); + } else { + document = searcher.storedFields().document(scoreDocs[index].doc); + } + return transformer.apply(document); + } else { + return null; + } + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + } + + @Override + public T fetchFirst() { + return oneResult(false); + } + + @Override + public T fetchOne() throws NonUniqueResultException { + return oneResult(true); + } + + public Q where(Predicate e) { + return queryMixin.where(e); + } + + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + @Override + public String toString() { + return createQuery().toString().replaceAll(JAVA_ISO_CONTROL, "_"); + } + + private int maxDoc() throws IOException { + return searcher.getIndexReader().maxDoc(); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/IgnoreCaseUnsupportedException.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/IgnoreCaseUnsupportedException.java new file mode 100644 index 000000000..7d6fd71d5 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/IgnoreCaseUnsupportedException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +/** + * Thrown for case ignore usage + * + * @author tiwe + */ +public class IgnoreCaseUnsupportedException extends UnsupportedOperationException { + + private static final long serialVersionUID = 412913389929530788L; + + public IgnoreCaseUnsupportedException() { + super("Ignore case queries are not supported with Lucene"); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneExpressions.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneExpressions.java new file mode 100644 index 000000000..27ad3d5bd --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneExpressions.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.util.automaton.LevenshteinAutomata; + +/** + * Utility methods to create filter expressions for Lucene queries that are not covered by the + * Querydsl standard expression model + * + * @author tiwe + */ +public final class LuceneExpressions { + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @return condition + */ + public static BooleanExpression fuzzyLike(Path path, String value) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term)); + } + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @param maxEdits must be >= 0 and <= {@link + * LevenshteinAutomata#MAXIMUM_SUPPORTED_DISTANCE}. + * @return condition + */ + public static BooleanExpression fuzzyLike(Path path, String value, int maxEdits) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term, maxEdits)); + } + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @param maxEdits must be >= 0 and <= {@link + * LevenshteinAutomata#MAXIMUM_SUPPORTED_DISTANCE}. + * @param prefixLength length of common (non-fuzzy) prefix + * @return condition + */ + public static BooleanExpression fuzzyLike( + Path path, String value, int maxEdits, int prefixLength) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term, maxEdits, prefixLength)); + } + + private LuceneExpressions() {} +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneOps.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneOps.java new file mode 100644 index 000000000..936e299c2 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneOps.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.Operator; + +/** + * Lucene specific operators + * + * @author tiwe + */ +public enum LuceneOps implements Operator { + LUCENE_QUERY(Object.class), + PHRASE(String.class), + TERM(String.class); + + private final Class type; + + LuceneOps(Class type) { + this.type = type; + } + + @Override + public Class getType() { + return type; + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneQuery.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneQuery.java new file mode 100644 index 000000000..8617228bc --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; + +/** + * {@code LuceneQuery} is a Querydsl query implementation for Lucene queries. + * + *

Example: + * + *

{@code
+ * QDocument doc = new QDocument("doc");
+ * IndexSearcher searcher = new IndexSearcher(index);
+ * LuceneQuery query = new LuceneQuery(true, searcher);
+ * List documents = query
+ *     .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle"))
+ *     .fetch();
+ * }
+ * + * @author vema + */ +public class LuceneQuery extends AbstractLuceneQuery { + + private static final Function TRANSFORMER = + new Function() { + @Override + public Document apply(Document input) { + return input; + } + }; + + public LuceneQuery(IndexSearcher searcher) { + super(searcher, TRANSFORMER); + } + + public LuceneQuery(LuceneSerializer luceneSerializer, IndexSearcher searcher) { + super(luceneSerializer, searcher, TRANSFORMER); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneSerializer.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneSerializer.java new file mode 100644 index 000000000..a8a69ce60 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/LuceneSerializer.java @@ -0,0 +1,572 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.Constant; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Operator; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.ParamNotSetException; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathType; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedNumericSortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.WildcardQuery; +import org.jetbrains.annotations.Nullable; + +/** + * Serializes Querydsl queries to Lucene queries. + * + * @author vema + */ +public class LuceneSerializer { + private static final Map, SortField.Type> sortFields = + new HashMap, SortField.Type>(); + + static { + sortFields.put(Integer.class, SortField.Type.INT); + sortFields.put(Float.class, SortField.Type.FLOAT); + sortFields.put(Long.class, SortField.Type.LONG); + sortFields.put(Double.class, SortField.Type.DOUBLE); + sortFields.put(BigDecimal.class, SortField.Type.DOUBLE); + sortFields.put(BigInteger.class, SortField.Type.LONG); + } + + public static final LuceneSerializer DEFAULT = new LuceneSerializer(false, true); + + private final boolean lowerCase; + + private final boolean splitTerms; + + private final Locale sortLocale; + + public LuceneSerializer(boolean lowerCase, boolean splitTerms) { + this(lowerCase, splitTerms, Locale.getDefault()); + } + + public LuceneSerializer(boolean lowerCase, boolean splitTerms, Locale sortLocale) { + this.lowerCase = lowerCase; + this.splitTerms = splitTerms; + this.sortLocale = sortLocale; + } + + private Query toQuery(Operation operation, QueryMetadata metadata) { + Operator op = operation.getOperator(); + if (op == Ops.OR) { + return toTwoHandSidedQuery(operation, Occur.SHOULD, metadata); + } else if (op == Ops.AND) { + return toTwoHandSidedQuery(operation, Occur.MUST, metadata); + } else if (op == Ops.NOT) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(toQuery(operation.getArg(0), metadata), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } else if (op == Ops.LIKE) { + return like(operation, metadata); + } else if (op == Ops.LIKE_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.EQ) { + return eq(operation, metadata, false); + } else if (op == Ops.EQ_IGNORE_CASE) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.NE) { + return ne(operation, metadata, false); + } else if (op == Ops.STARTS_WITH) { + return startsWith(metadata, operation, false); + } else if (op == Ops.STARTS_WITH_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.ENDS_WITH) { + return endsWith(operation, metadata, false); + } else if (op == Ops.ENDS_WITH_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.STRING_CONTAINS) { + return stringContains(operation, metadata, false); + } else if (op == Ops.STRING_CONTAINS_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.BETWEEN) { + return between(operation, metadata); + } else if (op == Ops.IN) { + return in(operation, metadata, false); + } else if (op == Ops.NOT_IN) { + return notIn(operation, metadata, false); + } else if (op == Ops.LT) { + return lt(operation, metadata); + } else if (op == Ops.GT) { + return gt(operation, metadata); + } else if (op == Ops.LOE) { + return le(operation, metadata); + } else if (op == Ops.GOE) { + return ge(operation, metadata); + } else if (op == LuceneOps.LUCENE_QUERY) { + @SuppressWarnings("unchecked") + Constant expectedConstant = (Constant) operation.getArg(0); + return expectedConstant.getConstant(); + } + throw new UnsupportedOperationException("Illegal operation " + operation); + } + + private Query toTwoHandSidedQuery(Operation operation, Occur occur, QueryMetadata metadata) { + Query lhs = toQuery(operation.getArg(0), metadata); + Query rhs = toQuery(operation.getArg(1), metadata); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(createBooleanClause(lhs, occur)); + builder.add(createBooleanClause(rhs, occur)); + return builder.build(); + } + + /** + * If the query is a BooleanQuery and it contains a single Occur.MUST_NOT clause it will be + * returned as is. Otherwise it will be wrapped in a BooleanClause with the given Occur. + */ + private BooleanClause createBooleanClause(Query query, Occur occur) { + if (query instanceof BooleanQuery booleanQuery) { + List clauses = booleanQuery.clauses(); + if (clauses.size() == 1 && clauses.get(0).occur().equals(Occur.MUST_NOT)) { + return clauses.get(0); + } + } + return new BooleanClause(query, occur); + } + + protected Query like(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convert(path, operation.getArg(1)); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String s : terms) { + builder.add(new WildcardQuery(new Term(field, "*" + s + "*")), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, terms[0])); + } + + protected Query eq(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + + if (Number.class.isAssignableFrom(operation.getArg(1).getType())) { + @SuppressWarnings("unchecked") + Constant rightArg = (Constant) operation.getArg(1); + return exactNumberQuery(field, rightArg.getConstant()); + } + + return eq(field, convert(path, operation.getArg(1), metadata), ignoreCase); + } + + private Query exactNumberQuery(String field, Number number) { + if (Integer.class.isInstance(number) + || Byte.class.isInstance(number) + || Short.class.isInstance(number)) { + return IntPoint.newExactQuery(field, number.intValue()); + } else if (Long.class.isInstance(number) || BigInteger.class.isInstance(number)) { + return LongPoint.newExactQuery(field, number.longValue()); + } else if (Double.class.isInstance(number) || BigDecimal.class.isInstance(number)) { + return DoublePoint.newExactQuery(field, number.doubleValue()); + } else if (Float.class.isInstance(number)) { + return FloatPoint.newExactQuery(field, number.floatValue()); + } else { + throw new IllegalArgumentException("Unsupported numeric type " + number.getClass().getName()); + } + } + + protected Query eq(String field, String[] terms, boolean ignoreCase) { + if (terms.length > 1) { + PhraseQuery.Builder builder = new PhraseQuery.Builder(); + for (String s : terms) { + builder.add(new Term(field, s)); + } + return builder.build(); + } + return new TermQuery(new Term(field, terms[0])); + } + + protected Query in(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + Path path = getPath(operation.getArg(0)); + String field = toField(path); + @SuppressWarnings("unchecked") + Constant> expectedConstant = (Constant>) operation.getArg(1); + Collection values = expectedConstant.getConstant(); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + if (Number.class.isAssignableFrom(path.getType())) { + for (Object value : values) { + builder.add(exactNumberQuery(field, (Number) value), Occur.SHOULD); + } + } else { + for (Object value : values) { + String[] str = convert(path, value); + builder.add(eq(field, str, ignoreCase), Occur.SHOULD); + } + } + return builder.build(); + } + + protected Query notIn(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(in(operation, metadata, false), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } + + protected Query ne(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(eq(operation, metadata, ignoreCase), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } + + protected Query startsWith(QueryMetadata metadata, Operation operation, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i = 0; i < terms.length; ++i) { + String s = i == 0 ? terms[i] + "*" : "*" + terms[i] + "*"; + builder.add(new WildcardQuery(new Term(field, s)), Occur.MUST); + } + return builder.build(); + } + return new PrefixQuery(new Term(field, terms[0])); + } + + protected Query stringContains( + Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String s : terms) { + builder.add(new WildcardQuery(new Term(field, "*" + s + "*")), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, "*" + terms[0] + "*")); + } + + protected Query endsWith(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i = 0; i < terms.length; ++i) { + String s = i == terms.length - 1 ? "*" + terms[i] : "*" + terms[i] + "*"; + builder.add(new WildcardQuery(new Term(field, s)), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, "*" + terms[0])); + } + + protected Query between(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range( + path, toField(path), operation.getArg(1), operation.getArg(2), true, true, metadata); + } + + protected Query lt(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), null, operation.getArg(1), false, false, metadata); + } + + protected Query gt(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), operation.getArg(1), null, false, false, metadata); + } + + protected Query le(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), null, operation.getArg(1), true, true, metadata); + } + + protected Query ge(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), operation.getArg(1), null, true, true, metadata); + } + + protected Query range( + Path leftHandSide, + String field, + @Nullable Expression min, + @Nullable Expression max, + boolean minInc, + boolean maxInc, + QueryMetadata metadata) { + if (min != null && Number.class.isAssignableFrom(min.getType()) + || max != null && Number.class.isAssignableFrom(max.getType())) { + @SuppressWarnings("unchecked") + Constant minConstant = (Constant) min; + @SuppressWarnings("unchecked") + Constant maxConstant = (Constant) max; + + Class numType = + minConstant != null ? minConstant.getType() : maxConstant.getType(); + @SuppressWarnings("unchecked") + Class unboundedNumType = (Class) numType; + return numericRange( + unboundedNumType, + field, + minConstant == null ? null : minConstant.getConstant(), + maxConstant == null ? null : maxConstant.getConstant(), + minInc, + maxInc); + } + return stringRange(leftHandSide, field, min, max, minInc, maxInc, metadata); + } + + protected Query numericRange( + Class clazz, + String field, + @Nullable N min, + @Nullable N max, + boolean minInc, + boolean maxInc) { + if (clazz.equals(Integer.class) || clazz.equals(Byte.class) || clazz.equals(Short.class)) { + int lower = min != null ? adjustBound(min.intValue(), minInc, true) : Integer.MIN_VALUE; + int upper = max != null ? adjustBound(max.intValue(), maxInc, false) : Integer.MAX_VALUE; + return IntPoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Long.class)) { + long lower = min != null ? adjustBound(min.longValue(), minInc, true) : Long.MIN_VALUE; + long upper = max != null ? adjustBound(max.longValue(), maxInc, false) : Long.MAX_VALUE; + return LongPoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Double.class)) { + double lower = + min != null + ? (minInc ? min.doubleValue() : Math.nextUp(min.doubleValue())) + : Double.NEGATIVE_INFINITY; + double upper = + max != null + ? (maxInc ? max.doubleValue() : Math.nextDown(max.doubleValue())) + : Double.POSITIVE_INFINITY; + return DoublePoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Float.class)) { + float lower = + min != null + ? (minInc ? min.floatValue() : Math.nextUp(min.floatValue())) + : Float.NEGATIVE_INFINITY; + float upper = + max != null + ? (maxInc ? max.floatValue() : Math.nextDown(max.floatValue())) + : Float.POSITIVE_INFINITY; + return FloatPoint.newRangeQuery(field, lower, upper); + } else { + throw new IllegalArgumentException("Unsupported numeric type " + clazz.getName()); + } + } + + private int adjustBound(int value, boolean inclusive, boolean isLower) { + if (inclusive) { + return value; + } + return isLower ? Math.addExact(value, 1) : Math.addExact(value, -1); + } + + private long adjustBound(long value, boolean inclusive, boolean isLower) { + if (inclusive) { + return value; + } + return isLower ? Math.addExact(value, 1L) : Math.addExact(value, -1L); + } + + protected Query stringRange( + Path leftHandSide, + String field, + @Nullable Expression min, + @Nullable Expression max, + boolean minInc, + boolean maxInc, + QueryMetadata metadata) { + + if (min == null) { + return TermRangeQuery.newStringRange( + field, null, convert(leftHandSide, max, metadata)[0], minInc, maxInc); + } else if (max == null) { + return TermRangeQuery.newStringRange( + field, convert(leftHandSide, min, metadata)[0], null, minInc, maxInc); + } else { + return TermRangeQuery.newStringRange( + field, + convert(leftHandSide, min, metadata)[0], + convert(leftHandSide, max, metadata)[0], + minInc, + maxInc); + } + } + + private Path getPath(Expression leftHandSide) { + if (leftHandSide instanceof Path) { + return (Path) leftHandSide; + } else if (leftHandSide instanceof Operation) { + Operation operation = (Operation) leftHandSide; + if (operation.getOperator() == Ops.LOWER || operation.getOperator() == Ops.UPPER) { + return (Path) operation.getArg(0); + } + } + throw new IllegalArgumentException("Unable to transform " + leftHandSide + " to path"); + } + + /** + * template method, override to customize + * + * @param path path + * @return field name + */ + protected String toField(Path path) { + PathMetadata md = path.getMetadata(); + if (md.getPathType() == PathType.COLLECTION_ANY) { + return toField(md.getParent()); + } else { + String rv = md.getName(); + if (md.getParent() != null) { + Path parent = md.getParent(); + if (parent.getMetadata().getPathType() != PathType.VARIABLE) { + rv = toField(parent) + "." + rv; + } + } + return rv; + } + } + + private void verifyArguments(Operation operation) { + List> arguments = operation.getArgs(); + for (int i = 1; i < arguments.size(); ++i) { + if (!(arguments.get(i) instanceof Constant) + && !(arguments.get(i) instanceof ParamExpression) + && !(arguments.get(i) instanceof PhraseElement) + && !(arguments.get(i) instanceof TermElement)) { + throw new IllegalArgumentException( + "operand was of unsupported type " + arguments.get(i).getClass().getName()); + } + } + } + + protected String[] convert( + Path leftHandSide, Expression rightHandSide, QueryMetadata metadata) { + if (rightHandSide instanceof Operation) { + Operation operation = (Operation) rightHandSide; + if (operation.getOperator() == LuceneOps.PHRASE) { + return operation.getArg(0).toString().split("\\s+"); + } else if (operation.getOperator() == LuceneOps.TERM) { + return new String[] {operation.getArg(0).toString()}; + } else { + throw new IllegalArgumentException(rightHandSide.toString()); + } + } else if (rightHandSide instanceof ParamExpression) { + Object value = metadata.getParams().get(rightHandSide); + if (value == null) { + throw new ParamNotSetException((ParamExpression) rightHandSide); + } + return convert(leftHandSide, value); + + } else if (rightHandSide instanceof Constant) { + return convert(leftHandSide, ((Constant) rightHandSide).getConstant()); + } else { + throw new IllegalArgumentException(rightHandSide.toString()); + } + } + + protected String[] convert(Path leftHandSide, Object rightHandSide) { + String str = rightHandSide.toString(); + if (lowerCase) { + str = str.toLowerCase(); + } + if (splitTerms) { + if (str.equals("")) { + return new String[] {str}; + } else { + return str.split("\\s+"); + } + } else { + return new String[] {str}; + } + } + + private String[] convertEscaped( + Path leftHandSide, Expression rightHandSide, QueryMetadata metadata) { + String[] str = convert(leftHandSide, rightHandSide, metadata); + for (int i = 0; i < str.length; i++) { + str[i] = QueryParser.escape(str[i]); + } + return str; + } + + public Query toQuery(Expression expr, QueryMetadata metadata) { + if (expr instanceof Operation) { + return toQuery((Operation) expr, metadata); + } else { + return toQuery(ExpressionUtils.extract(expr), metadata); + } + } + + public Sort toSort(List> orderBys) { + List sorts = new ArrayList(orderBys.size()); + for (OrderSpecifier order : orderBys) { + if (!(order.getTarget() instanceof Path)) { + throw new IllegalArgumentException("argument was not of type Path."); + } + Class type = order.getTarget().getType(); + boolean reverse = !order.isAscending(); + Path path = getPath(order.getTarget()); + if (Number.class.isAssignableFrom(type)) { + sorts.add(new SortedNumericSortField(toField(path), sortFields.get(type), reverse)); + } else { + sorts.add(new SortField(toField(path), SortField.Type.STRING, reverse)); + } + } + return new Sort(sorts.toArray(new SortField[0])); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/PhraseElement.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/PhraseElement.java new file mode 100644 index 000000000..f1dab7193 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/PhraseElement.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.StringOperation; + +/** + * {@code PhraseElement} represents the embedded String as a phrase + * + * @author tiwe + */ +public class PhraseElement extends StringOperation { + + private static final long serialVersionUID = 2350215644019186076L; + + public PhraseElement(String str) { + super(LuceneOps.PHRASE, ConstantImpl.create(str)); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/QueryElement.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/QueryElement.java new file mode 100644 index 000000000..1983c39a7 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/QueryElement.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.BooleanOperation; +import org.apache.lucene.search.Query; + +/** + * {@code QueryElement} wraps a Lucene Query + * + * @author tiwe + */ +public class QueryElement extends BooleanOperation { + + private static final long serialVersionUID = 470868107363840155L; + + public QueryElement(Query query) { + super(LuceneOps.LUCENE_QUERY, ConstantImpl.create(query)); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/ResultIterator.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/ResultIterator.java new file mode 100644 index 000000000..00ba4ede4 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/ResultIterator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.CloseableIterator; +import com.querydsl.core.QueryException; +import java.io.IOException; +import java.util.Set; +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ScoreDoc; +import org.jetbrains.annotations.Nullable; + +/** + * {@code ResultIterator} is a {@link CloseableIterator} implementation for Lucene query results + * + * @author tiwe + * @param + */ +public final class ResultIterator implements CloseableIterator { + + private final ScoreDoc[] scoreDocs; + + private int cursor; + + private final IndexSearcher searcher; + + @Nullable private final Set fieldsToLoad; + + private final Function transformer; + + public ResultIterator( + ScoreDoc[] scoreDocs, + int offset, + IndexSearcher searcher, + @Nullable Set fieldsToLoad, + Function transformer) { + this.scoreDocs = scoreDocs.clone(); + this.cursor = offset; + this.searcher = searcher; + this.fieldsToLoad = fieldsToLoad; + this.transformer = transformer; + } + + @Override + public boolean hasNext() { + return cursor != scoreDocs.length; + } + + @Override + public T next() { + try { + Document document; + if (fieldsToLoad != null) { + document = searcher.storedFields().document(scoreDocs[cursor++].doc, fieldsToLoad); + } else { + document = searcher.storedFields().document(scoreDocs[cursor++].doc); + } + return transformer.apply(document); + } catch (IOException e) { + throw new QueryException(e); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() {} +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TermElement.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TermElement.java new file mode 100644 index 000000000..8fa5a599d --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TermElement.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.StringOperation; + +/** + * {@code TermElement} represents the embedded String as a term + * + * @author tiwe + */ +public class TermElement extends StringOperation { + + private static final long serialVersionUID = 2350215644019186076L; + + public TermElement(String str) { + super(LuceneOps.TERM, ConstantImpl.create(str)); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TypedQuery.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TypedQuery.java new file mode 100644 index 000000000..da26bf825 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/TypedQuery.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; + +/** + * {@code TypedQuery} is a typed query implementation for Lucene queries. + * + *

Converts Lucene documents to typed results via a constructor supplied transformer + * + * @param result type + * @author laim + * @author tiwe + */ +public class TypedQuery extends AbstractLuceneQuery> { + + /** + * Create a new TypedQuery instance + * + * @param searcher index searcher + * @param transformer transformer to transform Lucene documents to result objects + */ + public TypedQuery(IndexSearcher searcher, Function transformer) { + super(searcher, transformer); + } + + /** + * Create a new TypedQuery instance + * + * @param serializer serializer + * @param searcher index searcher + * @param transformer transformer to transform Lucene documents to result objects + */ + public TypedQuery( + LuceneSerializer serializer, IndexSearcher searcher, Function transformer) { + super(serializer, searcher, transformer); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/package-info.java b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/package-info.java new file mode 100644 index 000000000..cf99c7637 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/main/java/com/querydsl/lucene10/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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. + */ + +/** Lucene 10 support */ +package com.querydsl.lucene10; diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneQueryTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneQueryTest.java new file mode 100644 index 000000000..aa9fb833a --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneQueryTest.java @@ -0,0 +1,608 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.QueryException; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.ParamNotSetException; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.Param; +import com.querydsl.core.types.dsl.StringPath; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.DoubleDocValuesField; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.SortedDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Sort; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.util.BytesRef; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for LuceneQuery + * + * @author vema + */ +public class LuceneQueryTest { + + private LuceneQuery query; + private StringPath title; + private NumberPath year; + private NumberPath gross; + + private final StringPath sort = Expressions.stringPath("sort"); + + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + + private Document createDocument( + final String docTitle, + final String docAuthor, + final String docText, + final int docYear, + final double docGross) { + Document doc = new Document(); + + doc.add(new TextField("title", docTitle, Store.YES)); + doc.add(new SortedDocValuesField("title", new BytesRef(docTitle))); + + doc.add(new TextField("author", docAuthor, Store.YES)); + doc.add(new SortedDocValuesField("author", new BytesRef(docAuthor))); + + doc.add(new TextField("text", docText, Store.YES)); + doc.add(new SortedDocValuesField("text", new BytesRef(docText))); + + doc.add(new IntPoint("year", docYear)); + doc.add(new StoredField("year", docYear)); + doc.add(new NumericDocValuesField("year", docYear)); + + doc.add(new DoublePoint("gross", docGross)); + doc.add(new StoredField("gross", docGross)); + doc.add(new DoubleDocValuesField("gross", docGross)); + + return doc; + } + + @Before + public void setUp() throws Exception { + final QDocument entityPath = new QDocument("doc"); + title = entityPath.title; + year = entityPath.year; + gross = entityPath.gross; + + idx = new ByteBuffersDirectory(); + writer = createWriter(idx); + + writer.addDocument( + createDocument( + "Jurassic Park", "Michael Crichton", "It's a UNIX system! I know this!", 1990, 90.00)); + writer.addDocument( + createDocument( + "Nummisuutarit", "Aleksis Kivi", "ESKO. Ja iloitset ja riemuitset?", 1864, 10.00)); + writer.addDocument( + createDocument( + "The Lord of the Rings", + "John R. R. Tolkien", + "One Ring to rule them all, One Ring to find them, One Ring to bring them all and in" + + " the darkness bind them", + 1954, + 89.00)); + writer.addDocument( + createDocument( + "Introduction to Algorithms", + "Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein", + "Bubble sort", + 1990, + 30.50)); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + query = new LuceneQuery(new LuceneSerializer(true, true), searcher); + } + + private IndexWriter createWriter(ByteBuffersDirectory idx) throws Exception { + IndexWriterConfig config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + return new IndexWriter(idx, config); + } + + @After + public void tearDown() throws Exception { + searcher.getIndexReader().close(); + } + + @Test + public void between() { + assertThat(query.where(year.between(1950, 1990)).fetchCount()).isEqualTo(3); + } + + @Test + public void count_empty_where_clause() { + assertThat(query.fetchCount()).isEqualTo(4); + } + + @Test + public void exists() { + assertThat(query.where(title.eq("Jurassic Park")).fetchCount() > 0).isTrue(); + assertThat(query.where(title.eq("Jurassic Park X")).fetchCount() > 0).isFalse(); + } + + @Test + public void notExists() { + assertThat(query.where(title.eq("Jurassic Park")).fetchCount() == 0).isFalse(); + assertThat(query.where(title.eq("Jurassic Park X")).fetchCount() == 0).isTrue(); + } + + @Test + public void count() { + query.where(title.eq("Jurassic Park")); + assertThat(query.fetchCount()).isEqualTo(1); + } + + @Test(expected = UnsupportedOperationException.class) + public void countDistinct() { + query.where(year.between(1900, 3000)); + assertThat(query.distinct().fetchCount()).isEqualTo(3); + } + + @Test + public void in() { + assertThat(query.where(title.in("Jurassic Park", "Nummisuutarit")).fetchCount()).isEqualTo(2); + } + + @Test + public void in2() { + assertThat(query.where(year.in(1990, 1864)).fetchCount()).isEqualTo(3); + } + + @Test + public void list_sorted_by_year_ascending() { + query.where(year.between(1800, 2000)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted() { + query.where(year.between(1800, 2000)); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted_limit_2() { + query.where(year.between(1800, 2000)); + query.limit(2); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_by_year_limit_1() { + query.where(year.between(1800, 2000)); + query.limit(1); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(1); + } + + @Test + public void list_not_sorted_offset_2() { + query.where(year.between(1800, 2000)); + query.offset(2); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year_offset_2() { + query.where(year.between(1800, 2000)); + query.offset(2); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year_restrict_limit_2_offset_1() { + query.where(year.between(1800, 2000)); + query.restrict(new QueryModifiers(2L, 1L)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year() { + query.where(year.between(1800, 2000)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sort() { + Sort sort = LuceneSerializer.DEFAULT.toSort(Collections.singletonList(year.asc())); + + query.where(year.between(1800, 2000)); + query.sort(sort); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_with_filter() { + assertThat(query.fetch()).hasSize(4); + assertThat(query.filter(IntPoint.newExactQuery("year", 1990)).fetch()).hasSize(2); + } + + @Test + public void list_sorted_descending_by_year() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_gross() { + query.where(gross.between(0.0, 1000.00)); + query.orderBy(gross.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_year_and_ascending_by_title() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + query.orderBy(title.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_year_and_descending_by_title() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + query.orderBy(title.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void offset() { + assertThat(query.where(title.eq("Jurassic Park")).offset(30).fetch()).isEmpty(); + } + + @Test + public void load_list() { + Document document = query.where(title.ne("")).load(title).fetch().get(0); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_list_fieldSelector() { + Document document = + query.where(title.ne("")).load(Collections.singleton("title")).fetch().get(0); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_singleResult() { + Document document = query.where(title.ne("")).load(title).fetchFirst(); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_singleResult_fieldSelector() { + Document document = query.where(title.ne("")).load(Collections.singleton("title")).fetchFirst(); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void singleResult() { + assertThat(query.where(title.ne("")).fetchFirst()).isNotNull(); + } + + @Test + public void single_result_takes_limit() { + assertThat(query.where(title.ne("")).limit(1).fetchFirst().get("title")) + .isEqualTo("Jurassic Park"); + } + + @Test + public void single_result_considers_limit_and_actual_result_size() { + query.where(title.startsWith("Nummi")); + final Document document = query.limit(3).fetchFirst(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void single_result_returns_null_if_nothing_is_in_range() { + query.where(title.startsWith("Nummi")); + assertThat(query.offset(10).fetchFirst()).isNull(); + } + + @Test + public void single_result_considers_offset() { + assertThat(query.where(title.ne("")).offset(3).fetchFirst().get("title")) + .isEqualTo("Introduction to Algorithms"); + } + + @Test + public void single_result_considers_limit_and_offset() { + assertThat(query.where(title.ne("")).limit(1).offset(2).fetchFirst().get("title")) + .isEqualTo("The Lord of the Rings"); + } + + @Test(expected = NonUniqueResultException.class) + public void uniqueResult_contract() { + query.where(title.ne("")).fetchOne(); + } + + @Test + public void unique_result_takes_limit() { + assertThat(query.where(title.ne("")).limit(1).fetchOne().get("title")) + .isEqualTo("Jurassic Park"); + } + + @Test + public void unique_result_considers_limit_and_actual_result_size() { + query.where(title.startsWith("Nummi")); + final Document document = query.limit(3).fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void unique_result_returns_null_if_nothing_is_in_range() { + query.where(title.startsWith("Nummi")); + assertThat(query.offset(10).fetchOne()).isNull(); + } + + @Test + public void unique_result_considers_offset() { + assertThat(query.where(title.ne("")).offset(3).fetchOne().get("title")) + .isEqualTo("Introduction to Algorithms"); + } + + @Test + public void unique_result_considers_limit_and_offset() { + assertThat(query.where(title.ne("")).limit(1).offset(2).fetchOne().get("title")) + .isEqualTo("The Lord of the Rings"); + } + + @Test + public void uniqueResult() { + query.where(title.startsWith("Nummi")); + final Document document = query.fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void uniqueResult_with_param() { + final Param param = new Param(String.class, "title"); + query.set(param, "Nummi"); + query.where(title.startsWith(param)); + final Document document = query.fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test(expected = ParamNotSetException.class) + public void uniqueResult_param_not_set() { + final Param param = new Param(String.class, "title"); + query.where(title.startsWith(param)); + query.fetchOne(); + } + + @Test(expected = QueryException.class) + public void uniqueResult_finds_more_than_one_result() { + query.where(year.eq(1990)); + query.fetchOne(); + } + + @Test + public void uniqueResult_finds_no_results() { + query.where(year.eq(2200)); + assertThat(query.fetchOne()).isNull(); + } + + @Test(expected = UnsupportedOperationException.class) + public void listDistinct() { + query.where(year.between(1900, 2000).or(title.startsWith("Jura"))); + query.orderBy(year.asc()); + final List documents = query.distinct().fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(3); + } + + @Test + public void listResults() { + query.where(year.between(1800, 2000)); + query.restrict(new QueryModifiers(2L, 1L)); + query.orderBy(year.asc()); + final QueryResults results = query.fetchResults(); + assertThat(results.isEmpty()).isFalse(); + assertThat(results.getResults()).hasSize(2); + assertThat(results.getLimit()).isEqualTo(2); + assertThat(results.getOffset()).isEqualTo(1); + assertThat(results.getTotal()).isEqualTo(4); + } + + @Test(expected = UnsupportedOperationException.class) + public void listDistinctResults() { + query.where(year.between(1800, 2000).or(title.eq("The Lord of the Rings"))); + query.restrict(new QueryModifiers(1L, 1L)); + query.orderBy(year.asc()); + final QueryResults results = query.distinct().fetchResults(); + assertThat(results.isEmpty()).isFalse(); + assertThat(results.getResults().get(0).get("year")).isEqualTo("1954"); + assertThat(results.getLimit()).isEqualTo(1); + assertThat(results.getOffset()).isEqualTo(1); + assertThat(results.getTotal()).isEqualTo(4); + } + + @Test + public void list_all() { + final List results = + query.where(title.like("*")).orderBy(title.asc(), year.desc()).fetch(); + assertThat(results).hasSize(4); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_limit_negative() { + query.where(year.between(1800, 2000)); + query.limit(-1); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_limit_negative() { + query.where(year.between(1800, 2000)); + query.limit(-1); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_limit_0() { + query.where(year.between(1800, 2000)); + query.limit(0); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_limit_0() { + query.where(year.between(1800, 2000)); + query.limit(0); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_offset_negative() { + query.where(year.between(1800, 2000)); + query.offset(-1); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_offset_negative() { + query.where(year.between(1800, 2000)); + query.offset(-1); + query.fetch(); + } + + @Test + public void list_sorted_ascending_offset_0() { + query.where(year.between(1800, 2000)); + query.offset(0); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted_offset_0() { + query.where(year.between(1800, 2000)); + query.offset(0); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void iterate() { + query.where(year.between(1800, 2000)); + final Iterator iterator = query.iterate(); + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + ++count; + } + assertThat(count).isEqualTo(4); + } + + @Test + public void all_by_excluding_where() { + assertThat(query.fetch()).hasSize(4); + } + + @Test + public void empty_index_should_return_empty_list() throws Exception { + idx = new ByteBuffersDirectory(); + + writer = createWriter(idx); + writer.close(); + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + query = new LuceneQuery(new LuceneSerializer(true, true), searcher); + assertThat(query.fetch()).isEmpty(); + } + + @Test(expected = QueryException.class) + public void + list_results_throws_an_illegal_argument_exception_when_sum_of_limit_and_offset_is_negative() { + query.limit(1).offset(Integer.MAX_VALUE).fetchResults(); + } + + @Test + public void limit_max_value() { + assertThat(query.limit(Long.MAX_VALUE).fetch()).hasSize(4); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerNotTokenizedTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerNotTokenizedTest.java new file mode 100644 index 000000000..8170af781 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerNotTokenizedTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static com.querydsl.lucene10.QPerson.person; +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import java.time.LocalDate; +import java.util.Arrays; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.junit.Before; +import org.junit.Test; + +public class LuceneSerializerNotTokenizedTest { + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + private LuceneSerializer serializer; + + private final QueryMetadata metadata = new DefaultQueryMetadata(); + + private final Person clooney = new Person("actor_1", "George Clooney", LocalDate.of(1961, 4, 6)); + private final Person pitt = new Person("actor_2", "Brad Pitt", LocalDate.of(1963, 12, 18)); + + private void testQuery(Expression expr, String expectedQuery, int expectedHits) + throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value()).isEqualTo(expectedHits); + assertThat(query.toString()).isEqualTo(expectedQuery); + } + + private Document createDocument(Person person) { + Document doc = new Document(); + doc.add(new StringField("id", person.getId(), Store.YES)); + doc.add(new StringField("name", person.getName(), Store.YES)); + doc.add(new StringField("birthDate", person.getBirthDate().toString(), Store.YES)); + return doc; + } + + @Before + public void before() throws Exception { + serializer = new LuceneSerializer(false, false); + idx = new ByteBuffersDirectory(); + IndexWriterConfig config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + writer = new IndexWriter(idx, config); + + writer.addDocument(createDocument(clooney)); + writer.addDocument(createDocument(pitt)); + + Document document = new Document(); + for (String movie : Arrays.asList("Interview with the Vampire", "Up in the Air")) { + document.add(new StringField("movie", movie, Store.YES)); + } + writer.addDocument(document); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + } + + @Test + public void equals_by_id_matches() throws Exception { + testQuery(person.id.eq("actor_1"), "id:actor_1", 1); + } + + @Test + public void equals_by_id_does_not_match() throws Exception { + testQuery(person.id.eq("actor_8"), "id:actor_8", 0); + } + + @Test + public void equals_by_name_matches() throws Exception { + testQuery(person.name.eq("George Clooney"), "name:George Clooney", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void equals_by_name_ignoring_case_does_not_match() throws Exception { + testQuery(person.name.equalsIgnoreCase("george clooney"), "name:george clooney", 0); + } + + @Test + public void equals_by_name_does_not_match() throws Exception { + testQuery(person.name.eq("George Looney"), "name:George Looney", 0); + } + + @Test + public void starts_with_name_should_match() throws Exception { + testQuery(person.name.startsWith("George C"), "name:George C*", 1); + } + + @Test + public void starts_with_name_should_not_match() throws Exception { + testQuery(person.name.startsWith("George L"), "name:George L*", 0); + } + + @Test + public void ends_with_name_should_match() throws Exception { + testQuery(person.name.endsWith("e Clooney"), "name:*e Clooney", 1); + } + + @Test + public void ends_with_name_should_not_match() throws Exception { + testQuery(person.name.endsWith("e Looney"), "name:*e Looney", 0); + } + + @Test + public void contains_name_should_match() throws Exception { + testQuery(person.name.contains("oney"), "name:*oney*", 1); + } + + @Test + public void contains_name_should_not_match() throws Exception { + testQuery(person.name.contains("bloney"), "name:*bloney*", 0); + } + + @Test + public void in_names_should_match_2() throws Exception { + testQuery( + person.name.in("Brad Pitt", "George Clooney"), "name:Brad Pitt name:George Clooney", 2); + } + + @Test + public void or_by_name_should_match_2() throws Exception { + testQuery( + person.name.eq("Brad Pitt").or(person.name.eq("George Clooney")), + "name:Brad Pitt name:George Clooney", + 2); + } + + @Test + public void equals_by_birth_date() throws Exception { + testQuery(person.birthDate.eq(clooney.getBirthDate()), "birthDate:1961-04-06", 1); + } + + @Test + public void between_phrase() throws Exception { + testQuery( + person.name.between("Brad Pitt", "George Clooney"), + "name:[Brad Pitt TO George Clooney]", + 2); + } + + @Test + public void not_equals_finds_the_actors_and_movies() throws Exception { + testQuery(person.name.ne("Michael Douglas"), "-name:Michael Douglas +*:*", 3); + } + + @Test + public void not_equals_finds_only_clooney_and_movies() throws Exception { + testQuery(person.name.ne("Brad Pitt"), "-name:Brad Pitt +*:*", 2); + } + + @Test + public void and_with_two_not_equals_doesnt_find_the_actors() throws Exception { + testQuery( + person.name.ne("Brad Pitt").and(person.name.ne("George Clooney")), + "+(-name:Brad Pitt +*:*) +(-name:George Clooney +*:*)", + 1); + } + + @Test + public void or_with_two_not_equals_finds_movies_and_actors() throws Exception { + testQuery( + person.name.ne("Brad Pitt").or(person.name.ne("George Clooney")), + "(-name:Brad Pitt +*:*) (-name:George Clooney +*:*)", + 3); + } + + @Test + public void negation_of_equals_finds_movies_and_actors() throws Exception { + testQuery(person.name.eq("Michael Douglas").not(), "-name:Michael Douglas +*:*", 3); + } + + @Test + public void negation_of_equals_finds_pitt_and_movies() throws Exception { + testQuery(person.name.eq("Brad Pitt").not(), "-name:Brad Pitt +*:*", 2); + } + + @Test + public void multiple_field_search_from_movies() throws Exception { + StringPath movie = Expressions.stringPath("movie"); + testQuery(movie.in("Interview with the Vampire"), "movie:Interview with the Vampire", 1); + testQuery(movie.eq("Up in the Air"), "movie:Up in the Air", 1); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerTest.java new file mode 100644 index 000000000..cc63870e2 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/LuceneSerializerTest.java @@ -0,0 +1,715 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.MatchingFiltersFactory; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QuerydslModule; +import com.querydsl.core.StringConstant; +import com.querydsl.core.Target; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Operator; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CollectionPath; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.core.types.dsl.StringPath; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Tests for LuceneSerializer + * + * @author vema + */ +public class LuceneSerializerTest { + private LuceneSerializer serializer; + private PathBuilder entityPath; + private StringPath title; + private StringPath author; + private StringPath text; + private StringPath rating; + private StringPath publisher; + private NumberPath year; + private NumberPath gross; + private CollectionPath titles; + + private NumberPath longField; + private NumberPath shortField; + private NumberPath byteField; + private NumberPath floatField; + + private IndexWriterConfig config; + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + + private static final Set UNSUPPORTED_OPERATORS = + Collections.unmodifiableSet( + EnumSet.of( + Ops.STARTS_WITH_IC, Ops.EQ_IGNORE_CASE, Ops.ENDS_WITH_IC, Ops.STRING_CONTAINS_IC)); + + private final QueryMetadata metadata = new DefaultQueryMetadata(); + + private Document createDocument() { + Document doc = new Document(); + + doc.add(new TextField("title", new StringReader("Jurassic Park"))); + doc.add(new TextField("author", new StringReader("Michael Crichton"))); + doc.add(new TextField("text", new StringReader("It's a UNIX system! I know this!"))); + doc.add(new TextField("rating", new StringReader("Good"))); + doc.add(new StringField("publisher", "", Store.YES)); + doc.add(new IntPoint("year", 1990)); + doc.add(new StoredField("year", 1990)); + doc.add(new DoublePoint("gross", 900.0)); + doc.add(new StoredField("gross", 900.0)); + + doc.add(new LongPoint("longField", 1)); + doc.add(new StoredField("longField", 1L)); + doc.add(new IntPoint("shortField", 1)); + doc.add(new StoredField("shortField", 1)); + doc.add(new IntPoint("byteField", 1)); + doc.add(new StoredField("byteField", 1)); + doc.add(new FloatPoint("floatField", 1)); + doc.add(new StoredField("floatField", 1.0f)); + + return doc; + } + + @Before + public void setUp() throws Exception { + serializer = new LuceneSerializer(true, true); + entityPath = new PathBuilder(Object.class, "obj"); + title = entityPath.getString("title"); + author = entityPath.getString("author"); + text = entityPath.getString("text"); + publisher = entityPath.getString("publisher"); + year = entityPath.getNumber("year", Integer.class); + rating = entityPath.getString("rating"); + gross = entityPath.getNumber("gross", Double.class); + titles = entityPath.getCollection("title", String.class, StringPath.class); + + longField = entityPath.getNumber("longField", Long.class); + shortField = entityPath.getNumber("shortField", Short.class); + byteField = entityPath.getNumber("byteField", Byte.class); + floatField = entityPath.getNumber("floatField", Float.class); + + idx = new ByteBuffersDirectory(); + config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + writer = new IndexWriter(idx, config); + + writer.addDocument(createDocument()); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + } + + @After + public void tearDown() throws Exception { + searcher.getIndexReader().close(); + } + + private void testQuery(Expression expr, int expectedHits) throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value()).isEqualTo(expectedHits); + } + + private void testQuery(Expression expr, String expectedQuery, int expectedHits) + throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value()).isEqualTo(expectedHits); + assertThat(query.toString()).isEqualTo(expectedQuery); + } + + @Test + public void queryElement() throws Exception { + Query query1 = serializer.toQuery(author.like("Michael"), metadata); + Query query2 = serializer.toQuery(text.like("Text"), metadata); + + BooleanExpression query = Expressions.anyOf(new QueryElement(query1), new QueryElement(query2)); + testQuery(query, "author:michael text:text", 1); + } + + @Test + public void like() throws Exception { + testQuery(author.like("*ichael*"), "author:*ichael*", 1); + } + + @Test + public void like_custom_wildcard_single_character() throws Exception { + testQuery(author.like("Mi?hael"), "author:mi?hael", 1); + } + + @Test + public void like_custom_wildcard_multiple_character() throws Exception { + testQuery(text.like("*U*X*"), "text:*u*x*", 1); + } + + @Test + public void like_phrase() throws Exception { + testQuery(title.like("*rassic Par*"), "+title:**rassic* +title:*par**", 1); + } + + @Test + public void like_or_like() throws Exception { + testQuery(title.like("House").or(author.like("*ichae*")), "title:house author:*ichae*", 1); + } + + @Test + public void like_and_like() throws Exception { + testQuery(title.like("*assic*").and(rating.like("G?od")), "+title:*assic* +rating:g?od", 1); + } + + @Test + public void eq() throws Exception { + testQuery(rating.eq("good"), "rating:good", 1); + } + + @Test + public void eq_with_deep_path() throws Exception { + StringPath deepPath = entityPath.get("property1", Object.class).getString("property2"); + testQuery(deepPath.eq("good"), "property1.property2:good", 0); + } + + @Test + public void fuzzyLike() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good"), "rating:Good~2", 1); + } + + @Test + public void fuzzyLike_with_similarity() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good", 2), "rating:Good~2", 1); + } + + @Test + public void fuzzyLike_with_similarity_and_prefix() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good", 2, 0), "rating:Good~2", 1); + } + + @Test + public void eq_numeric_integer() throws Exception { + testQuery(year.eq(1990), 1); + } + + @Test + public void eq_numeric_double() throws Exception { + testQuery(gross.eq(900.00), 1); + } + + @Test + public void eq_numeric() throws Exception { + testQuery(longField.eq(1L), 1); + testQuery(shortField.eq((short) 1), 1); + testQuery(byteField.eq((byte) 1), 1); + testQuery(floatField.eq((float) 1.0), 1); + } + + @Test + public void equals_ignores_case() throws Exception { + testQuery(title.eq("Jurassic"), "title:jurassic", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void title_equals_ignore_case_or_year_equals() throws Exception { + testQuery(title.equalsIgnoreCase("House").or(year.eq(1990)), 1); + } + + @Test + public void eq_and_eq() throws Exception { + testQuery(title.eq("Jurassic Park").and(year.eq(1990)), 1); + } + + @Test + public void eq_and_eq_and_eq() throws Exception { + testQuery(title.eq("Jurassic Park").and(year.eq(1990)).and(author.eq("Michael Crichton")), 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void equals_ignore_case_and_or() throws Exception { + testQuery( + title + .equalsIgnoreCase("Jurassic Park") + .and(rating.equalsIgnoreCase("Bad")) + .or(author.equalsIgnoreCase("Michael Crichton")), + 1); + } + + @Test + public void eq_or_eq_and_eq_does_not_find_results() throws Exception { + testQuery( + title.eq("jeeves").or(rating.eq("superb")).and(author.eq("michael crichton")), + "+(title:jeeves rating:superb) +author:\"michael crichton\"", + 0); + } + + @Test + public void eq_phrase() throws Exception { + testQuery(title.eq("Jurassic Park"), "title:\"jurassic park\"", 1); + } + + @Test + @Ignore("Not easily done in Lucene!") + public void publisher_equals_empty_string() throws Exception { + testQuery(publisher.eq(""), "publisher:", 1); + } + + @Test + public void eq_phrase_should_not_find_results_but_luceNe_semantics_differs_from_querydsls() + throws Exception { + testQuery(text.eq("UNIX System"), "text:\"unix system\"", 1); + } + + @Test + public void eq_phrase_does_not_find_results_because_word_in_middle() throws Exception { + testQuery(title.eq("Jurassic Amusement Park"), "title:\"jurassic amusement park\"", 0); + } + + @Test + public void like_not_does_not_find_results() throws Exception { + testQuery(title.like("*H*e*").not(), "-title:*h*e* +*:*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void title_equals_ignore_case_negation_or_rating_equals_ignore_case() throws Exception { + testQuery( + title.equalsIgnoreCase("House").not().or(rating.equalsIgnoreCase("Good")), + "-title:house rating:good", + 1); + } + + @Test + public void eq_not_does_not_find_results() throws Exception { + testQuery(title.eq("Jurassic Park").not(), "-title:\"jurassic park\" +*:*", 0); + } + + @Test + public void title_equals_not_house() throws Exception { + testQuery(title.eq("house").not(), "-title:house +*:*", 1); + } + + @Test + public void eq_and_eq_not_does_not_find_results_because_second_expression_finds_nothing() + throws Exception { + testQuery( + rating.eq("superb").and(title.eq("house").not()), "+rating:superb +(-title:house +*:*)", 0); + } + + @Test + public void not_equals_finds_one() throws Exception { + testQuery(title.ne("house"), "-title:house +*:*", 1); + } + + @Test + public void not_equals_finds_none() throws Exception { + testQuery(title.ne("Jurassic Park"), "-title:\"jurassic park\" +*:*", 0); + } + + @Test + public void nothing_found_with_not_equals_or_equals() throws Exception { + testQuery( + title.ne("jurassic park").or(rating.eq("lousy")), + "(-title:\"jurassic park\" +*:*) rating:lousy", + 0); + } + + @Test + public void ne_and_eq() throws Exception { + testQuery(title.ne("house").and(rating.eq("good")), "+(-title:house +*:*) +rating:good", 1); + } + + @Test + public void startsWith() throws Exception { + testQuery(title.startsWith("Jurassi"), "title:jurassi*", 1); + } + + @Test + public void startsWith_phrase() throws Exception { + testQuery(title.startsWith("jurassic par"), "+title:jurassic* +title:*par*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void starts_with_ignore_case_phrase_does_not_find_results() throws Exception { + testQuery(title.startsWithIgnoreCase("urassic Par"), "+title:urassic* +title:*par*", 0); + } + + @Test + public void endsWith() throws Exception { + testQuery(title.endsWith("ark"), "title:*ark", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void ends_with_ignore_case_phrase() throws Exception { + testQuery(title.endsWithIgnoreCase("sic Park"), "+title:*sic* +title:*park", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void ends_with_ignore_case_phrase_does_not_find_results() throws Exception { + testQuery(title.endsWithIgnoreCase("sic Par"), "+title:*sic* +title:*par", 0); + } + + @Test + public void contains() throws Exception { + testQuery(title.contains("rassi"), "title:*rassi*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void contains_ignore_case_phrase() throws Exception { + testQuery(title.containsIgnoreCase("rassi Pa"), "+title:*rassi* +title:*pa*", 1); + } + + @Test + public void contains_user_inputted_wildcards_dont_work() throws Exception { + testQuery(title.contains("r*i"), "title:*r\\*i*", 0); + } + + @Test + public void between() throws Exception { + testQuery(title.between("Indiana", "Kundun"), "title:[indiana TO kundun]", 1); + } + + @Test + public void between_numeric_integer() throws Exception { + testQuery(year.between(1980, 2000), 1); + } + + @Test + public void between_numeric_double() throws Exception { + testQuery(gross.between(10.00, 19030.00), 1); + } + + @Test + public void between_numeric() throws Exception { + testQuery(longField.between(0L, 2L), 1); + testQuery(shortField.between((short) 0, (short) 2), 1); + testQuery(byteField.between((byte) 0, (byte) 2), 1); + testQuery(floatField.between((float) 0.0, (float) 2.0), 1); + } + + @Test + public void between_is_inclusive_from_start() throws Exception { + testQuery(title.between("Jurassic", "Kundun"), "title:[jurassic TO kundun]", 1); + } + + @Test + public void between_is_inclusive_to_end() throws Exception { + testQuery(title.between("Indiana", "Jurassic"), "title:[indiana TO jurassic]", 1); + } + + @Test + public void between_does_not_find_results() throws Exception { + testQuery(title.between("Indiana", "Jurassib"), "title:[indiana TO jurassib]", 0); + } + + @Test + public void in() throws Exception { + testQuery(title.in(Arrays.asList("jurassic", "park")), "title:jurassic title:park", 1); + testQuery(title.in("jurassic", "park"), "title:jurassic title:park", 1); + testQuery(title.eq("jurassic").or(title.eq("park")), "title:jurassic title:park", 1); + } + + @Test + public void lt() throws Exception { + testQuery(rating.lt("Superb"), "rating:{* TO superb}", 1); + } + + @Test + public void lt_numeric_integer() throws Exception { + testQuery(year.lt(1991), 1); + } + + @Test + public void lt_numeric_double() throws Exception { + testQuery(gross.lt(10000.0), 1); + } + + @Test + public void lt_not_in_range_because_equal() throws Exception { + testQuery(rating.lt("Good"), "rating:{* TO good}", 0); + } + + @Test + public void lt_numeric_integer_not_in_range_because_equal() throws Exception { + testQuery(year.lt(1990), 0); + } + + @Test + public void lt_numeric_double_not_in_range_because_equal() throws Exception { + testQuery(gross.lt(900.0), 0); + } + + @Test + public void loe() throws Exception { + testQuery(rating.loe("Superb"), "rating:[* TO superb]", 1); + } + + @Test + public void loe_numeric_integer() throws Exception { + testQuery(year.loe(1991), 1); + } + + @Test + public void loe_numeric_double() throws Exception { + testQuery(gross.loe(903.0), 1); + } + + @Test + public void loe_equal() throws Exception { + testQuery(rating.loe("Good"), "rating:[* TO good]", 1); + } + + @Test + public void loe_numeric_integer_equal() throws Exception { + testQuery(year.loe(1990), 1); + } + + @Test + public void loe_numeric_double_equal() throws Exception { + testQuery(gross.loe(900.0), 1); + } + + @Test + public void loe_not_found() throws Exception { + testQuery(rating.loe("Bad"), "rating:[* TO bad]", 0); + } + + @Test + public void loe_numeric_integer_not_found() throws Exception { + testQuery(year.loe(1989), 0); + } + + @Test + public void loe_numeric_double_not_found() throws Exception { + testQuery(gross.loe(899.9), 0); + } + + @Test + public void gt() throws Exception { + testQuery(rating.gt("Bad"), "rating:{bad TO *}", 1); + } + + @Test + public void gt_numeric_integer() throws Exception { + testQuery(year.gt(1989), 1); + } + + @Test + public void gt_numeric_double() throws Exception { + testQuery(gross.gt(100.00), 1); + } + + @Test + public void gt_not_in_range_because_equal() throws Exception { + testQuery(rating.gt("Good"), "rating:{good TO *}", 0); + } + + @Test + public void gt_numeric_integer_not_in_range_because_equal() throws Exception { + testQuery(year.gt(1990), 0); + } + + @Test + public void gt_numeric_double_not_in_range_because_equal() throws Exception { + testQuery(gross.gt(900.00), 0); + } + + @Test + public void goe() throws Exception { + testQuery(rating.goe("Bad"), "rating:[bad TO *]", 1); + } + + @Test + public void goe_numeric_integer() throws Exception { + testQuery(year.goe(1989), 1); + } + + @Test + public void goe_numeric_double() throws Exception { + testQuery(gross.goe(320.50), 1); + } + + @Test + public void goe_equal() throws Exception { + testQuery(rating.goe("Good"), "rating:[good TO *]", 1); + } + + @Test + public void goe_numeric_integer_equal() throws Exception { + testQuery(year.goe(1990), 1); + } + + @Test + public void goe_numeric_double_equal() throws Exception { + testQuery(gross.goe(900.00), 1); + } + + @Test + public void goe_not_found() throws Exception { + testQuery(rating.goe("Hood"), "rating:[hood TO *]", 0); + } + + @Test + public void goe_numeric_integer_not_found() throws Exception { + testQuery(year.goe(1991), 0); + } + + @Test + public void goe_numeric_double_not_found() throws Exception { + testQuery(gross.goe(900.10), 0); + } + + @Test + public void equals_empty_string() throws Exception { + testQuery(title.eq(""), "title:", 0); + } + + @Test + public void not_equals_empty_string() throws Exception { + testQuery(title.ne(""), "-title: +*:*", 1); + } + + @Test + public void contains_empty_string() throws Exception { + testQuery(title.contains(""), "title:**", 1); + } + + @Test + public void like_empty_string() throws Exception { + testQuery(title.like(""), "title:", 0); + } + + @Test + public void starts_with_empty_string() throws Exception { + testQuery(title.startsWith(""), "title:*", 1); + } + + @Test + public void ends_with_empty_string() throws Exception { + testQuery(title.endsWith(""), "title:*", 1); + } + + @Test + public void between_empty_strings() throws Exception { + testQuery(title.between("", ""), "title:[ TO ]", 0); + } + + @Test + public void booleanBuilder() throws Exception { + testQuery(new BooleanBuilder(gross.goe(900.10)), 0); + } + + @Test + @Ignore + public void fuzzy() throws Exception { + fail("Not yet implemented!"); + } + + @Test + @Ignore + public void proximity() throws Exception { + fail("Not yet implemented!"); + } + + @Test + @Ignore + public void boost() throws Exception { + fail("Not yet implemented!"); + } + + @Test + public void pathAny() throws Exception { + testQuery(titles.any().eq("Jurassic"), "title:jurassic", 1); + } + + private boolean unsupportedOperation(Predicate filter) { + return UNSUPPORTED_OPERATORS.contains(((Operation) filter).getOperator()); + } + + @Test + public void various() throws Exception { + MatchingFiltersFactory filters = + new MatchingFiltersFactory(QuerydslModule.COLLECTIONS, Target.LUCENE); + for (Predicate filter : filters.string(title, StringConstant.create("jurassic park"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 1); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + + for (Predicate filter : filters.string(author, StringConstant.create("michael crichton"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 1); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + + for (Predicate filter : filters.string(title, StringConstant.create("1990"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 0); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/Person.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/Person.java new file mode 100644 index 000000000..9673dcaf0 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/Person.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.annotations.QueryEntity; +import java.time.LocalDate; + +@QueryEntity +public class Person { + private final String id; + private final String name; + private final LocalDate birthDate; + + public Person(String id, String name, LocalDate birthDate) { + this.id = id; + this.name = name; + this.birthDate = birthDate; + } + + public String getId() { + return id; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public String getName() { + return name; + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/PhraseElementTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/PhraseElementTest.java new file mode 100644 index 000000000..a80996622 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/PhraseElementTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import org.junit.Test; + +public class PhraseElementTest { + + @Test + public void test() { + StringPath title = Expressions.stringPath("title"); + LuceneSerializer serializer = new LuceneSerializer(false, false); + QueryMetadata metadata = new DefaultQueryMetadata(); + assertThat(serializer.toQuery(title.eq("Hello World"), metadata).toString()) + .isEqualTo("title:Hello World"); + assertThat(serializer.toQuery(title.eq(new PhraseElement("Hello World")), metadata).toString()) + .isEqualTo("title:\"Hello World\""); + } + + @Test + public void equals() { + PhraseElement el1 = new PhraseElement("x"), + el2 = new PhraseElement("x"), + el3 = new PhraseElement("y"); + assertThat(el2).isEqualTo(el1); + assertThat(el1.equals(el3)).isFalse(); + } + + @Test + public void hashCode_() { + PhraseElement el1 = new PhraseElement("x"), el2 = new PhraseElement("x"); + assertThat(el2.hashCode()).isEqualTo(el1.hashCode()); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QDocument.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QDocument.java new file mode 100644 index 000000000..2b666f20b --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QDocument.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import com.querydsl.core.types.PathMetadataFactory; +import com.querydsl.core.types.dsl.EntityPathBase; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.StringPath; +import org.apache.lucene.document.Document; + +public class QDocument extends EntityPathBase { + + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(final String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final NumberPath year = createNumber("year", Integer.class); + + public final StringPath title = createString("title"); + + public final NumberPath gross = createNumber("gross", Double.class); +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QueryElementTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QueryElementTest.java new file mode 100644 index 000000000..dc7627c75 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/QueryElementTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.TermQuery; +import org.junit.Ignore; +import org.junit.Test; + +public class QueryElementTest { + + @Test + @Ignore + public void test() { + QueryElement element = new QueryElement(new TermQuery(new Term("str", "text"))); + assertThat(element.toString()).isEqualTo("str:text"); + + QueryElement element2 = new QueryElement(new TermQuery(new Term("str", "text"))); + assertThat(element).isEqualTo(element2); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/TermElementTest.java b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/TermElementTest.java new file mode 100644 index 000000000..bc4a91e0e --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/java/com/querydsl/lucene10/TermElementTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene10; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import org.junit.Test; + +public class TermElementTest { + + @Test + public void test() { + StringPath title = Expressions.stringPath("title"); + LuceneSerializer serializer = new LuceneSerializer(false, true); + QueryMetadata metadata = new DefaultQueryMetadata(); + assertThat(serializer.toQuery(title.eq("Hello World"), metadata).toString()) + .isEqualTo("title:\"Hello World\""); + assertThat(serializer.toQuery(title.eq(new TermElement("Hello World")), metadata).toString()) + .isEqualTo("title:Hello World"); + } + + @Test + public void testEqualsAndHashCode() { + TermElement el1 = new TermElement("x"), el2 = new TermElement("x"), el3 = new TermElement("y"); + assertThat(el2).isEqualTo(el1); + assertThat(el1.equals(el3)).isFalse(); + assertThat(el2.hashCode()).isEqualTo(el1.hashCode()); + } +} diff --git a/querydsl-libraries/querydsl-lucene10/src/test/resources/log4j.properties.example b/querydsl-libraries/querydsl-lucene10/src/test/resources/log4j.properties.example new file mode 100644 index 000000000..e9b03f31d --- /dev/null +++ b/querydsl-libraries/querydsl-lucene10/src/test/resources/log4j.properties.example @@ -0,0 +1,9 @@ +# Configure an appender that logs to console +log4j.rootLogger=info, A1 +log4j.threshold=debug +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c{1} - %m%n + +# Configuration of logging levels for different packages +log4j.logger.com.querydsl.lucene10=DEBUG diff --git a/querydsl-libraries/querydsl-lucene9/README.md b/querydsl-libraries/querydsl-lucene9/README.md new file mode 100644 index 000000000..2d0ad121f --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/README.md @@ -0,0 +1,57 @@ +## Querydsl Lucene 9 + +The Lucene module provides integration with the Lucene 9 indexing library. + +**Maven integration** + + Add the following dependencies to your Maven project : +```XML + + io.github.openfeign.querydsl + querydsl-lucene9 + ${querydsl.version} + +``` + +**Creating the query types** + +With fields year and title a manually created query type could look something like this: + +```JAVA +public class QDocument extends EntityPathBase{ + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final StringPath year = createString("year"); + + public final StringPath title = createString("title"); +} +``` + +QDocument represents a Lucene document with the fields year and title. + +Code generation is not available for Lucene, since no schema data is available. + +**Querying** + +Querying with Querydsl Lucene is as simple as this: + +```JAVA +QDocument doc = new QDocument("doc"); + +IndexSearcher searcher = new IndexSearcher(index); +LuceneQuery query = new LuceneQuery(true, searcher); +List documents = query + .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle")) + .fetch(); +``` + +which is transformed into the following Lucene query : +``` ++year:[1800 TO 2000] +title:huckle* +``` + +For more information on the Querydsl Lucene module visit the reference documentation http://www.querydsl.com/static/querydsl/latest/reference/html/ch02s05.html diff --git a/querydsl-libraries/querydsl-lucene9/pom.xml b/querydsl-libraries/querydsl-lucene9/pom.xml new file mode 100644 index 000000000..e09a3ab04 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + + io.github.openfeign.querydsl + querydsl-libraries + 7.2-SNAPSHOT + + + querydsl-lucene9 + Querydsl - Lucene 9 support + Lucene 9 support for Querydsl + + + 9.12.3 + org.apache.lucene.*;version="[9.0.0,10)", + ${osgi.import.package.root} + + + + + org.jetbrains + annotations + provided + + + org.apache.lucene + lucene-core + ${lucene.version} + provided + + + org.apache.lucene + lucene-analysis-common + ${lucene.version} + provided + + + org.apache.lucene + lucene-queryparser + ${lucene.version} + provided + + + io.github.openfeign.querydsl + querydsl-core + ${project.version} + + + + + io.github.openfeign.querydsl + querydsl-core + ${project.version} + test-jar + test + + + + io.github.openfeign.querydsl + querydsl-apt + ${project.version} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.querydsl.lucene9 + + + + + + + org.apache.felix + maven-bundle-plugin + + + + org.apache.maven.plugins + maven-compiler-plugin + + + com.querydsl.apt.QuerydslAnnotationProcessor + + + + + + diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/AbstractLuceneQuery.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/AbstractLuceneQuery.java new file mode 100644 index 000000000..8c2b34929 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/AbstractLuceneQuery.java @@ -0,0 +1,351 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.CloseableIterator; +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.Fetchable; +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.QueryException; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.SimpleQuery; +import com.querydsl.core.support.QueryMixin; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.Predicate; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.TotalHitCountCollectorManager; +import org.jetbrains.annotations.Nullable; + +/** + * AbstractLuceneQuery is an abstract super class for Lucene query implementations + * + * @author tiwe + * @param projection type + * @param concrete subtype of querydsl + */ +public abstract class AbstractLuceneQuery> + implements SimpleQuery, Fetchable { + + private static final String JAVA_ISO_CONTROL = "[\\p{Cntrl}&&[^\r\n\t]]"; + + private final QueryMixin queryMixin; + + private final IndexSearcher searcher; + + private final LuceneSerializer serializer; + + private final Function transformer; + + @Nullable private Set fieldsToLoad; + + private List filters = Collections.emptyList(); + + @Nullable private Query filter; + + @Nullable private Sort querySort; + + @SuppressWarnings("unchecked") + public AbstractLuceneQuery( + LuceneSerializer serializer, IndexSearcher searcher, Function transformer) { + queryMixin = new QueryMixin((Q) this, new DefaultQueryMetadata()); + this.serializer = serializer; + this.searcher = searcher; + this.transformer = transformer; + } + + public AbstractLuceneQuery(IndexSearcher searcher, Function transformer) { + this(LuceneSerializer.DEFAULT, searcher, transformer); + } + + private long innerCount() { + try { + final int maxDoc = searcher.getIndexReader().maxDoc(); + if (maxDoc == 0) { + return 0; + } + return searcher.search(createQuery(), new TotalHitCountCollectorManager()); + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + } + + @Override + public long fetchCount() { + return innerCount(); + } + + protected Query createQuery() { + Query originalQuery; + if (queryMixin.getMetadata().getWhere() == null) { + originalQuery = new MatchAllDocsQuery(); + } else { + originalQuery = + serializer.toQuery(queryMixin.getMetadata().getWhere(), queryMixin.getMetadata()); + } + Query filter = getFilter(); + if (filter != null) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(originalQuery, Occur.MUST); + builder.add(filter, Occur.FILTER); + return builder.build(); + } + return originalQuery; + } + + @Override + public Q distinct() { + throw new UnsupportedOperationException("use distinct(path) instead"); + } + + /** + * Apply the given Lucene Query as a filter to the search results + * + * @param filter filter query + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q filter(Query filter) { + if (filters.isEmpty()) { + this.filter = filter; + filters = Collections.singletonList(filter); + } else { + this.filter = null; + if (filters.size() == 1) { + filters = new ArrayList<>(); + } + filters.add(filter); + } + return (Q) this; + } + + private Query getFilter() { + if (filter == null && !filters.isEmpty()) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (Query f : filters) { + builder.add(f, Occur.SHOULD); + } + filter = builder.build(); + } + return filter; + } + + @Override + public Q limit(long limit) { + return queryMixin.limit(limit); + } + + @Override + public CloseableIterator iterate() { + final QueryMetadata metadata = queryMixin.getMetadata(); + final List> orderBys = metadata.getOrderBy(); + final Integer queryLimit = metadata.getModifiers().getLimitAsInteger(); + final Integer queryOffset = metadata.getModifiers().getOffsetAsInteger(); + Sort sort = querySort; + int limit; + final int offset = queryOffset != null ? queryOffset : 0; + try { + limit = maxDoc(); + if (limit == 0) { + return CloseableIterator.of(Collections.emptyIterator()); + } + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + if (queryLimit != null && queryLimit < limit) { + limit = queryLimit; + } + if (sort == null && !orderBys.isEmpty()) { + sort = serializer.toSort(orderBys); + } + + try { + ScoreDoc[] scoreDocs; + int sumOfLimitAndOffset = limit + offset; + if (sumOfLimitAndOffset < 1) { + throw new QueryException( + "The given limit (" + + limit + + ") and offset (" + + offset + + ") cause an integer overflow."); + } + if (sort != null) { + scoreDocs = searcher.search(createQuery(), sumOfLimitAndOffset, sort).scoreDocs; + } else { + scoreDocs = searcher.search(createQuery(), sumOfLimitAndOffset, Sort.INDEXORDER).scoreDocs; + } + if (offset < scoreDocs.length) { + return new ResultIterator(scoreDocs, offset, searcher, fieldsToLoad, transformer); + } + return CloseableIterator.of(Collections.emptyIterator()); + } catch (final IOException e) { + throw new QueryException(e); + } + } + + private List innerList() { + return CloseableIterator.asList(iterate()); + } + + @Override + public List fetch() { + return innerList(); + } + + /** + * Set the given fields to load + * + * @param fieldsToLoad fields to load + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q load(Set fieldsToLoad) { + this.fieldsToLoad = fieldsToLoad; + return (Q) this; + } + + /** + * Load only the fields of the given paths + * + * @param paths fields to load + * @return the current object + */ + @SuppressWarnings("unchecked") + public Q load(Path... paths) { + Set fields = new HashSet(); + for (Path path : paths) { + fields.add(serializer.toField(path)); + } + this.fieldsToLoad = fields; + return (Q) this; + } + + @Override + public QueryResults fetchResults() { + List documents = innerList(); + return new QueryResults(documents, queryMixin.getMetadata().getModifiers(), innerCount()); + } + + @Override + public Q offset(long offset) { + return queryMixin.offset(offset); + } + + public Q orderBy(OrderSpecifier o) { + return queryMixin.orderBy(o); + } + + @Override + public Q orderBy(OrderSpecifier... o) { + return queryMixin.orderBy(o); + } + + @Override + public Q restrict(QueryModifiers modifiers) { + return queryMixin.restrict(modifiers); + } + + @Override + public

Q set(ParamExpression

param, P value) { + return queryMixin.set(param, value); + } + + @SuppressWarnings("unchecked") + public Q sort(Sort sort) { + this.querySort = sort; + return (Q) this; + } + + @Nullable + private T oneResult(boolean unique) { + try { + int maxDoc = maxDoc(); + if (maxDoc == 0) { + return null; + } + final ScoreDoc[] scoreDocs = + searcher.search(createQuery(), maxDoc, Sort.INDEXORDER).scoreDocs; + int index = 0; + QueryModifiers modifiers = queryMixin.getMetadata().getModifiers(); + Long offset = modifiers.getOffset(); + if (offset != null) { + index = offset.intValue(); + } + Long limit = modifiers.getLimit(); + if (unique + && (limit == null ? scoreDocs.length - index > 1 : limit > 1 && scoreDocs.length > 1)) { + throw new NonUniqueResultException( + "Unique result requested, but " + scoreDocs.length + " found."); + } else if (scoreDocs.length > index) { + Document document; + if (fieldsToLoad != null) { + document = searcher.storedFields().document(scoreDocs[index].doc, fieldsToLoad); + } else { + document = searcher.storedFields().document(scoreDocs[index].doc); + } + return transformer.apply(document); + } else { + return null; + } + } catch (IOException | IllegalArgumentException e) { + throw new QueryException(e); + } + } + + @Override + public T fetchFirst() { + return oneResult(false); + } + + @Override + public T fetchOne() throws NonUniqueResultException { + return oneResult(true); + } + + public Q where(Predicate e) { + return queryMixin.where(e); + } + + @Override + public Q where(Predicate... e) { + return queryMixin.where(e); + } + + @Override + public String toString() { + return createQuery().toString().replaceAll(JAVA_ISO_CONTROL, "_"); + } + + private int maxDoc() throws IOException { + return searcher.getIndexReader().maxDoc(); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/IgnoreCaseUnsupportedException.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/IgnoreCaseUnsupportedException.java new file mode 100644 index 000000000..53b699c6b --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/IgnoreCaseUnsupportedException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +/** + * Thrown for case ignore usage + * + * @author tiwe + */ +public class IgnoreCaseUnsupportedException extends UnsupportedOperationException { + + private static final long serialVersionUID = 412913389929530788L; + + public IgnoreCaseUnsupportedException() { + super("Ignore case queries are not supported with Lucene"); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneExpressions.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneExpressions.java new file mode 100644 index 000000000..bb5fb9541 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneExpressions.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.BooleanExpression; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.FuzzyQuery; +import org.apache.lucene.util.automaton.LevenshteinAutomata; + +/** + * Utility methods to create filter expressions for Lucene queries that are not covered by the + * Querydsl standard expression model + * + * @author tiwe + */ +public final class LuceneExpressions { + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @return condition + */ + public static BooleanExpression fuzzyLike(Path path, String value) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term)); + } + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @param maxEdits must be >= 0 and <= {@link + * LevenshteinAutomata#MAXIMUM_SUPPORTED_DISTANCE}. + * @return condition + */ + public static BooleanExpression fuzzyLike(Path path, String value, int maxEdits) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term, maxEdits)); + } + + /** + * Create a fuzzy query + * + * @param path path + * @param value value to match + * @param maxEdits must be >= 0 and <= {@link + * LevenshteinAutomata#MAXIMUM_SUPPORTED_DISTANCE}. + * @param prefixLength length of common (non-fuzzy) prefix + * @return condition + */ + public static BooleanExpression fuzzyLike( + Path path, String value, int maxEdits, int prefixLength) { + Term term = new Term(path.getMetadata().getName(), value); + return new QueryElement(new FuzzyQuery(term, maxEdits, prefixLength)); + } + + private LuceneExpressions() {} +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneOps.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneOps.java new file mode 100644 index 000000000..d741ae084 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneOps.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.Operator; + +/** + * Lucene specific operators + * + * @author tiwe + */ +public enum LuceneOps implements Operator { + LUCENE_QUERY(Object.class), + PHRASE(String.class), + TERM(String.class); + + private final Class type; + + LuceneOps(Class type) { + this.type = type; + } + + @Override + public Class getType() { + return type; + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneQuery.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneQuery.java new file mode 100644 index 000000000..ee15915c2 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneQuery.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; + +/** + * {@code LuceneQuery} is a Querydsl query implementation for Lucene queries. + * + *

Example: + * + *

{@code
+ * QDocument doc = new QDocument("doc");
+ * IndexSearcher searcher = new IndexSearcher(index);
+ * LuceneQuery query = new LuceneQuery(true, searcher);
+ * List documents = query
+ *     .where(doc.year.between("1800", "2000").and(doc.title.startsWith("Huckle"))
+ *     .fetch();
+ * }
+ * + * @author vema + */ +public class LuceneQuery extends AbstractLuceneQuery { + + private static final Function TRANSFORMER = + new Function() { + @Override + public Document apply(Document input) { + return input; + } + }; + + public LuceneQuery(IndexSearcher searcher) { + super(searcher, TRANSFORMER); + } + + public LuceneQuery(LuceneSerializer luceneSerializer, IndexSearcher searcher) { + super(luceneSerializer, searcher, TRANSFORMER); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneSerializer.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneSerializer.java new file mode 100644 index 000000000..41eebacce --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/LuceneSerializer.java @@ -0,0 +1,572 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.Constant; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.ExpressionUtils; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Operator; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.ParamExpression; +import com.querydsl.core.types.ParamNotSetException; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathType; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.PhraseQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SortedNumericSortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; +import org.apache.lucene.search.WildcardQuery; +import org.jetbrains.annotations.Nullable; + +/** + * Serializes Querydsl queries to Lucene queries. + * + * @author vema + */ +public class LuceneSerializer { + private static final Map, SortField.Type> sortFields = + new HashMap, SortField.Type>(); + + static { + sortFields.put(Integer.class, SortField.Type.INT); + sortFields.put(Float.class, SortField.Type.FLOAT); + sortFields.put(Long.class, SortField.Type.LONG); + sortFields.put(Double.class, SortField.Type.DOUBLE); + sortFields.put(BigDecimal.class, SortField.Type.DOUBLE); + sortFields.put(BigInteger.class, SortField.Type.LONG); + } + + public static final LuceneSerializer DEFAULT = new LuceneSerializer(false, true); + + private final boolean lowerCase; + + private final boolean splitTerms; + + private final Locale sortLocale; + + public LuceneSerializer(boolean lowerCase, boolean splitTerms) { + this(lowerCase, splitTerms, Locale.getDefault()); + } + + public LuceneSerializer(boolean lowerCase, boolean splitTerms, Locale sortLocale) { + this.lowerCase = lowerCase; + this.splitTerms = splitTerms; + this.sortLocale = sortLocale; + } + + private Query toQuery(Operation operation, QueryMetadata metadata) { + Operator op = operation.getOperator(); + if (op == Ops.OR) { + return toTwoHandSidedQuery(operation, Occur.SHOULD, metadata); + } else if (op == Ops.AND) { + return toTwoHandSidedQuery(operation, Occur.MUST, metadata); + } else if (op == Ops.NOT) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(toQuery(operation.getArg(0), metadata), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } else if (op == Ops.LIKE) { + return like(operation, metadata); + } else if (op == Ops.LIKE_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.EQ) { + return eq(operation, metadata, false); + } else if (op == Ops.EQ_IGNORE_CASE) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.NE) { + return ne(operation, metadata, false); + } else if (op == Ops.STARTS_WITH) { + return startsWith(metadata, operation, false); + } else if (op == Ops.STARTS_WITH_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.ENDS_WITH) { + return endsWith(operation, metadata, false); + } else if (op == Ops.ENDS_WITH_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.STRING_CONTAINS) { + return stringContains(operation, metadata, false); + } else if (op == Ops.STRING_CONTAINS_IC) { + throw new IgnoreCaseUnsupportedException(); + } else if (op == Ops.BETWEEN) { + return between(operation, metadata); + } else if (op == Ops.IN) { + return in(operation, metadata, false); + } else if (op == Ops.NOT_IN) { + return notIn(operation, metadata, false); + } else if (op == Ops.LT) { + return lt(operation, metadata); + } else if (op == Ops.GT) { + return gt(operation, metadata); + } else if (op == Ops.LOE) { + return le(operation, metadata); + } else if (op == Ops.GOE) { + return ge(operation, metadata); + } else if (op == LuceneOps.LUCENE_QUERY) { + @SuppressWarnings("unchecked") + Constant expectedConstant = (Constant) operation.getArg(0); + return expectedConstant.getConstant(); + } + throw new UnsupportedOperationException("Illegal operation " + operation); + } + + private Query toTwoHandSidedQuery(Operation operation, Occur occur, QueryMetadata metadata) { + Query lhs = toQuery(operation.getArg(0), metadata); + Query rhs = toQuery(operation.getArg(1), metadata); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(createBooleanClause(lhs, occur)); + builder.add(createBooleanClause(rhs, occur)); + return builder.build(); + } + + /** + * If the query is a BooleanQuery and it contains a single Occur.MUST_NOT clause it will be + * returned as is. Otherwise it will be wrapped in a BooleanClause with the given Occur. + */ + private BooleanClause createBooleanClause(Query query, Occur occur) { + if (query instanceof BooleanQuery booleanQuery) { + List clauses = booleanQuery.clauses(); + if (clauses.size() == 1 && clauses.get(0).getOccur().equals(Occur.MUST_NOT)) { + return clauses.get(0); + } + } + return new BooleanClause(query, occur); + } + + protected Query like(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convert(path, operation.getArg(1)); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String s : terms) { + builder.add(new WildcardQuery(new Term(field, "*" + s + "*")), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, terms[0])); + } + + protected Query eq(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + + if (Number.class.isAssignableFrom(operation.getArg(1).getType())) { + @SuppressWarnings("unchecked") + Constant rightArg = (Constant) operation.getArg(1); + return exactNumberQuery(field, rightArg.getConstant()); + } + + return eq(field, convert(path, operation.getArg(1), metadata), ignoreCase); + } + + private Query exactNumberQuery(String field, Number number) { + if (Integer.class.isInstance(number) + || Byte.class.isInstance(number) + || Short.class.isInstance(number)) { + return IntPoint.newExactQuery(field, number.intValue()); + } else if (Long.class.isInstance(number) || BigInteger.class.isInstance(number)) { + return LongPoint.newExactQuery(field, number.longValue()); + } else if (Double.class.isInstance(number) || BigDecimal.class.isInstance(number)) { + return DoublePoint.newExactQuery(field, number.doubleValue()); + } else if (Float.class.isInstance(number)) { + return FloatPoint.newExactQuery(field, number.floatValue()); + } else { + throw new IllegalArgumentException("Unsupported numeric type " + number.getClass().getName()); + } + } + + protected Query eq(String field, String[] terms, boolean ignoreCase) { + if (terms.length > 1) { + PhraseQuery.Builder builder = new PhraseQuery.Builder(); + for (String s : terms) { + builder.add(new Term(field, s)); + } + return builder.build(); + } + return new TermQuery(new Term(field, terms[0])); + } + + protected Query in(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + Path path = getPath(operation.getArg(0)); + String field = toField(path); + @SuppressWarnings("unchecked") + Constant> expectedConstant = (Constant>) operation.getArg(1); + Collection values = expectedConstant.getConstant(); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + if (Number.class.isAssignableFrom(path.getType())) { + for (Object value : values) { + builder.add(exactNumberQuery(field, (Number) value), Occur.SHOULD); + } + } else { + for (Object value : values) { + String[] str = convert(path, value); + builder.add(eq(field, str, ignoreCase), Occur.SHOULD); + } + } + return builder.build(); + } + + protected Query notIn(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(in(operation, metadata, false), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } + + protected Query ne(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + builder.add(new BooleanClause(eq(operation, metadata, ignoreCase), Occur.MUST_NOT)); + builder.add(new BooleanClause(new MatchAllDocsQuery(), Occur.MUST)); + return builder.build(); + } + + protected Query startsWith(QueryMetadata metadata, Operation operation, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i = 0; i < terms.length; ++i) { + String s = i == 0 ? terms[i] + "*" : "*" + terms[i] + "*"; + builder.add(new WildcardQuery(new Term(field, s)), Occur.MUST); + } + return builder.build(); + } + return new PrefixQuery(new Term(field, terms[0])); + } + + protected Query stringContains( + Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String s : terms) { + builder.add(new WildcardQuery(new Term(field, "*" + s + "*")), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, "*" + terms[0] + "*")); + } + + protected Query endsWith(Operation operation, QueryMetadata metadata, boolean ignoreCase) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + String field = toField(path); + String[] terms = convertEscaped(path, operation.getArg(1), metadata); + if (terms.length > 1) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (int i = 0; i < terms.length; ++i) { + String s = i == terms.length - 1 ? "*" + terms[i] : "*" + terms[i] + "*"; + builder.add(new WildcardQuery(new Term(field, s)), Occur.MUST); + } + return builder.build(); + } + return new WildcardQuery(new Term(field, "*" + terms[0])); + } + + protected Query between(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range( + path, toField(path), operation.getArg(1), operation.getArg(2), true, true, metadata); + } + + protected Query lt(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), null, operation.getArg(1), false, false, metadata); + } + + protected Query gt(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), operation.getArg(1), null, false, false, metadata); + } + + protected Query le(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), null, operation.getArg(1), true, true, metadata); + } + + protected Query ge(Operation operation, QueryMetadata metadata) { + verifyArguments(operation); + Path path = getPath(operation.getArg(0)); + return range(path, toField(path), operation.getArg(1), null, true, true, metadata); + } + + protected Query range( + Path leftHandSide, + String field, + @Nullable Expression min, + @Nullable Expression max, + boolean minInc, + boolean maxInc, + QueryMetadata metadata) { + if (min != null && Number.class.isAssignableFrom(min.getType()) + || max != null && Number.class.isAssignableFrom(max.getType())) { + @SuppressWarnings("unchecked") + Constant minConstant = (Constant) min; + @SuppressWarnings("unchecked") + Constant maxConstant = (Constant) max; + + Class numType = + minConstant != null ? minConstant.getType() : maxConstant.getType(); + @SuppressWarnings("unchecked") + Class unboundedNumType = (Class) numType; + return numericRange( + unboundedNumType, + field, + minConstant == null ? null : minConstant.getConstant(), + maxConstant == null ? null : maxConstant.getConstant(), + minInc, + maxInc); + } + return stringRange(leftHandSide, field, min, max, minInc, maxInc, metadata); + } + + protected Query numericRange( + Class clazz, + String field, + @Nullable N min, + @Nullable N max, + boolean minInc, + boolean maxInc) { + if (clazz.equals(Integer.class) || clazz.equals(Byte.class) || clazz.equals(Short.class)) { + int lower = min != null ? adjustBound(min.intValue(), minInc, true) : Integer.MIN_VALUE; + int upper = max != null ? adjustBound(max.intValue(), maxInc, false) : Integer.MAX_VALUE; + return IntPoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Long.class)) { + long lower = min != null ? adjustBound(min.longValue(), minInc, true) : Long.MIN_VALUE; + long upper = max != null ? adjustBound(max.longValue(), maxInc, false) : Long.MAX_VALUE; + return LongPoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Double.class)) { + double lower = + min != null + ? (minInc ? min.doubleValue() : Math.nextUp(min.doubleValue())) + : Double.NEGATIVE_INFINITY; + double upper = + max != null + ? (maxInc ? max.doubleValue() : Math.nextDown(max.doubleValue())) + : Double.POSITIVE_INFINITY; + return DoublePoint.newRangeQuery(field, lower, upper); + } else if (clazz.equals(Float.class)) { + float lower = + min != null + ? (minInc ? min.floatValue() : Math.nextUp(min.floatValue())) + : Float.NEGATIVE_INFINITY; + float upper = + max != null + ? (maxInc ? max.floatValue() : Math.nextDown(max.floatValue())) + : Float.POSITIVE_INFINITY; + return FloatPoint.newRangeQuery(field, lower, upper); + } else { + throw new IllegalArgumentException("Unsupported numeric type " + clazz.getName()); + } + } + + private int adjustBound(int value, boolean inclusive, boolean isLower) { + if (inclusive) { + return value; + } + return isLower ? Math.addExact(value, 1) : Math.addExact(value, -1); + } + + private long adjustBound(long value, boolean inclusive, boolean isLower) { + if (inclusive) { + return value; + } + return isLower ? Math.addExact(value, 1L) : Math.addExact(value, -1L); + } + + protected Query stringRange( + Path leftHandSide, + String field, + @Nullable Expression min, + @Nullable Expression max, + boolean minInc, + boolean maxInc, + QueryMetadata metadata) { + + if (min == null) { + return TermRangeQuery.newStringRange( + field, null, convert(leftHandSide, max, metadata)[0], minInc, maxInc); + } else if (max == null) { + return TermRangeQuery.newStringRange( + field, convert(leftHandSide, min, metadata)[0], null, minInc, maxInc); + } else { + return TermRangeQuery.newStringRange( + field, + convert(leftHandSide, min, metadata)[0], + convert(leftHandSide, max, metadata)[0], + minInc, + maxInc); + } + } + + private Path getPath(Expression leftHandSide) { + if (leftHandSide instanceof Path) { + return (Path) leftHandSide; + } else if (leftHandSide instanceof Operation) { + Operation operation = (Operation) leftHandSide; + if (operation.getOperator() == Ops.LOWER || operation.getOperator() == Ops.UPPER) { + return (Path) operation.getArg(0); + } + } + throw new IllegalArgumentException("Unable to transform " + leftHandSide + " to path"); + } + + /** + * template method, override to customize + * + * @param path path + * @return field name + */ + protected String toField(Path path) { + PathMetadata md = path.getMetadata(); + if (md.getPathType() == PathType.COLLECTION_ANY) { + return toField(md.getParent()); + } else { + String rv = md.getName(); + if (md.getParent() != null) { + Path parent = md.getParent(); + if (parent.getMetadata().getPathType() != PathType.VARIABLE) { + rv = toField(parent) + "." + rv; + } + } + return rv; + } + } + + private void verifyArguments(Operation operation) { + List> arguments = operation.getArgs(); + for (int i = 1; i < arguments.size(); ++i) { + if (!(arguments.get(i) instanceof Constant) + && !(arguments.get(i) instanceof ParamExpression) + && !(arguments.get(i) instanceof PhraseElement) + && !(arguments.get(i) instanceof TermElement)) { + throw new IllegalArgumentException( + "operand was of unsupported type " + arguments.get(i).getClass().getName()); + } + } + } + + protected String[] convert( + Path leftHandSide, Expression rightHandSide, QueryMetadata metadata) { + if (rightHandSide instanceof Operation) { + Operation operation = (Operation) rightHandSide; + if (operation.getOperator() == LuceneOps.PHRASE) { + return operation.getArg(0).toString().split("\\s+"); + } else if (operation.getOperator() == LuceneOps.TERM) { + return new String[] {operation.getArg(0).toString()}; + } else { + throw new IllegalArgumentException(rightHandSide.toString()); + } + } else if (rightHandSide instanceof ParamExpression) { + Object value = metadata.getParams().get(rightHandSide); + if (value == null) { + throw new ParamNotSetException((ParamExpression) rightHandSide); + } + return convert(leftHandSide, value); + + } else if (rightHandSide instanceof Constant) { + return convert(leftHandSide, ((Constant) rightHandSide).getConstant()); + } else { + throw new IllegalArgumentException(rightHandSide.toString()); + } + } + + protected String[] convert(Path leftHandSide, Object rightHandSide) { + String str = rightHandSide.toString(); + if (lowerCase) { + str = str.toLowerCase(); + } + if (splitTerms) { + if (str.equals("")) { + return new String[] {str}; + } else { + return str.split("\\s+"); + } + } else { + return new String[] {str}; + } + } + + private String[] convertEscaped( + Path leftHandSide, Expression rightHandSide, QueryMetadata metadata) { + String[] str = convert(leftHandSide, rightHandSide, metadata); + for (int i = 0; i < str.length; i++) { + str[i] = QueryParser.escape(str[i]); + } + return str; + } + + public Query toQuery(Expression expr, QueryMetadata metadata) { + if (expr instanceof Operation) { + return toQuery((Operation) expr, metadata); + } else { + return toQuery(ExpressionUtils.extract(expr), metadata); + } + } + + public Sort toSort(List> orderBys) { + List sorts = new ArrayList(orderBys.size()); + for (OrderSpecifier order : orderBys) { + if (!(order.getTarget() instanceof Path)) { + throw new IllegalArgumentException("argument was not of type Path."); + } + Class type = order.getTarget().getType(); + boolean reverse = !order.isAscending(); + Path path = getPath(order.getTarget()); + if (Number.class.isAssignableFrom(type)) { + sorts.add(new SortedNumericSortField(toField(path), sortFields.get(type), reverse)); + } else { + sorts.add(new SortField(toField(path), SortField.Type.STRING, reverse)); + } + } + return new Sort(sorts.toArray(new SortField[0])); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/PhraseElement.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/PhraseElement.java new file mode 100644 index 000000000..7c6eb8f79 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/PhraseElement.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.StringOperation; + +/** + * {@code PhraseElement} represents the embedded String as a phrase + * + * @author tiwe + */ +public class PhraseElement extends StringOperation { + + private static final long serialVersionUID = 2350215644019186076L; + + public PhraseElement(String str) { + super(LuceneOps.PHRASE, ConstantImpl.create(str)); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/QueryElement.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/QueryElement.java new file mode 100644 index 000000000..889775168 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/QueryElement.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.BooleanOperation; +import org.apache.lucene.search.Query; + +/** + * {@code QueryElement} wraps a Lucene Query + * + * @author tiwe + */ +public class QueryElement extends BooleanOperation { + + private static final long serialVersionUID = 470868107363840155L; + + public QueryElement(Query query) { + super(LuceneOps.LUCENE_QUERY, ConstantImpl.create(query)); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/ResultIterator.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/ResultIterator.java new file mode 100644 index 000000000..f5e99daa7 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/ResultIterator.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.CloseableIterator; +import com.querydsl.core.QueryException; +import java.io.IOException; +import java.util.Set; +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.ScoreDoc; +import org.jetbrains.annotations.Nullable; + +/** + * {@code ResultIterator} is a {@link CloseableIterator} implementation for Lucene query results + * + * @author tiwe + * @param + */ +public final class ResultIterator implements CloseableIterator { + + private final ScoreDoc[] scoreDocs; + + private int cursor; + + private final IndexSearcher searcher; + + @Nullable private final Set fieldsToLoad; + + private final Function transformer; + + public ResultIterator( + ScoreDoc[] scoreDocs, + int offset, + IndexSearcher searcher, + @Nullable Set fieldsToLoad, + Function transformer) { + this.scoreDocs = scoreDocs.clone(); + this.cursor = offset; + this.searcher = searcher; + this.fieldsToLoad = fieldsToLoad; + this.transformer = transformer; + } + + @Override + public boolean hasNext() { + return cursor != scoreDocs.length; + } + + @Override + public T next() { + try { + Document document; + if (fieldsToLoad != null) { + document = searcher.storedFields().document(scoreDocs[cursor++].doc, fieldsToLoad); + } else { + document = searcher.storedFields().document(scoreDocs[cursor++].doc); + } + return transformer.apply(document); + } catch (IOException e) { + throw new QueryException(e); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public void close() {} +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TermElement.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TermElement.java new file mode 100644 index 000000000..f20f64608 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TermElement.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.ConstantImpl; +import com.querydsl.core.types.dsl.StringOperation; + +/** + * {@code TermElement} represents the embedded String as a term + * + * @author tiwe + */ +public class TermElement extends StringOperation { + + private static final long serialVersionUID = 2350215644019186076L; + + public TermElement(String str) { + super(LuceneOps.TERM, ConstantImpl.create(str)); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TypedQuery.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TypedQuery.java new file mode 100644 index 000000000..9aa620ad2 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/TypedQuery.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import java.util.function.Function; +import org.apache.lucene.document.Document; +import org.apache.lucene.search.IndexSearcher; + +/** + * {@code TypedQuery} is a typed query implementation for Lucene queries. + * + *

Converts Lucene documents to typed results via a constructor supplied transformer + * + * @param result type + * @author laim + * @author tiwe + */ +public class TypedQuery extends AbstractLuceneQuery> { + + /** + * Create a new TypedQuery instance + * + * @param searcher index searcher + * @param transformer transformer to transform Lucene documents to result objects + */ + public TypedQuery(IndexSearcher searcher, Function transformer) { + super(searcher, transformer); + } + + /** + * Create a new TypedQuery instance + * + * @param serializer serializer + * @param searcher index searcher + * @param transformer transformer to transform Lucene documents to result objects + */ + public TypedQuery( + LuceneSerializer serializer, IndexSearcher searcher, Function transformer) { + super(serializer, searcher, transformer); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/package-info.java b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/package-info.java new file mode 100644 index 000000000..13e0ddcc7 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/main/java/com/querydsl/lucene9/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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. + */ + +/** Lucene 9 support */ +package com.querydsl.lucene9; diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneQueryTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneQueryTest.java new file mode 100644 index 000000000..b0c55a757 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneQueryTest.java @@ -0,0 +1,608 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.NonUniqueResultException; +import com.querydsl.core.QueryException; +import com.querydsl.core.QueryModifiers; +import com.querydsl.core.QueryResults; +import com.querydsl.core.types.ParamNotSetException; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.Param; +import com.querydsl.core.types.dsl.StringPath; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.DoubleDocValuesField; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.NumericDocValuesField; +import org.apache.lucene.document.SortedDocValuesField; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Sort; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.util.BytesRef; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for LuceneQuery + * + * @author vema + */ +public class LuceneQueryTest { + + private LuceneQuery query; + private StringPath title; + private NumberPath year; + private NumberPath gross; + + private final StringPath sort = Expressions.stringPath("sort"); + + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + + private Document createDocument( + final String docTitle, + final String docAuthor, + final String docText, + final int docYear, + final double docGross) { + Document doc = new Document(); + + doc.add(new TextField("title", docTitle, Store.YES)); + doc.add(new SortedDocValuesField("title", new BytesRef(docTitle))); + + doc.add(new TextField("author", docAuthor, Store.YES)); + doc.add(new SortedDocValuesField("author", new BytesRef(docAuthor))); + + doc.add(new TextField("text", docText, Store.YES)); + doc.add(new SortedDocValuesField("text", new BytesRef(docText))); + + doc.add(new IntPoint("year", docYear)); + doc.add(new StoredField("year", docYear)); + doc.add(new NumericDocValuesField("year", docYear)); + + doc.add(new DoublePoint("gross", docGross)); + doc.add(new StoredField("gross", docGross)); + doc.add(new DoubleDocValuesField("gross", docGross)); + + return doc; + } + + @Before + public void setUp() throws Exception { + final QDocument entityPath = new QDocument("doc"); + title = entityPath.title; + year = entityPath.year; + gross = entityPath.gross; + + idx = new ByteBuffersDirectory(); + writer = createWriter(idx); + + writer.addDocument( + createDocument( + "Jurassic Park", "Michael Crichton", "It's a UNIX system! I know this!", 1990, 90.00)); + writer.addDocument( + createDocument( + "Nummisuutarit", "Aleksis Kivi", "ESKO. Ja iloitset ja riemuitset?", 1864, 10.00)); + writer.addDocument( + createDocument( + "The Lord of the Rings", + "John R. R. Tolkien", + "One Ring to rule them all, One Ring to find them, One Ring to bring them all and in" + + " the darkness bind them", + 1954, + 89.00)); + writer.addDocument( + createDocument( + "Introduction to Algorithms", + "Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein", + "Bubble sort", + 1990, + 30.50)); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + query = new LuceneQuery(new LuceneSerializer(true, true), searcher); + } + + private IndexWriter createWriter(ByteBuffersDirectory idx) throws Exception { + IndexWriterConfig config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + return new IndexWriter(idx, config); + } + + @After + public void tearDown() throws Exception { + searcher.getIndexReader().close(); + } + + @Test + public void between() { + assertThat(query.where(year.between(1950, 1990)).fetchCount()).isEqualTo(3); + } + + @Test + public void count_empty_where_clause() { + assertThat(query.fetchCount()).isEqualTo(4); + } + + @Test + public void exists() { + assertThat(query.where(title.eq("Jurassic Park")).fetchCount() > 0).isTrue(); + assertThat(query.where(title.eq("Jurassic Park X")).fetchCount() > 0).isFalse(); + } + + @Test + public void notExists() { + assertThat(query.where(title.eq("Jurassic Park")).fetchCount() == 0).isFalse(); + assertThat(query.where(title.eq("Jurassic Park X")).fetchCount() == 0).isTrue(); + } + + @Test + public void count() { + query.where(title.eq("Jurassic Park")); + assertThat(query.fetchCount()).isEqualTo(1); + } + + @Test(expected = UnsupportedOperationException.class) + public void countDistinct() { + query.where(year.between(1900, 3000)); + assertThat(query.distinct().fetchCount()).isEqualTo(3); + } + + @Test + public void in() { + assertThat(query.where(title.in("Jurassic Park", "Nummisuutarit")).fetchCount()).isEqualTo(2); + } + + @Test + public void in2() { + assertThat(query.where(year.in(1990, 1864)).fetchCount()).isEqualTo(3); + } + + @Test + public void list_sorted_by_year_ascending() { + query.where(year.between(1800, 2000)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted() { + query.where(year.between(1800, 2000)); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted_limit_2() { + query.where(year.between(1800, 2000)); + query.limit(2); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_by_year_limit_1() { + query.where(year.between(1800, 2000)); + query.limit(1); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(1); + } + + @Test + public void list_not_sorted_offset_2() { + query.where(year.between(1800, 2000)); + query.offset(2); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year_offset_2() { + query.where(year.between(1800, 2000)); + query.offset(2); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year_restrict_limit_2_offset_1() { + query.where(year.between(1800, 2000)); + query.restrict(new QueryModifiers(2L, 1L)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(2); + } + + @Test + public void list_sorted_ascending_by_year() { + query.where(year.between(1800, 2000)); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sort() { + Sort sort = LuceneSerializer.DEFAULT.toSort(Collections.singletonList(year.asc())); + + query.where(year.between(1800, 2000)); + query.sort(sort); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_with_filter() { + assertThat(query.fetch()).hasSize(4); + assertThat(query.filter(IntPoint.newExactQuery("year", 1990)).fetch()).hasSize(2); + } + + @Test + public void list_sorted_descending_by_year() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_gross() { + query.where(gross.between(0.0, 1000.00)); + query.orderBy(gross.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_year_and_ascending_by_title() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + query.orderBy(title.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_sorted_descending_by_year_and_descending_by_title() { + query.where(year.between(1800, 2000)); + query.orderBy(year.desc()); + query.orderBy(title.desc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void offset() { + assertThat(query.where(title.eq("Jurassic Park")).offset(30).fetch()).isEmpty(); + } + + @Test + public void load_list() { + Document document = query.where(title.ne("")).load(title).fetch().get(0); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_list_fieldSelector() { + Document document = + query.where(title.ne("")).load(Collections.singleton("title")).fetch().get(0); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_singleResult() { + Document document = query.where(title.ne("")).load(title).fetchFirst(); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void load_singleResult_fieldSelector() { + Document document = query.where(title.ne("")).load(Collections.singleton("title")).fetchFirst(); + assertThat(document.get("title")).isNotNull(); + assertThat(document.get("year")).isNull(); + } + + @Test + public void singleResult() { + assertThat(query.where(title.ne("")).fetchFirst()).isNotNull(); + } + + @Test + public void single_result_takes_limit() { + assertThat(query.where(title.ne("")).limit(1).fetchFirst().get("title")) + .isEqualTo("Jurassic Park"); + } + + @Test + public void single_result_considers_limit_and_actual_result_size() { + query.where(title.startsWith("Nummi")); + final Document document = query.limit(3).fetchFirst(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void single_result_returns_null_if_nothing_is_in_range() { + query.where(title.startsWith("Nummi")); + assertThat(query.offset(10).fetchFirst()).isNull(); + } + + @Test + public void single_result_considers_offset() { + assertThat(query.where(title.ne("")).offset(3).fetchFirst().get("title")) + .isEqualTo("Introduction to Algorithms"); + } + + @Test + public void single_result_considers_limit_and_offset() { + assertThat(query.where(title.ne("")).limit(1).offset(2).fetchFirst().get("title")) + .isEqualTo("The Lord of the Rings"); + } + + @Test(expected = NonUniqueResultException.class) + public void uniqueResult_contract() { + query.where(title.ne("")).fetchOne(); + } + + @Test + public void unique_result_takes_limit() { + assertThat(query.where(title.ne("")).limit(1).fetchOne().get("title")) + .isEqualTo("Jurassic Park"); + } + + @Test + public void unique_result_considers_limit_and_actual_result_size() { + query.where(title.startsWith("Nummi")); + final Document document = query.limit(3).fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void unique_result_returns_null_if_nothing_is_in_range() { + query.where(title.startsWith("Nummi")); + assertThat(query.offset(10).fetchOne()).isNull(); + } + + @Test + public void unique_result_considers_offset() { + assertThat(query.where(title.ne("")).offset(3).fetchOne().get("title")) + .isEqualTo("Introduction to Algorithms"); + } + + @Test + public void unique_result_considers_limit_and_offset() { + assertThat(query.where(title.ne("")).limit(1).offset(2).fetchOne().get("title")) + .isEqualTo("The Lord of the Rings"); + } + + @Test + public void uniqueResult() { + query.where(title.startsWith("Nummi")); + final Document document = query.fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test + public void uniqueResult_with_param() { + final Param param = new Param(String.class, "title"); + query.set(param, "Nummi"); + query.where(title.startsWith(param)); + final Document document = query.fetchOne(); + assertThat(document.get("title")).isEqualTo("Nummisuutarit"); + } + + @Test(expected = ParamNotSetException.class) + public void uniqueResult_param_not_set() { + final Param param = new Param(String.class, "title"); + query.where(title.startsWith(param)); + query.fetchOne(); + } + + @Test(expected = QueryException.class) + public void uniqueResult_finds_more_than_one_result() { + query.where(year.eq(1990)); + query.fetchOne(); + } + + @Test + public void uniqueResult_finds_no_results() { + query.where(year.eq(2200)); + assertThat(query.fetchOne()).isNull(); + } + + @Test(expected = UnsupportedOperationException.class) + public void listDistinct() { + query.where(year.between(1900, 2000).or(title.startsWith("Jura"))); + query.orderBy(year.asc()); + final List documents = query.distinct().fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(3); + } + + @Test + public void listResults() { + query.where(year.between(1800, 2000)); + query.restrict(new QueryModifiers(2L, 1L)); + query.orderBy(year.asc()); + final QueryResults results = query.fetchResults(); + assertThat(results.isEmpty()).isFalse(); + assertThat(results.getResults()).hasSize(2); + assertThat(results.getLimit()).isEqualTo(2); + assertThat(results.getOffset()).isEqualTo(1); + assertThat(results.getTotal()).isEqualTo(4); + } + + @Test(expected = UnsupportedOperationException.class) + public void listDistinctResults() { + query.where(year.between(1800, 2000).or(title.eq("The Lord of the Rings"))); + query.restrict(new QueryModifiers(1L, 1L)); + query.orderBy(year.asc()); + final QueryResults results = query.distinct().fetchResults(); + assertThat(results.isEmpty()).isFalse(); + assertThat(results.getResults().get(0).get("year")).isEqualTo("1954"); + assertThat(results.getLimit()).isEqualTo(1); + assertThat(results.getOffset()).isEqualTo(1); + assertThat(results.getTotal()).isEqualTo(4); + } + + @Test + public void list_all() { + final List results = + query.where(title.like("*")).orderBy(title.asc(), year.desc()).fetch(); + assertThat(results).hasSize(4); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_limit_negative() { + query.where(year.between(1800, 2000)); + query.limit(-1); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_limit_negative() { + query.where(year.between(1800, 2000)); + query.limit(-1); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_limit_0() { + query.where(year.between(1800, 2000)); + query.limit(0); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_limit_0() { + query.where(year.between(1800, 2000)); + query.limit(0); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_sorted_ascending_offset_negative() { + query.where(year.between(1800, 2000)); + query.offset(-1); + query.orderBy(year.asc()); + query.fetch(); + } + + @Test(expected = IllegalArgumentException.class) + public void list_not_sorted_offset_negative() { + query.where(year.between(1800, 2000)); + query.offset(-1); + query.fetch(); + } + + @Test + public void list_sorted_ascending_offset_0() { + query.where(year.between(1800, 2000)); + query.offset(0); + query.orderBy(year.asc()); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void list_not_sorted_offset_0() { + query.where(year.between(1800, 2000)); + query.offset(0); + final List documents = query.fetch(); + assertThat(documents).isNotEmpty(); + assertThat(documents).hasSize(4); + } + + @Test + public void iterate() { + query.where(year.between(1800, 2000)); + final Iterator iterator = query.iterate(); + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + ++count; + } + assertThat(count).isEqualTo(4); + } + + @Test + public void all_by_excluding_where() { + assertThat(query.fetch()).hasSize(4); + } + + @Test + public void empty_index_should_return_empty_list() throws Exception { + idx = new ByteBuffersDirectory(); + + writer = createWriter(idx); + writer.close(); + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + query = new LuceneQuery(new LuceneSerializer(true, true), searcher); + assertThat(query.fetch()).isEmpty(); + } + + @Test(expected = QueryException.class) + public void + list_results_throws_an_illegal_argument_exception_when_sum_of_limit_and_offset_is_negative() { + query.limit(1).offset(Integer.MAX_VALUE).fetchResults(); + } + + @Test + public void limit_max_value() { + assertThat(query.limit(Long.MAX_VALUE).fetch()).hasSize(4); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerNotTokenizedTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerNotTokenizedTest.java new file mode 100644 index 000000000..3fee3f913 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerNotTokenizedTest.java @@ -0,0 +1,216 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static com.querydsl.lucene9.QPerson.person; +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import java.time.LocalDate; +import java.util.Arrays; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.StringField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.junit.Before; +import org.junit.Test; + +public class LuceneSerializerNotTokenizedTest { + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + private LuceneSerializer serializer; + + private final QueryMetadata metadata = new DefaultQueryMetadata(); + + private final Person clooney = new Person("actor_1", "George Clooney", LocalDate.of(1961, 4, 6)); + private final Person pitt = new Person("actor_2", "Brad Pitt", LocalDate.of(1963, 12, 18)); + + private void testQuery(Expression expr, String expectedQuery, int expectedHits) + throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value).isEqualTo(expectedHits); + assertThat(query.toString()).isEqualTo(expectedQuery); + } + + private Document createDocument(Person person) { + Document doc = new Document(); + doc.add(new StringField("id", person.getId(), Store.YES)); + doc.add(new StringField("name", person.getName(), Store.YES)); + doc.add(new StringField("birthDate", person.getBirthDate().toString(), Store.YES)); + return doc; + } + + @Before + public void before() throws Exception { + serializer = new LuceneSerializer(false, false); + idx = new ByteBuffersDirectory(); + IndexWriterConfig config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + writer = new IndexWriter(idx, config); + + writer.addDocument(createDocument(clooney)); + writer.addDocument(createDocument(pitt)); + + Document document = new Document(); + for (String movie : Arrays.asList("Interview with the Vampire", "Up in the Air")) { + document.add(new StringField("movie", movie, Store.YES)); + } + writer.addDocument(document); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + } + + @Test + public void equals_by_id_matches() throws Exception { + testQuery(person.id.eq("actor_1"), "id:actor_1", 1); + } + + @Test + public void equals_by_id_does_not_match() throws Exception { + testQuery(person.id.eq("actor_8"), "id:actor_8", 0); + } + + @Test + public void equals_by_name_matches() throws Exception { + testQuery(person.name.eq("George Clooney"), "name:George Clooney", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void equals_by_name_ignoring_case_does_not_match() throws Exception { + testQuery(person.name.equalsIgnoreCase("george clooney"), "name:george clooney", 0); + } + + @Test + public void equals_by_name_does_not_match() throws Exception { + testQuery(person.name.eq("George Looney"), "name:George Looney", 0); + } + + @Test + public void starts_with_name_should_match() throws Exception { + testQuery(person.name.startsWith("George C"), "name:George C*", 1); + } + + @Test + public void starts_with_name_should_not_match() throws Exception { + testQuery(person.name.startsWith("George L"), "name:George L*", 0); + } + + @Test + public void ends_with_name_should_match() throws Exception { + testQuery(person.name.endsWith("e Clooney"), "name:*e Clooney", 1); + } + + @Test + public void ends_with_name_should_not_match() throws Exception { + testQuery(person.name.endsWith("e Looney"), "name:*e Looney", 0); + } + + @Test + public void contains_name_should_match() throws Exception { + testQuery(person.name.contains("oney"), "name:*oney*", 1); + } + + @Test + public void contains_name_should_not_match() throws Exception { + testQuery(person.name.contains("bloney"), "name:*bloney*", 0); + } + + @Test + public void in_names_should_match_2() throws Exception { + testQuery( + person.name.in("Brad Pitt", "George Clooney"), "name:Brad Pitt name:George Clooney", 2); + } + + @Test + public void or_by_name_should_match_2() throws Exception { + testQuery( + person.name.eq("Brad Pitt").or(person.name.eq("George Clooney")), + "name:Brad Pitt name:George Clooney", + 2); + } + + @Test + public void equals_by_birth_date() throws Exception { + testQuery(person.birthDate.eq(clooney.getBirthDate()), "birthDate:1961-04-06", 1); + } + + @Test + public void between_phrase() throws Exception { + testQuery( + person.name.between("Brad Pitt", "George Clooney"), + "name:[Brad Pitt TO George Clooney]", + 2); + } + + @Test + public void not_equals_finds_the_actors_and_movies() throws Exception { + testQuery(person.name.ne("Michael Douglas"), "-name:Michael Douglas +*:*", 3); + } + + @Test + public void not_equals_finds_only_clooney_and_movies() throws Exception { + testQuery(person.name.ne("Brad Pitt"), "-name:Brad Pitt +*:*", 2); + } + + @Test + public void and_with_two_not_equals_doesnt_find_the_actors() throws Exception { + testQuery( + person.name.ne("Brad Pitt").and(person.name.ne("George Clooney")), + "+(-name:Brad Pitt +*:*) +(-name:George Clooney +*:*)", + 1); + } + + @Test + public void or_with_two_not_equals_finds_movies_and_actors() throws Exception { + testQuery( + person.name.ne("Brad Pitt").or(person.name.ne("George Clooney")), + "(-name:Brad Pitt +*:*) (-name:George Clooney +*:*)", + 3); + } + + @Test + public void negation_of_equals_finds_movies_and_actors() throws Exception { + testQuery(person.name.eq("Michael Douglas").not(), "-name:Michael Douglas +*:*", 3); + } + + @Test + public void negation_of_equals_finds_pitt_and_movies() throws Exception { + testQuery(person.name.eq("Brad Pitt").not(), "-name:Brad Pitt +*:*", 2); + } + + @Test + public void multiple_field_search_from_movies() throws Exception { + StringPath movie = Expressions.stringPath("movie"); + testQuery(movie.in("Interview with the Vampire"), "movie:Interview with the Vampire", 1); + testQuery(movie.eq("Up in the Air"), "movie:Up in the Air", 1); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerTest.java new file mode 100644 index 000000000..1c72f9e66 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/LuceneSerializerTest.java @@ -0,0 +1,715 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.MatchingFiltersFactory; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.QuerydslModule; +import com.querydsl.core.StringConstant; +import com.querydsl.core.Target; +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Operation; +import com.querydsl.core.types.Operator; +import com.querydsl.core.types.Ops; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CollectionPath; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.PathBuilder; +import com.querydsl.core.types.dsl.StringPath; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.DoublePoint; +import org.apache.lucene.document.Field.Store; +import org.apache.lucene.document.FloatPoint; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Tests for LuceneSerializer + * + * @author vema + */ +public class LuceneSerializerTest { + private LuceneSerializer serializer; + private PathBuilder entityPath; + private StringPath title; + private StringPath author; + private StringPath text; + private StringPath rating; + private StringPath publisher; + private NumberPath year; + private NumberPath gross; + private CollectionPath titles; + + private NumberPath longField; + private NumberPath shortField; + private NumberPath byteField; + private NumberPath floatField; + + private IndexWriterConfig config; + private ByteBuffersDirectory idx; + private IndexWriter writer; + private IndexSearcher searcher; + + private static final Set UNSUPPORTED_OPERATORS = + Collections.unmodifiableSet( + EnumSet.of( + Ops.STARTS_WITH_IC, Ops.EQ_IGNORE_CASE, Ops.ENDS_WITH_IC, Ops.STRING_CONTAINS_IC)); + + private final QueryMetadata metadata = new DefaultQueryMetadata(); + + private Document createDocument() { + Document doc = new Document(); + + doc.add(new TextField("title", new StringReader("Jurassic Park"))); + doc.add(new TextField("author", new StringReader("Michael Crichton"))); + doc.add(new TextField("text", new StringReader("It's a UNIX system! I know this!"))); + doc.add(new TextField("rating", new StringReader("Good"))); + doc.add(new StringField("publisher", "", Store.YES)); + doc.add(new IntPoint("year", 1990)); + doc.add(new StoredField("year", 1990)); + doc.add(new DoublePoint("gross", 900.0)); + doc.add(new StoredField("gross", 900.0)); + + doc.add(new LongPoint("longField", 1)); + doc.add(new StoredField("longField", 1L)); + doc.add(new IntPoint("shortField", 1)); + doc.add(new StoredField("shortField", 1)); + doc.add(new IntPoint("byteField", 1)); + doc.add(new StoredField("byteField", 1)); + doc.add(new FloatPoint("floatField", 1)); + doc.add(new StoredField("floatField", 1.0f)); + + return doc; + } + + @Before + public void setUp() throws Exception { + serializer = new LuceneSerializer(true, true); + entityPath = new PathBuilder(Object.class, "obj"); + title = entityPath.getString("title"); + author = entityPath.getString("author"); + text = entityPath.getString("text"); + publisher = entityPath.getString("publisher"); + year = entityPath.getNumber("year", Integer.class); + rating = entityPath.getString("rating"); + gross = entityPath.getNumber("gross", Double.class); + titles = entityPath.getCollection("title", String.class, StringPath.class); + + longField = entityPath.getNumber("longField", Long.class); + shortField = entityPath.getNumber("shortField", Short.class); + byteField = entityPath.getNumber("byteField", Byte.class); + floatField = entityPath.getNumber("floatField", Float.class); + + idx = new ByteBuffersDirectory(); + config = + new IndexWriterConfig(new StandardAnalyzer()) + .setOpenMode(IndexWriterConfig.OpenMode.CREATE); + writer = new IndexWriter(idx, config); + + writer.addDocument(createDocument()); + + writer.close(); + + IndexReader reader = DirectoryReader.open(idx); + searcher = new IndexSearcher(reader); + } + + @After + public void tearDown() throws Exception { + searcher.getIndexReader().close(); + } + + private void testQuery(Expression expr, int expectedHits) throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value).isEqualTo(expectedHits); + } + + private void testQuery(Expression expr, String expectedQuery, int expectedHits) + throws Exception { + Query query = serializer.toQuery(expr, metadata); + TopDocs docs = searcher.search(query, 100); + assertThat(docs.totalHits.value).isEqualTo(expectedHits); + assertThat(query.toString()).isEqualTo(expectedQuery); + } + + @Test + public void queryElement() throws Exception { + Query query1 = serializer.toQuery(author.like("Michael"), metadata); + Query query2 = serializer.toQuery(text.like("Text"), metadata); + + BooleanExpression query = Expressions.anyOf(new QueryElement(query1), new QueryElement(query2)); + testQuery(query, "author:michael text:text", 1); + } + + @Test + public void like() throws Exception { + testQuery(author.like("*ichael*"), "author:*ichael*", 1); + } + + @Test + public void like_custom_wildcard_single_character() throws Exception { + testQuery(author.like("Mi?hael"), "author:mi?hael", 1); + } + + @Test + public void like_custom_wildcard_multiple_character() throws Exception { + testQuery(text.like("*U*X*"), "text:*u*x*", 1); + } + + @Test + public void like_phrase() throws Exception { + testQuery(title.like("*rassic Par*"), "+title:**rassic* +title:*par**", 1); + } + + @Test + public void like_or_like() throws Exception { + testQuery(title.like("House").or(author.like("*ichae*")), "title:house author:*ichae*", 1); + } + + @Test + public void like_and_like() throws Exception { + testQuery(title.like("*assic*").and(rating.like("G?od")), "+title:*assic* +rating:g?od", 1); + } + + @Test + public void eq() throws Exception { + testQuery(rating.eq("good"), "rating:good", 1); + } + + @Test + public void eq_with_deep_path() throws Exception { + StringPath deepPath = entityPath.get("property1", Object.class).getString("property2"); + testQuery(deepPath.eq("good"), "property1.property2:good", 0); + } + + @Test + public void fuzzyLike() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good"), "rating:Good~2", 1); + } + + @Test + public void fuzzyLike_with_similarity() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good", 2), "rating:Good~2", 1); + } + + @Test + public void fuzzyLike_with_similarity_and_prefix() throws Exception { + testQuery(LuceneExpressions.fuzzyLike(rating, "Good", 2, 0), "rating:Good~2", 1); + } + + @Test + public void eq_numeric_integer() throws Exception { + testQuery(year.eq(1990), 1); + } + + @Test + public void eq_numeric_double() throws Exception { + testQuery(gross.eq(900.00), 1); + } + + @Test + public void eq_numeric() throws Exception { + testQuery(longField.eq(1L), 1); + testQuery(shortField.eq((short) 1), 1); + testQuery(byteField.eq((byte) 1), 1); + testQuery(floatField.eq((float) 1.0), 1); + } + + @Test + public void equals_ignores_case() throws Exception { + testQuery(title.eq("Jurassic"), "title:jurassic", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void title_equals_ignore_case_or_year_equals() throws Exception { + testQuery(title.equalsIgnoreCase("House").or(year.eq(1990)), 1); + } + + @Test + public void eq_and_eq() throws Exception { + testQuery(title.eq("Jurassic Park").and(year.eq(1990)), 1); + } + + @Test + public void eq_and_eq_and_eq() throws Exception { + testQuery(title.eq("Jurassic Park").and(year.eq(1990)).and(author.eq("Michael Crichton")), 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void equals_ignore_case_and_or() throws Exception { + testQuery( + title + .equalsIgnoreCase("Jurassic Park") + .and(rating.equalsIgnoreCase("Bad")) + .or(author.equalsIgnoreCase("Michael Crichton")), + 1); + } + + @Test + public void eq_or_eq_and_eq_does_not_find_results() throws Exception { + testQuery( + title.eq("jeeves").or(rating.eq("superb")).and(author.eq("michael crichton")), + "+(title:jeeves rating:superb) +author:\"michael crichton\"", + 0); + } + + @Test + public void eq_phrase() throws Exception { + testQuery(title.eq("Jurassic Park"), "title:\"jurassic park\"", 1); + } + + @Test + @Ignore("Not easily done in Lucene!") + public void publisher_equals_empty_string() throws Exception { + testQuery(publisher.eq(""), "publisher:", 1); + } + + @Test + public void eq_phrase_should_not_find_results_but_luceNe_semantics_differs_from_querydsls() + throws Exception { + testQuery(text.eq("UNIX System"), "text:\"unix system\"", 1); + } + + @Test + public void eq_phrase_does_not_find_results_because_word_in_middle() throws Exception { + testQuery(title.eq("Jurassic Amusement Park"), "title:\"jurassic amusement park\"", 0); + } + + @Test + public void like_not_does_not_find_results() throws Exception { + testQuery(title.like("*H*e*").not(), "-title:*h*e* +*:*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void title_equals_ignore_case_negation_or_rating_equals_ignore_case() throws Exception { + testQuery( + title.equalsIgnoreCase("House").not().or(rating.equalsIgnoreCase("Good")), + "-title:house rating:good", + 1); + } + + @Test + public void eq_not_does_not_find_results() throws Exception { + testQuery(title.eq("Jurassic Park").not(), "-title:\"jurassic park\" +*:*", 0); + } + + @Test + public void title_equals_not_house() throws Exception { + testQuery(title.eq("house").not(), "-title:house +*:*", 1); + } + + @Test + public void eq_and_eq_not_does_not_find_results_because_second_expression_finds_nothing() + throws Exception { + testQuery( + rating.eq("superb").and(title.eq("house").not()), "+rating:superb +(-title:house +*:*)", 0); + } + + @Test + public void not_equals_finds_one() throws Exception { + testQuery(title.ne("house"), "-title:house +*:*", 1); + } + + @Test + public void not_equals_finds_none() throws Exception { + testQuery(title.ne("Jurassic Park"), "-title:\"jurassic park\" +*:*", 0); + } + + @Test + public void nothing_found_with_not_equals_or_equals() throws Exception { + testQuery( + title.ne("jurassic park").or(rating.eq("lousy")), + "(-title:\"jurassic park\" +*:*) rating:lousy", + 0); + } + + @Test + public void ne_and_eq() throws Exception { + testQuery(title.ne("house").and(rating.eq("good")), "+(-title:house +*:*) +rating:good", 1); + } + + @Test + public void startsWith() throws Exception { + testQuery(title.startsWith("Jurassi"), "title:jurassi*", 1); + } + + @Test + public void startsWith_phrase() throws Exception { + testQuery(title.startsWith("jurassic par"), "+title:jurassic* +title:*par*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void starts_with_ignore_case_phrase_does_not_find_results() throws Exception { + testQuery(title.startsWithIgnoreCase("urassic Par"), "+title:urassic* +title:*par*", 0); + } + + @Test + public void endsWith() throws Exception { + testQuery(title.endsWith("ark"), "title:*ark", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void ends_with_ignore_case_phrase() throws Exception { + testQuery(title.endsWithIgnoreCase("sic Park"), "+title:*sic* +title:*park", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void ends_with_ignore_case_phrase_does_not_find_results() throws Exception { + testQuery(title.endsWithIgnoreCase("sic Par"), "+title:*sic* +title:*par", 0); + } + + @Test + public void contains() throws Exception { + testQuery(title.contains("rassi"), "title:*rassi*", 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void contains_ignore_case_phrase() throws Exception { + testQuery(title.containsIgnoreCase("rassi Pa"), "+title:*rassi* +title:*pa*", 1); + } + + @Test + public void contains_user_inputted_wildcards_dont_work() throws Exception { + testQuery(title.contains("r*i"), "title:*r\\*i*", 0); + } + + @Test + public void between() throws Exception { + testQuery(title.between("Indiana", "Kundun"), "title:[indiana TO kundun]", 1); + } + + @Test + public void between_numeric_integer() throws Exception { + testQuery(year.between(1980, 2000), 1); + } + + @Test + public void between_numeric_double() throws Exception { + testQuery(gross.between(10.00, 19030.00), 1); + } + + @Test + public void between_numeric() throws Exception { + testQuery(longField.between(0L, 2L), 1); + testQuery(shortField.between((short) 0, (short) 2), 1); + testQuery(byteField.between((byte) 0, (byte) 2), 1); + testQuery(floatField.between((float) 0.0, (float) 2.0), 1); + } + + @Test + public void between_is_inclusive_from_start() throws Exception { + testQuery(title.between("Jurassic", "Kundun"), "title:[jurassic TO kundun]", 1); + } + + @Test + public void between_is_inclusive_to_end() throws Exception { + testQuery(title.between("Indiana", "Jurassic"), "title:[indiana TO jurassic]", 1); + } + + @Test + public void between_does_not_find_results() throws Exception { + testQuery(title.between("Indiana", "Jurassib"), "title:[indiana TO jurassib]", 0); + } + + @Test + public void in() throws Exception { + testQuery(title.in(Arrays.asList("jurassic", "park")), "title:jurassic title:park", 1); + testQuery(title.in("jurassic", "park"), "title:jurassic title:park", 1); + testQuery(title.eq("jurassic").or(title.eq("park")), "title:jurassic title:park", 1); + } + + @Test + public void lt() throws Exception { + testQuery(rating.lt("Superb"), "rating:{* TO superb}", 1); + } + + @Test + public void lt_numeric_integer() throws Exception { + testQuery(year.lt(1991), 1); + } + + @Test + public void lt_numeric_double() throws Exception { + testQuery(gross.lt(10000.0), 1); + } + + @Test + public void lt_not_in_range_because_equal() throws Exception { + testQuery(rating.lt("Good"), "rating:{* TO good}", 0); + } + + @Test + public void lt_numeric_integer_not_in_range_because_equal() throws Exception { + testQuery(year.lt(1990), 0); + } + + @Test + public void lt_numeric_double_not_in_range_because_equal() throws Exception { + testQuery(gross.lt(900.0), 0); + } + + @Test + public void loe() throws Exception { + testQuery(rating.loe("Superb"), "rating:[* TO superb]", 1); + } + + @Test + public void loe_numeric_integer() throws Exception { + testQuery(year.loe(1991), 1); + } + + @Test + public void loe_numeric_double() throws Exception { + testQuery(gross.loe(903.0), 1); + } + + @Test + public void loe_equal() throws Exception { + testQuery(rating.loe("Good"), "rating:[* TO good]", 1); + } + + @Test + public void loe_numeric_integer_equal() throws Exception { + testQuery(year.loe(1990), 1); + } + + @Test + public void loe_numeric_double_equal() throws Exception { + testQuery(gross.loe(900.0), 1); + } + + @Test + public void loe_not_found() throws Exception { + testQuery(rating.loe("Bad"), "rating:[* TO bad]", 0); + } + + @Test + public void loe_numeric_integer_not_found() throws Exception { + testQuery(year.loe(1989), 0); + } + + @Test + public void loe_numeric_double_not_found() throws Exception { + testQuery(gross.loe(899.9), 0); + } + + @Test + public void gt() throws Exception { + testQuery(rating.gt("Bad"), "rating:{bad TO *}", 1); + } + + @Test + public void gt_numeric_integer() throws Exception { + testQuery(year.gt(1989), 1); + } + + @Test + public void gt_numeric_double() throws Exception { + testQuery(gross.gt(100.00), 1); + } + + @Test + public void gt_not_in_range_because_equal() throws Exception { + testQuery(rating.gt("Good"), "rating:{good TO *}", 0); + } + + @Test + public void gt_numeric_integer_not_in_range_because_equal() throws Exception { + testQuery(year.gt(1990), 0); + } + + @Test + public void gt_numeric_double_not_in_range_because_equal() throws Exception { + testQuery(gross.gt(900.00), 0); + } + + @Test + public void goe() throws Exception { + testQuery(rating.goe("Bad"), "rating:[bad TO *]", 1); + } + + @Test + public void goe_numeric_integer() throws Exception { + testQuery(year.goe(1989), 1); + } + + @Test + public void goe_numeric_double() throws Exception { + testQuery(gross.goe(320.50), 1); + } + + @Test + public void goe_equal() throws Exception { + testQuery(rating.goe("Good"), "rating:[good TO *]", 1); + } + + @Test + public void goe_numeric_integer_equal() throws Exception { + testQuery(year.goe(1990), 1); + } + + @Test + public void goe_numeric_double_equal() throws Exception { + testQuery(gross.goe(900.00), 1); + } + + @Test + public void goe_not_found() throws Exception { + testQuery(rating.goe("Hood"), "rating:[hood TO *]", 0); + } + + @Test + public void goe_numeric_integer_not_found() throws Exception { + testQuery(year.goe(1991), 0); + } + + @Test + public void goe_numeric_double_not_found() throws Exception { + testQuery(gross.goe(900.10), 0); + } + + @Test + public void equals_empty_string() throws Exception { + testQuery(title.eq(""), "title:", 0); + } + + @Test + public void not_equals_empty_string() throws Exception { + testQuery(title.ne(""), "-title: +*:*", 1); + } + + @Test + public void contains_empty_string() throws Exception { + testQuery(title.contains(""), "title:**", 1); + } + + @Test + public void like_empty_string() throws Exception { + testQuery(title.like(""), "title:", 0); + } + + @Test + public void starts_with_empty_string() throws Exception { + testQuery(title.startsWith(""), "title:*", 1); + } + + @Test + public void ends_with_empty_string() throws Exception { + testQuery(title.endsWith(""), "title:*", 1); + } + + @Test + public void between_empty_strings() throws Exception { + testQuery(title.between("", ""), "title:[ TO ]", 0); + } + + @Test + public void booleanBuilder() throws Exception { + testQuery(new BooleanBuilder(gross.goe(900.10)), 0); + } + + @Test + @Ignore + public void fuzzy() throws Exception { + fail("Not yet implemented!"); + } + + @Test + @Ignore + public void proximity() throws Exception { + fail("Not yet implemented!"); + } + + @Test + @Ignore + public void boost() throws Exception { + fail("Not yet implemented!"); + } + + @Test + public void pathAny() throws Exception { + testQuery(titles.any().eq("Jurassic"), "title:jurassic", 1); + } + + private boolean unsupportedOperation(Predicate filter) { + return UNSUPPORTED_OPERATORS.contains(((Operation) filter).getOperator()); + } + + @Test + public void various() throws Exception { + MatchingFiltersFactory filters = + new MatchingFiltersFactory(QuerydslModule.COLLECTIONS, Target.LUCENE); + for (Predicate filter : filters.string(title, StringConstant.create("jurassic park"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 1); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + + for (Predicate filter : filters.string(author, StringConstant.create("michael crichton"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 1); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + + for (Predicate filter : filters.string(title, StringConstant.create("1990"))) { + if (unsupportedOperation(filter)) { + continue; + } + try { + testQuery(filter, 0); + } catch (IllegalArgumentException | UnsupportedOperationException | AssertionError e) { + // some operations are unsupported or incompatible with Lucene's term-based index + } + } + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/Person.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/Person.java new file mode 100644 index 000000000..7429ffba5 --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/Person.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.annotations.QueryEntity; +import java.time.LocalDate; + +@QueryEntity +public class Person { + private final String id; + private final String name; + private final LocalDate birthDate; + + public Person(String id, String name, LocalDate birthDate) { + this.id = id; + this.name = name; + this.birthDate = birthDate; + } + + public String getId() { + return id; + } + + public LocalDate getBirthDate() { + return birthDate; + } + + public String getName() { + return name; + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/PhraseElementTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/PhraseElementTest.java new file mode 100644 index 000000000..096f0540a --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/PhraseElementTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import org.junit.Test; + +public class PhraseElementTest { + + @Test + public void test() { + StringPath title = Expressions.stringPath("title"); + LuceneSerializer serializer = new LuceneSerializer(false, false); + QueryMetadata metadata = new DefaultQueryMetadata(); + assertThat(serializer.toQuery(title.eq("Hello World"), metadata).toString()) + .isEqualTo("title:Hello World"); + assertThat(serializer.toQuery(title.eq(new PhraseElement("Hello World")), metadata).toString()) + .isEqualTo("title:\"Hello World\""); + } + + @Test + public void equals() { + PhraseElement el1 = new PhraseElement("x"), + el2 = new PhraseElement("x"), + el3 = new PhraseElement("y"); + assertThat(el2).isEqualTo(el1); + assertThat(el1.equals(el3)).isFalse(); + } + + @Test + public void hashCode_() { + PhraseElement el1 = new PhraseElement("x"), el2 = new PhraseElement("x"); + assertThat(el2.hashCode()).isEqualTo(el1.hashCode()); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QDocument.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QDocument.java new file mode 100644 index 000000000..542e2b9dc --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QDocument.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import com.querydsl.core.types.PathMetadataFactory; +import com.querydsl.core.types.dsl.EntityPathBase; +import com.querydsl.core.types.dsl.NumberPath; +import com.querydsl.core.types.dsl.StringPath; +import org.apache.lucene.document.Document; + +public class QDocument extends EntityPathBase { + + private static final long serialVersionUID = -4872833626508344081L; + + public QDocument(final String var) { + super(Document.class, PathMetadataFactory.forVariable(var)); + } + + public final NumberPath year = createNumber("year", Integer.class); + + public final StringPath title = createString("title"); + + public final NumberPath gross = createNumber("gross", Double.class); +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QueryElementTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QueryElementTest.java new file mode 100644 index 000000000..416ac7cec --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/QueryElementTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.lucene.index.Term; +import org.apache.lucene.search.TermQuery; +import org.junit.Ignore; +import org.junit.Test; + +public class QueryElementTest { + + @Test + @Ignore + public void test() { + QueryElement element = new QueryElement(new TermQuery(new Term("str", "text"))); + assertThat(element.toString()).isEqualTo("str:text"); + + QueryElement element2 = new QueryElement(new TermQuery(new Term("str", "text"))); + assertThat(element).isEqualTo(element2); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/TermElementTest.java b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/TermElementTest.java new file mode 100644 index 000000000..d1a10e7ce --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/java/com/querydsl/lucene9/TermElementTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015, The Querydsl Team (http://www.querydsl.com/team) + * + * 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.querydsl.lucene9; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.querydsl.core.DefaultQueryMetadata; +import com.querydsl.core.QueryMetadata; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringPath; +import org.junit.Test; + +public class TermElementTest { + + @Test + public void test() { + StringPath title = Expressions.stringPath("title"); + LuceneSerializer serializer = new LuceneSerializer(false, true); + QueryMetadata metadata = new DefaultQueryMetadata(); + assertThat(serializer.toQuery(title.eq("Hello World"), metadata).toString()) + .isEqualTo("title:\"Hello World\""); + assertThat(serializer.toQuery(title.eq(new TermElement("Hello World")), metadata).toString()) + .isEqualTo("title:Hello World"); + } + + @Test + public void testEqualsAndHashCode() { + TermElement el1 = new TermElement("x"), el2 = new TermElement("x"), el3 = new TermElement("y"); + assertThat(el2).isEqualTo(el1); + assertThat(el1.equals(el3)).isFalse(); + assertThat(el2.hashCode()).isEqualTo(el1.hashCode()); + } +} diff --git a/querydsl-libraries/querydsl-lucene9/src/test/resources/log4j.properties.example b/querydsl-libraries/querydsl-lucene9/src/test/resources/log4j.properties.example new file mode 100644 index 000000000..0d5df631c --- /dev/null +++ b/querydsl-libraries/querydsl-lucene9/src/test/resources/log4j.properties.example @@ -0,0 +1,9 @@ +# Configure an appender that logs to console +log4j.rootLogger=info, A1 +log4j.threshold=debug +log4j.appender.A1=org.apache.log4j.ConsoleAppender +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c{1} - %m%n + +# Configuration of logging levels for different packages +log4j.logger.com.querydsl.lucene9=DEBUG