Skip to content

Commit

Permalink
Merge #592
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
4 people authored Jan 8, 2025
2 parents 4e4ce29 + c88e22f commit 1dadb40
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 1 deletion.
13 changes: 13 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -795,3 +795,16 @@ update_dictionary_1: |-
await client.Index("books").UpdateDictionaryAsync(newDictionary);
reset_dictionary_1: |-
await client.Index("books").ResetDictionaryAsync();
facet_search_1: |-
var query = new SearchFacetsQuery()
{
FacetQuery = "fiction",
Filter = "rating > 3"
};
await client.Index("books").FacetSearchAsync("genres", query);
facet_search_3: |-
var query = new SearchFacetsQuery()
{
FacetQuery = "c"
};
await client.Index("books").FacetSearchAsync("genres", query);
47 changes: 47 additions & 0 deletions src/Meilisearch/FacetSearchQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Meilisearch
{
/// <summary>
/// Wrapper for facet search query
/// </summary>
public class FacetSearchQuery
{
/// <summary>
/// Gets or sets the facetName property
/// </summary>
[JsonPropertyName("facetName")]
public string FacetName { get; set; }

/// <summary>
/// Gets or sets the facetQuery property
/// </summary>
[JsonPropertyName("facetQuery")]
public string FacetQuery { get; set; }

/// <summary>
/// Gets or sets the q property
/// </summary>
[JsonPropertyName("q")]
public string Query { get; set; }

/// <summary>
/// Gets or sets the filter property
/// </summary>
[JsonPropertyName("filter")]
public dynamic Filter { get; set; }

/// <summary>
/// Gets or sets the matchingStrategy property, can be <c>last</c>, <c>all</c> or <c>frequency</c>.
/// </summary>
[JsonPropertyName("matchingStrategy")]
public string MatchingStrategy { get; set; }

/// <summary>
/// Gets or sets the attributesToSearchOn property
/// </summary>
[JsonPropertyName("attributesToSearchOn")]
public IEnumerable<string> AttributesToSearchOn { get; set; }
}
}
47 changes: 47 additions & 0 deletions src/Meilisearch/FacetSearchResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace Meilisearch
{
/// <summary>
/// Wrapper for FacetSearchResponse
/// </summary>
public class FacetSearchResult
{
/// <summary>
/// Gets or sets the facetHits property
/// </summary>
[JsonPropertyName("facetHits")]
public IEnumerable<FacetHit> FacetHits { get; set; }

/// <summary>
/// Gets or sets the facet query
/// </summary>
[JsonPropertyName("facetQuery")]
public string FacetQuery { get; set; }

/// <summary>
/// Gets or sets the processingTimeMs property
/// </summary>
[JsonPropertyName("processingTimeMs")]
public int ProcessingTimeMs { get; set; }

/// <summary>
/// Wrapper for Facet Hit
/// </summary>
public class FacetHit
{
/// <summary>
/// Gets or sets the value property
/// </summary>
[JsonPropertyName("value")]
public string Value { get; set; }

/// <summary>
/// Gets or sets the count property
/// </summary>
[JsonPropertyName("count")]
public int Count { get; set; }
}
}
}
31 changes: 30 additions & 1 deletion src/Meilisearch/Index.Documents.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -543,5 +542,35 @@ public async Task<ISearchable<T>> SearchAsync<T>(string query,
.ReadFromJsonAsync<ISearchable<T>>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
/// Search index facets
/// </summary>
/// <param name="facetName">Name of the facet to search.</param>
/// <param name="query">The search criteria to find the facet matches.</param>
/// <param name="cancellationToken">The cancellation token for this call.</param>
/// <returns>Facets meeting the search criteria.</returns>
public async Task<FacetSearchResult> FacetSearchAsync(string facetName,
FacetSearchQuery query = default, CancellationToken cancellationToken = default)
{
FacetSearchQuery body;
if (query == null)
{
body = new FacetSearchQuery() { FacetName = facetName };
}
else
{
body = query;
body.FacetName = facetName;
}

var responseMessage = await _http.PostAsJsonAsync($"indexes/{Uid}/facet-search", body,
Constants.JsonSerializerOptionsRemoveNulls, cancellationToken: cancellationToken)
.ConfigureAwait(false);

return await responseMessage.Content
.ReadFromJsonAsync<FacetSearchResult>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
}
}
242 changes: 242 additions & 0 deletions tests/Meilisearch.Tests/FacetSearchTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
using System.Linq;
using System.Threading.Tasks;

using FluentAssertions;

using Xunit;

