Skip to content

Commit

Permalink
chore: connected search service to main service (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bendomey authored Feb 6, 2025
1 parent 6c82608 commit 80d8731
Show file tree
Hide file tree
Showing 21 changed files with 242 additions and 70 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/search-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ jobs:
--build-arg "COLLECTIONS_COLLECTION=${{ secrets.STAGING_SEARCH_SERVICE_COLLECTIONS_COLLECTION }}"
--build-arg "CONTENT_TAGS_COLLECTION=${{ secrets.STAGING_SEARCH_SERVICE_CONTENT_TAGS_COLLECTION }}"
--build-arg "CONTENT_COLLECTIONS_COLLECTION=${{ secrets.STAGING_SEARCH_SERVICE_CONTENT_COLLECTIONS_COLLECTION }}"
--build-arg "JWT_SECRET_KEY=${{ secrets.STAGING_SEARCH_SERVICE_JWT_SECRET_KEY }}"
--build-arg "JWT_ISSUER=${{ secrets.STAGING_SEARCH_SERVICE_JWT_ISSUER }}"
--build-arg "JWT_AUTHORIZED_APPS=${{ secrets.STAGING_SEARCH_SERVICE_JWT_AUTHORIZED_APPS }}"
2 changes: 2 additions & 0 deletions services/main/Configurations/AppConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ public class AppConstants
public string EmailFrom { get; init; } = "Mfoni Notifications <noreply@notifications.mfoni.app>";
public string RegisterSwaggerDocs { get; set; } = null!;
public string SentryDSN { get; set; } = null!;
public string SearchServiceURL { get; set; } = null!;
public string SearchServiceAuthToken { get; set; } = null!;
}
54 changes: 45 additions & 9 deletions services/main/Controllers/Content.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System.Net;
using System.Security.Claims;
using main.Configuratons;
using main.Domains;
using main.DTOs;
using main.Lib;
using main.Middlewares;
using main.Transformations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Any;

namespace main.Controllers;
Expand All @@ -24,6 +26,8 @@ public class ContentController : ControllerBase
private readonly ContentTransformer _contentTransformer;
private readonly ContentLikeTransformer _contentLikeTransformer;
private readonly SearchTagService _searchTagsService;
private readonly Searchcontent.SearchContentService.SearchContentServiceClient _searchServiceRpcClient;
private readonly AppConstants _appConstants;

public ContentController(
ILogger<ContentController> logger,
Expand All @@ -34,7 +38,9 @@ public ContentController(
ContentTransformer contentTransformer,
ContentLikeTransformer contentLikeTransformer,
SearchTagService searchTagsService,
CollectionContentService collectionContentService
CollectionContentService collectionContentService,
Searchcontent.SearchContentService.SearchContentServiceClient searchServiceRpcClient,
IOptions<AppConstants> appConstants
)
{
_logger = logger;
Expand All @@ -46,6 +52,8 @@ CollectionContentService collectionContentService
_contentLikeTransformer = contentLikeTransformer;
_searchTagsService = searchTagsService;
_collectionContentService = collectionContentService;
_searchServiceRpcClient = searchServiceRpcClient;
_appConstants = appConstants.Value;
}

/// <summary>
Expand Down Expand Up @@ -419,17 +427,45 @@ public async Task<IActionResult> TextualSearch(
_logger.LogInformation("Getting contents by textual search");
var queryFilter = HttpLib.GenerateFilterQuery<Models.Content>(page, pageSize, sort, sortBy, populate);

var contents = await _searchContentService.TextualSearch(queryFilter, search, new GetContentsInput
Searchcontent.SearchResponse? searchResponse = null;

try
{
License = license,
Orientation = orientation
});
var searchResults = await _searchServiceRpcClient.SearchAsync(
new Searchcontent.SearchRequest
{
Keyword = search,
// TODO: add these to the search filters
// License = license,
// Orientation = orientation

Take = queryFilter.Limit,
Skip = queryFilter.Skip,
},
new Grpc.Core.Metadata
{
new Grpc.Core.Metadata.Entry("Authorization", "Bearer " + _appConstants.SearchServiceAuthToken)
}
);

if (searchResults is null)
{
throw new Exception();
}

var count = await _searchContentService.TextualSearchCount(search, new GetContentsInput
searchResponse = searchResults;
}
catch (System.Exception)
{
License = license,
Orientation = orientation
});
// We're hoping search service will notify us.
throw new HttpRequestException("Failed to search content");
}

var contentIds = searchResponse is not null ? searchResponse.Contents.Select(c => c).ToList() : new List<string>();

