Skip to content

Commit d9f09e7

Browse files
authored
Merge pull request #1199 from awais786/api-for-metadata
Api for metadata
2 parents 978a134 + 8d7fb4d commit d9f09e7

4 files changed

Lines changed: 168 additions & 1 deletion

File tree

meilisearch/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class Paths:
4545
prefix_search = "prefix-search"
4646
proximity_precision = "proximity-precision"
4747
localized_attributes = "localized-attributes"
48+
fields = "fields"
4849
edit = "edit"
4950
network = "network"
5051
experimental_features = "experimental-features"

meilisearch/index.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from meilisearch._utils import iso_to_date_time
2424
from meilisearch.config import Config
2525
from meilisearch.errors import version_error_hint_message
26-
from meilisearch.models.document import Document, DocumentsResults
26+
from meilisearch.models.document import Document, DocumentsResults, FieldsResults
2727
from meilisearch.models.embedders import (
2828
CompositeEmbedder,
2929
Embedders,
@@ -2555,6 +2555,66 @@ def reset_localized_attributes(self) -> TaskInfo:
25552555

25562556
return TaskInfo(**task)
25572557

2558+
def get_fields(
2559+
self,
2560+
offset: Optional[int] = None,
2561+
limit: Optional[int] = None,
2562+
filter: Optional[MutableMapping[str, Any]] = None, # pylint: disable=redefined-builtin
2563+
) -> FieldsResults:
2564+
"""Get all fields of the index.
2565+
2566+
Returns detailed metadata about all fields in the index, including
2567+
display, search, filtering, and localization settings for each field.
2568+
2569+
https://www.meilisearch.com/docs/reference/api/indexes#get-fields
2570+
2571+
Parameters
2572+
----------
2573+
offset (optional):
2574+
Number of fields to skip. Defaults to 0.
2575+
limit (optional):
2576+
Maximum number of fields to return. Defaults to 20.
2577+
filter (optional):
2578+
Dictionary containing filter configuration. All filter properties are optional
2579+
and can be combined using AND logic. Available filters:
2580+
- attributePatterns: List of attribute patterns (supports wildcards: * for any characters)
2581+
Examples: ["cuisine.*", "*_id"] matches cuisine.type and all fields ending with _id
2582+
- displayed: Boolean - true for only displayed fields, false for only hidden fields
2583+
- searchable: Boolean - true for only searchable fields, false for only non-searchable fields
2584+
- sortable: Boolean - true for only sortable fields, false for only non-sortable fields
2585+
- distinct: Boolean - true for only the distinct field, false for only non-distinct fields
2586+
- rankingRule: Boolean - true for only fields used in ranking, false for fields not used in ranking
2587+
- filterable: Boolean - true for only filterable fields, false for only non-filterable fields
2588+
2589+
Returns
2590+
-------
2591+
FieldsResults:
2592+
Object containing:
2593+
- results: List of field metadata dictionaries
2594+
- offset: Number of fields skipped
2595+
- limit: Maximum fields returned
2596+
- total: Total number of fields in the index
2597+
2598+
Raises
2599+
------
2600+
MeilisearchApiError
2601+
An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors
2602+
"""
2603+
body: Dict[str, Any] = {}
2604+
if offset is not None:
2605+
body["offset"] = offset
2606+
if limit is not None:
2607+
body["limit"] = limit
2608+
if filter is not None:
2609+
body["filter"] = filter
2610+
2611+
response = self.http.post(
2612+
f"{self.config.paths.index}/{self.uid}/{self.config.paths.fields}",
2613+
body=body,
2614+
)
2615+
2616+
return FieldsResults(response)
2617+
25582618
@staticmethod
25592619
def _batch(
25602620
documents: Sequence[Mapping[str, Any]], batch_size: int

meilisearch/models/document.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ def __init__(self, resp: Dict[str, Any]) -> None:
2020
self.offset: int = resp["offset"]
2121
self.limit: int = resp["limit"]
2222
self.total: int = resp["total"]
23+
24+
25+
class FieldsResults:
26+
"""Response object for get_fields containing pagination metadata and field list."""
27+
28+
def __init__(self, resp: Dict[str, Any]) -> None:
29+
self.results: List[Dict[str, Any]] = resp["results"]
30+
self.offset: int = resp["offset"]
31+
self.limit: int = resp["limit"]
32+
self.total: int = resp["total"]

tests/index/test_index.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,99 @@ def test_index_update_without_params(client):
283283
index.update()
284284

285285
assert "primary_key" in str(exc.value) or "new_uid" in str(exc.value)
286+
287+
288+
@pytest.mark.usefixtures("indexes_sample")
289+
def test_get_fields(client, small_movies):
290+
"""Tests getting all fields of an index via the new /fields endpoint."""
291+
index = client.index(uid=common.INDEX_UID)
292+
task = index.add_documents(small_movies)
293+
client.wait_for_task(task.task_uid)
294+
295+
fields = index.get_fields()
296+
297+
assert hasattr(fields, "results")
298+
assert hasattr(fields, "offset")
299+
assert hasattr(fields, "limit")
300+
assert hasattr(fields, "total")
301+
assert isinstance(fields.results, list)
302+
assert len(fields.results) > 0
303+
assert "name" in fields.results[0]
304+
assert "searchable" in fields.results[0]
305+
assert "filterable" in fields.results[0]
306+
assert "sortable" in fields.results[0]
307+
308+
309+
@pytest.mark.usefixtures("indexes_sample")
310+
def test_get_fields_with_configurations(client, small_movies):
311+
"""Tests get_fields() reflects index settings configurations."""
312+
index = client.index(uid=common.INDEX_UID)
313+
task = index.add_documents(small_movies)
314+
client.wait_for_task(task.task_uid)
315+
316+
task = index.update_searchable_attributes(["title"])
317+
client.wait_for_task(task.task_uid)
318+
319+
fields = index.get_fields()
320+
title_field = next((f for f in fields.results if f["name"] == "title"), None)
321+
322+
assert title_field is not None
323+
assert title_field["searchable"]["enabled"] is True
324+
325+
326+
@pytest.mark.usefixtures("indexes_sample")
327+
def test_get_fields_with_filter(client, small_movies):
328+
"""Tests get_fields() with filter parameters."""
329+
index = client.index(uid=common.INDEX_UID)
330+
task = index.add_documents(small_movies)
331+
client.wait_for_task(task.task_uid)
332+
333+
task = index.update_searchable_attributes(["title"])
334+
client.wait_for_task(task.task_uid)
335+
336+
# Filter only searchable fields
337+
searchable_fields = index.get_fields(filter={"searchable": True})
338+
339+
assert isinstance(searchable_fields.results, list)
340+
assert len(searchable_fields.results) > 0
341+
assert all(field["searchable"]["enabled"] is True for field in searchable_fields.results)
342+
343+
344+
@pytest.mark.usefixtures("indexes_sample")
345+
def test_get_fields_with_pagination(client, small_movies):
346+
"""Tests get_fields() with pagination parameters."""
347+
index = client.index(uid=common.INDEX_UID)
348+
task = index.add_documents(small_movies)
349+
client.wait_for_task(task.task_uid)
350+
351+
# Get all fields first to know total count
352+
all_fields = index.get_fields()
353+
total_fields = all_fields.total
354+
355+
# Test pagination with offset and limit
356+
page1 = index.get_fields(offset=0, limit=2)
357+
assert isinstance(page1.results, list)
358+
assert len(page1.results) <= 2
359+
assert page1.offset == 0
360+
assert page1.limit == 2
361+
362+
# If we have more than 2 fields, test second page
363+
if total_fields > 2:
364+
page2 = index.get_fields(offset=2, limit=2)
365+
assert isinstance(page2.results, list)
366+
assert len(page2.results) <= 2
367+
assert page2.offset == 2
368+
369+
# Verify pages don't overlap
370+
page1_names = {f["name"] for f in page1.results}
371+
page2_names = {f["name"] for f in page2.results}
372+
assert page1_names.isdisjoint(page2_names)
373+
374+
# Test with just limit (no offset)
375+
limited = index.get_fields(limit=3)
376+
assert isinstance(limited.results, list)
377+
assert len(limited.results) <= 3
378+
379+
# Test with just offset (no limit, uses default)
380+
offset_only = index.get_fields(offset=1)
381+
assert isinstance(offset_only.results, list)

0 commit comments

Comments
 (0)