search refactoring

This commit is contained in:
sbwalker 2024-06-08 16:14:56 -04:00
parent 175675ad99
commit bc0573918f
16 changed files with 183 additions and 166 deletions

View File

@ -127,19 +127,19 @@ namespace Oqtane.Managers.Search
searchContent.Title = !string.IsNullOrEmpty(page.Title) ? page.Title : page.Name;
}
if (searchContent.Properties == null)
if (searchContent.SearchContentProperties == null)
{
searchContent.Properties = new List<SearchContentProperty>();
searchContent.SearchContentProperties = new List<SearchContentProperty>();
}
if(!searchContent.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName))
if(!searchContent.SearchContentProperties.Any(i => i.Name == Constants.SearchPageIdPropertyName))
{
searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = pageModule.PageId.ToString() });
searchContent.SearchContentProperties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = pageModule.PageId.ToString() });
}
if (!searchContent.Properties.Any(i => i.Name == Constants.SearchModuleIdPropertyName))
if (!searchContent.SearchContentProperties.Any(i => i.Name == Constants.SearchModuleIdPropertyName))
{
searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchModuleIdPropertyName, Value = pageModule.ModuleId.ToString() });
searchContent.SearchContentProperties.Add(new SearchContentProperty { Name = Constants.SearchModuleIdPropertyName, Value = pageModule.ModuleId.ToString() });
}
}
}

View File

@ -24,7 +24,7 @@ namespace Oqtane.Managers.Search
public string GetUrl(SearchResult searchResult, SearchQuery searchQuery)
{
var pageRepository = _serviceProvider.GetRequiredService<IPageRepository>();
var pageIdValue = searchResult.Properties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
var pageIdValue = searchResult.SearchContentProperties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
if(!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId))
{
var page = pageRepository.GetPage(pageId);
@ -39,7 +39,7 @@ namespace Oqtane.Managers.Search
public bool Visible(SearchContent searchResult, SearchQuery searchQuery)
{
var pageIdValue = searchResult.Properties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
var pageIdValue = searchResult.SearchContentProperties?.FirstOrDefault(i => i.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
if (!string.IsNullOrEmpty(pageIdValue) && int.TryParse(pageIdValue, out int pageId))
{
return CanViewPage(pageId, searchQuery.User);

View File

@ -64,14 +64,14 @@ namespace Oqtane.Managers.Search
IsActive = !page.IsDeleted && Utilities.IsPageModuleVisible(page.EffectiveDate, page.ExpiryDate)
};
if (searchContent.Properties == null)
if (searchContent.SearchContentProperties == null)
{
searchContent.Properties = new List<SearchContentProperty>();
searchContent.SearchContentProperties = new List<SearchContentProperty>();
}
if (!searchContent.Properties.Any(i => i.Name == Constants.SearchPageIdPropertyName))
if (!searchContent.SearchContentProperties.Any(i => i.Name == Constants.SearchPageIdPropertyName))
{
searchContent.Properties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = page.PageId.ToString() });
searchContent.SearchContentProperties.Add(new SearchContentProperty { Name = Constants.SearchPageIdPropertyName, Value = page.PageId.ToString() });
}
searchContentList.Add(searchContent);

View File

@ -1,42 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentWordsEntityBuilder : BaseEntityBuilder<SearchContentWordsEntityBuilder>
{
private const string _entityTableName = "SearchContentWords";
private readonly PrimaryKey<SearchContentWordsEntityBuilder> _primaryKey = new("PK_SearchContentWords", x => x.WordId);
private readonly ForeignKey<SearchContentWordsEntityBuilder> _searchContentForeignKey = new("FK_SearchContentWords_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade);
private readonly ForeignKey<SearchContentWordsEntityBuilder> _wordSourceForeignKey = new("FK_SearchContentWords_WordSource", x => x.WordSourceId, "SearchContentWordSource", "WordSourceId", ReferentialAction.Cascade);
public SearchContentWordsEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_searchContentForeignKey);
ForeignKeys.Add(_wordSourceForeignKey);
}
protected override SearchContentWordsEntityBuilder BuildTable(ColumnsBuilder table)
{
WordId = AddAutoIncrementColumn(table, "WordId");
SearchContentId = AddIntegerColumn(table, "SearchContentId");
WordSourceId = AddIntegerColumn(table, "WordSourceId");
Count = AddIntegerColumn(table, "Count");
return this;
}
public OperationBuilder<AddColumnOperation> WordId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchContentId { get; private set; }
public OperationBuilder<AddColumnOperation> WordSourceId { get; private set; }
public OperationBuilder<AddColumnOperation> Count { get; private set; }
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentWordEntityBuilder : BaseEntityBuilder<SearchContentWordEntityBuilder>
{
private const string _entityTableName = "SearchContentWord";
private readonly PrimaryKey<SearchContentWordEntityBuilder> _primaryKey = new("PK_SearchContentWord", x => x.SearchWordId);
private readonly ForeignKey<SearchContentWordEntityBuilder> _foreignKey1 = new("FK_SearchContentWord_SearchContent", x => x.SearchContentId, "SearchContent", "SearchContentId", ReferentialAction.Cascade);
private readonly ForeignKey<SearchContentWordEntityBuilder> _foreignKey2 = new("FK_SearchContentWord_SearchWord", x => x.SearchWordId, "SearchWord", "SearchWordId", ReferentialAction.Cascade);
public SearchContentWordEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
ForeignKeys.Add(_foreignKey1);
ForeignKeys.Add(_foreignKey2);
}
protected override SearchContentWordEntityBuilder BuildTable(ColumnsBuilder table)
{
SearchContentWordId = AddAutoIncrementColumn(table, "SearchContentWordId");
SearchContentId = AddIntegerColumn(table, "SearchContentId");
SearchWordId = AddIntegerColumn(table, "SearchWordId");
Count = AddIntegerColumn(table, "Count");
CreatedOn = AddDateTimeColumn(table, "CreatedOn");
ModifiedOn = AddDateTimeColumn(table, "ModifiedOn");
return this;
}
public OperationBuilder<AddColumnOperation> SearchContentWordId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchContentId { get; private set; }
public OperationBuilder<AddColumnOperation> SearchWordId { get; private set; }
public OperationBuilder<AddColumnOperation> Count { get; private set; }
public OperationBuilder<AddColumnOperation> CreatedOn { get; private set; }
public OperationBuilder<AddColumnOperation> ModifiedOn { get; private set; }
}
}