namespace Meilisearch.Tests
{
public abstract class FacetSearchTests<TFixture> : IAsyncLifetime where TFixture : IndexFixture
{
private Index _indexForFaceting;

private readonly TFixture _fixture;

public FacetSearchTests(TFixture fixture)
{
_fixture = fixture;
}

public async Task InitializeAsync()
{
await _fixture.DeleteAllIndexes(); // Test context cleaned for each [Fact]
_indexForFaceting = await _fixture.SetUpIndexForFaceting("IndexForFaceting-SearchTests");
}

public Task DisposeAsync() => Task.CompletedTask;

[Fact]
public async Task BasicFacetSearch()
{
var results = await _indexForFaceting.FacetSearchAsync("genre");

Assert.Equal(4, results.FacetHits.Count());
Assert.Null(results.FacetQuery);
}

//[Fact] //these may or may not be required.
//public async Task BasicFacetSearchWithNoFacet()
//{
// var results = await _indexForFaceting.SearchFacetsAsync(null);

// results.FacetHits.Should().BeEmpty();
//}

//[Fact]
//public async Task BasicFacetSearchWithEmptyFacet()
//{
// var results = await _indexForFaceting.SearchFacetsAsync(string.Empty);

// results.FacetHits.Should().BeEmpty();
//}

[Fact]
public async Task FacetSearchWithFilter()
{
var query = new FacetSearchQuery()
{
Filter = "genre = SF"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
Assert.Equal("SF", results.FacetHits.First().Value);
Assert.Equal(2, results.FacetHits.First().Count);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithFilterWithSpaces()
{
var query = new FacetSearchQuery()
{
Filter = "genre = 'sci fi'"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
Assert.Equal("sci fi", results.FacetHits.First().Value);
Assert.Equal(1, results.FacetHits.First().Count);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithFilterFacetIsNotNull()
{
var query = new FacetSearchQuery()
{
Filter = "genre IS NOT NULL"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Equal(4, results.FacetHits.Count());
Assert.Equal("Action", results.FacetHits.First().Value);
Assert.Equal(3, results.FacetHits.First().Count);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithMultipleFilter()
{
var newFilters = new Settings
{
FilterableAttributes = new string[] { "genre", "id" },
};
var task = await _indexForFaceting.UpdateSettingsAsync(newFilters);
task.TaskUid.Should().BeGreaterOrEqualTo(0);
await _indexForFaceting.WaitForTaskAsync(task.TaskUid);

var query = new FacetSearchQuery()
{
Filter = "genre = SF AND id != 13"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
Assert.Equal("SF", results.FacetHits.First().Value);
Assert.Equal(1, results.FacetHits.First().Count);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithFilterFacetIsNull()
{
var query = new FacetSearchQuery()
{
Filter = "genre IS NULL"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Empty(results.FacetHits);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithFacetQuery()
{
var query = new FacetSearchQuery()
{
FacetQuery = "SF"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
Assert.Equal("SF", results.FacetHits.First().Value);
Assert.Equal(2, results.FacetHits.First().Count);
results.FacetQuery.Should().NotBeNullOrEmpty();
}

[Fact]
public async Task FacetSearchWithFacetQueryWithSpaces()
{
var query = new FacetSearchQuery()
{
FacetQuery = "sci fi"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
Assert.Equal("sci fi", results.FacetHits.First().Value);
Assert.Equal(1, results.FacetHits.First().Count);
results.FacetQuery.Should().NotBeNullOrEmpty();
}

[Fact]
public async Task FacetSearchWithLooseFacetQuery()
{
var query = new FacetSearchQuery()
{
FacetQuery = "s"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Equal(2, results.FacetHits.Count());
Assert.Equal("sci fi", results.FacetHits.First().Value);
Assert.Equal(1, results.FacetHits.First().Count);
results.FacetQuery.Should().NotBeNullOrEmpty();
}

[Fact]
public async Task FacetSearchWithLooseQuery()
{
var query = new FacetSearchQuery()
{
Query = "s"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Equal(3, results.FacetHits.Count());
Assert.Contains(results.FacetHits, x => x.Value.Equals("Action") && x.Count == 1);
Assert.Contains(results.FacetHits, x => x.Value.Equals("SF") && x.Count == 2);
Assert.Contains(results.FacetHits, x => x.Value.Equals("sci fi") && x.Count == 1);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithMultipleQueryAndLastMatchingStrategy()
{
var query = new FacetSearchQuery()
{
Query = "action spider man",
MatchingStrategy = "last"
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
results.FacetHits.First().Count.Should().Be(3);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithMultipleQueryAndAllMatchingStrategy()
{
var query = new FacetSearchQuery()
{
Query = "action spider man",
MatchingStrategy = "all",
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
results.FacetHits.First().Count.Should().Be(1);
Assert.Null(results.FacetQuery);
}

[Fact]
public async Task FacetSearchWithMultipleQueryAndAllMatchingStrategyAndAttributesToSearchOn()
{
var query = new FacetSearchQuery()
{
Query = "spider man",
MatchingStrategy = "all",
AttributesToSearchOn = new[] { "name" }
};
var results = await _indexForFaceting.FacetSearchAsync("genre", query);

Assert.Single(results.FacetHits);
results.FacetHits.First().Count.Should().Be(1);
Assert.Null(results.FacetQuery);
}
}
}
Loading

0 comments on commit 1dadb40

Please sign in to comment.