Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions redisvl/schema/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,6 @@ def field_data(self) -> dict[str, Any]:
}
if self.initial_cap is not None: # Only include it if it's set
field_data["INITIAL_CAP"] = self.initial_cap
if self.index_missing: # Only include it if it's set
field_data["INDEXMISSING"] = True
return field_data


Expand Down Expand Up @@ -557,7 +555,13 @@ def as_redis_field(self) -> RedisField:
field_data = self.attrs.field_data
if self.attrs.block_size is not None:
field_data["BLOCK_SIZE"] = self.attrs.block_size
return RedisVectorField(name, self.attrs.algorithm, field_data, as_name=as_name)
return RedisVectorField(
name,
self.attrs.algorithm,
field_data,
as_name=as_name,
index_missing=self.attrs.index_missing,
)


class HNSWVectorField(BaseField):
Expand All @@ -578,7 +582,13 @@ def as_redis_field(self) -> RedisField:
"EPSILON": self.attrs.epsilon,
}
)
return RedisVectorField(name, self.attrs.algorithm, field_data, as_name=as_name)
return RedisVectorField(
name,
self.attrs.algorithm,
field_data,
as_name=as_name,
index_missing=self.attrs.index_missing,
)


class SVSVectorField(BaseField):
Expand All @@ -605,7 +615,13 @@ def as_redis_field(self) -> RedisField:
field_data["REDUCE"] = self.attrs.reduce
if self.attrs.training_threshold is not None:
field_data["TRAINING_THRESHOLD"] = self.attrs.training_threshold
return RedisVectorField(name, self.attrs.algorithm, field_data, as_name=as_name)
return RedisVectorField(
name,
self.attrs.algorithm,
field_data,
as_name=as_name,
index_missing=self.attrs.index_missing,
)


FIELD_TYPE_MAP = {
Expand Down
40 changes: 40 additions & 0 deletions tests/integration/test_field_modifier_ordering_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,46 @@ def test_indexmissing_enables_ismissing_query(self, client, redis_url, worker_id
)
index.delete(drop=True)

@pytest.mark.parametrize("algorithm", ["flat", "hnsw"])
def test_vector_field_index_missing(self, client, redis_url, worker_id, algorithm):
"""Test that a vector field with INDEXMISSING creates and supports ismissing()."""
skip_if_search_version_below_for_indexmissing(client)
name = f"test_vec_ismissing_{algorithm}_{worker_id}"
prefix = f"vec_ismiss_{algorithm}_{worker_id}:"
schema_dict = {
"index": {"name": name, "prefix": prefix, "storage_type": "hash"},
"fields": [
{
"name": "embedding",
"type": "vector",
"attrs": {
"algorithm": algorithm,
"dims": 3,
"index_missing": True,
},
}
],
}

schema = IndexSchema.from_dict(schema_dict)
index = SearchIndex(schema=schema, redis_url=redis_url)

index.create(overwrite=True)

client.hset(f"{prefix}1", "embedding", b"\x00" * 12)
client.hset(f"{prefix}2", "other_field", "no embedding")

result = client.execute_command(
"FT.SEARCH", name, "ismissing(@embedding)", "DIALECT", "2"
)

assert result[0] == 1
assert f"{prefix}2" in str(result)

# Cleanup
client.delete(f"{prefix}1", f"{prefix}2")
index.delete(drop=True)


class TestIndexEmptyIntegration:
"""Integration tests for INDEXEMPTY functionality."""
Expand Down
45 changes: 22 additions & 23 deletions tests/unit/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,33 +383,32 @@ def test_vector_fields_redis_creation_with_index_missing():
assert redis_field.name == "hnsw_embedding"


def test_vector_field_data_includes_index_missing():
"""Test that vector field field_data includes INDEXMISSING when enabled."""

# Test with index_missing=True
flat_field_with_missing = FlatVectorField(
@pytest.mark.parametrize(
"field_class,algorithm",
[
(FlatVectorField, "flat"),
(HNSWVectorField, "hnsw"),
(SVSVectorField, "svs-vamana"),
],
)
def test_vector_field_index_missing_follows_attribute_block(field_class, algorithm):
"""INDEXMISSING is a field modifier, so it must follow the vector attribute
block rather than be counted and sent inside it."""
field = field_class(
name="embedding",
attrs={"algorithm": "flat", "dims": 128, "index_missing": True},
attrs={"algorithm": algorithm, "dims": 128, "index_missing": True},
)
field_data = flat_field_with_missing.attrs.field_data
assert "INDEXMISSING" in field_data
assert field_data["INDEXMISSING"] == True
assert "INDEXMISSING" not in field.attrs.field_data

# Test with index_missing=False (default)
flat_field_without_missing = FlatVectorField(
name="embedding", attrs={"algorithm": "flat", "dims": 128}
)
field_data = flat_field_without_missing.attrs.field_data
assert "INDEXMISSING" not in field_data
args = field.as_redis_field().redis_args()
attr_count = args[3]
assert "INDEXMISSING" not in args[: 4 + attr_count]
assert args[4 + attr_count :] == ["INDEXMISSING"]

# Test HNSW field with index_missing=True
hnsw_field_with_missing = HNSWVectorField(
name="embedding",
attrs={"algorithm": "hnsw", "dims": 256, "index_missing": True},
)
field_data = hnsw_field_with_missing.attrs.field_data
assert "INDEXMISSING" in field_data
assert field_data["INDEXMISSING"] == True
# Without index_missing, nothing follows the attribute block
field = field_class(name="embedding", attrs={"algorithm": algorithm, "dims": 128})
args = field.as_redis_field().redis_args()
assert len(args) == 4 + args[3]


def test_field_factory_with_new_attributes():
Expand Down