var contents = await _searchContentService.TextualSearch(contentIds);

var count = await _searchContentService.TextualSearchCount(contentIds);


var outContents = new List<OutputContent>();
Expand Down
62 changes: 6 additions & 56 deletions services/main/Domains/Content/SearchContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,75 +166,25 @@ public async Task<string[]> AskRekognitionForMatch(byte[] imageBytes)
}

public async Task<List<Content>> TextualSearch(
FilterQuery<Content> queryFilter,
string query,
GetContentsInput input
List<string> contentIds
)
{
// TODO: implement search with ELASTICSEARCH

FilterDefinitionBuilder<Content> builder = Builders<Content>.Filter;
var filter = builder.Empty;

if (input.Orientation != "ALL")
{
var orientationFilter = builder.Eq(r => r.Media.Orientation, input.Orientation);
filter &= orientationFilter;
}

if (input.License != "ALL")
{
if (input.License == "FREE")
{
var licenseFilter = builder.Eq(r => r.Amount, 0);
filter &= licenseFilter;
}
else if (input.License == "PREMIUM")
{
var licenseFilter = builder.Gt(r => r.Amount, 0);
filter &= licenseFilter;
}
}
var contentIdsObject = contentIds.Select(id => ObjectId.Parse(id)).ToList();
var filter = Builders<Content>.Filter.In("_id", contentIdsObject);

var contents = await _contentsCollection
.Find(filter)
.Skip(queryFilter.Skip)
.Limit(queryFilter.Limit)
.Sort(queryFilter.Sort)
.ToListAsync();

return contents;
}

