Skip to content
12 changes: 12 additions & 0 deletions src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ pub(crate) fn extract_value(any: &PyAny) -> PyResult<Value> {
if let Ok(facet) = any.extract::<Facet>() {
return Ok(Value::Facet(facet.inner));
}
if let Ok(b) = any.extract::<Vec<u8>>() {
return Ok(Value::Bytes(b));
}
Err(to_pyerr(format!("Value unsupported {any:?}")))
}

Expand Down Expand Up @@ -300,6 +303,15 @@ impl Document {
add_value(self, field_name, value);
}

/// Add a float value to the document.
///
/// Args:
/// field_name (str): The field name for which we are adding the value.
/// value (f64): The float that will be added to the document.
fn add_float(&mut self, field_name: String, value: f64) {
add_value(self, field_name, value);
}

/// Add a date value to the document.
///
/// Args:
Expand Down
28 changes: 25 additions & 3 deletions src/schemabuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ impl SchemaBuilder {
) -> PyResult<Self> {
let builder = &mut self.builder;

let opts = SchemaBuilder::build_int_option(stored, indexed, fast)?;
let opts = SchemaBuilder::build_numeric_option(stored, indexed, fast)?;

if let Some(builder) = builder.write().unwrap().as_mut() {
builder.add_i64_field(name, opts);
Expand All @@ -132,6 +132,28 @@ impl SchemaBuilder {
Ok(self.clone())
}

#[pyo3(signature = (name, stored = false, indexed = false, fast = None))]
fn add_float_field(
&mut self,
name: &str,
stored: bool,
indexed: bool,
fast: Option<&str>,
) -> PyResult<Self> {
let builder = &mut self.builder;

let opts = SchemaBuilder::build_numeric_option(stored, indexed, fast)?;

if let Some(builder) = builder.write().unwrap().as_mut() {
builder.add_f64_field(name, opts);
} else {
return Err(exceptions::PyValueError::new_err(
"Schema builder object isn't valid anymore.",
));
}
Ok(self.clone())
}

/// Add a new unsigned integer field to the schema.
///
/// Args:
Expand Down Expand Up @@ -162,7 +184,7 @@ impl SchemaBuilder {
) -> PyResult<Self> {
let builder = &mut self.builder;

let opts = SchemaBuilder::build_int_option(stored, indexed, fast)?;
let opts = SchemaBuilder::build_numeric_option(stored, indexed, fast)?;

if let Some(builder) = builder.write().unwrap().as_mut() {
builder.add_u64_field(name, opts);
Expand Down Expand Up @@ -343,7 +365,7 @@ impl SchemaBuilder {
}

impl SchemaBuilder {
fn build_int_option(
fn build_numeric_option(
stored: bool,
indexed: bool,
fast: Option<&str>,
Expand Down
123 changes: 117 additions & 6 deletions tests/tantivy_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from io import BytesIO

import pytest
import tantivy
from tantivy import Document, Index, SchemaBuilder, SnippetGenerator
Expand All @@ -12,6 +14,16 @@ def schema():
)


def schema_numeric_fields():
return (
SchemaBuilder()
.add_integer_field("id", stored=True, indexed=True)
.add_float_field("rating", stored=True, indexed=True)
.add_text_field("body", stored=True)
.build()
)


def create_index(dir=None):
# assume all tests will use the same documents for now
# other methods may set up function-local indexes
Expand Down Expand Up @@ -66,11 +78,53 @@ def create_index(dir=None):
return index


def create_index_with_numeric_fields(dir=None):
index = Index(schema_numeric_fields(), dir)
writer = index.writer()

doc = Document()
doc.add_integer("id", 1)
doc.add_float("rating", 3.5)
doc.add_text(
"body",
(
"He was an old man who fished alone in a skiff in"
"the Gulf Stream and he had gone eighty-four days "
"now without taking a fish."
),
)
writer.add_document(doc)
doc = Document.from_dict(
{
"id": 2,
"rating": 4.5,
"body": (
"A few miles south of Soledad, the Salinas River drops "
"in close to the hillside bank and runs deep and "
"green. The water is warm too, for it has slipped "
"twinkling over the yellow sands in the sunlight "
"before reaching the narrow pool. On one side of the "
"river the golden foothill slopes curve up to the "
"strong and rocky Gabilan Mountains, but on the valley "
"side the water is lined with trees—willows fresh and "
"green with every spring, carrying in their lower leaf "
"junctures the debris of the winter’s flooding; and "
"sycamores with mottled, white, recumbent limbs and "
"branches that arch over the pool"
),
}
)
writer.add_document(doc)
writer.commit()
index.reload()
return index


def spanish_schema():
return (
SchemaBuilder()
.add_text_field("title", stored=True, tokenizer_name='es_stem')
.add_text_field("body", tokenizer_name='es_stem')
.add_text_field("title", stored=True, tokenizer_name="es_stem")
.add_text_field("body", tokenizer_name="es_stem")
.build()
)

Expand Down Expand Up @@ -126,6 +180,11 @@ def ram_index():
return create_index()


@pytest.fixture(scope="class")
def ram_index_numeric_fields():
return create_index_with_numeric_fields()


@pytest.fixture(scope="class")
def spanish_index():
return create_spanish_index()
Expand Down Expand Up @@ -184,6 +243,25 @@ def test_and_query(self, ram_index):

assert len(result.hits) == 1

def test_and_query_numeric_fields(self, ram_index_numeric_fields):
index = ram_index_numeric_fields
searcher = index.searcher()

# 1 result
float_query = index.parse_query("3.5", ["rating"])
result = searcher.search(float_query)
assert len(result.hits) == 1
assert searcher.doc(result.hits[0][1])["rating"][0] == 3.5

integer_query = index.parse_query("1", ["id"])
result = searcher.search(integer_query)
assert len(result.hits) == 1

# 0 result
integer_query = index.parse_query("10", ["id"])
result = searcher.search(integer_query)
assert len(result.hits) == 0

def test_and_query_parser_default_fields(self, ram_index):
query = ram_index.parse_query("winter", default_field_names=["title"])
assert repr(query) == """Query(TermQuery(Term(type=Str, field=0, "winter")))"""
Expand Down Expand Up @@ -343,8 +421,12 @@ def test_create_readers(self):


class TestSearcher(object):
def test_searcher_repr(self, ram_index):
def test_searcher_repr(self, ram_index, ram_index_numeric_fields):
assert repr(ram_index.searcher()) == "Searcher(num_docs=3, num_segments=1)"
assert (
repr(ram_index_numeric_fields.searcher())
== "Searcher(num_docs=2, num_segments=1)"
)


class TestDocument(object):
Expand Down Expand Up @@ -469,9 +551,11 @@ def test_document_snippet(self, dir_index):
result = searcher.search(query)
assert len(result.hits) == 1

snippet_generator = SnippetGenerator.create(searcher, query, doc_schema, "title")
snippet_generator = SnippetGenerator.create(
searcher, query, doc_schema, "title"
)

for (score, doc_address) in result.hits:
for score, doc_address in result.hits:
doc = searcher.doc(doc_address)
snippet = snippet_generator.snippet_from_doc(doc)
highlights = snippet.highlighted()
Expand All @@ -480,4 +564,31 @@ def test_document_snippet(self, dir_index):
assert first.start == 20
assert first.end == 23
html_snippet = snippet.to_html()
assert html_snippet == 'The Old Man and the <b>Sea</b>'
assert html_snippet == "The Old Man and the <b>Sea</b>"


@pytest.mark.parametrize("bytes_kwarg", [True, False])
@pytest.mark.parametrize(
"bytes_payload",
[
b"abc",
bytearray(b"abc"),
memoryview(b"abc"),
BytesIO(b"abc").read(),
BytesIO(b"abc").getbuffer(),
],
)
def test_bytes(bytes_kwarg, bytes_payload):
schema = SchemaBuilder().add_bytes_field("embedding").build()
index = Index(schema)
writer = index.writer()

if bytes_kwarg:
doc = Document(id=1, embedding=bytes_payload)
else:
doc = Document(id=1)
doc.add_bytes("embedding", bytes_payload)

writer.add_document(doc)
writer.commit()
index.reload()