159 lines
5.3 KiB
Python
159 lines
5.3 KiB
Python
# Copyright 2025-present the zvec project
|
|
#
|
|
# 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.
|
|
"""Tests for FTS (Full-Text Search) query support in the Python SDK."""
|
|
|
|
import pickle
|
|
|
|
import pytest
|
|
|
|
from zvec.model.param.query import Fts, Query
|
|
|
|
|
|
class TestFtsQueryValidation:
|
|
"""Test FTS parameter validation in Query dataclass."""
|
|
|
|
def test_fts_query_string_only(self):
|
|
"""Query with only query_string in Fts should be valid."""
|
|
q = Query(
|
|
field_name="content", fts=Fts(query_string='+hello -world "exact phrase"')
|
|
)
|
|
q._validate()
|
|
assert q.fts.query_string == '+hello -world "exact phrase"'
|
|
assert q.fts.match_string is None
|
|
assert q.has_fts() is True
|
|
|
|
def test_fts_match_string_only(self):
|
|
"""Query with only match_string in Fts should be valid."""
|
|
q = Query(field_name="content", fts=Fts(match_string="machine learning"))
|
|
q._validate()
|
|
assert q.fts.match_string == "machine learning"
|
|
assert q.fts.query_string is None
|
|
assert q.has_fts() is True
|
|
|
|
def test_fts_query_string_and_match_string_mutually_exclusive(self):
|
|
"""Cannot provide both query_string and match_string in Fts."""
|
|
q = Query(
|
|
field_name="content",
|
|
fts=Fts(query_string="+hello", match_string="hello world"),
|
|
)
|
|
with pytest.raises(ValueError, match="mutually exclusive"):
|
|
q._validate()
|
|
|
|
def test_no_fts(self):
|
|
"""Query without FTS fields should have has_fts() == False."""
|
|
q = Query(field_name="embedding", vector=[0.1, 0.2, 0.3])
|
|
assert q.has_fts() is False
|
|
|
|
def test_vector_and_fts_mutually_exclusive(self):
|
|
"""Cannot combine vector search with FTS in a single Query."""
|
|
q = Query(
|
|
field_name="embedding",
|
|
vector=[0.1, 0.2, 0.3],
|
|
fts=Fts(match_string="deep learning"),
|
|
)
|
|
with pytest.raises(ValueError, match="Cannot combine fts with vector search"):
|
|
q._validate()
|
|
|
|
def test_fts_without_vector_or_id(self):
|
|
"""Query with only FTS (no vector, no id) should be valid."""
|
|
q = Query(field_name="content", fts=Fts(query_string="hello"))
|
|
q._validate()
|
|
assert q.has_vector() is False
|
|
assert q.has_id() is False
|
|
assert q.has_fts() is True
|
|
|
|
|
|
class TestFtsQueryBinding:
|
|
"""Test FTS binding layer (_Fts)."""
|
|
|
|
def test_import_fts_query(self):
|
|
"""_Fts should be importable from _zvec.param."""
|
|
from zvec._zvec.param import _Fts
|
|
|
|
fts = _Fts()
|
|
assert fts.query_string == ""
|
|
assert fts.match_string == ""
|
|
|
|
def test_fts_query_set_fields(self):
|
|
"""Setting fields on _Fts should work."""
|
|
from zvec._zvec.param import _Fts
|
|
|
|
fts = _Fts()
|
|
fts.query_string = "+hello -world"
|
|
assert fts.query_string == "+hello -world"
|
|
|
|
fts2 = _Fts()
|
|
fts2.match_string = "machine learning"
|
|
assert fts2.match_string == "machine learning"
|
|
|
|
def test_fts_query_pickle(self):
|
|
"""_Fts should support pickling."""
|
|
from zvec._zvec.param import _Fts
|
|
|
|
fts = _Fts()
|
|
fts.query_string = "+vector search"
|
|
fts.match_string = ""
|
|
|
|
data = pickle.dumps(fts)
|
|
restored = pickle.loads(data)
|
|
assert restored.query_string == "+vector search"
|
|
assert restored.match_string == ""
|
|
|
|
def test_search_query_fts_field(self):
|
|
"""_SearchQuery should have fts field."""
|
|
from zvec._zvec.param import _Fts, _SearchQuery
|
|
|
|
vq = _SearchQuery()
|
|
# fts should be None by default (optional)
|
|
assert vq.fts is None
|
|
|
|
# set fts
|
|
fts = _Fts()
|
|
fts.query_string = "hello"
|
|
vq.fts = fts
|
|
assert vq.fts is not None
|
|
assert vq.fts.query_string == "hello"
|
|
|
|
def test_search_query_pickle_with_fts(self):
|
|
"""_SearchQuery with fts should survive pickling."""
|
|
from zvec._zvec.param import _Fts, _SearchQuery
|
|
|
|
vq = _SearchQuery()
|
|
vq.topk = 10
|
|
vq.field_name = "embedding"
|
|
fts = _Fts()
|
|
fts.match_string = "test query"
|
|
vq.fts = fts
|
|
|
|
data = pickle.dumps(vq)
|
|
restored = pickle.loads(data)
|
|
assert restored.topk == 10
|
|
assert restored.field_name == "embedding"
|
|
assert restored.fts is not None
|
|
assert restored.fts.match_string == "test query"
|
|
|
|
def test_search_query_pickle_without_fts(self):
|
|
"""_SearchQuery without fts should survive pickling."""
|
|
from zvec._zvec.param import _SearchQuery
|
|
|
|
vq = _SearchQuery()
|
|
vq.topk = 5
|
|
vq.field_name = "vec"
|
|
|
|
data = pickle.dumps(vq)
|
|
restored = pickle.loads(data)
|
|
assert restored.topk == 5
|
|
assert restored.field_name == "vec"
|
|
assert restored.fts is None
|