Skip to content

Changes related to the next Meilisearch release (v1.14.0) #1914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,14 @@ get_filterable_attributes_1: |-
update_filterable_attributes_1: |-
client.index('movies')
.updateFilterableAttributes([
'genres',
'director'
"genres",
{
attributePatterns: ["genre"],
features: {
facetSearch: true,
filter: { equality: true, comparison: false },
},
}
])
reset_filterable_attributes_1: |-
client.index('movies').resetFilterableAttributes()
Expand Down
4 changes: 2 additions & 2 deletions src/indexes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,8 +782,8 @@ export class Index<T extends RecordAny = RecordAny> {
*
* @returns Promise containing an array of filterable-attributes
*/
async getFilterableAttributes(): Promise<string[]> {
return await this.httpRequest.get<string[]>({
async getFilterableAttributes(): Promise<FilterableAttributes> {
return await this.httpRequest.get<FilterableAttributes>({
path: `indexes/${this.uid}/settings/filterable-attributes`,
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/types/task_and_batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ type BatchStats = {
status: Record<TaskStatus, number>;
types: Record<TaskType, number>;
indexUids: Record<string, number>;
progressTrace: Record<string, number>;
// Do not type this field because the keys can change in a breaking way
internalDatabaseSizes?: Record<string, number>;
};

/**
Expand Down
26 changes: 25 additions & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ export type Crop = {
// `facetName` becomes mandatory when using `searchForFacetValues`
export type SearchForFacetValuesParams = Omit<SearchParams, "facetName"> & {
facetName: string;
/**
* If true, the facet search will return the exhaustive count of the facet
* values.
*/
exhaustiveFacetCount?: boolean;
};

export type FacetHit = {
Expand Down Expand Up @@ -469,6 +474,7 @@ export type RawDocumentAdditionOptions = DocumentOptions & {
};

export type DocumentsQuery<T = RecordAny> = ResourceQuery & {
ids?: string[] | number[];
fields?: Fields<T>;
filter?: Filter;
limit?: number;
Expand Down Expand Up @@ -496,7 +502,17 @@ export type UpdateDocumentsByFunctionOptions = {
** Settings
*/

export type FilterableAttributes = string[] | null;
type GranularFilterableAttribute = {
attributePatterns: string[];
features: {
facetSearch: boolean;
filter: { equality: boolean; comparison: boolean };
};
};

export type FilterableAttributes =
| (string | GranularFilterableAttribute)[]
| null;
export type DistinctAttribute = string | null;
export type SearchableAttributes = string[] | null;
export type SortableAttributes = string[] | null;
Expand Down Expand Up @@ -541,6 +557,7 @@ export type HuggingFaceEmbedder = {
revision?: string;
documentTemplate?: string;
distribution?: Distribution;
pooling?: "useModel" | "forceMean" | "forceCls";
documentTemplateMaxBytes?: number;
binaryQuantized?: boolean;
};
Expand Down Expand Up @@ -578,12 +595,19 @@ export type OllamaEmbedder = {
binaryQuantized?: boolean;
};

export type CompositeEmbedder = {
source: "composite";
searchEmbedder: Embedder;
indexingEmbedder: Embedder;
};

export type Embedder =
| OpenAiEmbedder
| HuggingFaceEmbedder
| UserProvidedEmbedder
| RestEmbedder
| OllamaEmbedder
| CompositeEmbedder
| null;

export type Embedders = Record<string, Embedder> | null;
Expand Down
39 changes: 39 additions & 0 deletions tests/__snapshots__/facet_search.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ exports[`Test on POST search > Admin key: basic facet value search 1`] = `
}
`;

exports[`Test on POST search > Admin key: facet value search with exhaustive facet count 1`] = `
{
"facetHits": [
{
"count": 1,
"value": "adventure",
},
],
"facetQuery": "a",
"processingTimeMs": 0,
}
`;

exports[`Test on POST search > Admin key: facet value search with filter 1`] = `
{
"facetHits": [
Expand Down Expand Up @@ -85,6 +98,19 @@ exports[`Test on POST search > Master key: basic facet value search 1`] = `
}
`;

exports[`Test on POST search > Master key: facet value search with exhaustive facet count 1`] = `
{
"facetHits": [
{
"count": 1,
"value": "adventure",
},
],
"facetQuery": "a",
"processingTimeMs": 0,
}
`;

exports[`Test on POST search > Master key: facet value search with filter 1`] = `
{
"facetHits": [
Expand Down Expand Up @@ -153,6 +179,19 @@ exports[`Test on POST search > Search key: basic facet value search 1`] = `
}
`;

exports[`Test on POST search > Search key: facet value search with exhaustive facet count 1`] = `
{
"facetHits": [
{
"count": 1,
"value": "adventure",
},
],
"facetQuery": "a",
"processingTimeMs": 0,
}
`;

exports[`Test on POST search > Search key: facet value search with filter 1`] = `
{
"facetHits": [
Expand Down
2 changes: 2 additions & 0 deletions tests/__snapshots__/settings.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ exports[`Test on settings > Admin key: Update embedders settings 1`] = `
{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
"pooling": "useModel",
"source": "huggingFace",
},
},
Expand Down Expand Up @@ -958,6 +959,7 @@ exports[`Test on settings > Master key: Update embedders settings 1`] = `
{% endif %}{% endfor %}",
"documentTemplateMaxBytes": 400,
"model": "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
"pooling": "useModel",
"source": "huggingFace",
},
},
Expand Down
3 changes: 3 additions & 0 deletions tests/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])(
const batches = await client.batches.getBatches();
const batch = await client.batches.getBatch(batches.results[0].uid);
expect(batch.uid).toEqual(batches.results[0].uid);

// Can't use toMatchSnapshot because the output changes every time
expect(batch.details).toBeDefined();
expect(batch.stats).toHaveProperty("totalNbTasks");
expect(batch.stats).toHaveProperty("status");
expect(batch.stats).toHaveProperty("types");
expect(batch.stats).toHaveProperty("indexUids");
expect(batch.stats).toHaveProperty("progressTrace");
expect(batch.duration).toBeDefined();
expect(batch.startedAt).toBeDefined();
expect(batch.finishedAt).toBeDefined();
Expand Down
12 changes: 12 additions & 0 deletions tests/documents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ describe("Documents tests", () => {
expect(document.id).toBeUndefined();
});

test(`${permission} key: Get multiple documents by IDs`, async () => {
const client = await getClient(permission);
await client.index(indexPk.uid).addDocuments(dataset).waitTask();

const documents = await client
.index(indexPk.uid)
.getDocuments<Book>({ ids: [1, 2] });

expect(documents.results.length).toEqual(2);
expect(documents.results.map(({ id }) => id).sort()).toEqual([1, 2]);
});

test(`${permission} key: Get documents with string fields`, async () => {
const client = await getClient(permission);

Expand Down
48 changes: 48 additions & 0 deletions tests/embedders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
BAD_HOST,
MeiliSearch,
getClient,
getKey,
HOST,
} from "./utils/meilisearch-test-utils.js";

const index = {
Expand Down Expand Up @@ -130,6 +132,7 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])(
mean: 0.7,
sigma: 0.3,
},
pooling: "useModel",
documentTemplateMaxBytes: 500,
binaryQuantized: false,
},
Expand Down Expand Up @@ -235,6 +238,51 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])(
expect(response).toEqual(newEmbedder);
});

test(`${permission} key: Update embedders with composite embedder`, async () => {
const adminKey = await getKey("Admin");

// first enable the network endpoint.
await fetch(`${HOST}/experimental-features`, {
body: JSON.stringify({ compositeEmbedders: true }),
headers: {
Authorization: `Bearer ${adminKey}`,
"Content-Type": "application/json",
},
method: "PATCH",
});

const client = await getClient(permission);
const embedders = {
default: {
source: "composite",
searchEmbedder: {
source: "huggingFace",
model:
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
pooling: "useModel",
},
indexingEmbedder: {
source: "huggingFace",
model:
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
documentTemplate: "{{doc.title}}",
pooling: "useModel",
documentTemplateMaxBytes: 500,
},
},
} satisfies Embedders;

const task = await client
.index(index.uid)
.updateEmbedders(embedders)
.waitTask();
const response: Embedders = await client.index(index.uid).getEmbedders();

const processedTask = await client.tasks.getTask(task.uid);
expect(processedTask.status).toEqual("succeeded");
expect(response).toEqual(embedders);
});

test(`${permission} key: Reset embedders`, async () => {
const client = await getClient(permission);
await client.index(index.uid).resetEmbedders().waitTask();
Expand Down
15 changes: 15 additions & 0 deletions tests/facet_search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ describe.each([
// @TODO: This is flaky, processingTimeMs is not guaranteed
expect(response).toMatchSnapshot();
});

test(`${permission} key: facet value search with exhaustive facet count`, async () => {
const client = await getClient(permission);

const params = {
facetName: "genres",
facetQuery: "a",
q: "Alice",
exhaustiveFacetCount: true,
};
const response = await client.index(index.uid).searchForFacetValues(params);

// @TODO: This is flaky, processingTimeMs is not guaranteed
expect(response).toMatchSnapshot();
});
});

afterAll(() => {
Expand Down
21 changes: 21 additions & 0 deletions tests/filterable_attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ describe.each([{ permission: "Master" }, { permission: "Admin" }])(
expect(response?.sort()).toEqual([]);
});

test(`${permission} key: Update attributes with granular attribute syntax`, async () => {
const client = await getClient(permission);
const newFilterableAttributes = [
"author",
{
attributePatterns: ["genre"],
features: {
facetSearch: true,
filter: { equality: true, comparison: false },
},
},
];
await client
.index(index.uid)
.updateFilterableAttributes(newFilterableAttributes)
.waitTask();

const response = await client.index(index.uid).getFilterableAttributes();
expect(response).toEqual(newFilterableAttributes);
});

test(`${permission} key: Reset attributes for filtering`, async () => {
const client = await getClient(permission);
await client.index(index.uid).resetFilterableAttributes().waitTask();
Expand Down
Loading