mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.7 KiB
178 lines
5.7 KiB
// ==========================================================================
|
|
// Squidex Headless CMS
|
|
// ==========================================================================
|
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
// All rights reserved. Licensed under the MIT license.
|
|
// ==========================================================================
|
|
|
|
using MongoDB.Bson;
|
|
using MongoDB.Bson.Serialization.Attributes;
|
|
using MongoDB.Driver;
|
|
using Squidex.Domain.Apps.Core.Apps;
|
|
using Squidex.Domain.Apps.Core.Schemas;
|
|
using Squidex.Infrastructure;
|
|
|
|
namespace Squidex.Domain.Apps.Entities.Contents.Text;
|
|
|
|
public abstract class MongoTextIndexBase<T>(IMongoDatabase database, string shardKey, CommandFactory<T> factory) : MongoRepositoryBase<MongoTextIndexEntity<T>>(database), ITextIndex, IDeleter where T : class
|
|
{
|
|
protected sealed class MongoTextResult
|
|
{
|
|
[BsonId]
|
|
[BsonElement]
|
|
public ObjectId Id { get; set; }
|
|
|
|
[BsonRequired]
|
|
[BsonElement("c")]
|
|
public DomainId ContentId { get; set; }
|
|
|
|
[BsonIgnoreIfDefault]
|
|
[BsonElement("score")]
|
|
public double Score { get; set; }
|
|
}
|
|
|
|
protected override async Task SetupCollectionAsync(IMongoCollection<MongoTextIndexEntity<T>> collection,
|
|
CancellationToken ct)
|
|
{
|
|
var isFerreDb = await Database.IsFerretDbAsync(ct);
|
|
|
|
if (isFerreDb)
|
|
{
|
|
await collection.Indexes.CreateManyAsync(
|
|
[
|
|
new CreateIndexModel<MongoTextIndexEntity<T>>(
|
|
Index
|
|
.Ascending(x => x.AppId)
|
|
.Ascending(x => x.ContentId)),
|
|
|
|
new CreateIndexModel<MongoTextIndexEntity<T>>(
|
|
Index
|
|
.Ascending(x => x.AppId)
|
|
.Ascending(x => x.SchemaId)
|
|
.Ascending(x => x.GeoField)),
|
|
|
|
new CreateIndexModel<MongoTextIndexEntity<T>>(
|
|
Index
|
|
.Geo2DSphere(x => x.GeoObject)),
|
|
], ct);
|
|
}
|
|
else
|
|
{
|
|
await collection.Indexes.CreateManyAsync(
|
|
[
|
|
new CreateIndexModel<MongoTextIndexEntity<T>>(
|
|
Index
|
|
.Ascending(x => x.AppId)
|
|
.Ascending(x => x.ContentId)),
|
|
|
|
new CreateIndexModel<MongoTextIndexEntity<T>>(
|
|
Index
|
|
.Ascending(x => x.AppId)
|
|
.Ascending(x => x.SchemaId)
|
|
.Ascending(x => x.GeoField)
|
|
.Geo2DSphere(x => x.GeoObject)),
|
|
], ct);
|
|
}
|
|
}
|
|
|
|
protected override string CollectionName()
|
|
{
|
|
return $"TextIndex2{shardKey}";
|
|
}
|
|
|
|
Task IDeleter.DeleteAppAsync(App app,
|
|
CancellationToken ct)
|
|
{
|
|
return Collection.DeleteManyAsync(Filter.Eq(x => x.AppId, app.Id), ct);
|
|
}
|
|
|
|
Task IDeleter.DeleteSchemaAsync(App app, Schema schema,
|
|
CancellationToken ct)
|
|
{
|
|
return Collection.DeleteManyAsync(Filter.Eq(x => x.SchemaId, schema.Id), ct);
|
|
}
|
|
|
|
public async virtual Task ExecuteAsync(IndexCommand[] commands,
|
|
CancellationToken ct = default)
|
|
{
|
|
var writes = new List<WriteModel<MongoTextIndexEntity<T>>>(commands.Length);
|
|
|
|
foreach (var command in commands)
|
|
{
|
|
factory.CreateCommands(command, writes);
|
|
}
|
|
|
|
if (writes.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await Collection.BulkWriteAsync(writes, BulkUnordered, ct);
|
|
}
|
|
catch (MongoBulkWriteException ex)
|
|
{
|
|
// Ignore invalid geo data when writing content. Our insert is unordered anyway.
|
|
if (!ex.WriteErrors.All(e =>
|
|
MongoDbErrorCodes.IsInvalidGeoData(e) ||
|
|
MongoDbErrorCodes.IsInvalidDocumentDbGeoData(e)))
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual async Task<List<DomainId>?> SearchAsync(App app, GeoQuery query, SearchScope scope,
|
|
CancellationToken ct = default)
|
|
{
|
|
Guard.NotNull(app);
|
|
Guard.NotNull(query);
|
|
|
|
// The distance must be converted to radian (in contrast to MongoDB, which uses degrees).
|
|
var radian = query.Radius / 6378100;
|
|
|
|
// Use the filter in the correct order to leverage the index in the best way.
|
|
var findFilter =
|
|
Filter.And(
|
|
Filter.Eq(x => x.AppId, app.Id),
|
|
Filter.Eq(x => x.SchemaId, query.SchemaId),
|
|
Filter.Eq(x => x.GeoField, query.Field),
|
|
Filter.GeoWithinCenterSphere(x => x.GeoObject, query.Longitude, query.Latitude, radian),
|
|
FilterByScope(scope));
|
|
|
|
var byGeo =
|
|
await GetCollection(scope).Find(findFilter).Limit(query.Take)
|
|
.Project<MongoTextResult>(Projection.Include(x => x.ContentId))
|
|
.ToListAsync(ct);
|
|
|
|
return byGeo.Select(x => x.ContentId).ToList();
|
|
}
|
|
|
|
public abstract Task<List<DomainId>?> SearchAsync(App app, TextQuery query, SearchScope scope,
|
|
CancellationToken ct = default);
|
|
|
|
protected static FilterDefinition<MongoTextIndexEntity<T>> FilterByScope(SearchScope scope)
|
|
{
|
|
if (scope == SearchScope.All)
|
|
{
|
|
return Filter.Eq(x => x.ServeAll, true);
|
|
}
|
|
else
|
|
{
|
|
return Filter.Eq(x => x.ServePublished, true);
|
|
}
|
|
}
|
|
|
|
protected IMongoCollection<MongoTextIndexEntity<T>> GetCollection(SearchScope scope)
|
|
{
|
|
if (scope == SearchScope.All)
|
|
{
|
|
return Collection;
|
|
}
|
|
else
|
|
{
|
|
return Collection.WithReadPreference(ReadPreference.Secondary);
|
|
}
|
|
}
|
|
}
|
|
|