View File

@ -1,31 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchContentWordSourceEntityBuilder : BaseEntityBuilder<SearchContentWordSourceEntityBuilder>
{
private const string _entityTableName = "SearchContentWordSource";
private readonly PrimaryKey<SearchContentWordSourceEntityBuilder> _primaryKey = new("PK_SearchContentWordSource", x => x.WordSourceId);
public SearchContentWordSourceEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override SearchContentWordSourceEntityBuilder BuildTable(ColumnsBuilder table)
{
WordSourceId = AddAutoIncrementColumn(table, "WordSourceId");
Word = AddStringColumn(table, "Word", 255);
return this;
}
public OperationBuilder<AddColumnOperation> WordSourceId { get; private set; }
public OperationBuilder<AddColumnOperation> Word { get; private set; }
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases.Interfaces;
namespace Oqtane.Migrations.EntityBuilders
{
public class SearchWordEntityBuilder : BaseEntityBuilder<SearchWordEntityBuilder>
{
private const string _entityTableName = "SearchWord";
private readonly PrimaryKey<SearchWordEntityBuilder> _primaryKey = new("PK_SearchWord", x => x.SearchWordId);
public SearchWordEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database)
{
EntityTableName = _entityTableName;
PrimaryKey = _primaryKey;
}
protected override SearchWordEntityBuilder BuildTable(ColumnsBuilder table)
{
SearchWordId = AddAutoIncrementColumn(table, "SearchWordId");
Word = AddStringColumn(table, "Word", 255);
CreatedOn = AddDateTimeColumn(table, "CreatedOn");
return this;
}
public OperationBuilder<AddColumnOperation> SearchWordId { get; private set; }
public OperationBuilder<AddColumnOperation> Word { get; private set; }
public OperationBuilder<AddColumnOperation> CreatedOn { get; private set; }
}
}

View File

