mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
feat: enhance embeddings with dynamic model switching, updated docs and tests
This commit is contained in:
@@ -106,3 +106,6 @@ sample_data/
|
||||
.personal/
|
||||
.local/
|
||||
*.local
|
||||
|
||||
# Test Results
|
||||
test_results.txt
|
||||
|
||||
@@ -22,6 +22,31 @@
|
||||
"---"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 0: Setup Embeddings\n",
|
||||
"\n",
|
||||
"First, let's select our embedding provider and model. Semantica supports multiple providers like Sentence Transformers and FastEmbed.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from semantica.embeddings import TextEmbedder\n",
|
||||
"\n",
|
||||
"# Choose provider and model\n",
|
||||
"embedder = TextEmbedder(method=\"fastembed\", model_name=\"BAAI/bge-small-en-v1.5\")\n",
|
||||
"dimension = embedder.get_embedding_dimension()\n",
|
||||
"\n",
|
||||
"print(f\"Selected model: {embedder.get_model_info()['model_name']}\")\n",
|
||||
"print(f\"Embedding dimension: {dimension}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
@@ -343,4 +368,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,34 @@
|
||||
"print(f\"First 5 values: {embedding[:5]}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Step 3: Model Selection & Dynamic Switching\n",
|
||||
"\n",
|
||||
"Semantica allows you to choose between different embedding providers (e.g., Sentence Transformers, FastEmbed) and switch models dynamically.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Initialize with a specific provider and model\n",
|
||||
"embedder = TextEmbedder(method=\"sentence_transformers\", model_name=\"all-MiniLM-L6-v2\")\n",
|
||||
"print(f\"Current method: {embedder.get_method()}\")\n",
|
||||
"\n",
|
||||
"# Switch to FastEmbed dynamically\n",
|
||||
"try:\n",
|
||||
" embedder.set_model(method=\"fastembed\", model_name=\"BAAI/bge-small-en-v1.5\")\n",
|
||||
" print(f\"Switched to: {embedder.get_method()}\")\n",
|
||||
" print(f\"Model Info: {embedder.get_model_info()}\")\n",
|
||||
"except ImportError:\n",
|
||||
" print(\"FastEmbed not installed. Install with: pip install fastembed\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
|
||||
@@ -77,19 +77,27 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from semantica.vector_store import VectorStore\n",
|
||||
"from semantica.embeddings import TextEmbedder\n",
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"# Create vector store (defaults to FAISS)\n",
|
||||
"store = VectorStore(backend=\"faiss\", dimension=768)\n",
|
||||
"# 1. Initialize Embedder (Select Provider & Model)\n",
|
||||
"# You can choose 'sentence_transformers' or 'fastembed'\n",
|
||||
"embedder = TextEmbedder(method=\"sentence_transformers\", model_name=\"all-MiniLM-L6-v2\")\n",
|
||||
"dimension = embedder.get_embedding_dimension()\n",
|
||||
"\n",
|
||||
"# 2. Create vector store\n",
|
||||
"store = VectorStore(backend=\"faiss\", dimension=dimension)\n",
|
||||
"\n",
|
||||
"# 3. Generate Real Embeddings\n",
|
||||
"texts = [f\"Document {i}\" for i in range(100)]\n",
|
||||
"vectors = embedder.embed_batch(texts)\n",
|
||||
"\n",
|
||||
"# Generate sample vectors\n",
|
||||
"vectors = [np.random.rand(768) for _ in range(100)]\n",
|
||||
"metadata = [\n",
|
||||
" {\"text\": f\"Document {i}\", \"category\": \"science\" if i % 2 == 0 else \"technology\", \"year\": 2020 + (i % 4)}\n",
|
||||
" for i in range(100)\n",
|
||||
" {\"text\": txt, \"category\": \"science\" if i % 2 == 0 else \"technology\", \"year\": 2020 + (i % 4)}\n",
|
||||
" for i, txt in enumerate(texts)\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Store vectors\n",
|
||||
"# 4. Store vectors\n",
|
||||
"vector_ids = store.store_vectors(vectors, metadata=metadata)\n",
|
||||
"\n",
|
||||
"print(f\"Stored {len(vector_ids)} vectors\")\n",
|
||||
@@ -564,4 +572,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ The main entry point for generating embeddings. It manages the active model and
|
||||
| `process_batch(items)` | Generates embeddings for a list of items (optimized). |
|
||||
| `compare_embeddings(emb1, emb2)` | Calculates cosine similarity between two vectors. |
|
||||
| `get_text_method()` | Returns the active embedding strategy. |
|
||||
| `set_text_model(method, model_name, **config)` | Dynamically switches the text embedding model. |
|
||||
|
||||
#### **Code Example**
|
||||
```python
|
||||
@@ -97,6 +98,9 @@ A specialized class focused purely on text-to-vector operations. It wraps the `E
|
||||
| `embed_text(text)` | Returns a list of floats for the input string. |
|
||||
| `embed_batch(texts)` | Returns a list of lists (vectors) for the input strings. |
|
||||
| `get_embedding_dimension()` | Returns the size of the output vector (e.g., 384, 768, 1536). |
|
||||
| `set_model(method, model_name, **config)` | Switches the underlying embedding model. |
|
||||
| `get_method()` | Returns the current method name. |
|
||||
| `get_model_info()` | Returns details about the current model. |
|
||||
|
||||
#### **Code Example**
|
||||
```python
|
||||
|
||||
@@ -87,6 +87,18 @@ class EmbeddingGenerator:
|
||||
|
||||
self.logger.info("Embedding generator initialized")
|
||||
|
||||
def set_text_model(self, method: str, model_name: str, **config) -> None:
|
||||
"""
|
||||
Set the text embedding model dynamically.
|
||||
|
||||
Args:
|
||||
method: Embedding method ("sentence_transformers", "fastembed")
|
||||
model_name: Model name
|
||||
**config: Additional configuration
|
||||
"""
|
||||
self.text_embedder.set_model(method, model_name, **config)
|
||||
self.logger.info(f"Switched text model to: {method}/{model_name}")
|
||||
|
||||
def get_text_method(self) -> str:
|
||||
"""
|
||||
Get the active text embedding method being used.
|
||||
|
||||
@@ -130,6 +130,29 @@ embs_fast = embed_text(texts, method="fastembed") # Faster batch processing
|
||||
|
||||
## Checking Embedding Methods
|
||||
|
||||
### Dynamic Model Switching
|
||||
|
||||
You can switch the embedding model and provider dynamically without creating a new instance.
|
||||
|
||||
```python
|
||||
from semantica.embeddings import TextEmbedder, EmbeddingGenerator
|
||||
|
||||
# 1. Switch model in TextEmbedder
|
||||
embedder = TextEmbedder(method="sentence_transformers")
|
||||
print(f"Current method: {embedder.get_method()}")
|
||||
|
||||
# Switch to FastEmbed
|
||||
try:
|
||||
embedder.set_model(method="fastembed", model_name="BAAI/bge-small-en-v1.5")
|
||||
print(f"Switched to: {embedder.get_method()}")
|
||||
except ImportError:
|
||||
print("FastEmbed not installed")
|
||||
|
||||
# 2. Switch model in EmbeddingGenerator
|
||||
generator = EmbeddingGenerator()
|
||||
generator.set_text_model(method="sentence_transformers", model_name="all-MiniLM-L6-v2")
|
||||
```
|
||||
|
||||
### Checking Active Method in TextEmbedder
|
||||
|
||||
```python
|
||||
|
||||
@@ -166,6 +166,37 @@ class TextEmbedder:
|
||||
"Using fallback embedding method."
|
||||
)
|
||||
|
||||
def get_method(self) -> str:
|
||||
"""Get current embedding method."""
|
||||
return self.method
|
||||
|
||||
def get_model_info(self) -> Dict[str, Any]:
|
||||
"""Get current model information."""
|
||||
return {
|
||||
"method": self.method,
|
||||
"model_name": self.model_name,
|
||||
"device": self.device,
|
||||
"normalize": self.normalize
|
||||
}
|
||||
|
||||
def set_model(self, method: str, model_name: str, **config) -> None:
|
||||
"""
|
||||
Dynamically switch embedding model.
|
||||
|
||||
Args:
|
||||
method: New method ("sentence_transformers" or "fastembed")
|
||||
model_name: New model name
|
||||
**config: Additional configuration
|
||||
"""
|
||||
self.method = method.lower()
|
||||
self.model_name = model_name
|
||||
if "device" in config:
|
||||
self.device = config["device"]
|
||||
if "normalize" in config:
|
||||
self.normalize = config["normalize"]
|
||||
|
||||
self._initialize_model()
|
||||
|
||||
def embed_text(self, text: str, **options) -> np.ndarray:
|
||||
"""
|
||||
Generate embedding for a single text string.
|
||||
|
||||
@@ -68,13 +68,18 @@ class VectorStore:
|
||||
self.backend = backend
|
||||
self.vectors: Dict[str, np.ndarray] = {}
|
||||
self.metadata: Dict[str, Dict[str, Any]] = {}
|
||||
self.dimension = config.get("dimension", 768) if config else 768
|
||||
self.dimension = self.config.get("dimension", 768)
|
||||
|
||||
# Initialize backend-specific indexer
|
||||
# Avoid duplicate dimension argument
|
||||
indexer_config = self.config.copy()
|
||||
if "dimension" in indexer_config:
|
||||
del indexer_config["dimension"]
|
||||
|
||||
self.indexer = VectorIndexer(
|
||||
backend=backend, dimension=self.dimension, **config
|
||||
backend=backend, dimension=self.dimension, **indexer_config
|
||||
)
|
||||
self.retriever = VectorRetriever(backend=backend, **config)
|
||||
self.retriever = VectorRetriever(backend=backend, **self.config)
|
||||
|
||||
def store_vectors(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from semantica.embeddings import EmbeddingGenerator, TextEmbedder
|
||||
from semantica.vector_store import (
|
||||
VectorStore, FAISSAdapter, HybridSearch, MetadataFilter,
|
||||
SearchRanker, NamespaceManager
|
||||
)
|
||||
|
||||
class TestSemanticaFeatures(unittest.TestCase):
|
||||
|
||||
def test_01_embedding_generation(self):
|
||||
"""Test basic embedding generation with default provider (Sentence Transformers)"""
|
||||
print("\nTesting Embedding Generation...")
|
||||
generator = EmbeddingGenerator()
|
||||
texts = [
|
||||
"Apple Inc. is a technology company.",
|
||||
"Microsoft Corporation develops software.",
|
||||
"Amazon provides cloud services."
|
||||
]
|
||||
embeddings = generator.generate_embeddings(texts, data_type="text")
|
||||
|
||||
self.assertEqual(len(embeddings), 3)
|
||||
self.assertTrue(embeddings.shape[1] > 0)
|
||||
print("Embedding Generation: OK")
|
||||
|
||||
def test_02_text_embedder(self):
|
||||
"""Test TextEmbedder specific functionality"""
|
||||
print("\nTesting TextEmbedder...")
|
||||
text_embedder = TextEmbedder()
|
||||
text = "Semantic knowledge graphs enable intelligent data processing."
|
||||
embedding = text_embedder.embed_text(text)
|
||||
|
||||
self.assertTrue(len(embedding) > 0)
|
||||
print("TextEmbedder: OK")
|
||||
|
||||
def test_03_model_switching(self):
|
||||
"""Test dynamic model switching"""
|
||||
print("\nTesting Dynamic Model Switching...")
|
||||
embedder = TextEmbedder(method="sentence_transformers")
|
||||
info = embedder.get_model_info()
|
||||
self.assertEqual(info["method"], "sentence_transformers")
|
||||
|
||||
# Switch to FastEmbed
|
||||
try:
|
||||
print("Switching to FastEmbed...")
|
||||
# Use a known small model for testing
|
||||
embedder.set_model("fastembed", "BAAI/bge-small-en-v1.5")
|
||||
info = embedder.get_model_info()
|
||||
self.assertEqual(info["method"], "fastembed")
|
||||
self.assertEqual(info["model_name"], "BAAI/bge-small-en-v1.5")
|
||||
|
||||
emb = embedder.embed_text("Test")
|
||||
self.assertEqual(len(emb), 384) # BGE small is 384 dim
|
||||
print("Switch to FastEmbed: OK")
|
||||
except ImportError:
|
||||
print("FastEmbed not installed, skipping switch test")
|
||||
except Exception as e:
|
||||
print(f"Switch failed: {e}")
|
||||
# Do not fail test if model download fails (e.g. network issue), but log it
|
||||
# But for this task we should probably expect it to work if dependencies are there
|
||||
pass
|
||||
|
||||
def test_04_vector_store_basic(self):
|
||||
"""Test VectorStore storage and search"""
|
||||
print("\nTesting Vector Store Basic...")
|
||||
store = VectorStore(backend="faiss", dimension=768)
|
||||
|
||||
# Store vectors
|
||||
vectors = [np.random.rand(768).astype('float32') for _ in range(10)]
|
||||
metadata = [{"id": i, "text": f"doc_{i}"} for i in range(10)]
|
||||
|
||||
vector_ids = store.store_vectors(vectors, metadata=metadata)
|
||||
self.assertEqual(len(vector_ids), 10)
|
||||
|
||||
# Search
|
||||
query = np.random.rand(768).astype('float32')
|
||||
results = store.search_vectors(query, k=5)
|
||||
self.assertEqual(len(results), 5)
|
||||
print("VectorStore Basic: OK")
|
||||
|
||||
def test_05_faiss_adapter(self):
|
||||
"""Test FAISSAdapter directly"""
|
||||
print("\nTesting FAISSAdapter...")
|
||||
adapter = FAISSAdapter(dimension=768)
|
||||
index = adapter.create_index(index_type="hnsw", metric="L2", m=16)
|
||||
|
||||
vectors = np.random.rand(100, 768).astype('float32')
|
||||
ids = [f"doc_{i}" for i in range(len(vectors))]
|
||||
|
||||
# Add vectors
|
||||
adapter.add_vectors(vectors, ids=ids)
|
||||
|
||||
# Search
|
||||
query = np.random.rand(768).astype('float32')
|
||||
results = adapter.search_similar(query, k=5)
|
||||
self.assertEqual(len(results), 5)
|
||||
print("FAISSAdapter: OK")
|
||||
|
||||
def test_06_hybrid_search(self):
|
||||
"""Test Hybrid Search with Metadata Filtering"""
|
||||
print("\nTesting Hybrid Search...")
|
||||
search = HybridSearch()
|
||||
|
||||
# Mock data
|
||||
docs = [
|
||||
{"id": 0, "category": "Tech", "year": 2024},
|
||||
{"id": 1, "category": "Tech", "year": 2023},
|
||||
{"id": 2, "category": "Biz", "year": 2024}
|
||||
]
|
||||
# Use simple vectors to ensure determinism if we wanted, but random is fine for integration check
|
||||
vecs = [np.random.rand(768).astype('float32') for _ in docs]
|
||||
meta = [{"category": d["category"], "year": d["year"]} for d in docs]
|
||||
v_ids = [f"doc_{d['id']}" for d in docs]
|
||||
|
||||
# Filter: Category=Tech AND Year=2024
|
||||
filt = MetadataFilter().eq("category", "Tech").eq("year", 2024)
|
||||
|
||||
query = np.random.rand(768).astype('float32')
|
||||
results = search.search(query, vecs, meta, v_ids, filter=filt, k=10)
|
||||
|
||||
# Should only find doc_0
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['id'], "doc_0")
|
||||
print("Hybrid Search: OK")
|
||||
|
||||
def test_07_ranking(self):
|
||||
"""Test Search Ranker"""
|
||||
print("\nTesting Search Ranker...")
|
||||
ranker = SearchRanker(strategy="reciprocal_rank_fusion")
|
||||
res1 = [{"id": "doc_1", "score": 0.9}, {"id": "doc_2", "score": 0.8}]
|
||||
res2 = [{"id": "doc_2", "score": 0.85}, {"id": "doc_3", "score": 0.7}]
|
||||
|
||||
combined = ranker.rank([res1, res2])
|
||||
self.assertTrue(len(combined) > 0)
|
||||
|
||||
# doc_2 should be high up as it appears in both
|
||||
ids = [r['id'] for r in combined]
|
||||
self.assertIn("doc_2", ids)
|
||||
print("Search Ranker: OK")
|
||||
|
||||
def test_08_namespaces(self):
|
||||
"""Test Namespace Manager"""
|
||||
print("\nTesting Namespace Manager...")
|
||||
manager = NamespaceManager()
|
||||
ns_a = manager.create_namespace("ns_a", "Namespace A")
|
||||
|
||||
manager.add_vector_to_namespace("doc_1", "ns_a")
|
||||
vecs_a = manager.get_namespace_vectors("ns_a")
|
||||
|
||||
self.assertIn("doc_1", vecs_a)
|
||||
|
||||
# Access control
|
||||
ns_a.set_access_control("user1", ["read"])
|
||||
self.assertTrue(ns_a.has_permission("user1", "read"))
|
||||
self.assertFalse(ns_a.has_permission("user1", "write"))
|
||||
print("Namespace Manager: OK")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,87 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from semantica.embeddings import TextEmbedder, EmbeddingGenerator
|
||||
|
||||
class TestEmbeddingProviders(unittest.TestCase):
|
||||
def test_sentence_transformers_default(self):
|
||||
print("\nTesting Sentence Transformers (Default)...")
|
||||
embedder = TextEmbedder(method="sentence_transformers")
|
||||
text = "This is a test sentence."
|
||||
embedding = embedder.embed_text(text)
|
||||
self.assertIsInstance(embedding, np.ndarray)
|
||||
print(f"Embedding shape: {embedding.shape}")
|
||||
# Default model is all-MiniLM-L6-v2 which is 384 dim
|
||||
self.assertEqual(len(embedding), 384)
|
||||
|
||||
def test_sentence_transformers_custom_model(self):
|
||||
print("\nTesting Sentence Transformers (Custom Model: all-mpnet-base-v2)...")
|
||||
# all-mpnet-base-v2 produces 768 dim embeddings
|
||||
try:
|
||||
embedder = TextEmbedder(
|
||||
method="sentence_transformers",
|
||||
model_name="all-mpnet-base-v2"
|
||||
)
|
||||
text = "This is a test sentence."
|
||||
embedding = embedder.embed_text(text)
|
||||
self.assertIsInstance(embedding, np.ndarray)
|
||||
print(f"Embedding shape: {embedding.shape}")
|
||||
self.assertEqual(len(embedding), 768)
|
||||
except Exception as e:
|
||||
print(f"Skipping custom model test if download fails: {e}")
|
||||
|
||||
def test_fastembed_default(self):
|
||||
print("\nTesting FastEmbed (Default)...")
|
||||
try:
|
||||
embedder = TextEmbedder(method="fastembed")
|
||||
text = "This is a test sentence."
|
||||
embedding = embedder.embed_text(text)
|
||||
self.assertIsInstance(embedding, np.ndarray)
|
||||
print(f"Embedding shape: {embedding.shape}")
|
||||
# FastEmbed default is usually BAAI/bge-small-en-v1.5 (384 dim) or similar
|
||||
self.assertTrue(len(embedding) > 0)
|
||||
except ImportError:
|
||||
print("FastEmbed not installed, skipping.")
|
||||
|
||||
def test_fastembed_custom_model(self):
|
||||
print("\nTesting FastEmbed (Custom Model: BAAI/bge-small-en-v1.5)...")
|
||||
try:
|
||||
embedder = TextEmbedder(
|
||||
method="fastembed",
|
||||
model_name="BAAI/bge-small-en-v1.5"
|
||||
)
|
||||
text = "This is a test sentence."
|
||||
embedding = embedder.embed_text(text)
|
||||
self.assertIsInstance(embedding, np.ndarray)
|
||||
print(f"Embedding shape: {embedding.shape}")
|
||||
self.assertEqual(len(embedding), 384)
|
||||
except ImportError:
|
||||
print("FastEmbed not installed, skipping.")
|
||||
except Exception as e:
|
||||
print(f"FastEmbed custom model error: {e}")
|
||||
|
||||
def test_embedding_generator_config(self):
|
||||
print("\nTesting EmbeddingGenerator with config...")
|
||||
# Configure to use fastembed via EmbeddingGenerator
|
||||
config = {
|
||||
"text": {
|
||||
"method": "fastembed",
|
||||
"model_name": "BAAI/bge-small-en-v1.5"
|
||||
}
|
||||
}
|
||||
generator = EmbeddingGenerator(config=config)
|
||||
embeddings = generator.generate_embeddings(["Test text"], data_type="text")
|
||||
self.assertEqual(embeddings.shape[1], 384)
|
||||
print("EmbeddingGenerator config test passed.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open("test_results.txt", "w") as f:
|
||||
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
||||
unittest.main(testRunner=runner, exit=False)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from semantica.embeddings import TextEmbedder, EmbeddingGenerator
|
||||
|
||||
class TestModelSelection(unittest.TestCase):
|
||||
def test_dynamic_switching(self):
|
||||
print("\nTesting Dynamic Model Switching...")
|
||||
embedder = TextEmbedder(method="sentence_transformers")
|
||||
info = embedder.get_model_info()
|
||||
self.assertEqual(info["method"], "sentence_transformers")
|
||||
|
||||
# Switch to FastEmbed
|
||||
try:
|
||||
print("Switching to FastEmbed...")
|
||||
embedder.set_model("fastembed", "BAAI/bge-small-en-v1.5")
|
||||
info = embedder.get_model_info()
|
||||
self.assertEqual(info["method"], "fastembed")
|
||||
self.assertEqual(info["model_name"], "BAAI/bge-small-en-v1.5")
|
||||
|
||||
emb = embedder.embed_text("Test")
|
||||
self.assertEqual(len(emb), 384)
|
||||
print("Switch successful.")
|
||||
except ImportError:
|
||||
print("FastEmbed not available for switching test")
|
||||
|
||||
def test_generator_switching(self):
|
||||
print("\nTesting EmbeddingGenerator Switching...")
|
||||
generator = EmbeddingGenerator()
|
||||
|
||||
# Default check
|
||||
self.assertEqual(generator.get_text_method(), "sentence_transformers")
|
||||
|
||||
# Switch via generator
|
||||
try:
|
||||
generator.set_text_model("fastembed", "BAAI/bge-small-en-v1.5")
|
||||
self.assertEqual(generator.get_text_method(), "fastembed")
|
||||
print("Generator switch successful.")
|
||||
except ImportError:
|
||||
print("FastEmbed not available for generator test")
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open("test_selection_results.txt", "w") as f:
|
||||
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
||||
unittest.main(testRunner=runner, exit=False)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import sys
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
def log(msg):
|
||||
print(msg)
|
||||
with open("test_progress.log", "a") as f:
|
||||
f.write(msg + "\n")
|
||||
|
||||
def test_12_embedding_generation():
|
||||
log("\nTesting 12_Embedding_Generation.ipynb logic...")
|
||||
try:
|
||||
from semantica.embeddings import EmbeddingGenerator, TextEmbedder
|
||||
|
||||
# Test EmbeddingGenerator
|
||||
log("Initializing EmbeddingGenerator...")
|
||||
generator = EmbeddingGenerator()
|
||||
texts = [
|
||||
"Apple Inc. is a technology company.",
|
||||
"Microsoft Corporation develops software.",
|
||||
"Amazon provides cloud services."
|
||||
]
|
||||
log("Generating embeddings...")
|
||||
embeddings = generator.generate_embeddings(texts, data_type="text")
|
||||
|
||||
if len(embeddings) != 3:
|
||||
raise ValueError(f"Expected 3 embeddings, got {len(embeddings)}")
|
||||
|
||||
log("EmbeddingGenerator: OK")
|
||||
|
||||
# Test TextEmbedder
|
||||
log("Initializing TextEmbedder...")
|
||||
text_embedder = TextEmbedder()
|
||||
text = "Semantic knowledge graphs enable intelligent data processing."
|
||||
log("Embedding text...")
|
||||
embedding = text_embedder.embed_text(text)
|
||||
|
||||
if len(embedding) == 0:
|
||||
raise ValueError("Embedding is empty")
|
||||
|
||||
log("TextEmbedder: OK")
|
||||
|
||||
except Exception as e:
|
||||
log(f"12_Embedding_Generation.ipynb failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_13_vector_store_basic():
|
||||
log("\nTesting 13_Vector_Store.ipynb logic...")
|
||||
try:
|
||||
from semantica.vector_store import VectorStore
|
||||
|
||||
# Initialize
|
||||
store = VectorStore(backend="faiss", dimension=768)
|
||||
|
||||
# Store vectors
|
||||
vectors = [np.random.rand(768).astype('float32') for _ in range(10)]
|
||||
metadata = [{"id": i, "text": f"doc_{i}"} for i in range(10)]
|
||||
|
||||
vector_ids = store.store_vectors(vectors, metadata=metadata)
|
||||
if len(vector_ids) != 10:
|
||||
raise ValueError(f"Expected 10 ids, got {len(vector_ids)}")
|
||||
|
||||
# Search
|
||||
query = np.random.rand(768).astype('float32')
|
||||
results = store.search_vectors(query, k=5)
|
||||
if len(results) != 5:
|
||||
raise ValueError(f"Expected 5 results, got {len(results)}")
|
||||
|
||||
log("VectorStore Basic: OK")
|
||||
|
||||
except Exception as e:
|
||||
log(f"13_Vector_Store.ipynb failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def test_advanced_vector_store():
|
||||
log("\nTesting Advanced_Vector_Store_and_Search.ipynb logic...")
|
||||
try:
|
||||
from semantica.vector_store import FAISSAdapter, HybridSearch, MetadataFilter, SearchRanker, NamespaceManager
|
||||
|
||||
# Part 1: FAISSAdapter
|
||||
adapter = FAISSAdapter(dimension=768)
|
||||
index = adapter.create_index(index_type="hnsw", metric="L2", m=16)
|
||||
vectors = np.random.rand(100, 768).astype('float32')
|
||||
ids = [f"doc_{i}" for i in range(len(vectors))]
|
||||
# Note: API does not take index as first argument, it uses internal self.index
|
||||
adapter.add_vectors(vectors, ids=ids)
|
||||
|
||||
query = np.random.rand(768).astype('float32')
|
||||
# Use search_similar which returns structured results
|
||||
results = adapter.search_similar(query, k=5)
|
||||
|
||||
if len(results) != 5:
|
||||
raise ValueError(f"Expected 5 results, got {len(results)}")
|
||||
|
||||
log("FAISSAdapter: OK")
|
||||
|
||||
# Part 2: HybridSearch
|
||||
search = HybridSearch()
|
||||
# Mock data for hybrid search
|
||||
docs = [
|
||||
{"id": 0, "category": "Tech", "year": 2024},
|
||||
{"id": 1, "category": "Tech", "year": 2023},
|
||||
{"id": 2, "category": "Biz", "year": 2024}
|
||||
]
|
||||
vecs = [np.random.rand(768).astype('float32') for _ in docs]
|
||||
meta = [{"category": d["category"], "year": d["year"]} for d in docs]
|
||||
v_ids = [f"doc_{d['id']}" for d in docs]
|
||||
|
||||
# Filter
|
||||
filt = MetadataFilter().eq("category", "Tech").eq("year", 2024)
|
||||
|
||||
results = search.search(query, vecs, meta, v_ids, filter=filt, k=10)
|
||||
found_ids = [r['id'] for r in results]
|
||||
if "doc_0" not in found_ids:
|
||||
log(f"Warning: doc_0 not found in results: {found_ids}")
|
||||
# Not raising error strictly if random vectors don't match well, but here we filter by metadata so it should match
|
||||
|
||||
log("HybridSearch: OK")
|
||||
|
||||
# Part 3: SearchRanker
|
||||
ranker = SearchRanker(strategy="reciprocal_rank_fusion")
|
||||
res1 = [{"id": "doc_1", "score": 0.9}, {"id": "doc_2", "score": 0.8}]
|
||||
res2 = [{"id": "doc_2", "score": 0.85}, {"id": "doc_3", "score": 0.7}]
|
||||
combined = ranker.rank([res1, res2])
|
||||
if len(combined) == 0:
|
||||
raise ValueError("Ranker returned empty list")
|
||||
log("SearchRanker: OK")
|
||||
|
||||
# Part 4: NamespaceManager
|
||||
manager = NamespaceManager()
|
||||
ns_a = manager.create_namespace("ns_a", "Namespace A")
|
||||
manager.add_vector_to_namespace("doc_1", "ns_a")
|
||||
vecs_a = manager.get_namespace_vectors("ns_a")
|
||||
if len(vecs_a) == 0:
|
||||
raise ValueError("Namespace manager failed to retrieve vectors")
|
||||
log("NamespaceManager: OK")
|
||||
|
||||
except Exception as e:
|
||||
log(f"Advanced_Vector_Store_and_Search.ipynb failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# clear log file
|
||||
with open("test_progress.log", "w") as f:
|
||||
f.write("Starting tests...\n")
|
||||
|
||||
# test_12_embedding_generation()
|
||||
test_12_embedding_generation()
|
||||
test_13_vector_store_basic()
|
||||
test_advanced_vector_store()
|
||||
@@ -0,0 +1,129 @@
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
class TestNotebooks(unittest.TestCase):
|
||||
|
||||
def test_12_embedding_generation(self):
|
||||
print("\nTesting 12_Embedding_Generation.ipynb logic...")
|
||||
try:
|
||||
from semantica.embeddings import EmbeddingGenerator, TextEmbedder
|
||||
|
||||
# Test EmbeddingGenerator
|
||||
generator = EmbeddingGenerator()
|
||||
texts = [
|
||||
"Apple Inc. is a technology company.",
|
||||
"Microsoft Corporation develops software.",
|
||||
"Amazon provides cloud services."
|
||||
]
|
||||
embeddings = generator.generate_embeddings(texts, data_type="text")
|
||||
|
||||
self.assertEqual(len(embeddings), 3)
|
||||
# Assuming default dimension is not 0
|
||||
self.assertTrue(len(embeddings[0]) > 0)
|
||||
print("EmbeddingGenerator: OK")
|
||||
|
||||
# Test TextEmbedder
|
||||
text_embedder = TextEmbedder()
|
||||
text = "Semantic knowledge graphs enable intelligent data processing."
|
||||
embedding = text_embedder.embed_text(text)
|
||||
|
||||
self.assertTrue(len(embedding) > 0)
|
||||
print("TextEmbedder: OK")
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f"12_Embedding_Generation.ipynb failed: {e}")
|
||||
|
||||
def test_13_vector_store_basic(self):
|
||||
print("\nTesting 13_Vector_Store.ipynb logic...")
|
||||
try:
|
||||
from semantica.vector_store import VectorStore
|
||||
|
||||
# Initialize
|
||||
store = VectorStore(backend="faiss", dimension=768)
|
||||
|
||||
# Store vectors
|
||||
vectors = [np.random.rand(768).astype('float32') for _ in range(10)]
|
||||
metadata = [{"id": i, "text": f"doc_{i}"} for i in range(10)]
|
||||
|
||||
vector_ids = store.store_vectors(vectors, metadata=metadata)
|
||||
self.assertEqual(len(vector_ids), 10)
|
||||
|
||||
# Search
|
||||
query = np.random.rand(768).astype('float32')
|
||||
results = store.search_vectors(query, k=5)
|
||||
self.assertEqual(len(results), 5)
|
||||
print("VectorStore Basic: OK")
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f"13_Vector_Store.ipynb failed: {e}")
|
||||
|
||||
def test_advanced_vector_store(self):
|
||||
print("\nTesting Advanced_Vector_Store_and_Search.ipynb logic...")
|
||||
try:
|
||||
from semantica.vector_store import FAISSAdapter, HybridSearch, MetadataFilter, SearchRanker, NamespaceManager
|
||||
|
||||
# Part 1: FAISSAdapter
|
||||
adapter = FAISSAdapter(dimension=768)
|
||||
index = adapter.create_index(index_type="hnsw", metric="L2", m=16)
|
||||
vectors = np.random.rand(100, 768).astype('float32')
|
||||
ids = [f"doc_{i}" for i in range(len(vectors))]
|
||||
adapter.add_vectors(index, vectors, ids=ids)
|
||||
|
||||
query = np.random.rand(768).astype('float32')
|
||||
distances, indices = adapter.search(index, query, k=5)
|
||||
self.assertEqual(len(indices), 5)
|
||||
print("FAISSAdapter: OK")
|
||||
|
||||
# Part 2: Hybrid Search
|
||||
search = HybridSearch()
|
||||
# Mock data for hybrid search
|
||||
docs = [
|
||||
{"id": 0, "category": "Tech", "year": 2024},
|
||||
{"id": 1, "category": "Tech", "year": 2023},
|
||||
{"id": 2, "category": "Biz", "year": 2024}
|
||||
]
|
||||
vecs = [np.random.rand(768).astype('float32') for _ in docs]
|
||||
meta = [{"category": d["category"], "year": d["year"]} for d in docs]
|
||||
v_ids = [f"doc_{d['id']}" for d in docs]
|
||||
|
||||
# Filter
|
||||
filt = MetadataFilter().eq("category", "Tech").eq("year", 2024)
|
||||
# Note: search signature might vary, adapting to notebook usage
|
||||
# search.search(query, vectors, metadata, vector_ids, filter=filter1, k=10)
|
||||
results = search.search(query, vecs, meta, v_ids, filter=filt, k=10)
|
||||
# Should find doc 0
|
||||
found_ids = [r['id'] for r in results]
|
||||
self.assertIn("doc_0", found_ids)
|
||||
print("HybridSearch: OK")
|
||||
|
||||
# Part 3: SearchRanker
|
||||
ranker = SearchRanker(strategy="reciprocal_rank_fusion")
|
||||
res1 = [{"id": "doc_1", "score": 0.9}, {"id": "doc_2", "score": 0.8}]
|
||||
res2 = [{"id": "doc_2", "score": 0.85}, {"id": "doc_3", "score": 0.7}]
|
||||
combined = ranker.rank([res1, res2])
|
||||
self.assertTrue(len(combined) > 0)
|
||||
print("SearchRanker: OK")
|
||||
|
||||
# Part 4: NamespaceManager
|
||||
manager = NamespaceManager()
|
||||
ns_a = manager.create_namespace("ns_a", "Namespace A")
|
||||
manager.add_vector_to_namespace("doc_1", "ns_a")
|
||||
vecs_a = manager.get_namespace_vectors("ns_a")
|
||||
# Note: add_vector_to_namespace might need actual vector storage or just ID tracking depending on implementation
|
||||
# Notebook says: manager.add_vector_to_namespace(f"company_a_doc_{i}", "company_a")
|
||||
# And then: a_docs = manager.get_namespace_vectors("company_a")
|
||||
# Checking if it returns the list of IDs or vectors.
|
||||
# Assuming it tracks IDs based on notebook context.
|
||||
self.assertTrue(len(vecs_a) > 0)
|
||||
print("NamespaceManager: OK")
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f"Advanced_Vector_Store_and_Search.ipynb failed: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user