Skip to content

Commit c72f12d

Browse files
committed
feat: Adding support Multimodal embedders.
1 parent 50a5689 commit c72f12d

File tree

2 files changed

+218
-41
lines changed

2 files changed

+218
-41
lines changed
Lines changed: 179 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,193 @@
1-
"""Tests for experimental features API."""
1+
# pylint: disable=redefined-outer-name
2+
"""Tests for indexingFragments and searchFragments in embedders (multimodal feature).
23
4+
These tests validate CONFIGURATION ONLY, not AI functionality.
5+
They only ensure fragments can be configured and stored in Meilisearch.
6+
No AI calls or document indexing/searching occurs.
7+
"""
38

4-
def test_get_experimental_features(client):
5-
"""Test getting experimental features."""
6-
response = client.get_experimental_features()
7-
assert isinstance(response, dict)
8-
assert "multimodal" in response or "vectorStoreSetting" in response
9+
import pytest
910

11+
DUMMY_URL = "http://localhost:8000/embed"
12+
TEST_MODEL = "test-model"
13+
MULTIMODAL_MODEL = "multimodal"
1014

11-
def test_update_experimental_features(client):
12-
"""Test updating experimental features."""
13-
# Enable multimodal
14-
response = client.update_experimental_features({"multimodal": True})
15-
assert isinstance(response, dict)
16-
assert response.get("multimodal") is True
1715

18-
# Disable multimodal
19-
response = client.update_experimental_features({"multimodal": False})
20-
assert isinstance(response, dict)
21-
assert response.get("multimodal") is False
16+
def apply_embedders(index, config):
17+
"""Helper to update embedders and wait for task completion."""
18+
response = index.update_embedders(config)
19+
update = index.wait_for_task(response.task_uid)
20+
assert update.status == "succeeded"
21+
return index.get_embedders()
2222

2323

24-
def test_enable_multimodal(client):
25-
"""Test enabling multimodal experimental feature."""
26-
response = client.enable_multimodal()
27-
assert isinstance(response, dict)
28-
assert response.get("multimodal") is True
24+
def test_rest_embedder_with_fragments(empty_index, multimodal_enabled):
25+
"""Tests that REST embedder can be configured with indexingFragments and searchFragments."""
26+
index = empty_index()
2927

30-
# Verify it's enabled
31-
features = client.get_experimental_features()
32-
assert features.get("multimodal") is True
28+
config = {
29+
"rest_fragments": {
30+
"source": "rest",
31+
"url": DUMMY_URL,
32+
"apiKey": "test-key",
33+
"dimensions": 512,
34+
"indexingFragments": {"text": {"value": "{{doc.title}} - {{doc.description}}"}},
35+
"searchFragments": {"text": {"value": "{{fragment}}"}},
36+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
37+
"response": {"data": [{"embedding": "{{embedding}}"}]},
38+
"headers": {"Authorization": "Bearer test-key"},
39+
}
40+
}
3341

42+
embedders = apply_embedders(index, config)
3443

35-
def test_disable_multimodal(client):
36-
"""Test disabling multimodal experimental feature."""
37-
# First enable it
38-
client.enable_multimodal()
44+
e = embedders.embedders["rest_fragments"]
45+
assert e.source == "rest"
46+
assert e.url == DUMMY_URL
47+
assert e.dimensions == 512
48+
assert e.indexing_fragments is not None
49+
assert e.search_fragments is not None
3950

40-
# Then disable it
41-
response = client.disable_multimodal()
42-
assert isinstance(response, dict)
43-
assert response.get("multimodal") is False
4451

45-
# Verify it's disabled
46-
features = client.get_experimental_features()
47-
assert features.get("multimodal") is False
52+
def test_rest_embedder_with_multiple_fragments(empty_index, multimodal_enabled):
53+
"""Tests REST embedder with multiple fragment types."""
54+
index = empty_index()
4855

56+
config = {
57+
"multi_fragments": {
58+
"source": "rest",
59+
"url": DUMMY_URL,
60+
"dimensions": 1024,
61+
"indexingFragments": {
62+
"text": {"value": "{{doc.title}}"},
63+
"description": {"value": "{{doc.overview}}"}
64+
},
65+
"searchFragments": {
66+
"text": {"value": "{{fragment}}"},
67+
"description": {"value": "{{fragment}}"}
68+
},
69+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
70+
"response": {"data": [{"embedding": "{{embedding}}"}]},
71+
}
72+
}
4973

