Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions java/lance-jni/src/blocking_dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,7 @@ fn inner_describe_indices<'local>(
for_column: for_column.as_deref(),
has_name: has_name.as_deref(),
must_support_fts,
fts_document_granularity: None,
must_support_exact_equality,
})
})?;
Expand Down Expand Up @@ -3579,6 +3580,7 @@ fn inner_get_zonemap_stats<'local>(
for_column: Some(&column_name),
has_name: None,
must_support_fts: false,
fts_document_granularity: None,
must_support_exact_equality: false,
}))
.await
Expand Down
33 changes: 29 additions & 4 deletions java/lance-jni/src/blocking_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ use lance::dataset::scanner::{
ExecutionSummaryCounts, Scanner,
};
use lance_index::scalar::FullTextSearchQuery;
use lance_index::scalar::inverted::query::{
BooleanQuery as FtsBooleanQuery, BoostQuery as FtsBoostQuery, FtsQuery,
MatchQuery as FtsMatchQuery, MultiMatchQuery as FtsMultiMatchQuery, Occur as FtsOccur,
PhraseQuery as FtsPhraseQuery,
use lance_index::scalar::inverted::{
DocumentGranularity,
query::{
BooleanQuery as FtsBooleanQuery, BoostQuery as FtsBoostQuery, FtsQuery,
MatchQuery as FtsMatchQuery, MultiMatchQuery as FtsMultiMatchQuery, Occur as FtsOccur,
PhraseQuery as FtsPhraseQuery,
},
};
use lance_io::ffi::to_ffi_arrow_array_stream;
use lance_linalg::distance::DistanceType;
Expand Down Expand Up @@ -114,6 +117,7 @@ pub(crate) fn build_full_text_search_query<'a>(
let max_expansions = env.get_int_as_usize_from_method(&java_obj, "getMaxExpansions")?;
let operator = env.get_fts_operator_from_method(&java_obj)?;
let prefix_length = env.get_u32_from_method(&java_obj, "getPrefixLength")?;
let document_granularity = get_document_granularity(env, &java_obj)?;

let mut query = FtsMatchQuery::new(query_text);
query = query.with_column(Some(column));
Expand All @@ -123,17 +127,24 @@ pub(crate) fn build_full_text_search_query<'a>(
.with_max_expansions(max_expansions)
.with_operator(operator)
.with_prefix_length(prefix_length);
if let Some(document_granularity) = document_granularity {
query = query.with_document_granularity(document_granularity);
}

Ok(FtsQuery::Match(query))
}
"MATCH_PHRASE" => {
let query_text = env.get_string_from_method(&java_obj, "getQueryText")?;
let column = env.get_string_from_method(&java_obj, "getColumn")?;
let slop = env.get_u32_from_method(&java_obj, "getSlop")?;
let document_granularity = get_document_granularity(env, &java_obj)?;

let mut query = FtsPhraseQuery::new(query_text);
query = query.with_column(Some(column));
query = query.with_slop(slop);
if let Some(document_granularity) = document_granularity {
query = query.with_document_granularity(document_granularity);
}

Ok(FtsQuery::Phrase(query))
}
Expand Down Expand Up @@ -229,6 +240,20 @@ pub(crate) fn build_full_text_search_query<'a>(
}
}

fn get_document_granularity(
env: &mut JNIEnv<'_>,
java_obj: &JObject,
) -> Result<Option<DocumentGranularity>> {
env.get_optional_from_method(
java_obj,
"getDocumentGranularity",
|env, granularity_obj| {
let value = env.get_string_from_method(&granularity_obj, "toRustString")?;
DocumentGranularity::try_from(value.as_str()).map_err(Error::from)
},
)
}

/// Scanner options passed from JNI - shared between blocking and async scanners
pub(crate) struct ScannerOptions<'a> {
pub fragment_ids_obj: JObject<'a>,
Expand Down
34 changes: 34 additions & 0 deletions java/src/main/java/org/lance/DocumentGranularity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 org.lance;

/** The unit treated as one full-text-search document. */
public enum DocumentGranularity {
/** All text selected from one dataset row belongs to one document. */
ROW("row"),

/** Each element of the deepest list on the field path is one document. */
LIST_ELEMENT("list_element");

private final String rustString;

DocumentGranularity(String rustString) {
this.rustString = rustString;
}

/** Return the stable value understood by the Rust API and serialized index parameters. */
public String toRustString() {
return rustString;
}
}
Comment thread
Xuanwo marked this conversation as resolved.
18 changes: 18 additions & 0 deletions java/src/main/java/org/lance/index/scalar/InvertedIndexParams.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package org.lance.index.scalar;

