a7d6d88f6f
CI / changes (push) Has been cancelled
CI / cd libs/checkpoint (push) Has been cancelled
CI / cd libs/checkpoint-conformance (push) Has been cancelled
CI / cd libs/checkpoint-postgres (push) Has been cancelled
CI / cd libs/checkpoint-sqlite (push) Has been cancelled
CI / cd libs/cli (push) Has been cancelled
CI / cd libs/prebuilt (push) Has been cancelled
CI / cd libs/sdk-py (push) Has been cancelled
CI / cd libs/langgraph (push) Has been cancelled
CI / Check SDK methods matching (push) Has been cancelled
CI / Check CLI schema hasn't changed #3.13 (push) Has been cancelled
CI / CLI integration test (push) Has been cancelled
CI / sdk-py integration test (push) Has been cancelled
CI / CI Success (push) Has been cancelled
baseline / benchmark (push) Has been cancelled
Deploy Redirects to GitHub Pages / deploy (push) Has been cancelled
1232 lines
46 KiB
Python
1232 lines
46 KiB
Python
import os
|
|
import re
|
|
import tempfile
|
|
import uuid
|
|
from collections.abc import Generator, Iterable
|
|
from contextlib import contextmanager
|
|
from typing import Any, Literal, cast
|
|
|
|
import pytest
|
|
from langchain_core.embeddings import Embeddings
|
|
from langgraph.store.base import (
|
|
GetOp,
|
|
Item,
|
|
ListNamespacesOp,
|
|
MatchCondition,
|
|
PutOp,
|
|
SearchOp,
|
|
)
|
|
|
|
from langgraph.store.sqlite import SqliteStore
|
|
from langgraph.store.sqlite.base import SqliteIndexConfig
|
|
|
|
|
|
# Local embeddings implementation for testing vector search
|
|
class CharacterEmbeddings(Embeddings):
|
|
"""Simple character-frequency based embeddings using random projections."""
|
|
|
|
def __init__(self, dims: int = 50, seed: int = 42):
|
|
"""Initialize with embedding dimensions and random seed."""
|
|
import math
|
|
import random
|
|
from collections import defaultdict
|
|
|
|
self._rng = random.Random(seed)
|
|
self.dims = dims
|
|
# Create projection vector for each character lazily
|
|
self._char_projections: dict[str, list[float]] = defaultdict(
|
|
lambda: [
|
|
self._rng.gauss(0, 1 / math.sqrt(self.dims)) for _ in range(self.dims)
|
|
]
|
|
)
|
|
|
|
def _embed_one(self, text: str) -> list[float]:
|
|
"""Embed a single text."""
|
|
import math
|
|
from collections import Counter
|
|
|
|
counts = Counter(text)
|
|
total = sum(counts.values())
|
|
|
|
if total == 0:
|
|
return [0.0] * self.dims
|
|
|
|
embedding = [0.0] * self.dims
|
|
for char, count in counts.items():
|
|
weight = count / total
|
|
char_proj = self._char_projections[char]
|
|
for i, proj in enumerate(char_proj):
|
|
embedding[i] += weight * proj
|
|
|
|
norm = math.sqrt(sum(x * x for x in embedding))
|
|
if norm > 0:
|
|
embedding = [x / norm for x in embedding]
|
|
|
|
return embedding
|
|
|
|
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
"""Embed a list of documents."""
|
|
return [self._embed_one(text) for text in texts]
|
|
|
|
def embed_query(self, text: str) -> list[float]:
|
|
"""Embed a query string."""
|
|
return self._embed_one(text)
|
|
|
|
def __eq__(self, other: Any) -> bool:
|
|
return isinstance(other, CharacterEmbeddings) and self.dims == other.dims
|
|
|
|
|
|
@pytest.fixture(scope="function", params=["memory", "file"])
|
|
def store(request: Any) -> Generator[SqliteStore, None, None]:
|
|
"""Create a SqliteStore for testing."""
|
|
if request.param == "memory":
|
|
# In-memory store
|
|
with SqliteStore.from_conn_string(":memory:") as store:
|
|
store.setup()
|
|
yield store
|
|
else:
|
|
# Temporary file store
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_file.close()
|
|
try:
|
|
with SqliteStore.from_conn_string(temp_file.name) as store:
|
|
store.setup()
|
|
yield store
|
|
finally:
|
|
os.unlink(temp_file.name)
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def fake_embeddings() -> CharacterEmbeddings:
|
|
"""Create fake embeddings for testing."""
|
|
return CharacterEmbeddings(dims=500)
|
|
|
|
|
|
# Define vector types and distance types for parametrized tests
|
|
VECTOR_TYPES = ["cosine"] # SQLite only supports cosine similarity
|
|
|
|
|
|
@contextmanager
|
|
def create_vector_store(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
text_fields: list[str] | None = None,
|
|
distance_type: str = "cosine",
|
|
conn_type: Literal["memory", "file"] = "memory",
|
|
) -> Generator[SqliteStore, None, None]:
|
|
"""Create a SqliteStore with vector search enabled."""
|
|
index_config: SqliteIndexConfig = {
|
|
"dims": fake_embeddings.dims,
|
|
"embed": fake_embeddings,
|
|
"text_fields": text_fields,
|
|
"distance_type": distance_type, # This is for API consistency but SQLite only supports cosine
|
|
}
|
|
if conn_type == "memory":
|
|
conn_str = ":memory:"
|
|
else:
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
temp_file.close()
|
|
conn_str = temp_file.name
|
|
|
|
try:
|
|
with SqliteStore.from_conn_string(conn_str, index=index_config) as store:
|
|
store.setup()
|
|
yield store
|
|
finally:
|
|
if conn_type == "file":
|
|
os.unlink(conn_str)
|
|
|
|
|
|
def test_batch_order(store: SqliteStore) -> None:
|
|
# Setup test data
|
|
store.put(("test", "foo"), "key1", {"data": "value1"})
|
|
store.put(("test", "bar"), "key2", {"data": "value2"})
|
|
|
|
ops = [
|
|
GetOp(namespace=("test", "foo"), key="key1"),
|
|
PutOp(namespace=("test", "bar"), key="key2", value={"data": "value2"}),
|
|
SearchOp(
|
|
namespace_prefix=("test",), filter={"data": "value1"}, limit=10, offset=0
|
|
),
|
|
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
|
|
GetOp(namespace=("test",), key="key3"),
|
|
]
|
|
|
|
results = store.batch(
|
|
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
|
)
|
|
assert len(results) == 5
|
|
assert isinstance(results[0], Item)
|
|
assert isinstance(results[0].value, dict)
|
|
assert results[0].value == {"data": "value1"}
|
|
assert results[0].key == "key1"
|
|
assert results[0].namespace == ("test", "foo")
|
|
assert results[1] is None # Put operation returns None
|
|
assert isinstance(results[2], list)
|
|
assert len(results[2]) == 1
|
|
assert results[2][0].key == "key1"
|
|
assert results[2][0].value == {"data": "value1"}
|
|
assert isinstance(results[3], list)
|
|
assert len(results[3]) > 0 # Should contain at least our test namespaces
|
|
assert ("test", "foo") in results[3]
|
|
assert ("test", "bar") in results[3]
|
|
assert results[4] is None # Non-existent key returns None
|
|
|
|
# Test reordered operations
|
|
ops_reordered = [
|
|
SearchOp(namespace_prefix=("test",), filter=None, limit=5, offset=0),
|
|
GetOp(namespace=("test", "bar"), key="key2"),
|
|
ListNamespacesOp(match_conditions=None, max_depth=None, limit=5, offset=0),
|
|
PutOp(namespace=("test",), key="key3", value={"data": "value3"}),
|
|
GetOp(namespace=("test", "foo"), key="key1"),
|
|
]
|
|
|
|
results_reordered = store.batch(
|
|
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops_reordered)
|
|
)
|
|
assert len(results_reordered) == 5
|
|
assert isinstance(results_reordered[0], list)
|
|
assert len(results_reordered[0]) >= 2 # Should find at least our two test items
|
|
assert isinstance(results_reordered[1], Item)
|
|
assert results_reordered[1].value == {"data": "value2"}
|
|
assert results_reordered[1].key == "key2"
|
|
assert results_reordered[1].namespace == ("test", "bar")
|
|
assert isinstance(results_reordered[2], list)
|
|
assert len(results_reordered[2]) > 0
|
|
assert results_reordered[3] is None # Put operation returns None
|
|
assert isinstance(results_reordered[4], Item)
|
|
assert results_reordered[4].value == {"data": "value1"}
|
|
assert results_reordered[4].key == "key1"
|
|
assert results_reordered[4].namespace == ("test", "foo")
|
|
|
|
# Verify the put worked
|
|
item3 = store.get(("test",), "key3")
|
|
assert item3 is not None
|
|
assert item3.value == {"data": "value3"}
|
|
|
|
|
|
def test_batch_get_ops(store: SqliteStore) -> None:
|
|
# Setup test data
|
|
store.put(("test",), "key1", {"data": "value1"})
|
|
store.put(("test",), "key2", {"data": "value2"})
|
|
|
|
ops = [
|
|
GetOp(namespace=("test",), key="key1"),
|
|
GetOp(namespace=("test",), key="key2"),
|
|
GetOp(namespace=("test",), key="key3"), # Non-existent key
|
|
]
|
|
|
|
results = store.batch(ops)
|
|
|
|
assert len(results) == 3
|
|
assert results[0] is not None
|
|
assert results[1] is not None
|
|
assert results[2] is None
|
|
assert results[0].key == "key1"
|
|
assert results[1].key == "key2"
|
|
|
|
|
|
def test_batch_put_ops(store: SqliteStore) -> None:
|
|
ops = [
|
|
PutOp(namespace=("test",), key="key1", value={"data": "value1"}),
|
|
PutOp(namespace=("test",), key="key2", value={"data": "value2"}),
|
|
PutOp(namespace=("test",), key="key3", value=None), # Delete operation
|
|
]
|
|
|
|
results = store.batch(ops)
|
|
assert len(results) == 3
|
|
assert all(result is None for result in results)
|
|
|
|
# Verify the puts worked
|
|
item1 = store.get(("test",), "key1")
|
|
item2 = store.get(("test",), "key2")
|
|
item3 = store.get(("test",), "key3")
|
|
|
|
assert item1 and item1.value == {"data": "value1"}
|
|
assert item2 and item2.value == {"data": "value2"}
|
|
assert item3 is None
|
|
|
|
|
|
def test_batch_search_ops(store: SqliteStore) -> None:
|
|
# Setup test data
|
|
test_data = [
|
|
(("test", "foo"), "key1", {"data": "value1", "tag": "a"}),
|
|
(("test", "bar"), "key2", {"data": "value2", "tag": "a"}),
|
|
(("test", "baz"), "key3", {"data": "value3", "tag": "b"}),
|
|
]
|
|
for namespace, key, value in test_data:
|
|
store.put(namespace, key, value)
|
|
|
|
ops = [
|
|
SearchOp(namespace_prefix=("test",), filter={"tag": "a"}, limit=10, offset=0),
|
|
SearchOp(namespace_prefix=("test",), filter=None, limit=2, offset=0),
|
|
SearchOp(namespace_prefix=("test", "foo"), filter=None, limit=10, offset=0),
|
|
]
|
|
|
|
results = store.batch(ops)
|
|
assert len(results) == 3
|
|
|
|
# First search should find items with tag "a"
|
|
assert len(results[0]) == 2
|
|
assert all(item.value["tag"] == "a" for item in results[0])
|
|
|
|
# Second search should return first 2 items
|
|
assert len(results[1]) == 2
|
|
|
|
# Third search should only find items in test/foo namespace
|
|
assert len(results[2]) == 1
|
|
assert results[2][0].namespace == ("test", "foo")
|
|
|
|
|
|
def test_batch_list_namespaces_ops(store: SqliteStore) -> None:
|
|
# Setup test data with various namespaces
|
|
test_data = [
|
|
(("test", "documents", "public"), "doc1", {"content": "public doc"}),
|
|
(("test", "documents", "private"), "doc2", {"content": "private doc"}),
|
|
(("test", "images", "public"), "img1", {"content": "public image"}),
|
|
(("prod", "documents", "public"), "doc3", {"content": "prod doc"}),
|
|
]
|
|
for namespace, key, value in test_data:
|
|
store.put(namespace, key, value)
|
|
|
|
ops = [
|
|
ListNamespacesOp(match_conditions=None, max_depth=None, limit=10, offset=0),
|
|
ListNamespacesOp(match_conditions=None, max_depth=2, limit=10, offset=0),
|
|
ListNamespacesOp(
|
|
match_conditions=tuple([MatchCondition("suffix", ("public",))]),
|
|
max_depth=None,
|
|
limit=10,
|
|
offset=0,
|
|
),
|
|
]
|
|
|
|
results = store.batch(
|
|
cast(Iterable[GetOp | PutOp | SearchOp | ListNamespacesOp], ops)
|
|
)
|
|
assert len(results) == 3
|
|
|
|
# First operation should list all namespaces
|
|
assert len(results[0]) == len(test_data)
|
|
|
|
# Second operation should only return namespaces up to depth 2
|
|
assert all(len(ns) <= 2 for ns in results[1])
|
|
|
|
# Third operation should only return namespaces ending with "public"
|
|
assert all(ns[-1] == "public" for ns in results[2])
|
|
|
|
|
|
class TestSqliteStore:
|
|
def test_basic_store_ops(self) -> None:
|
|
with SqliteStore.from_conn_string(":memory:") as store:
|
|
store.setup()
|
|
namespace = ("test", "documents")
|
|
item_id = "doc1"
|
|
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
|
|
|
store.put(namespace, item_id, item_value)
|
|
item = store.get(namespace, item_id)
|
|
|
|
assert item
|
|
assert item.namespace == namespace
|
|
assert item.key == item_id
|
|
assert item.value == item_value
|
|
|
|
# Test update
|
|
# Small delay to ensure the updated timestamp is different
|
|
import time
|
|
|
|
time.sleep(0.01)
|
|
|
|
updated_value = {"title": "Updated Document", "content": "Hello, Updated!"}
|
|
store.put(namespace, item_id, updated_value)
|
|
updated_item = store.get(namespace, item_id)
|
|
|
|
assert updated_item.value == updated_value
|
|
# Don't check timestamps because SQLite execution might be too fast
|
|
# assert updated_item.updated_at > item.updated_at
|
|
|
|
# Test get from non-existent namespace
|
|
different_namespace = ("test", "other_documents")
|
|
item_in_different_namespace = store.get(different_namespace, item_id)
|
|
assert item_in_different_namespace is None
|
|
|
|
# Test delete
|
|
store.delete(namespace, item_id)
|
|
deleted_item = store.get(namespace, item_id)
|
|
assert deleted_item is None
|
|
|
|
def test_list_namespaces(self) -> None:
|
|
with SqliteStore.from_conn_string(":memory:") as store:
|
|
store.setup()
|
|
# Create test data with various namespaces
|
|
test_namespaces = [
|
|
("test", "documents", "public"),
|
|
("test", "documents", "private"),
|
|
("test", "images", "public"),
|
|
("test", "images", "private"),
|
|
("prod", "documents", "public"),
|
|
("prod", "documents", "private"),
|
|
]
|
|
|
|
# Insert test data
|
|
for namespace in test_namespaces:
|
|
store.put(namespace, "dummy", {"content": "dummy"})
|
|
|
|
# Test listing with various filters
|
|
all_namespaces = store.list_namespaces()
|
|
assert len(all_namespaces) == len(test_namespaces)
|
|
|
|
# Test prefix filtering
|
|
test_prefix_namespaces = store.list_namespaces(prefix=["test"])
|
|
assert len(test_prefix_namespaces) == 4
|
|
assert all(ns[0] == "test" for ns in test_prefix_namespaces)
|
|
|
|
# Test suffix filtering
|
|
public_namespaces = store.list_namespaces(suffix=["public"])
|
|
assert len(public_namespaces) == 3
|
|
assert all(ns[-1] == "public" for ns in public_namespaces)
|
|
|
|
# Test max depth
|
|
depth_2_namespaces = store.list_namespaces(max_depth=2)
|
|
assert all(len(ns) <= 2 for ns in depth_2_namespaces)
|
|
|
|
# Test pagination
|
|
paginated_namespaces = store.list_namespaces(limit=3)
|
|
assert len(paginated_namespaces) == 3
|
|
|
|
# Cleanup
|
|
for namespace in test_namespaces:
|
|
store.delete(namespace, "dummy")
|
|
|
|
def test_search(self) -> None:
|
|
with SqliteStore.from_conn_string(":memory:") as store:
|
|
store.setup()
|
|
# Create test data
|
|
test_data = [
|
|
(
|
|
("test", "docs"),
|
|
"doc1",
|
|
{"title": "First Doc", "author": "Alice", "tags": ["important"]},
|
|
),
|
|
(
|
|
("test", "docs"),
|
|
"doc2",
|
|
{"title": "Second Doc", "author": "Bob", "tags": ["draft"]},
|
|
),
|
|
(
|
|
("test", "images"),
|
|
"img1",
|
|
{"title": "Image 1", "author": "Alice", "tags": ["final"]},
|
|
),
|
|
]
|
|
|
|
for namespace, key, value in test_data:
|
|
store.put(namespace, key, value)
|
|
|
|
# Test basic search
|
|
all_items = store.search(["test"])
|
|
assert len(all_items) == 3
|
|
|
|
# Test namespace filtering
|
|
docs_items = store.search(["test", "docs"])
|
|
assert len(docs_items) == 2
|
|
assert all(item.namespace == ("test", "docs") for item in docs_items)
|
|
|
|
# Test value filtering
|
|
alice_items = store.search(["test"], filter={"author": "Alice"})
|
|
assert len(alice_items) == 2
|
|
assert all(item.value["author"] == "Alice" for item in alice_items)
|
|
|
|
# Test pagination
|
|
paginated_items = store.search(["test"], limit=2)
|
|
assert len(paginated_items) == 2
|
|
|
|
offset_items = store.search(["test"], offset=2)
|
|
assert len(offset_items) == 1
|
|
|
|
# Cleanup
|
|
for namespace, key, _ in test_data:
|
|
store.delete(namespace, key)
|
|
|
|
|
|
def test_vector_store_initialization(fake_embeddings: CharacterEmbeddings) -> None:
|
|
"""Test store initialization with embedding config."""
|
|
# Basic initialization
|
|
with create_vector_store(fake_embeddings) as store:
|
|
assert store.index_config is not None
|
|
assert store.embeddings == fake_embeddings
|
|
assert store.index_config["dims"] == fake_embeddings.dims
|
|
assert store.index_config.get("text_fields") is None
|
|
|
|
# With text fields specified
|
|
text_fields = ["content", "title"]
|
|
with create_vector_store(fake_embeddings, text_fields=text_fields) as store:
|
|
assert store.index_config is not None
|
|
assert store.embeddings == fake_embeddings
|
|
assert store.index_config["dims"] == fake_embeddings.dims
|
|
assert store.index_config["text_fields"] == text_fields
|
|
|
|
# Ensure store setup properly creates the vector tables
|
|
with create_vector_store(fake_embeddings) as store:
|
|
# Check if vector tables exist
|
|
cursor = store.conn.cursor()
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%vector%'"
|
|
)
|
|
tables = cursor.fetchall()
|
|
assert len(tables) >= 1, "Vector tables were not created"
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
@pytest.mark.parametrize("conn_type", ["memory", "file"])
|
|
def test_vector_insert_with_auto_embedding(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
conn_type: Literal["memory", "file"],
|
|
) -> None:
|
|
"""Test inserting items that get auto-embedded."""
|
|
with create_vector_store(
|
|
fake_embeddings, distance_type=distance_type, conn_type=conn_type
|
|
) as store:
|
|
docs = [
|
|
("doc1", {"text": "short text"}),
|
|
("doc2", {"text": "longer text document"}),
|
|
("doc3", {"text": "longest text document here"}),
|
|
("doc4", {"description": "text in description field"}),
|
|
("doc5", {"content": "text in content field"}),
|
|
("doc6", {"body": "text in body field"}),
|
|
]
|
|
|
|
for key, value in docs:
|
|
store.put(("test",), key, value)
|
|
|
|
results = store.search(("test",), query="long text")
|
|
assert len(results) > 0
|
|
|
|
doc_order = [r.key for r in results]
|
|
assert "doc2" in doc_order
|
|
assert "doc3" in doc_order
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
@pytest.mark.parametrize("conn_type", ["memory", "file"])
|
|
def test_vector_update_with_embedding(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
conn_type: Literal["memory", "file"],
|
|
) -> None:
|
|
"""Test that updating items properly updates their embeddings."""
|
|
with create_vector_store(
|
|
fake_embeddings, distance_type=distance_type, conn_type=conn_type
|
|
) as store:
|
|
store.put(("test",), "doc1", {"text": "zany zebra Xerxes"})
|
|
store.put(("test",), "doc2", {"text": "something about dogs"})
|
|
store.put(("test",), "doc3", {"text": "text about birds"})
|
|
|
|
results_initial = store.search(("test",), query="Zany Xerxes")
|
|
assert len(results_initial) > 0
|
|
assert results_initial[0].key == "doc1"
|
|
initial_score = results_initial[0].score
|
|
|
|
store.put(("test",), "doc1", {"text": "new text about dogs"})
|
|
|
|
results_after = store.search(("test",), query="Zany Xerxes")
|
|
after_score = next((r.score for r in results_after if r.key == "doc1"), 0.0)
|
|
assert after_score < initial_score
|
|
|
|
results_new = store.search(("test",), query="new text about dogs")
|
|
for r in results_new:
|
|
if r.key == "doc1":
|
|
assert r.score > after_score
|
|
|
|
# Don't index this one
|
|
store.put(("test",), "doc4", {"text": "new text about dogs"}, index=False)
|
|
results_new = store.search(("test",), query="new text about dogs", limit=3)
|
|
assert not any(r.key == "doc4" for r in results_new)
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_vector_search_with_filters(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test combining vector search with filters."""
|
|
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
|
# Insert test documents
|
|
docs = [
|
|
("doc1", {"text": "red apple", "color": "red", "score": 4.5}),
|
|
("doc2", {"text": "red car", "color": "red", "score": 3.0}),
|
|
("doc3", {"text": "green apple", "color": "green", "score": 4.0}),
|
|
("doc4", {"text": "blue car", "color": "blue", "score": 3.5}),
|
|
]
|
|
for key, value in docs:
|
|
store.put(("test",), key, value)
|
|
|
|
results = store.search(("test",), query="apple", filter={"color": "red"})
|
|
|
|
# Check ordering and score - verify "doc1" is first result
|
|
assert len(results) == 2
|
|
assert results[0].key == "doc1"
|
|
|
|
results = store.search(("test",), query="car", filter={"color": "red"})
|
|
# Check ordering - verify "doc2" is first result
|
|
assert len(results) > 0
|
|
assert results[0].key == "doc2"
|
|
|
|
results = store.search(
|
|
("test",), query="bbbbluuu", filter={"score": {"$gt": 3.2}}
|
|
)
|
|
# There should be 3 documents with score > 3.2
|
|
assert len(results) == 3
|
|
# Check that the blue car is the most similar to "bbbbluuu" query
|
|
assert results[0].key == "doc4" # The blue car should be the most relevant
|
|
# Verify remaining docs are ordered by appropriate similarity
|
|
high_score_keys = [r.key for r in results]
|
|
assert "doc1" in high_score_keys # score 4.5
|
|
assert "doc3" in high_score_keys # score 4.0
|
|
|
|
# Multiple filters
|
|
results = store.search(
|
|
("test",), query="apple", filter={"score": {"$gte": 4.0}, "color": "green"}
|
|
)
|
|
# Check that doc3 is the top result
|
|
assert len(results) > 0
|
|
assert results[0].key == "doc3"
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_vector_search_pagination(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test pagination with vector search."""
|
|
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
|
# Insert multiple similar documents
|
|
for i in range(5):
|
|
store.put(("test",), f"doc{i}", {"text": f"test document number {i}"})
|
|
|
|
# Test with different page sizes
|
|
results_page1 = store.search(("test",), query="test", limit=2)
|
|
results_page2 = store.search(("test",), query="test", limit=2, offset=2)
|
|
|
|
assert len(results_page1) == 2
|
|
assert len(results_page2) == 2
|
|
# Make sure different pages have different results
|
|
assert results_page1[0].key != results_page2[0].key
|
|
assert results_page1[1].key != results_page2[0].key
|
|
assert results_page1[0].key != results_page2[1].key
|
|
assert results_page1[1].key != results_page2[1].key
|
|
|
|
# Check scores are in descending order within each page
|
|
assert results_page1[0].score >= results_page1[1].score
|
|
assert results_page2[0].score >= results_page2[1].score
|
|
|
|
# First page results should have higher scores than second page
|
|
all_results = store.search(("test",), query="test", limit=10)
|
|
assert len(all_results) == 5
|
|
assert (
|
|
all_results[0].score >= all_results[2].score
|
|
) # First page vs second page start
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_vector_search_edge_cases(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test edge cases in vector search."""
|
|
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
|
store.put(("test",), "doc1", {"text": "test document"})
|
|
|
|
results = store.search(("test",), query="")
|
|
assert len(results) == 1
|
|
|
|
results = store.search(("test",), query=None)
|
|
assert len(results) == 1
|
|
|
|
long_query = "test " * 100
|
|
results = store.search(("test",), query=long_query)
|
|
assert len(results) == 1
|
|
|
|
special_query = "test!@#$%^&*()"
|
|
results = store.search(("test",), query=special_query)
|
|
assert len(results) == 1
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_embed_with_path(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test vector search with specific text fields in SQLite store."""
|
|
with create_vector_store(
|
|
fake_embeddings,
|
|
text_fields=["key0", "key1", "key3"],
|
|
distance_type=distance_type,
|
|
) as store:
|
|
# This will have 2 vectors representing it
|
|
doc1 = {
|
|
# Omit key0 - check it doesn't raise an error
|
|
"key1": "xxx",
|
|
"key2": "yyy",
|
|
"key3": "zzz",
|
|
}
|
|
# This will have 3 vectors representing it
|
|
doc2 = {
|
|
"key0": "uuu",
|
|
"key1": "vvv",
|
|
"key2": "www",
|
|
"key3": "xxx",
|
|
}
|
|
store.put(("test",), "doc1", doc1)
|
|
store.put(("test",), "doc2", doc2)
|
|
|
|
# doc2.key3 and doc1.key1 both would have the highest score
|
|
results = store.search(("test",), query="xxx")
|
|
assert len(results) == 2
|
|
assert results[0].key != results[1].key
|
|
assert results[0].score > 0.9
|
|
assert results[1].score > 0.9
|
|
|
|
# ~Only match doc2
|
|
results = store.search(("test",), query="uuu")
|
|
assert len(results) == 2
|
|
assert results[0].key != results[1].key
|
|
assert results[0].key == "doc2"
|
|
assert results[0].score > results[1].score
|
|
|
|
# ~Only match doc1
|
|
results = store.search(("test",), query="zzz")
|
|
assert len(results) == 2
|
|
assert results[0].key != results[1].key
|
|
assert results[0].key == "doc1"
|
|
assert results[0].score > results[1].score
|
|
|
|
# Un-indexed - will have low results for both, Not zero (because we're projecting)
|
|
# but less than the above.
|
|
results = store.search(("test",), query="www")
|
|
assert len(results) == 2
|
|
assert results[0].key != results[1].key
|
|
assert results[0].score < 0.9
|
|
assert results[1].score < 0.9
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_embed_with_path_operation_config(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test operation-level field configuration for vector search."""
|
|
with create_vector_store(
|
|
fake_embeddings, text_fields=["key17"], distance_type=distance_type
|
|
) as store:
|
|
doc3 = {
|
|
"key0": "aaa",
|
|
"key1": "bbb",
|
|
"key2": "ccc",
|
|
"key3": "ddd",
|
|
}
|
|
doc4 = {
|
|
"key0": "eee",
|
|
"key1": "bbb", # Same as doc3.key1
|
|
"key2": "fff",
|
|
"key3": "ggg",
|
|
}
|
|
|
|
store.put(("test",), "doc3", doc3, index=["key0", "key1"])
|
|
store.put(("test",), "doc4", doc4, index=["key1", "key3"])
|
|
|
|
results = store.search(("test",), query="aaa")
|
|
assert len(results) == 2
|
|
assert results[0].key == "doc3"
|
|
assert len(set(r.key for r in results)) == 2
|
|
assert results[0].score > results[1].score
|
|
|
|
results = store.search(("test",), query="ggg")
|
|
assert len(results) == 2
|
|
assert results[0].key == "doc4"
|
|
assert results[0].score > results[1].score
|
|
|
|
results = store.search(("test",), query="bbb")
|
|
assert len(results) == 2
|
|
assert results[0].key != results[1].key
|
|
assert abs(results[0].score - results[1].score) < 0.1 # Similar scores
|
|
|
|
results = store.search(("test",), query="ccc")
|
|
assert len(results) == 2
|
|
assert all(
|
|
r.score < 0.9 for r in results
|
|
) # Unindexed field should have low scores
|
|
|
|
# Test index=False behavior
|
|
doc5 = {
|
|
"key0": "hhh",
|
|
"key1": "iii",
|
|
}
|
|
store.put(("test",), "doc5", doc5, index=False)
|
|
results = store.search(("test",))
|
|
assert len(results) == 3
|
|
assert any(r.key == "doc5" for r in results)
|
|
|
|
|
|
# Helper functions for vector similarity calculations
|
|
def _cosine_similarity(X: list[float], Y: list[list[float]]) -> list[float]:
|
|
"""
|
|
Compute cosine similarity between a vector X and a matrix Y.
|
|
Lazy import numpy for efficiency.
|
|
"""
|
|
|
|
similarities = []
|
|
for y in Y:
|
|
dot_product = sum(a * b for a, b in zip(X, y, strict=False))
|
|
norm1 = sum(a * a for a in X) ** 0.5
|
|
norm2 = sum(a * a for a in y) ** 0.5
|
|
similarity = dot_product / (norm1 * norm2) if norm1 > 0 and norm2 > 0 else 0.0
|
|
similarities.append(similarity)
|
|
|
|
return similarities
|
|
|
|
|
|
@pytest.mark.parametrize("query", ["aaa", "bbb", "ccc", "abcd", "poisson"])
|
|
@pytest.mark.parametrize("conn_type", ["memory", "file"])
|
|
def test_scores(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
query: str,
|
|
conn_type: Literal["memory", "file"],
|
|
) -> None:
|
|
"""Test operation-level field configuration for vector search."""
|
|
with create_vector_store(
|
|
fake_embeddings,
|
|
text_fields=["key0"],
|
|
distance_type="cosine",
|
|
conn_type=conn_type,
|
|
) as store:
|
|
doc = {
|
|
"key0": "aaa",
|
|
}
|
|
store.put(("test",), "doc", doc, index=["key0", "key1"])
|
|
|
|
results = store.search((), query=query)
|
|
vec0 = fake_embeddings.embed_query(doc["key0"])
|
|
vec1 = fake_embeddings.embed_query(query)
|
|
|
|
# SQLite uses cosine similarity by default
|
|
similarities = _cosine_similarity(vec1, [vec0])
|
|
|
|
assert len(results) == 1
|
|
assert results[0].score == pytest.approx(similarities[0], abs=1e-3)
|
|
|
|
|
|
def test_nonnull_migrations() -> None:
|
|
"""Test that all migration statements are non-null."""
|
|
_leading_comment_remover = re.compile(r"^/\*.*?\*/")
|
|
for migration in SqliteStore.MIGRATIONS:
|
|
statement = _leading_comment_remover.sub("", migration).split()[0]
|
|
assert statement.strip(), f"Empty migration statement found: {migration}"
|
|
|
|
|
|
def test_basic_store_operations(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
) -> None:
|
|
"""Test basic store operations with SQLite store."""
|
|
with create_vector_store(
|
|
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
) as store:
|
|
uid = uuid.uuid4().hex
|
|
namespace = (uid, "test", "documents")
|
|
item_id = "doc1"
|
|
item_value = {"title": "Test Document", "content": "Hello, World!"}
|
|
results = store.search((uid,))
|
|
assert len(results) == 0
|
|
|
|
store.put(namespace, item_id, item_value)
|
|
item = store.get(namespace, item_id)
|
|
|
|
assert item is not None
|
|
assert item.namespace == namespace
|
|
assert item.key == item_id
|
|
assert item.value == item_value
|
|
assert item.created_at is not None
|
|
assert item.updated_at is not None
|
|
|
|
updated_value = {
|
|
"title": "Updated Test Document",
|
|
"content": "Hello, LangGraph!",
|
|
}
|
|
store.put(namespace, item_id, updated_value)
|
|
updated_item = store.get(namespace, item_id)
|
|
assert updated_item is not None
|
|
|
|
assert updated_item.value == updated_value
|
|
assert updated_item.updated_at >= item.updated_at
|
|
|
|
different_namespace = (uid, "test", "other_documents")
|
|
item_in_different_namespace = store.get(different_namespace, item_id)
|
|
assert item_in_different_namespace is None
|
|
|
|
new_item_id = "doc2"
|
|
new_item_value = {"title": "Another Document", "content": "Greetings!"}
|
|
store.put(namespace, new_item_id, new_item_value)
|
|
|
|
items = store.search((uid, "test"), limit=10)
|
|
assert len(items) == 2
|
|
assert any(item.key == item_id for item in items)
|
|
assert any(item.key == new_item_id for item in items)
|
|
|
|
namespaces = store.list_namespaces(prefix=(uid, "test"))
|
|
assert (uid, "test", "documents") in namespaces
|
|
|
|
store.delete(namespace, item_id)
|
|
store.delete(namespace, new_item_id)
|
|
deleted_item = store.get(namespace, item_id)
|
|
assert deleted_item is None
|
|
|
|
deleted_item = store.get(namespace, new_item_id)
|
|
assert deleted_item is None
|
|
|
|
empty_search_results = store.search((uid, "test"), limit=10)
|
|
assert len(empty_search_results) == 0
|
|
|
|
|
|
def test_list_namespaces_operations(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
) -> None:
|
|
"""Test list namespaces functionality with various filters."""
|
|
with create_vector_store(
|
|
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
) as store:
|
|
test_pref = str(uuid.uuid4())
|
|
test_namespaces = [
|
|
(test_pref, "test", "documents", "public", test_pref),
|
|
(test_pref, "test", "documents", "private", test_pref),
|
|
(test_pref, "test", "images", "public", test_pref),
|
|
(test_pref, "test", "images", "private", test_pref),
|
|
(test_pref, "prod", "documents", "public", test_pref),
|
|
(test_pref, "prod", "documents", "some", "nesting", "public", test_pref),
|
|
(test_pref, "prod", "documents", "private", test_pref),
|
|
]
|
|
|
|
# Add test data
|
|
for namespace in test_namespaces:
|
|
store.put(namespace, "dummy", {"content": "dummy"})
|
|
|
|
# Test prefix filtering
|
|
prefix_result = store.list_namespaces(prefix=(test_pref, "test"))
|
|
assert len(prefix_result) == 4
|
|
assert all(ns[1] == "test" for ns in prefix_result)
|
|
|
|
# Test specific prefix
|
|
specific_prefix_result = store.list_namespaces(
|
|
prefix=(test_pref, "test", "documents")
|
|
)
|
|
assert len(specific_prefix_result) == 2
|
|
assert all(ns[1:3] == ("test", "documents") for ns in specific_prefix_result)
|
|
|
|
# Test suffix filtering
|
|
suffix_result = store.list_namespaces(suffix=("public", test_pref))
|
|
assert len(suffix_result) == 4
|
|
assert all(ns[-2] == "public" for ns in suffix_result)
|
|
|
|
# Test combined prefix and suffix
|
|
prefix_suffix_result = store.list_namespaces(
|
|
prefix=(test_pref, "test"), suffix=("public", test_pref)
|
|
)
|
|
assert len(prefix_suffix_result) == 2
|
|
assert all(
|
|
ns[1] == "test" and ns[-2] == "public" for ns in prefix_suffix_result
|
|
)
|
|
|
|
# Test wildcard in prefix
|
|
wildcard_prefix_result = store.list_namespaces(
|
|
prefix=(test_pref, "*", "documents")
|
|
)
|
|
assert len(wildcard_prefix_result) == 5
|
|
assert all(ns[2] == "documents" for ns in wildcard_prefix_result)
|
|
|
|
# Test wildcard in suffix
|
|
wildcard_suffix_result = store.list_namespaces(
|
|
suffix=("*", "public", test_pref)
|
|
)
|
|
assert len(wildcard_suffix_result) == 4
|
|
assert all(ns[-2] == "public" for ns in wildcard_suffix_result)
|
|
|
|
wildcard_single = store.list_namespaces(
|
|
suffix=("some", "*", "public", test_pref)
|
|
)
|
|
assert len(wildcard_single) == 1
|
|
assert wildcard_single[0] == (
|
|
test_pref,
|
|
"prod",
|
|
"documents",
|
|
"some",
|
|
"nesting",
|
|
"public",
|
|
test_pref,
|
|
)
|
|
|
|
# Test max depth
|
|
max_depth_result = store.list_namespaces(max_depth=3)
|
|
assert all(len(ns) <= 3 for ns in max_depth_result)
|
|
|
|
max_depth_result = store.list_namespaces(
|
|
max_depth=4, prefix=(test_pref, "*", "documents")
|
|
)
|
|
assert len(set(res for res in max_depth_result)) == len(max_depth_result) == 5
|
|
|
|
# Test pagination
|
|
limit_result = store.list_namespaces(prefix=(test_pref,), limit=3)
|
|
assert len(limit_result) == 3
|
|
|
|
offset_result = store.list_namespaces(prefix=(test_pref,), offset=3)
|
|
assert len(offset_result) == len(test_namespaces) - 3
|
|
|
|
empty_prefix_result = store.list_namespaces(prefix=(test_pref,))
|
|
assert len(empty_prefix_result) == len(test_namespaces)
|
|
assert set(empty_prefix_result) == set(test_namespaces)
|
|
|
|
# Clean up
|
|
for namespace in test_namespaces:
|
|
store.delete(namespace, "dummy")
|
|
|
|
|
|
def test_search_items(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
) -> None:
|
|
"""Test search_items functionality by calling store methods directly."""
|
|
base = "test_search_items"
|
|
test_namespaces = [
|
|
(base, "documents", "user1"),
|
|
(base, "documents", "user2"),
|
|
(base, "reports", "department1"),
|
|
(base, "reports", "department2"),
|
|
]
|
|
test_items = [
|
|
{"title": "Doc 1", "author": "John Doe", "tags": ["important"]},
|
|
{"title": "Doc 2", "author": "Jane Smith", "tags": ["draft"]},
|
|
{"title": "Report A", "author": "John Doe", "tags": ["final"]},
|
|
{"title": "Report B", "author": "Alice Johnson", "tags": ["draft"]},
|
|
]
|
|
|
|
with create_vector_store(
|
|
fake_embeddings, text_fields=["key0", "key1", "key3"]
|
|
) as store:
|
|
# Insert test data
|
|
for ns, item in zip(test_namespaces, test_items, strict=False):
|
|
key = f"item_{ns[-1]}"
|
|
store.put(ns, key, item)
|
|
|
|
# 1. Search documents
|
|
docs = store.search((base, "documents"))
|
|
assert len(docs) == 2
|
|
assert all(item.namespace[1] == "documents" for item in docs)
|
|
|
|
# 2. Search reports
|
|
reports = store.search((base, "reports"))
|
|
assert len(reports) == 2
|
|
assert all(item.namespace[1] == "reports" for item in reports)
|
|
|
|
# 3. Pagination
|
|
first_page = store.search((base,), limit=2, offset=0)
|
|
second_page = store.search((base,), limit=2, offset=2)
|
|
assert len(first_page) == 2
|
|
assert len(second_page) == 2
|
|
keys_page1 = {item.key for item in first_page}
|
|
keys_page2 = {item.key for item in second_page}
|
|
assert keys_page1.isdisjoint(keys_page2)
|
|
all_items = store.search((base,))
|
|
assert len(all_items) == 4
|
|
|
|
john_items = store.search((base,), filter={"author": "John Doe"})
|
|
assert len(john_items) == 2
|
|
assert all(item.value["author"] == "John Doe" for item in john_items)
|
|
|
|
draft_items = store.search((base,), filter={"tags": ["draft"]})
|
|
assert len(draft_items) == 2
|
|
assert all("draft" in item.value["tags"] for item in draft_items)
|
|
|
|
for ns in test_namespaces:
|
|
key = f"item_{ns[-1]}"
|
|
store.delete(ns, key)
|
|
|
|
|
|
def test_sql_injection_vulnerability(store: SqliteStore) -> None:
|
|
"""Test that SQL injection via malicious filter keys is prevented."""
|
|
# Add public and private documents
|
|
store.put(("docs",), "public", {"access": "public", "data": "public info"})
|
|
store.put(
|
|
("docs",), "private", {"access": "private", "data": "secret", "password": "123"}
|
|
)
|
|
|
|
# Normal query - returns 1 public document
|
|
normal = store.search(("docs",), filter={"access": "public"})
|
|
assert len(normal) == 1
|
|
assert normal[0].value["access"] == "public"
|
|
|
|
# SQL injection attempt via malicious key should raise ValueError
|
|
malicious_key = "access') = 'public' OR '1'='1' OR json_extract(value, '$."
|
|
|
|
with pytest.raises(ValueError, match="Invalid filter key"):
|
|
store.search(("docs",), filter={malicious_key: "dummy"})
|
|
|
|
|
|
def test_sql_injection_filter_values(store: SqliteStore) -> None:
|
|
"""Test that SQL injection via malicious filter values is properly escaped."""
|
|
# Setup: Create documents with different access levels
|
|
store.put(("docs",), "doc1", {"access": "public", "title": "Public Document"})
|
|
store.put(("docs",), "doc2", {"access": "private", "title": "Private Document"})
|
|
store.put(("docs",), "doc3", {"access": "secret", "title": "Secret Document"})
|
|
|
|
# Test 1: Basic SQL injection attempt with single quote
|
|
malicious_value = "public' OR '1'='1"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
# Should return 0 results because the malicious value is escaped and won't match anything
|
|
assert len(results) == 0, "SQL injection via string value should be blocked"
|
|
|
|
# Test 2: SQL injection with comment
|
|
malicious_value = "public'; --"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
assert len(results) == 0, "SQL comment injection should be blocked"
|
|
|
|
# Test 3: UNION injection attempt
|
|
malicious_value = "public' UNION SELECT * FROM store --"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
assert len(results) == 0, "UNION injection should be blocked"
|
|
|
|
# Test 4: Parameterized queries handle strings with null bytes and SQL injection attempts safely
|
|
malicious_value = "public\x00' OR '1'='1"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
assert len(results) == 0, (
|
|
"Parameterized queries treat injection attempts as literal strings"
|
|
)
|
|
|
|
# Test 5: Multiple single quotes
|
|
malicious_value = "''''"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
assert len(results) == 0, "Multiple quotes should be handled safely"
|
|
|
|
# Test 6: Legitimate value with single quote should work
|
|
store.put(("docs",), "doc4", {"title": "O'Brien's Document", "access": "public"})
|
|
results = store.search(("docs",), filter={"title": "O'Brien's Document"})
|
|
assert len(results) == 1, "Legitimate single quotes should work"
|
|
assert results[0].value["title"] == "O'Brien's Document"
|
|
|
|
# Test 7: Unicode characters with injection attempt
|
|
malicious_value = "public' OR 'א'='א"
|
|
results = store.search(("docs",), filter={"access": malicious_value})
|
|
assert len(results) == 0, "Unicode-based injection should be blocked"
|
|
|
|
|
|
def test_numeric_filter_safety(store: SqliteStore) -> None:
|
|
"""Test that numeric filter values are handled safely."""
|
|
# Setup: Create documents with numeric fields
|
|
store.put(("items",), "item1", {"price": 10, "quantity": 5})
|
|
store.put(("items",), "item2", {"price": 20, "quantity": 3})
|
|
store.put(("items",), "item3", {"price": 30, "quantity": 1})
|
|
|
|
# Test 1: Normal numeric comparison
|
|
results = store.search(("items",), filter={"price": {"$gt": 15}})
|
|
assert len(results) == 2
|
|
assert all(r.value["price"] > 15 for r in results)
|
|
|
|
# Test 2: Special float values (infinity)
|
|
results = store.search(("items",), filter={"price": {"$lt": float("inf")}})
|
|
assert len(results) == 3, "All finite values should be less than infinity"
|
|
|
|
# Test 3: Special float values (negative infinity)
|
|
results = store.search(("items",), filter={"price": {"$gt": float("-inf")}})
|
|
assert len(results) == 3, (
|
|
"All finite values should be greater than negative infinity"
|
|
)
|
|
|
|
# Test 4: NaN handling - NaN comparisons should not cause errors
|
|
try:
|
|
results = store.search(("items",), filter={"price": {"$eq": float("nan")}})
|
|
# NaN never equals anything, including itself, so should return 0 results
|
|
assert len(results) == 0
|
|
except Exception as e:
|
|
pytest.fail(f"NaN handling should not raise exception: {e}")
|
|
|
|
# Test 5: Very large numbers
|
|
results = store.search(("items",), filter={"price": {"$lt": 10**100}})
|
|
assert len(results) == 3, "Very large numbers should be handled safely"
|
|
|
|
# Test 6: Negative numbers
|
|
store.put(("items",), "item4", {"price": -10, "quantity": 0})
|
|
results = store.search(("items",), filter={"price": {"$lt": 0}})
|
|
assert len(results) == 1
|
|
assert results[0].key == "item4"
|
|
|
|
|
|
def test_boolean_filter_safety(store: SqliteStore) -> None:
|
|
"""Test that boolean filter values are handled safely."""
|
|
store.put(("flags",), "flag1", {"active": True, "name": "Feature A"})
|
|
store.put(("flags",), "flag2", {"active": False, "name": "Feature B"})
|
|
store.put(("flags",), "flag3", {"active": True, "name": "Feature C"})
|
|
|
|
# Test boolean filters
|
|
results = store.search(("flags",), filter={"active": True})
|
|
assert len(results) == 2
|
|
assert all(r.value["active"] is True for r in results)
|
|
|
|
results = store.search(("flags",), filter={"active": False})
|
|
assert len(results) == 1
|
|
assert results[0].value["active"] is False
|
|
|
|
|
|
def test_filter_keys_with_hyphens_and_digits(store: SqliteStore) -> None:
|
|
"""Keys with hyphens or leading digits should be queryable via filters.
|
|
|
|
Current unquoted JSON path construction (e.g., '$.access-level' or '$.123abc')
|
|
is not valid JSON1 syntax, so this test will catch regressions in path handling.
|
|
"""
|
|
# Documents with top-level and nested keys requiring bracket-quoted JSON paths
|
|
store.put(
|
|
("docs",),
|
|
"hyphen",
|
|
{"access-level": "public", "user": {"access-level": "nested"}},
|
|
)
|
|
store.put(("docs",), "digit", {"123abc": "ok", "user": {"123abc": "ok2"}})
|
|
|
|
# Top-level hyphenated key
|
|
results = store.search(("docs",), filter={"access-level": "public"})
|
|
assert [r.key for r in results] == ["hyphen"]
|
|
|
|
# Nested hyphenated key via dotted path
|
|
results = store.search(("docs",), filter={"user.access-level": "nested"})
|
|
assert [r.key for r in results] == ["hyphen"]
|
|
|
|
# Top-level digit-starting key
|
|
results = store.search(("docs",), filter={"123abc": "ok"})
|
|
assert [r.key for r in results] == ["digit"]
|
|
|
|
# Nested digit-starting key via dotted path
|
|
results = store.search(("docs",), filter={"user.123abc": "ok2"})
|
|
assert [r.key for r in results] == ["digit"]
|
|
|
|
|
|
@pytest.mark.parametrize("distance_type", VECTOR_TYPES)
|
|
def test_non_ascii(
|
|
fake_embeddings: CharacterEmbeddings,
|
|
distance_type: str,
|
|
) -> None:
|
|
"""Test support for non-ascii characters"""
|
|
with create_vector_store(fake_embeddings, distance_type=distance_type) as store:
|
|
store.put(("user_123", "memories"), "1", {"text": "这是中文"}) # Chinese
|
|
store.put(
|
|
("user_123", "memories"), "2", {"text": "これは日本語です"}
|
|
) # Japanese
|
|
store.put(("user_123", "memories"), "3", {"text": "이건 한국어야"}) # Korean
|
|
store.put(("user_123", "memories"), "4", {"text": "Это русский"}) # Russian
|
|
store.put(("user_123", "memories"), "5", {"text": "यह रूसी है"}) # Hindi
|
|
|
|
result1 = store.search(("user_123", "memories"), query="这是中文")
|
|
result2 = store.search(("user_123", "memories"), query="これは日本語です")
|
|
result3 = store.search(("user_123", "memories"), query="이건 한국어야")
|
|
result4 = store.search(("user_123", "memories"), query="Это русский")
|
|
result5 = store.search(("user_123", "memories"), query="यह रूसी है")
|
|
|
|
assert result1[0].key == "1"
|
|
assert result2[0].key == "2"
|
|
assert result3[0].key == "3"
|
|
assert result4[0].key == "4"
|
|
assert result5[0].key == "5"
|