public async Task<long> TextualSearchCount(
string query,
GetContentsInput input
List<string> contentIds
)
{
// TODO: implement search with ELASTICSEARCH

FilterDefinitionBuilder<Content> builder = Builders<Content>.Filter;
var filter = builder.Empty;

if (input.Orientation != "ALL")
{
var orientationFilter = builder.Eq(r => r.Media.Orientation, input.Orientation);
filter &= orientationFilter;
}

if (input.License != "ALL")
{
if (input.License == "FREE")
{
var licenseFilter = builder.Eq(r => r.Amount, 0);
filter &= licenseFilter;
}
else if (input.License == "PREMIUM")
{
var licenseFilter = builder.Gt(r => r.Amount, 0);
filter &= licenseFilter;
}
}
var contentIdsObject = contentIds.Select(id => ObjectId.Parse(id)).ToList();
var filter = Builders<Content>.Filter.In("_id", contentIdsObject);

var contents = await _contentsCollection.CountDocumentsAsync(filter);

Expand Down
6 changes: 5 additions & 1 deletion services/main/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ build-docker:
docker build -t mfoni-api -f Dockerfile .

lint:
dotnet format main.sln
dotnet format main.sln

get-protos:
mkdir -p Protos
cp -r ../protos/* ./Protos
8 changes: 8 additions & 0 deletions services/main/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@
builder.Configuration.GetSection("RabbitMQConnection")
);

builder.Services.AddGrpcClient<Searchcontent.SearchContentService.SearchContentServiceClient>(options =>
{
options.Address = new Uri(builder
.Configuration.GetSection("AppConstants:SearchServiceURL")
.Get<string>()!
);
});

builder.Services.Configure<AppConstants>(builder.Configuration.GetSection("AppConstants"));

builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
Expand Down
19 changes: 19 additions & 0 deletions services/main/Protos/content_proto/search_content.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
syntax = "proto3";

option go_package="github.com/Bendomey/project-mfoni/services/search/internal/protos/content_proto";

package searchcontent;

service SearchContentService {
rpc Search (SearchRequest) returns (SearchResponse) {}
}

message SearchRequest {
string keyword = 1;
optional int64 take = 2;
optional int64 skip = 3;
}

message SearchResponse {
repeated string contents = 1;
}
4 changes: 3 additions & 1 deletion services/main/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"SmsAppSecret": "",
"ResendApiKey": "",
"RegisterSwaggerDocs": "true",
"SentryDSN": ""
"SentryDSN": "",
"SearchServiceURL": "http://0.0.0.0:5000",
"SearchServiceAuthToken": "your-token-here"
},
"Logging": {
"LogLevel": {
Expand Down
9 changes: 9 additions & 0 deletions services/main/main.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
<PackageReference Include="AWSSDK.Core" Version="3.7.302.8" />
<PackageReference Include="AWSSDK.Rekognition" Version="3.7.300.25" />
<PackageReference Include="AWSSDK.S3" Version="3.7.305.24" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.69.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="MongoDB.Driver" Version="2.23.1" />
<PackageReference Include="Nanoid" Version="3.0.0" />
Expand All @@ -28,6 +35,8 @@
<Content Include="Assets\Fonts\*.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

<Protobuf Include="Protos\content_proto\search_content.proto" GrpcServices="Client" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions services/search/.envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ export TAG_COLLECTION=
export COLLECTIONS_COLLECTION=
export CONTENT_TAGS_COLLECTION=
export CONTENT_COLLECTIONS_COLLECTION=

# JWT
JWT_SECRET_KEY=
JWT_ISSUER=
JWT_AUTHORIZED_APPS=
2 changes: 2 additions & 0 deletions services/search/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ dump.rdb
.env
.env.local
.envrc
.envrc.staging
.envrc.production


ngrok
Expand Down
9 changes: 9 additions & 0 deletions services/search/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ ENV CONTENT_TAGS_COLLECTION=$CONTENT_TAGS_COLLECTION
ARG CONTENT_COLLECTIONS_COLLECTION
ENV CONTENT_COLLECTIONS_COLLECTION=$CONTENT_COLLECTIONS_COLLECTION

ARG JWT_SECRET_KEY
ENV JWT_SECRET_KEY=$JWT_SECRET_KEY

ARG JWT_ISSUER
ENV JWT_ISSUER=$JWT_ISSUER

ARG JWT_AUTHORIZED_APPS
ENV JWT_AUTHORIZED_APPS=$JWT_AUTHORIZED_APPS

RUN mkdir /app/
WORKDIR /app/

Expand Down
5 changes: 5 additions & 0 deletions services/search/config/development.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ openSearch:
number_of_shards: ${NUMBER_OF_SHARDS}
number_of_replicas: ${NUMBER_OF_REPLICAS}

jwt:
secret_key: ${JWT_SECRET_KEY}
issuer: ${JWT_ISSUER}
authorized_apps: ${JWT_AUTHORIZED_APPS}

sentry:
dsn: ${SENTRY_DSN}
environment: ${SENTRY_ENVIRONMENT}
Expand Down
5 changes: 5 additions & 0 deletions services/search/config/production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ openSearch:
number_of_shards: ${NUMBER_OF_SHARDS}
number_of_replicas: ${NUMBER_OF_REPLICAS}

jwt:
secret_key: ${JWT_SECRET_KEY}
issuer: ${JWT_ISSUER}
authorized_apps: ${JWT_AUTHORIZED_APPS}

sentry:
dsn: ${SENTRY_DSN}
environment: ${SENTRY_ENVIRONMENT}
Expand Down
5 changes: 5 additions & 0 deletions services/search/config/staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ openSearch:
number_of_shards: ${NUMBER_OF_SHARDS}
number_of_replicas: ${NUMBER_OF_REPLICAS}

jwt:
secret_key: ${JWT_SECRET_KEY}
issuer: ${JWT_ISSUER}
authorized_apps: ${JWT_AUTHORIZED_APPS}

sentry:
dsn: ${SENTRY_DSN}
environment: ${SENTRY_ENVIRONMENT}
Expand Down
1 change: 1 addition & 0 deletions services/search/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
require (
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.17.2 // indirect
Expand Down
2 changes: 2 additions & 0 deletions services/search/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
Expand Down
15 changes: 13 additions & 2 deletions services/search/internal/handlers/content/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package contenthandler

import (
"context"
"errors"

"github.com/Bendomey/project-mfoni/services/search/internal/protos/content_proto"
"github.com/Bendomey/project-mfoni/services/search/internal/services"
Expand All @@ -17,8 +18,18 @@ type Handler struct {
content_proto.UnimplementedSearchContentServiceServer
}

func (s *Handler) Search(ctx context.Context, in *content_proto.SearchRequest) (*content_proto.SearchResponse, error) {
contents, contentsErr := s.Services.ContentService.Search(ctx, cleanUpSearchInput(in))
func (s *Handler) Search(
ctx context.Context,
input *content_proto.SearchRequest,
) (*content_proto.SearchResponse, error) {
// verify token
isTokenValid := lib.VerifyAuthToken(ctx, s.AppContext.Config)

if !isTokenValid {
return nil, errors.New("Unauthorized")
}

contents, contentsErr := s.Services.ContentService.Search(ctx, cleanUpSearchInput(input))

if contentsErr != nil {
logrus.Error("Error searching for content: ", contentsErr)
Expand Down
Loading

0 comments on commit 80d8731

Please sign in to comment.