import org.lance.DocumentGranularity;
import org.lance.util.JsonUtils;

import com.google.common.base.Preconditions;
Expand Down Expand Up @@ -56,6 +57,7 @@ public static final class Builder {
private Integer blockSize = 128;
private Boolean skipMerge;
private Integer formatVersion;
private DocumentGranularity documentGranularity = DocumentGranularity.ROW;

/**
* Configure the base tokenizer.
Expand Down Expand Up @@ -279,6 +281,21 @@ public Builder formatVersion(int formatVersion) {
return this;
}

/**
* Configure the unit treated as one FTS document.
*
* <p>{@link DocumentGranularity#LIST_ELEMENT} uses each element of the deepest list on the
* indexed field path as one document. The default is {@link DocumentGranularity#ROW}.
*
* @param documentGranularity document boundary semantics
* @return this builder
*/
public Builder documentGranularity(DocumentGranularity documentGranularity) {
this.documentGranularity =
Objects.requireNonNull(documentGranularity, "documentGranularity must not be null");
return this;
}

/** Build a {@link ScalarIndexParams} instance for an inverted index. */
public ScalarIndexParams build() {
if (formatVersion != null) {
Expand Down Expand Up @@ -339,6 +356,7 @@ public ScalarIndexParams build() {
if (formatVersion != null) {
params.put("format_version", formatVersion);
}
params.put("document_granularity", documentGranularity.toRustString());

String json = JsonUtils.toJson(params);
return ScalarIndexParams.create(INDEX_TYPE, json);
Expand Down
106 changes: 97 additions & 9 deletions java/src/main/java/org/lance/ipc/FullTextQuery.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
*/
package org.lance.ipc;

import org.lance.DocumentGranularity;

import com.google.common.base.MoreObjects;
import org.apache.arrow.util.Preconditions;

Expand All @@ -21,7 +23,13 @@
import java.util.Objects;
import java.util.Optional;

/** Base type for full text search queries used by Lance scanner. */
/**
* Base type for full text search queries used by Lance scanner.
*
* <p>Match and phrase overloads without a {@link DocumentGranularity} infer the unique indexed
* granularity for the field. They use row documents when no index exists and fail as ambiguous when
* row and list-element indexes coexist.
*/
public abstract class FullTextQuery {
public enum Type {
MATCH,
Expand Down Expand Up @@ -63,7 +71,14 @@ public FullTextQuery getQuery() {
public abstract Type getType();

public static FullTextQuery match(String queryText, String column) {
return match(queryText, column, 1.0f, Optional.empty(), 50, Operator.OR, 0);
return new MatchQuery(
queryText, column, 1.0f, Optional.empty(), 50, Operator.OR, 0, Optional.empty());
}

public static FullTextQuery match(
String queryText, String column, DocumentGranularity documentGranularity) {
return match(
queryText, column, 1.0f, Optional.empty(), 50, Operator.OR, 0, documentGranularity);
}

public static FullTextQuery match(
Expand All @@ -75,15 +90,58 @@ public static FullTextQuery match(
Operator operator,
int prefixLength) {
return new MatchQuery(
queryText, column, boost, fuzziness, maxExpansions, operator, prefixLength);
queryText,
column,
boost,
fuzziness,
maxExpansions,
operator,
prefixLength,
Optional.empty());
}

public static FullTextQuery match(
String queryText,
String column,
float boost,
Optional<Integer> fuzziness,
int maxExpansions,
Operator operator,
int prefixLength,
DocumentGranularity documentGranularity) {
return new MatchQuery(
queryText,
column,
boost,
fuzziness,
maxExpansions,
operator,
prefixLength,
Optional.of(
Objects.requireNonNull(documentGranularity, "documentGranularity must not be null")));
}

public static FullTextQuery phrase(String queryText, String column) {
return phrase(queryText, column, 0);
return new PhraseQuery(queryText, column, 0, Optional.empty());
}

public static FullTextQuery phrase(
String queryText, String column, DocumentGranularity documentGranularity) {
return phrase(queryText, column, 0, documentGranularity);
}

public static FullTextQuery phrase(String queryText, String column, int slop) {
return new PhraseQuery(queryText, column, slop);
return new PhraseQuery(queryText, column, slop, Optional.empty());
}

public static FullTextQuery phrase(
String queryText, String column, int slop, DocumentGranularity documentGranularity) {
return new PhraseQuery(
queryText,
column,
slop,
Optional.of(
Objects.requireNonNull(documentGranularity, "documentGranularity must not be null")));
}

public static FullTextQuery multiMatch(String queryText, List<String> columns) {
Expand Down Expand Up @@ -117,6 +175,7 @@ public static final class MatchQuery extends FullTextQuery {
private final int maxExpansions;
private final Operator operator;
private final int prefixLength;
private final Optional<DocumentGranularity> documentGranularity;

MatchQuery(
String queryText,
Expand All @@ -125,7 +184,8 @@ public static final class MatchQuery extends FullTextQuery {
Optional<Integer> fuzziness,
int maxExpansions,
Operator operator,
int prefixLength) {
int prefixLength,
Optional<DocumentGranularity> documentGranularity) {
Preconditions.checkArgument(
queryText != null && !queryText.isEmpty(), "queryText must not be null or empty");
Preconditions.checkArgument(
Expand All @@ -140,6 +200,7 @@ public static final class MatchQuery extends FullTextQuery {
this.maxExpansions = maxExpansions;
this.operator = operator == null ? Operator.OR : operator;
this.prefixLength = prefixLength;
this.documentGranularity = Objects.requireNonNull(documentGranularity);
}

@Override
Expand Down Expand Up @@ -175,6 +236,11 @@ public int getPrefixLength() {
return prefixLength;
}

/** Returns the explicit granularity, or empty when query planning should infer it. */
public Optional<DocumentGranularity> getDocumentGranularity() {
return documentGranularity;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -184,6 +250,7 @@ public boolean equals(Object o) {
&& maxExpansions == other.maxExpansions
&& prefixLength == other.prefixLength
&& operator == other.operator
&& Objects.equals(documentGranularity, other.documentGranularity)
&& Objects.equals(queryText, other.queryText)
&& Objects.equals(column, other.column)
&& Objects.equals(fuzziness, other.fuzziness);
Expand All @@ -192,7 +259,14 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
return Objects.hash(
queryText, column, boost, fuzziness, maxExpansions, operator, prefixLength);
queryText,
column,
boost,
fuzziness,
maxExpansions,
operator,
prefixLength,
documentGranularity);
}

@Override
Expand All @@ -206,6 +280,7 @@ public String toString() {
.add("maxExpansions", maxExpansions)
.add("operator", operator)
.add("prefixLength", prefixLength)
.add("documentGranularity", documentGranularity)
.toString();
}
}
Expand All @@ -215,8 +290,13 @@ public static final class PhraseQuery extends FullTextQuery {
private final String queryText;
private final String column;
private final int slop;
private final Optional<DocumentGranularity> documentGranularity;

PhraseQuery(String queryText, String column, int slop) {
PhraseQuery(
String queryText,
String column,
int slop,
Optional<DocumentGranularity> documentGranularity) {
Preconditions.checkArgument(
queryText != null && !queryText.isEmpty(), "queryText must not be null or empty");
Preconditions.checkArgument(
Expand All @@ -226,6 +306,7 @@ public static final class PhraseQuery extends FullTextQuery {
this.queryText = queryText;
this.column = column;
this.slop = slop;
this.documentGranularity = Objects.requireNonNull(documentGranularity);
}

@Override
Expand All @@ -245,19 +326,25 @@ public int getSlop() {
return slop;
}

/** Returns the explicit granularity, or empty when query planning should infer it. */
public Optional<DocumentGranularity> getDocumentGranularity() {
return documentGranularity;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PhraseQuery)) return false;
PhraseQuery other = (PhraseQuery) o;
return slop == other.slop
&& Objects.equals(documentGranularity, other.documentGranularity)
&& Objects.equals(queryText, other.queryText)
&& Objects.equals(column, other.column);
}

@Override
public int hashCode() {
return Objects.hash(queryText, column, slop);
return Objects.hash(queryText, column, slop, documentGranularity);
}

@Override
Expand All @@ -267,6 +354,7 @@ public String toString() {
.add("queryText", queryText)
.add("column", column)
.add("slop", slop)
.add("documentGranularity", documentGranularity)
.toString();
}
}
Expand Down
Loading
Loading