50-
def test_update_multiple_experimental_features(client):
51-
"""Test updating multiple experimental features at once."""
52-
response = client.update_experimental_features({"multimodal": True, "vectorStoreSetting": True})
53-
assert isinstance(response, dict)
54-
# At least one should be accepted (depending on Meilisearch version)
55-
assert "multimodal" in response or "vectorStoreSetting" in response
74+
embedders = apply_embedders(index, config)
75+
76+
e = embedders.embedders["multi_fragments"]
77+
assert e.source == "rest"
78+
assert len(e.indexing_fragments) >= 1
79+
assert len(e.search_fragments) >= 1
80+
81+
82+
def test_fragments_without_document_template(empty_index, multimodal_enabled):
83+
"""Tests fragments can be used without documentTemplate."""
84+
index = empty_index()
85+
86+
config = {
87+
"fragments_only": {
88+
"source": "rest",
89+
"url": DUMMY_URL,
90+
"dimensions": 512,
91+
"indexingFragments": {"text": {"value": "{{doc.content}}"}},
92+
"searchFragments": {"text": {"value": "{{fragment}}"}},
93+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
94+
"response": {"data": [{"embedding": "{{embedding}}"}]},
95+
}
96+
}
97+
98+
embedders = apply_embedders(index, config)
99+
e = embedders.embedders["fragments_only"]
100+
assert e.document_template is None
101+
assert e.indexing_fragments is not None
102+
assert e.search_fragments is not None
103+
104+
105+
def test_fragments_require_multimodal_feature(empty_index):
106+
"""Tests fragments require multimodal feature enabled."""
107+
index = empty_index()
108+
109+
config = {
110+
"test": {
111+
"source": "rest",
112+
"url": DUMMY_URL,
113+
"dimensions": 512,
114+
"indexingFragments": {"text": {"value": "{{doc.title}}"}},
115+
"searchFragments": {"text": {"value": "{{fragment}}"}},
116+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
117+
"response": {"data": [{"embedding": "{{embedding}}"}]},
118+
}
119+
}
120+
121+
# May succeed or fail depending on server config; both are acceptable
122+
try:
123+
embedders = apply_embedders(index, config)
124+
assert embedders.embedders["test"].indexing_fragments is not None
125+
except Exception:
126+
pass
127+
128+
129+
def test_update_fragments_separately(empty_index, multimodal_enabled):
130+
"""Tests updating indexingFragments and searchFragments separately."""
131+
index = empty_index()
132+
133+
initial_config = {
134+
"updatable": {
135+
"source": "rest",
136+
"url": DUMMY_URL,
137+
"dimensions": 512,
138+
"indexingFragments": {"text": {"value": "{{doc.title}}"}},
139+
"searchFragments": {"text": {"value": "{{fragment}}"}},
140+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
141+
"response": {"data": [{"embedding": "{{embedding}}"}]},
142+
}
143+
}
144+
145+
apply_embedders(index, initial_config)
146+
147+
updated_config = {
148+
"updatable": {
149+
"source": "rest",
150+
"url": DUMMY_URL,
151+
"dimensions": 512,
152+
"indexingFragments": {"text": {"value": "{{doc.title}} - {{doc.description}}"}},
153+
"searchFragments": {"text": {"value": "{{fragment}}"}},
154+
"request": {"input": ["{{fragment}}"], "model": TEST_MODEL},
155+
"response": {"data": [{"embedding": "{{embedding}}"}]},
156+
}
157+
}
158+
159+
embedders = apply_embedders(index, updated_config)
160+
assert embedders.embedders["updatable"].indexing_fragments is not None
161+
162+
163+
def test_profile_picture_and_title_fragments(empty_index, multimodal_enabled):
164+
"""Tests real-world use case: user profiles with picture and title."""
165+
index = empty_index()
166+
167+
config = {
168+
"user_profile": {
169+
"source": "rest",
170+
"url": DUMMY_URL,
171+
"dimensions": 768,
172+
"indexingFragments": {
173+
"user_name": {"value": "{{doc.name}}"},
174+
"avatar": {"value": "{{doc.profile_picture_url}}"},
175+
"biography": {"value": "{{doc.bio}}"},
176+
},
177+
"searchFragments": {
178+
"user_name": {"value": "{{fragment}}"},
179+
"avatar": {"value": "{{fragment}}"},
180+
"biography": {"value": "{{fragment}}"},
181+
},
182+
"request": {"input": ["{{fragment}}"], "model": MULTIMODAL_MODEL},
183+
"response": {"data": [{"embedding": "{{embedding}}"}]},
184+
}
185+
}
186+
187+
embedders = apply_embedders(index, config)
188+
e = embedders.embedders["user_profile"]
189+
190+
assert e.source == "rest"
191+
expected_keys = {"user_name", "avatar", "biography"}
192+
assert set(e.indexing_fragments.keys()) == expected_keys
193+
assert set(e.search_fragments.keys()) == expected_keys

tests/conftest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,42 @@ def enable_network_options():
308308
json={"network": False},
309309
timeout=10,
310310
)
311+
312+
313+
@fixture
314+
def experimental_features():
315+
"""
316+
Fixture to temporarily set experimental features for a test.
317+
318+
Usage:
319+
def test_example(experimental_features):
320+
experimental_features({"multimodal": True, "new_ui": True})
321+
"""
322+
def _set_features(features: dict):
323+
# Enable features
324+
requests.patch(
325+
f"{common.BASE_URL}/experimental-features",
326+
headers={"Authorization": f"Bearer {common.MASTER_KEY}"},
327+
json=features,
328+
timeout=10,
329+
)
330+
# Return features so we can reset later
331+
return features
332+
333+
yield _set_features
334+
335+
# Reset features after the test
336+
def _reset(features: dict):
337+
# Create a reset payload inside the function
338+
reset_payload = {key: False for key in features.keys()}
339+
requests.patch(
340+
f"{common.BASE_URL}/experimental-features",
341+
headers={"Authorization": f"Bearer {common.MASTER_KEY}"},
342+
json=reset_payload,
343+
timeout=10,
344+
)
345+
346+
@fixture
347+
def multimodal_enabled(experimental_features):
348+
"""Convenience fixture: enables multimodal experimental feature."""
349+
experimental_features({"multimodal": True})

0 commit comments

Comments
 (0)