Skip to content

Commit 224dd50

Browse files
tpayetclaude
andcommitted
Add hybrid search support to Meilisearch MCP server
- Update meilisearch dependency from >=0.33.0 to >=0.34.0 for stable AI-powered search - Add hybrid search parameters to search tool schema: - hybrid object with semanticRatio and embedder fields - vector parameter for custom vectors - retrieveVectors parameter to include vectors in results - Update client.py search method to pass new parameters to SDK - Add comprehensive tests for hybrid search functionality - Update README with hybrid search examples and documentation This enables users to leverage Meilisearch's semantic and hybrid search capabilities through the MCP interface, combining traditional keyword search with AI-powered semantic search for more relevant results. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 280d880 commit 224dd50

File tree

5 files changed

+227
-5
lines changed

5 files changed

+227
-5
lines changed

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,13 @@ Example usage through MCP:
133133

134134
### Search Functionality
135135

136-
The server provides a flexible search tool that can search across one or all indices:
136+
The server provides a flexible search tool that can search across one or all indices with support for hybrid search (combining keyword and semantic search):
137137

138138
- `search`: Search through Meilisearch indices with optional parameters
139139

140140
Example usage through MCP:
141141
```json
142-
// Search in a specific index
142+
// Traditional keyword search in a specific index
143143
{
144144
"name": "search",
145145
"arguments": {
@@ -149,6 +149,31 @@ Example usage through MCP:
149149
}
150150
}
151151

152+
// Hybrid search combining keyword and semantic search
153+
{
154+
"name": "search",
155+
"arguments": {
156+
"query": "artificial intelligence",
157+
"indexUid": "documents",
158+
"hybrid": {
159+
"semanticRatio": 0.7,
160+
"embedder": "default"
161+
},
162+
"limit": 20
163+
}
164+
}
165+
166+
// Semantic search with custom vector
167+
{
168+
"name": "search",
169+
"arguments": {
170+
"query": "machine learning",
171+
"indexUid": "articles",
172+
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
173+
"retrieveVectors": true
174+
}
175+
}
176+
152177
// Search across all indices
153178
{
154179
"name": "search",
@@ -167,6 +192,13 @@ Available search parameters:
167192
- `offset`: Number of results to skip (optional, default: 0)
168193
- `filter`: Filter expression (optional)
169194
- `sort`: Sorting rules (optional)
195+
- `hybrid`: Hybrid search configuration (optional)
196+
- `semanticRatio`: Balance between keyword (0.0) and semantic (1.0) search (optional, default: 0.5)
197+
- `embedder`: Name of the configured embedder to use (required when using hybrid)
198+
- `vector`: Custom vector for semantic search (optional)
199+
- `retrieveVectors`: Include vector data in search results (optional)
200+
201+
**Note**: To use hybrid search features, you need to have an embedder configured in your Meilisearch index settings. Refer to the [Meilisearch documentation on vector search](https://www.meilisearch.com/docs/learn/vector_search/vector_search_basics) for configuration details.
170202

171203
### Running the Server
172204

@@ -210,7 +242,7 @@ npx @modelcontextprotocol/inspector python -m src.meilisearch_mcp
210242
- `add-documents`: Add or update documents in an index
211243

212244
### Search
213-
- `search`: Flexible search across single or multiple indices with filtering and sorting options
245+
- `search`: Flexible search across single or multiple indices with support for hybrid search (keyword + semantic), custom vectors, and filtering/sorting options
214246

215247
### Settings Management
216248
- `get-settings`: View current settings for an index

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.5.0"
44
description = "MCP server for Meilisearch"
55
requires-python = ">=3.10"
66
dependencies = [
7-
"meilisearch>=0.33.0",
7+
"meilisearch>=0.34.0",
88
"mcp>=0.1.0",
99
"httpx>=0.24.0",
1010
"pydantic>=2.0.0"

src/meilisearch_mcp/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def search(
5252
offset: Optional[int] = 0,
5353
filter: Optional[str] = None,
5454
sort: Optional[List[str]] = None,
55+
hybrid: Optional[Dict[str, Any]] = None,
56+
vector: Optional[List[float]] = None,
57+
retrieve_vectors: Optional[bool] = None,
5558
**kwargs,
5659
) -> Dict[str, Any]:
5760
"""
@@ -70,6 +73,12 @@ def search(
7073
search_params["filter"] = filter
7174
if sort is not None:
7275
search_params["sort"] = sort
76+
if hybrid is not None:
77+
search_params["hybrid"] = hybrid
78+
if vector is not None:
79+
search_params["vector"] = vector
80+
if retrieve_vectors is not None:
81+
search_params["retrieveVectors"] = retrieve_vectors
7382

7483
# Add any additional parameters
7584
search_params.update({k: v for k, v in kwargs.items() if v is not None})

src/meilisearch_mcp/server.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async def handle_list_tools() -> list[types.Tool]:
211211
),
212212
types.Tool(
213213
name="search",
214-
description="Search through Meilisearch indices. If indexUid is not provided, it will search across all indices.",
214+
description="Search through Meilisearch indices with support for hybrid search (combining keyword and semantic search). If indexUid is not provided, it will search across all indices.",
215215
inputSchema={
216216
"type": "object",
217217
"properties": {
@@ -224,6 +224,32 @@ async def handle_list_tools() -> list[types.Tool]:
224224
"type": "array",
225225
"items": {"type": "string"},
226226
},
227+
"hybrid": {
228+
"type": "object",
229+
"properties": {
230+
"semanticRatio": {
231+
"type": "number",
232+
"minimum": 0.0,
233+
"maximum": 1.0,
234+
"description": "Balance between keyword (0.0) and semantic (1.0) search",
235+
},
236+
"embedder": {
237+
"type": "string",
238+
"description": "Name of the configured embedder to use",
239+
},
240+
},
241+
"required": ["embedder"],
242+
"additionalProperties": False,
243+
},
244+
"vector": {
245+
"type": "array",
246+
"items": {"type": "number"},
247+
"description": "Custom vector for semantic search",
248+
},
249+
"retrieveVectors": {
250+
"type": "boolean",
251+
"description": "Include vector data in search results",
252+
},
227253
},
228254
"required": ["query"],
229255
"additionalProperties": False,
@@ -498,6 +524,9 @@ async def handle_call_tool(
498524
offset=arguments.get("offset"),
499525
filter=arguments.get("filter"),
500526
sort=arguments.get("sort"),
527+
hybrid=arguments.get("hybrid"),
528+
vector=arguments.get("vector"),
529+
retrieve_vectors=arguments.get("retrieveVectors"),
501530
)
502531

503532
# Format the results for better readability

tests/test_mcp_client.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,158 @@ async def test_delete_index_integration_workflow(self, mcp_server):
612612
assert "Error:" in search_after_text
613613

614614

615+
class TestHybridSearch:
616+
"""Test hybrid search functionality with semantic search support"""
617+
618+
async def test_search_tool_has_hybrid_parameters(self, mcp_server):
619+
"""Test that search tool schema includes hybrid search parameters"""
620+
tools = await simulate_list_tools(mcp_server)
621+
622+
# Find the search tool
623+
search_tool = next((t for t in tools if t.name == "search"), None)
624+
assert search_tool is not None, "Search tool not found"
625+
626+
# Check that hybrid parameters are in the schema
627+
properties = search_tool.inputSchema["properties"]
628+
629+
# Check hybrid object parameter
630+
assert "hybrid" in properties
631+
hybrid_schema = properties["hybrid"]
632+
assert hybrid_schema["type"] == "object"
633+
assert "semanticRatio" in hybrid_schema["properties"]
634+
assert "embedder" in hybrid_schema["properties"]
635+
assert hybrid_schema["properties"]["semanticRatio"]["type"] == "number"
636+
assert hybrid_schema["properties"]["semanticRatio"]["minimum"] == 0.0
637+
assert hybrid_schema["properties"]["semanticRatio"]["maximum"] == 1.0
638+
assert hybrid_schema["properties"]["embedder"]["type"] == "string"
639+
assert hybrid_schema["required"] == ["embedder"]
640+
641+
# Check vector parameter
642+
assert "vector" in properties
643+
vector_schema = properties["vector"]
644+
assert vector_schema["type"] == "array"
645+
assert vector_schema["items"]["type"] == "number"
646+
647+
# Check retrieveVectors parameter
648+
assert "retrieveVectors" in properties
649+
assert properties["retrieveVectors"]["type"] == "boolean"
650+
651+
async def test_search_with_hybrid_parameters(self, mcp_server):
652+
"""Test that search accepts and processes hybrid search parameters"""
653+
# Create test index
654+
index_name = generate_unique_index_name("hybrid_search")
655+
await simulate_mcp_call(
656+
mcp_server, "create-index", {"uid": index_name, "primaryKey": "id"}
657+
)
658+
659+
# Add test documents
660+
documents = [
661+
{
662+
"id": 1,
663+
"title": "Python Programming",
664+
"description": "Learn Python basics",
665+
},
666+
{"id": 2, "title": "JavaScript Guide", "description": "Modern JS features"},
667+
{"id": 3, "title": "Machine Learning", "description": "AI and ML concepts"},
668+
]
669+
await simulate_mcp_call(
670+
mcp_server,
671+
"add-documents",
672+
{"indexUid": index_name, "documents": documents},
673+
)
674+
await wait_for_indexing()
675+
676+
# Note: This test simulates the API call structure but won't actually
677+
# perform semantic search without a configured embedder in Meilisearch
678+
679+
# Test search with hybrid parameters
680+
search_params = {
681+
"query": "programming",
682+
"indexUid": index_name,
683+
"hybrid": {"semanticRatio": 0.7, "embedder": "default"},
684+
"limit": 5,
685+
}
686+
687+
# The search should accept these parameters without error
688+
response = await simulate_mcp_call(mcp_server, "search", search_params)
689+
response_text = assert_text_content_response(response, "Search results")
690+
691+
# Even if embedder is not configured, the API should handle the request
692+
assert "Search results for 'programming'" in response_text
693+
694+
# Cleanup
695+
await simulate_mcp_call(mcp_server, "delete-index", {"uid": index_name})
696+
697+
async def test_search_with_vector_parameter(self, mcp_server):
698+
"""Test that search accepts vector parameter"""
699+
# Create test index
700+
index_name = generate_unique_index_name("vector_search")
701+
await simulate_mcp_call(
702+
mcp_server, "create-index", {"uid": index_name, "primaryKey": "id"}
703+
)
704+
705+
# Add test documents
706+
documents = [{"id": 1, "content": "Test document"}]
707+
await simulate_mcp_call(
708+
mcp_server,
709+
"add-documents",
710+
{"indexUid": index_name, "documents": documents},
711+
)
712+
await wait_for_indexing()
713+
714+
# Test search with vector parameter
715+
search_params = {
716+
"query": "test",
717+
"indexUid": index_name,
718+
"vector": [0.1, 0.2, 0.3, 0.4, 0.5],
719+
"retrieveVectors": True,
720+
}
721+
722+
# The search should accept these parameters without error
723+
response = await simulate_mcp_call(mcp_server, "search", search_params)
724+
response_text = assert_text_content_response(response, "Search results")
725+
assert "Search results for 'test'" in response_text
726+
727+
# Cleanup
728+
await simulate_mcp_call(mcp_server, "delete-index", {"uid": index_name})
729+
730+
async def test_search_semantic_only(self, mcp_server):
731+
"""Test semantic-only search with semanticRatio=1.0"""
732+
# Create test index
733+
index_name = generate_unique_index_name("semantic_only")
734+
await simulate_mcp_call(
735+
mcp_server, "create-index", {"uid": index_name, "primaryKey": "id"}
736+
)
737+
738+
# Add test documents
739+
documents = [
740+
{"id": 1, "content": "Artificial intelligence and machine learning"}
741+
]
742+
await simulate_mcp_call(
743+
mcp_server,
744+
"add-documents",
745+
{"indexUid": index_name, "documents": documents},
746+
)
747+
await wait_for_indexing()
748+
749+
# Test semantic-only search
750+
search_params = {
751+
"query": "AI ML",
752+
"indexUid": index_name,
753+
"hybrid": {
754+
"semanticRatio": 1.0, # Pure semantic search
755+
"embedder": "default",
756+
},
757+
}
758+
759+
response = await simulate_mcp_call(mcp_server, "search", search_params)
760+
response_text = assert_text_content_response(response, "Search results")
761+
assert "Search results for 'AI ML'" in response_text
762+
763+
# Cleanup
764+
await simulate_mcp_call(mcp_server, "delete-index", {"uid": index_name})
765+
766+
615767
class TestIssue27OpenAISchemaCompatibility:
616768
"""Test for issue #27 - Fix JSON schemas for OpenAI Agent SDK compatibility"""
617769

0 commit comments

Comments
 (0)