@ -23,22 +23,22 @@ namespace Oqtane.Migrations.Tenant
var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentPropertyEntityBuilder.Create();
var searchContentWordSourceEntityBuilder = new SearchContentWordSourceEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordSourceEntityBuilder.Create();
searchContentWordSourceEntityBuilder.AddIndex("IX_SearchContentWordSource", "Word", true);
var searchWordEntityBuilder = new SearchWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchWordEntityBuilder.Create();
searchWordEntityBuilder.AddIndex("IX_SearchWord", "Word", true);
var searchContentWordsEntityBuilder = new SearchContentWordsEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordsEntityBuilder.Create();
var searchContentWordEntityBuilder = new SearchContentWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordEntityBuilder.Create();
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var searchContentWordsEntityBuilder = new SearchContentWordsEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordsEntityBuilder.Drop();
var searchContentWordEntityBuilder = new SearchContentWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordEntityBuilder.Drop();
var searchContentWordSourceEntityBuilder = new SearchContentWordSourceEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentWordSourceEntityBuilder.DropIndex("IX_SearchContentWordSource");
searchContentWordSourceEntityBuilder.Drop();
var searchWordEntityBuilder = new SearchWordEntityBuilder(migrationBuilder, ActiveDatabase);
searchWordEntityBuilder.DropIndex("IX_SearchWord");
searchWordEntityBuilder.Drop();
var searchContentPropertyEntityBuilder = new SearchContentPropertyEntityBuilder(migrationBuilder, ActiveDatabase);
searchContentPropertyEntityBuilder.Drop();

View File

@ -4,13 +4,11 @@ using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using HtmlAgilityPack;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Services;
using Oqtane.Shared;
using static Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
namespace Oqtane.Providers
{
@ -64,7 +62,7 @@ namespace Oqtane.Providers
{
var totalResults = 0;
var searchContentList = await _searchContentRepository.GetSearchContentListAsync(searchQuery);
var searchContentList = await _searchContentRepository.GetSearchContentsAsync(searchQuery);
//convert the search content to search results.
var results = searchContentList
@ -107,7 +105,7 @@ namespace Oqtane.Providers
{
if (i.EntityName == EntityNames.Page || i.EntityName == EntityNames.Module)
{
var pageId = i.Properties.FirstOrDefault(p => p.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
var pageId = i.SearchContentProperties.FirstOrDefault(p => p.Name == Constants.SearchPageIdPropertyName)?.Value ?? string.Empty;
return !string.IsNullOrEmpty(pageId) ? pageId : i.UniqueKey;
}
else
@ -138,7 +136,7 @@ namespace Oqtane.Providers
Body = searchContent.Body,
Url = searchContent.Url,
ModifiedTime = searchContent.ModifiedTime,
Properties = searchContent.Properties,
SearchContentProperties = searchContent.SearchContentProperties,
Snippet = BuildSnippet(searchContent, searchQuery),
Score = CalculateScore(searchContent, searchQuery)
};
@ -151,7 +149,7 @@ namespace Oqtane.Providers
var score = 0f;
foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords))
{
score += searchContent.Words.Where(i => i.WordSource.Word.StartsWith(keyword)).Sum(i => i.Count);
score += searchContent.SearchContentWords.Where(i => i.SearchWord.Word.StartsWith(keyword)).Sum(i => i.Count);
}
return score / 100;
@ -206,31 +204,34 @@ namespace Oqtane.Providers
//analyze the search content and save the index words
var indexContent = $"{searchContent.Title} {searchContent.Description} {searchContent.Body} {searchContent.AdditionalContent}";
var words = GetWords(indexContent, WordMinLength);
var existWords = _searchContentRepository.GetWords(searchContent.SearchContentId);
var existingSearchContentWords = _searchContentRepository.GetSearchContentWords(searchContent.SearchContentId);
foreach (var kvp in words)
{
var word = existWords.FirstOrDefault(i => i.WordSource.Word == kvp.Key);
if (word != null)
var searchContentWord = existingSearchContentWords.FirstOrDefault(i => i.SearchWord.Word == kvp.Key);
if (searchContentWord != null)
{
word.Count = kvp.Value;
_searchContentRepository.UpdateSearchContentWords(word);
searchContentWord.Count = kvp.Value;
searchContentWord.ModifiedOn = DateTime.UtcNow;
_searchContentRepository.UpdateSearchContentWord(searchContentWord);
}
else
{
var wordSource = _searchContentRepository.GetSearchContentWordSource(kvp.Key);
if (wordSource == null)
var searchWord = _searchContentRepository.GetSearchWord(kvp.Key);
if (searchWord == null)
{
wordSource = _searchContentRepository.AddSearchContentWordSource(new SearchContentWordSource { Word = kvp.Key });
searchWord = _searchContentRepository.AddSearchWord(new SearchWord { Word = kvp.Key, CreatedOn = DateTime.UtcNow });
}
word = new SearchContentWords
searchContentWord = new SearchContentWord
{
SearchContentId = searchContent.SearchContentId,
WordSourceId = wordSource.WordSourceId,
Count = kvp.Value
SearchWordId = searchWord.SearchWordId,
Count = kvp.Value,
CreatedOn = DateTime.UtcNow,
ModifiedOn = DateTime.UtcNow
};
_searchContentRepository.AddSearchContentWords(word);
_searchContentRepository.AddSearchContentWord(searchContentWord);
}
}
}

