Skip to content

Commit 1dadb40

Browse files
meili-bors[bot]danFbachahmednfwelacurquiza
authored
Merge #592
592: Add Facet search support r=curquiza a=danFbach # Pull Request ## Related issue Fixes #461 ## What does this PR do? - implements facet search ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? Few notes: - Implemented the `FacetSearchAsync` method with a `facetName` parameter seperate from a `FacetSearchQuery` object parameter, similar to how `SearchAsync` separates out the `query` parameter from the `SearchQuery` object, since these parameters are **required**. - Not sure how we want to handle an empty or null `facetName`? This may change tests requirements. See commented out tests - Test `FacetSearchWithFilterFacetIsNull` is "wrong." Perhaps this is just how meilisearch works, but filtering by `genre IS NULL` returns nothing, but 2 entries in the `IndexForFaceting-SearchTests` have a `null` genre. Would like some feedback here - Any additional tests? Co-authored-by: Dan Fehrenbach <[email protected]> Co-authored-by: Ahmed Fwela <[email protected]> Co-authored-by: Clémentine <[email protected]>
2 parents 4e4ce29 + c88e22f commit 1dadb40

File tree

7 files changed

+395
-1
lines changed

7 files changed

+395
-1
lines changed

.code-samples.meilisearch.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,3 +795,16 @@ update_dictionary_1: |-
795795
await client.Index("books").UpdateDictionaryAsync(newDictionary);
796796
reset_dictionary_1: |-
797797
await client.Index("books").ResetDictionaryAsync();
798+
facet_search_1: |-
799+
var query = new SearchFacetsQuery()
800+
{
801+
FacetQuery = "fiction",
802+
Filter = "rating > 3"
803+
};
804+
await client.Index("books").FacetSearchAsync("genres", query);
805+
facet_search_3: |-
806+
var query = new SearchFacetsQuery()
807+
{
808+
FacetQuery = "c"
809+
};
810+
await client.Index("books").FacetSearchAsync("genres", query);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Meilisearch
5+
{
6+
/// <summary>
7+
/// Wrapper for facet search query
8+
/// </summary>
9+
public class FacetSearchQuery
10+
{
11+
/// <summary>
12+
/// Gets or sets the facetName property
13+
/// </summary>
14+
[JsonPropertyName("facetName")]
15+
public string FacetName { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets the facetQuery property
19+
/// </summary>
20+
[JsonPropertyName("facetQuery")]
21+
public string FacetQuery { get; set; }
22+
23+
/// <summary>
24+
/// Gets or sets the q property
25+
/// </summary>
26+
[JsonPropertyName("q")]
27+
public string Query { get; set; }
28+
29+
/// <summary>
30+
/// Gets or sets the filter property
31+
/// </summary>
32+
[JsonPropertyName("filter")]
33+
public dynamic Filter { get; set; }
34+
35+
/// <summary>
36+
/// Gets or sets the matchingStrategy property, can be <c>last</c>, <c>all</c> or <c>frequency</c>.
37+
/// </summary>
38+
[JsonPropertyName("matchingStrategy")]
39+
public string MatchingStrategy { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the attributesToSearchOn property
43+
/// </summary>
44+
[JsonPropertyName("attributesToSearchOn")]
45+
public IEnumerable<string> AttributesToSearchOn { get; set; }
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json.Serialization;
3+
4+
namespace Meilisearch
5+
{
6+
/// <summary>
7+
/// Wrapper for FacetSearchResponse
8+
/// </summary>
9+
public class FacetSearchResult
10+
{
11+
/// <summary>
12+
/// Gets or sets the facetHits property
13+
/// </summary>
14+
[JsonPropertyName("facetHits")]
15+
public IEnumerable<FacetHit> FacetHits { get; set; }
16+
17+
/// <summary>
18+
/// Gets or sets the facet query
19+
/// </summary>
20+
[JsonPropertyName("facetQuery")]
21+
public string FacetQuery { get; set; }
22+
23+
/// <summary>
24+
/// Gets or sets the processingTimeMs property
25+
/// </summary>
26+
[JsonPropertyName("processingTimeMs")]
27+
public int ProcessingTimeMs { get; set; }
28+
29+
/// <summary>
30+
/// Wrapper for Facet Hit
31+
/// </summary>
32+
public class FacetHit
33+
{
34+
/// <summary>
35+
/// Gets or sets the value property
36+
/// </summary>
37+
[JsonPropertyName("value")]
38+
public string Value { get; set; }
39+
40+
/// <summary>
41+
/// Gets or sets the count property
42+
/// </summary>
43+
[JsonPropertyName("count")]
44+
public int Count { get; set; }
45+
}
46+
}
47+
}

src/Meilisearch/Index.Documents.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.Collections.Generic;
32
using System.Linq;
43
using System.Net.Http;
@@ -543,5 +542,35 @@ public async Task<ISearchable<T>> SearchAsync<T>(string query,
543542
.ReadFromJsonAsync<ISearchable<T>>(cancellationToken: cancellationToken)
544543
.ConfigureAwait(false);
545544
}
545+
546+
/// <summary>
547+
/// Search index facets
548+
/// </summary>
549+
/// <param name="facetName">Name of the facet to search.</param>
550+
/// <param name="query">The search criteria to find the facet matches.</param>
551+
/// <param name="cancellationToken">The cancellation token for this call.</param>
552+
/// <returns>Facets meeting the search criteria.</returns>
553+
public async Task<FacetSearchResult> FacetSearchAsync(string facetName,
554+
FacetSearchQuery query = default, CancellationToken cancellationToken = default)
555+
{
556+
FacetSearchQuery body;
557+
if (query == null)
558+
{
559+
body = new FacetSearchQuery() { FacetName = facetName };
560+
}
561+
else
562+
{
563+
body = query;
564+
body.FacetName = facetName;
565+
}
566+
567+
var responseMessage = await _http.PostAsJsonAsync($"indexes/{Uid}/facet-search", body,
568+
Constants.JsonSerializerOptionsRemoveNulls, cancellationToken: cancellationToken)
569+
.ConfigureAwait(false);
570+
571+
return await responseMessage.Content
572+
.ReadFromJsonAsync<FacetSearchResult>(cancellationToken: cancellationToken)
573+
.ConfigureAwait(false);
574+
}
546575
}
547576
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
using System.Linq;
2+
using System.Threading.Tasks;
3+
4+
using FluentAssertions;
5+
6+
using Xunit;
7+
8+
namespace Meilisearch.Tests
9+
{
10+
public abstract class FacetSearchTests<TFixture> : IAsyncLifetime where TFixture : IndexFixture
11+
{
12+
private Index _indexForFaceting;
13+
14+
private readonly TFixture _fixture;
15+
16+
public FacetSearchTests(TFixture fixture)
17+
{
18+
_fixture = fixture;
19+
}
20+
21+
public async Task InitializeAsync()
22+
{
23+
await _fixture.DeleteAllIndexes(); // Test context cleaned for each [Fact]
24+
_indexForFaceting = await _fixture.SetUpIndexForFaceting("IndexForFaceting-SearchTests");
25+
}
26+
27+
public Task DisposeAsync() => Task.CompletedTask;
28+
29+
[Fact]
30+
public async Task BasicFacetSearch()
31+
{
32+
var results = await _indexForFaceting.FacetSearchAsync("genre");
33+
34+
Assert.Equal(4, results.FacetHits.Count());
35+
Assert.Null(results.FacetQuery);
36+
}
37+
38+
//[Fact] //these may or may not be required.
39+
//public async Task BasicFacetSearchWithNoFacet()
40+
//{
41+
// var results = await _indexForFaceting.SearchFacetsAsync(null);
42+
43+
// results.FacetHits.Should().BeEmpty();
44+
//}
45+
46+
//[Fact]
47+
//public async Task BasicFacetSearchWithEmptyFacet()
48+
//{
49+
// var results = await _indexForFaceting.SearchFacetsAsync(string.Empty);
50+
51+
// results.FacetHits.Should().BeEmpty();
52+
//}
53+
54+
[Fact]
55+
public async Task FacetSearchWithFilter()
56+
{
57+
var query = new FacetSearchQuery()
58+
{
59+
Filter = "genre = SF"
60+
};
61+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
62+
63+
Assert.Single(results.FacetHits);
64+
Assert.Equal("SF", results.FacetHits.First().Value);
65+
Assert.Equal(2, results.FacetHits.First().Count);
66+
Assert.Null(results.FacetQuery);
67+
}
68+
69+
[Fact]
70+
public async Task FacetSearchWithFilterWithSpaces()
71+
{
72+
var query = new FacetSearchQuery()
73+
{
74+
Filter = "genre = 'sci fi'"
75+
};
76+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
77+
78+
Assert.Single(results.FacetHits);
79+
Assert.Equal("sci fi", results.FacetHits.First().Value);
80+
Assert.Equal(1, results.FacetHits.First().Count);
81+
Assert.Null(results.FacetQuery);
82+
}
83+
84+
[Fact]
85+
public async Task FacetSearchWithFilterFacetIsNotNull()
86+
{
87+
var query = new FacetSearchQuery()
88+
{
89+
Filter = "genre IS NOT NULL"
90+
};
91+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
92+
93+
Assert.Equal(4, results.FacetHits.Count());
94+
Assert.Equal("Action", results.FacetHits.First().Value);
95+
Assert.Equal(3, results.FacetHits.First().Count);
96+
Assert.Null(results.FacetQuery);
97+
}
98+
99+
[Fact]
100+
public async Task FacetSearchWithMultipleFilter()
101+
{
102+
var newFilters = new Settings
103+
{
104+
FilterableAttributes = new string[] { "genre", "id" },
105+
};
106+
var task = await _indexForFaceting.UpdateSettingsAsync(newFilters);
107+
task.TaskUid.Should().BeGreaterOrEqualTo(0);
108+
await _indexForFaceting.WaitForTaskAsync(task.TaskUid);
109+
110+
var query = new FacetSearchQuery()
111+
{
112+
Filter = "genre = SF AND id != 13"
113+
};
114+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
115+
116+
Assert.Single(results.FacetHits);
117+
Assert.Equal("SF", results.FacetHits.First().Value);
118+
Assert.Equal(1, results.FacetHits.First().Count);
119+
Assert.Null(results.FacetQuery);
120+
}
121+
122+
[Fact]
123+
public async Task FacetSearchWithFilterFacetIsNull()
124+
{
125+
var query = new FacetSearchQuery()
126+
{
127+
Filter = "genre IS NULL"
128+
};
129+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
130+
131+
Assert.Empty(results.FacetHits);
132+
Assert.Null(results.FacetQuery);
133+
}
134+
135+
[Fact]
136+
public async Task FacetSearchWithFacetQuery()
137+
{
138+
var query = new FacetSearchQuery()
139+
{
140+
FacetQuery = "SF"
141+
};
142+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
143+
144+
Assert.Single(results.FacetHits);
145+
Assert.Equal("SF", results.FacetHits.First().Value);
146+
Assert.Equal(2, results.FacetHits.First().Count);
147+
results.FacetQuery.Should().NotBeNullOrEmpty();
148+
}
149+
150+
[Fact]
151+
public async Task FacetSearchWithFacetQueryWithSpaces()
152+
{
153+
var query = new FacetSearchQuery()
154+
{
155+
FacetQuery = "sci fi"
156+
};
157+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
158+
159+
Assert.Single(results.FacetHits);
160+
Assert.Equal("sci fi", results.FacetHits.First().Value);
161+
Assert.Equal(1, results.FacetHits.First().Count);
162+
results.FacetQuery.Should().NotBeNullOrEmpty();
163+
}
164+
165+
[Fact]
166+
public async Task FacetSearchWithLooseFacetQuery()
167+
{
168+
var query = new FacetSearchQuery()
169+
{
170+
FacetQuery = "s"
171+
};
172+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
173+
174+
Assert.Equal(2, results.FacetHits.Count());
175+
Assert.Equal("sci fi", results.FacetHits.First().Value);
176+
Assert.Equal(1, results.FacetHits.First().Count);
177+
results.FacetQuery.Should().NotBeNullOrEmpty();
178+
}
179+
180+
[Fact]
181+
public async Task FacetSearchWithLooseQuery()
182+
{
183+
var query = new FacetSearchQuery()
184+
{
185+
Query = "s"
186+
};
187+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
188+
189+
Assert.Equal(3, results.FacetHits.Count());
190+
Assert.Contains(results.FacetHits, x => x.Value.Equals("Action") && x.Count == 1);
191+
Assert.Contains(results.FacetHits, x => x.Value.Equals("SF") && x.Count == 2);
192+
Assert.Contains(results.FacetHits, x => x.Value.Equals("sci fi") && x.Count == 1);
193+
Assert.Null(results.FacetQuery);
194+
}
195+
196+
[Fact]
197+
public async Task FacetSearchWithMultipleQueryAndLastMatchingStrategy()
198+
{
199+
var query = new FacetSearchQuery()
200+
{
201+
Query = "action spider man",
202+
MatchingStrategy = "last"
203+
};
204+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
205+
206+
Assert.Single(results.FacetHits);
207+
results.FacetHits.First().Count.Should().Be(3);
208+
Assert.Null(results.FacetQuery);
209+
}
210+
211+
[Fact]
212+
public async Task FacetSearchWithMultipleQueryAndAllMatchingStrategy()
213+
{
214+
var query = new FacetSearchQuery()
215+
{
216+
Query = "action spider man",
217+
MatchingStrategy = "all",
218+
};
219+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
220+
221+
Assert.Single(results.FacetHits);
222+
results.FacetHits.First().Count.Should().Be(1);
223+
Assert.Null(results.FacetQuery);
224+
}
225+
226+
[Fact]
227+
public async Task FacetSearchWithMultipleQueryAndAllMatchingStrategyAndAttributesToSearchOn()
228+
{
229+
var query = new FacetSearchQuery()
230+
{
231+
Query = "spider man",
232+
MatchingStrategy = "all",
233+
AttributesToSearchOn = new[] { "name" }
234+
};
235+
var results = await _indexForFaceting.FacetSearchAsync("genre", query);
236+
237+
Assert.Single(results.FacetHits);
238+
results.FacetHits.First().Count.Should().Be(1);
239+
Assert.Null(results.FacetQuery);
240+
}
241+
}
242+
}

0 commit comments

Comments
 (0)