View File

@ -31,7 +31,7 @@ namespace Oqtane.Repository
public virtual DbSet<UrlMapping> UrlMapping { get; set; }
public virtual DbSet<SearchContent> SearchContent { get; set; }
public virtual DbSet<SearchContentProperty> SearchContentProperty { get; set; }
public virtual DbSet<SearchContentWords> SearchContentWords { get; set; }
public virtual DbSet<SearchContentWordSource> SearchContentWordSource { get; set; }
public virtual DbSet<SearchContentWord> SearchContentWord { get; set; }
public virtual DbSet<SearchWord> SearchWord { get; set; }
}
}

View File

@ -7,18 +7,18 @@ namespace Oqtane.Repository
{
public interface ISearchContentRepository
{
Task<IEnumerable<SearchContent>> GetSearchContentListAsync(SearchQuery searchQuery);
Task<IEnumerable<SearchContent>> GetSearchContentsAsync(SearchQuery searchQuery);
SearchContent AddSearchContent(SearchContent searchContent);
void DeleteSearchContent(int searchContentId);
void DeleteSearchContent(string entityName, int entryId);
void DeleteSearchContent(string uniqueKey);
void DeleteAllSearchContent();
SearchContentWordSource GetSearchContentWordSource(string word);
SearchContentWordSource AddSearchContentWordSource(SearchContentWordSource wordSource);
SearchWord GetSearchWord(string word);
SearchWord AddSearchWord(SearchWord searchWord);
IEnumerable<SearchContentWords> GetWords(int searchContentId);
SearchContentWords AddSearchContentWords(SearchContentWords word);
SearchContentWords UpdateSearchContentWords(SearchContentWords word);
IEnumerable<SearchContentWord> GetSearchContentWords(int searchContentId);
SearchContentWord AddSearchContentWord(SearchContentWord searchContentWord);
SearchContentWord UpdateSearchContentWord(SearchContentWord searchContentWord);
}
}

View File

@ -17,13 +17,13 @@ namespace Oqtane.Repository
_dbContextFactory = dbContextFactory;
}
public async Task<IEnumerable<SearchContent>> GetSearchContentListAsync(SearchQuery searchQuery)
public async Task<IEnumerable<SearchContent>> GetSearchContentsAsync(SearchQuery searchQuery)
{
using var db = _dbContextFactory.CreateDbContext();
var searchContentList = db.SearchContent.AsNoTracking()
.Include(i => i.Properties)
.Include(i => i.Words)
.ThenInclude(w => w.WordSource)
.Include(i => i.SearchContentProperties)
.Include(i => i.SearchContentWords)
.ThenInclude(w => w.SearchWord)
.Where(i => i.SiteId == searchQuery.SiteId && i.IsActive);
if (searchQuery.EntityNames != null && searchQuery.EntityNames.Any())
@ -45,7 +45,7 @@ namespace Oqtane.Repository
{
foreach (var property in searchQuery.Properties)
{
searchContentList = searchContentList.Where(i => i.Properties.Any(p => p.Name == property.Key && p.Value == property.Value));
searchContentList = searchContentList.Where(i => i.SearchContentProperties.Any(p => p.Name == property.Key && p.Value == property.Value));
}
}
@ -54,7 +54,7 @@ namespace Oqtane.Repository
{
foreach (var keyword in SearchUtils.GetKeywordsList(searchQuery.Keywords))
{
filteredContentList.AddRange(await searchContentList.Where(i => i.Words.Any(w => w.WordSource.Word.StartsWith(keyword))).ToListAsync());
filteredContentList.AddRange(await searchContentList.Where(i => i.SearchContentWords.Any(w => w.SearchWord.Word.StartsWith(keyword))).ToListAsync());
}
}
@ -66,9 +66,9 @@ namespace Oqtane.Repository
using var context = _dbContextFactory.CreateDbContext();
context.SearchContent.Add(searchContent);
if(searchContent.Properties != null && searchContent.Properties.Any())
if(searchContent.SearchContentProperties != null && searchContent.SearchContentProperties.Any())
{
foreach(var property in searchContent.Properties)
foreach(var property in searchContent.SearchContentProperties)
{
property.SearchContentId = searchContent.SearchContentId;
context.SearchContentProperty.Add(property);
@ -117,7 +117,7 @@ namespace Oqtane.Repository
db.SaveChanges();
}
public SearchContentWordSource GetSearchContentWordSource(string word)
public SearchWord GetSearchWord(string word)
{
if(string.IsNullOrEmpty(word))
{
@ -125,45 +125,45 @@ namespace Oqtane.Repository
}
using var db = _dbContextFactory.CreateDbContext();
return db.SearchContentWordSource.FirstOrDefault(i => i.Word == word);
return db.SearchWord.FirstOrDefault(i => i.Word == word);
}
public SearchContentWordSource AddSearchContentWordSource(SearchContentWordSource wordSource)
public SearchWord AddSearchWord(SearchWord searchWord)
{
using var db = _dbContextFactory.CreateDbContext();
db.SearchContentWordSource.Add(wordSource);
db.SearchWord.Add(searchWord);
db.SaveChanges();
return wordSource;
return searchWord;
}
public IEnumerable<SearchContentWords> GetWords(int searchContentId)
public IEnumerable<SearchContentWord> GetSearchContentWords(int searchContentId)
{
using var db = _dbContextFactory.CreateDbContext();
return db.SearchContentWords
.Include(i => i.WordSource)
return db.SearchContentWord
.Include(i => i.SearchWord)
.Where(i => i.SearchContentId == searchContentId).ToList();
}
public SearchContentWords AddSearchContentWords(SearchContentWords word)
public SearchContentWord AddSearchContentWord(SearchContentWord searchContentWord)
{
using var db = _dbContextFactory.CreateDbContext();
db.SearchContentWords.Add(word);
db.SearchContentWord.Add(searchContentWord);
db.SaveChanges();
return word;
return searchContentWord;
}
public SearchContentWords UpdateSearchContentWords(SearchContentWords word)
public SearchContentWord UpdateSearchContentWord(SearchContentWord searchContentWord)
{
using var db = _dbContextFactory.CreateDbContext();
db.Entry(word).State = EntityState.Modified;
db.Entry(searchContentWord).State = EntityState.Modified;
db.SaveChanges();
return word;
return searchContentWord;
}
}
}

View File

@ -31,9 +31,9 @@ namespace Oqtane.Models
public string AdditionalContent { get; set; }
public IList<SearchContentProperty> Properties { get; set; }
public IList<SearchContentProperty> SearchContentProperties { get; set; }
public IList<SearchContentWords> Words { get; set; }
public IList<SearchContentWord> SearchContentWords { get; set; }
public override string ToString()
{

View File

@ -0,0 +1,24 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models
{
public class SearchContentWord
{
[Key]
public int SearchContentWordId { get; set; }
public int SearchContentId { get; set; }
public int SearchWordId { get; set; }
public int Count { get; set; }
public DateTime CreatedOn { get; set; }
public DateTime ModifiedOn { get; set; }
public SearchWord SearchWord { get; set; }
}
}

View File

@ -1,19 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Models
{
public class SearchContentWords
{
[Key]
public int WordId { get; set; }
public int SearchContentId { get; set; }
public int WordSourceId { get; set; }
public int Count { get; set; }
public SearchContentWordSource WordSource { get; set; }
}
}

View File

@ -1,12 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Oqtane.Models
{
public class SearchContentWordSource
public class SearchWord
{
[Key]
public int WordSourceId { get; set; }
public int SearchWordId { get; set; }
public string Word { get; set; }
public DateTime CreatedOn { get; set; }
}
}