我们最近从ElasticSearch 1.5更新到5.6,解决了诸如现场爆炸和其他问题。但是,现在我们处于较新版本,我们看到了令人无法接受的(并且非常荒谬)性能问题。
Hits | Took | Nest 1.5 | Nest 5.6
---------------------------------
0 | 1ms | 100ms | 1190ms
1 | 1ms | 100ms | 720ms
2 | 4ms | 100ms | 350ms
42 | 10ms | 1100ms | 3270ms
63 | 9ms | 1700ms | 4700ms
100 | 25ms | 2800ms | 7400ms
我们有一个使用SingleNodeConnectionPool的静态Nest客户端。查询非常简单,我们正在分页更大的结果(通常不超过100)。当我们在1.5时,所有这些查询在3秒内回来。为什么Nest请求现在比以前慢3到4倍?
构建索引
PatternAnalyzer alphanumericAnalyzer = new PatternAnalyzer();
alphanumericAnalyzer.Lowercase = true;
alphanumericAnalyzer.Pattern = "[^a-zA-Z0-9áéíñóúüÁÉÍÑÓÚÜàâäôéèëêïîçùûüÿæœÀÂÄÔÉÈËÊÏΟÇÙÛÜÆŒäöüßÄÖÜẞàèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]";
CustomAnalyzer lowercaseKeywordAnalyzer = new CustomAnalyzer();
lowercaseKeywordAnalyzer.Tokenizer = "keyword";
lowercaseKeywordAnalyzer.Filter = new List<string>() { "lowercase" };
IndexSettings indexSettings = new IndexSettings();
indexSettings.NumberOfReplicas = NestClient.Config.Replicas;
indexSettings.NumberOfShards = NestClient.Config.Shards;
indexSettings.Analysis = new Analysis();
indexSettings.Analysis.Analyzers = new Analyzers();
indexSettings.Analysis.Tokenizers = new Tokenizers();
indexSettings.Analysis.Analyzers.Add("alphanumeric_analyzer", alphanumericAnalyzer);
indexSettings.Analysis.Analyzers.Add("keyword_analyzer", lowercaseKeywordAnalyzer);
indexSettings.Analysis.Tokenizers.Add("ngrams_tokenizer", new EdgeNGramTokenizer()
{
MaxGram = NestClient.Config.MaxGram,
MinGram = NestClient.Config.MinGram,
TokenChars = new List<TokenChar>()
{
TokenChar.Letter,
TokenChar.Digit
}
});
indexSettings.Analysis.Analyzers.Add("ngrams_analyzer", new CustomAnalyzer()
{
Filter = new List<string>()
{
"lowercase"
},
Tokenizer = "ngrams_tokenizer"
});
var createResponse = Client.CreateIndex(new CreateIndexRequest(IndexName)
{
Settings = indexSettings
}).Log(isIndexRebuild: true);
var mapResult = Client.Map<SearchAsset>(m => m
.AllField(x => x.Enabled(false))
.AutoMap()
).Log(isIndexRebuild: true);
搜索资产
[ElasticsearchType(IdProperty = "assetID")]
public class SearchAsset
{
public SearchAsset()
{
Extensions = new List<string>();
Metadata = new List<MetadataValue>();
Notes_Alphanumeric = new List<string>();
Notes_Ngrams = new List<string>();
UserFlags = new List<long>();
AssetTypes = new List<string>();
}
private string filename;
public long AssetID { get; set; }
public long JobID { get; set; }
public long JobFolderID { get; set; }
public int Width { get; set; }
public int Height { get; set; }
[Text(Analyzer = "keyword_analyzer", Fielddata = true)]
public string StorageFolderPath { get; set; }
public bool Selected { get; set; }
public long? SelectUserID { get; set; }
public DateTime? SelectDateTime { get; set; }
[Text(Analyzer = "keyword_analyzer", Fielddata = true)]
public string JobFolderName { get; set; }
[Text(Analyzer = "keyword_analyzer", Fielddata = true)]
public string Filename
{
get { return filename; }
set { filename = Filename_Alphanumeric = Filename_Ngrams = value; }
}
[Text(Analyzer = "alphanumeric_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public string Filename_Alphanumeric { get; private set; }
[Text(Analyzer = "ngrams_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public string Filename_Ngrams { get; private set; }
[Text(Analyzer = "keyword_analyzer", Fielddata = true)]
public string OriginalTypeCd { get; set; }
public int NoteCount { get; set; }
public int PageCount { get; set; }
public long Color { get; set; }
public bool HasMarkup { get; set; }
public long Status { get; set; }
public int TotalGalleryCount { get; set; }
public int ClosedGalleryCount { get; set; }
//HACK: We would ideally script these in ES, but Nest/Painless has poor documentation, and we have yet to get something working within that framework.
//Doing it here actually works, so relying on that instead.
public bool NoStatus { get { return TotalGalleryCount == 0; } }
public bool Flagged { get { return UserFlags.Count > 0; } }
public bool NotPending { get { return TotalGalleryCount > 0 && TotalGalleryCount == ClosedGalleryCount; } }
public bool Pending { get { return TotalGalleryCount > ClosedGalleryCount; } }
public bool Notes { get { return NoteCount > 0; } }
public long ByteCount { get; set; }
public DateTime AddedOn { get; set; }
[Object(Ignore = true)]
public IndexItemType IndexItemType { get; set; }
[Text(Analyzer = "keyword_analyzer", Fielddata = true)]
public List<string> Extensions { get; set; }
[Number]
public List<long> UserFlags { get; set; }
[Nested]
public List<MetadataValue> Metadata { get; private set; }
[Text]
public List<string> AssetTypes { get; set; }
[Text(Analyzer = "ngrams_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public List<string> Notes_Ngrams { get; private set; }
[Text(Analyzer = "alphanumeric_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public List<string> Notes_Alphanumeric { get; private set; }
}
public class MetadataValue
{
public long MetadataID { get; set; }
[Text(Analyzer = "ngrams_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public string Ngrams { get; set; }
[Text(Analyzer = "alphanumeric_analyzer", SearchAnalyzer = "alphanumeric_analyzer")]
public string Alphanumeric { get; set; }
public DateTime Date { get; set; }
}
嵌套查询代码
public SearchResult RunSearch(IUser user, AssetCollection collection, Aggregations aggregations = null, FieldSelectors selectors = null)
{
var elasticSearchManager = new ElasticSearchManager();
var query = ElasticSearchHelper.BuildWhereExpression(user, collection);
var sorts = ElasticSearchHelper.BuildOrderExpression<SearchAsset>(user, collection);
//We want to specify the SearchAsset type so that we can both specify an index on the request, and also rely on the type mapping in the settings
var request = new SearchRequest<SearchAsset>(elasticSearchManager.IndexName);
request.Sort = sorts;
request.Query = query;
request.From = collection.FirstIndex;
request.DocvalueFields = selectors.ElasticSearchFields.ToArray();
request.Size = collection.LastIndex - collection.FirstIndex;
request.Aggregations = new AggregationDictionary();
//INFO: This allows us to log the NEST request body
request.RequestConfiguration = new RequestConfiguration();
request.RequestConfiguration.DisableDirectStreaming = true;
var searchResponse = elasticSearchManager.Client.Search<SearchAsset>(request);
ESLogger.LogElasticSearchResponse(searchResponse);
Logger.Instance.LogInfo(new LogMessage(LogMessageAction.Search,
new SearchContextLogData<SearchAsset>(searchResponse, collection),
new UserContextLogData(user)
));
SearchResult result = new SearchResult()
{
TotalCount = aggregations.CalculateTotalCount ? searchResponse.Total : 0
};
foreach (var searchAsset in searchResponse.Hits)
{
var asset = selectors.ApplyElasticSearchToAssetSelectors(searchAsset);
result.Assets.Add(asset);
}
return result;
}
ElasticSearchManager
public class ElasticSearchManager
{
public IElasticClient Client { get; private set; }
public virtual string IndexName
{
get { return NestClient.IndexName; }
}
public ElasticSearchManager()
{
Client = NestClient.GetClient(null);
}
public IEnumerable<string> Tokenize(string field, string input)
{
var key = field + "_" + input;
var tokens = GetCachedTokens(key);
if (tokens != null)
return tokens;
tokens = new List<string>();
var response = Client.Analyze(x => x.Field(field).Index(IndexName).Text(input)).Log();
if (response.IsValid)
{
foreach (var token in response.Tokens)
{
tokens.Add(token.Token);
}
}
CacheTokens(key, tokens);
return tokens;
}
public IEnumerable<string> Tokenize<T>(Expression<Func<T, object>> field, string input)
where T : class
{
var fieldName = field.Body.ToString();
return Tokenize(fieldName, input);
}
private void CacheTokens(string input, IEnumerable<string> tokens)
{
if (HttpContext.Current != null)
{
HttpContext.Current.Items.Add(input, tokens);
}
}
private List<string> GetCachedTokens(string input)
{
if (HttpContext.Current != null)
{
return HttpContext.Current.Items[input] as List<string>;
}
return null;
}
private void LoadMappings()
{
var mapResult = Client.Map<SearchAsset>(m => m
.AllField(x => x.Enabled(false))
.AutoMap()
).Log(isIndexRebuild: true);
}
}
NestClient
//INFO: This class is a singleton for a reason
//Under the hood, Nest instantiate the Newtonsoft deserializer fresh for each instantiation of the client
//Therefore, if you instantiate the client fresh every request, then your deserializer gets 3x-4x slower
//Preserving the client as a singleton mitigates this cost
public static class NestClient
{
public static string IndexName { get; private set; }
public static ElasticSearchConfig Config
{
get
{
return SettingsManager.ElasticSearchConfig<ElasticSearchConfig>();
}
}
private static object syncRoot = new object();
private static IElasticClient instance = null;
public static IElasticClient GetClient(string indexName = null)
{
if (instance == null)
{
IndexName = indexName;
//if name != null, it will override the current index name in the db
//use to create a new index, then update db when index is done building
if (string.IsNullOrEmpty(indexName))
{
IndexName = Config.IndexName;
}
var uri = new Uri(Config.Url);
var pool = new SingleNodeConnectionPool(uri);
var settings = new ConnectionSettings(pool);
settings.DefaultIndex(IndexName);
//INFO: We want the SearchAsset object to be hard-bound to the index
settings.InferMappingFor<SearchAsset>(m => m.IndexName(IndexName));
instance = new ElasticClient(settings);
}
return instance;
}
}
示例查询输出
答案 0 :(得分:2)
事实证明,最糟糕的罪犯根本不是直接的NEST,而是后来的代码,它将ElasticSearch文档的C#表示转换为我们的业务级对象。
foreach (var searchAsset in searchResponse.Hits)
{
var asset = selectors.ApplyElasticSearchToAssetSelectors(searchAsset);
result.Assets.Add(asset);
}
我们的选择器(真正的翻译器/适配器)使用反射来查看ES文档并将它们转换为更永久的对象。
public class FullFieldSelectors : FilenameFieldSelectors
{
private readonly AssetTypesManager assetTypesManager;
private readonly long userID;
public FullFieldSelectors(long userID)
{
assetTypesManager = new AssetTypesManager();
this.userID = userID;
}
public override Asset ConvertToAsset(IHit<SearchAsset> hit)
{
var asset = base.ConvertToAsset(hit);
var searchAsset = hit.Source;
asset.JobID = hit.Fields.Values<SearchAsset, long>(f => f.JobID).FirstOrDefault();
asset.FolderID = hit.Fields.Values<SearchAsset, long>(f => f.JobFolderID).FirstOrDefault();
asset.PlusRating = hit.Fields.Values<SearchAsset, long>(f => f.Rating).FirstOrDefault();
asset.Select = hit.Fields.Values<SearchAsset, bool>(f => f.Selected).FirstOrDefault();
asset.Alt = hit.Fields.Values<SearchAsset, bool>(f => f.Alted).FirstOrDefault();
asset.Approve = hit.Fields.Values<SearchAsset, bool>(f => f.Approved).FirstOrDefault();
asset.Kill = hit.Fields.Values<SearchAsset, bool>(f => f.Killed).FirstOrDefault();
asset.Flag = ConvertNullEnumerable(hit.Fields.Values<SearchAsset, long>(f => f.UserFlags.Find(u => u == userID))).Contains(userID);
asset.Color = (AssetColorCd)hit.Fields.Values<SearchAsset, long>(f => f.Color).FirstOrDefault();
asset.FileExtension = ConvertNullEnumerable(hit.Fields.Values<SearchAsset, string>(f => f.Extensions.FirstOrDefault())).FirstOrDefault();
asset.OriginalType = assetTypesManager.Restore(ConvertNullEnumerable(hit.Fields.Values<SearchAsset, string>(f => f.OriginalTypeCd)).FirstOrDefault());
asset.NoteCount = hit.Fields.Values<SearchAsset, int>(f => f.NoteCount).FirstOrDefault();
asset.Status = (AssetStatus)hit.Fields.Values<SearchAsset, long>(f => f.Status).FirstOrDefault();
asset.ClosedGalleryCount = hit.Fields.Values<SearchAsset, int>(f => f.ClosedGalleryCount).FirstOrDefault();
asset.Finalized = hit.Fields.Values<SearchAsset, bool>(f => f.Finalized).FirstOrDefault();
asset.TotalGalleryCount = hit.Fields.Values<SearchAsset, int>(f => f.TotalGalleryCount).FirstOrDefault();
asset.Width = hit.Fields.Values<SearchAsset, int>(f => f.Width).FirstOrDefault();
asset.Height = hit.Fields.Values<SearchAsset, int>(f => f.Height).FirstOrDefault();
asset.PageCount = hit.Fields.Values<SearchAsset, int>(f => f.PageCount).FirstOrDefault();
asset.ByteCount = hit.Fields.Values<SearchAsset, long>(f => f.ByteCount).FirstOrDefault();
asset.HasMarkup = hit.Fields.Values<SearchAsset, bool>(f => f.HasMarkup).FirstOrDefault();
asset.StorageFolderPath = ConvertNullEnumerable(hit.Fields.Values<SearchAsset, string>(f => f.StorageFolderPath)).FirstOrDefault();
asset.NewStorageLocation = ConvertNullEnumerable(hit.Fields.Values<SearchAsset, bool>(f => f.NewStorageLocation)).FirstOrDefault();
asset.Archived = ConvertNullEnumerable(hit.Fields.Values<SearchAsset, bool>(f => f.Archived)).FirstOrDefault();
if (hit.Source != null && hit.Source.Lightboxes != null && hit.Source.Lightboxes.Count > 0)
{
asset.LightboxAsset = new LightboxAsset()
{
AddedBy = hit.Source.Lightboxes.First().AddedBy,
AssetID = asset.ID,
LightboxID = hit.Source.Lightboxes.First().LightboxID,
SeqOrder = hit.Source.Lightboxes.First().OrderID
};
}
return asset;
}
}
当我们在1.X并且有场爆炸时,这是有道理的,因为我们从来不知道文档会或不会有哪些字段。一旦我们达到5.X,并且我们用子文档固定了场爆炸,翻译变得更加稳定和可靠,因此不再需要反射的开销。
public class FullFieldSelectors : FilenameFieldSelectors
{
private readonly AssetTypesManager assetTypesManager;
private readonly long userID;
public FullFieldSelectors(long userID)
{
assetTypesManager = new AssetTypesManager();
this.userID = userID;
}
public override Asset ConvertToAsset(IHit<SearchAsset> hit)
{
var asset = base.ConvertToAsset(hit);
var searchAsset = hit.Source;
asset.JobID = searchAsset.JobID;
asset.FolderID = searchAsset.JobFolderID;
asset.PlusRating = searchAsset.Rating;
asset.Select = searchAsset.Selected;
asset.Alt = searchAsset.Alted;
asset.Approve = searchAsset.Approved;
asset.Kill = searchAsset.Killed;
asset.Flag = searchAsset.UserFlags != null && searchAsset.UserFlags.Contains(userID);
asset.Color = (AssetColorCd)searchAsset.Color;
asset.FileExtension = string.Empty;
if (searchAsset.Extensions != null && searchAsset.Extensions.Any())
asset.FileExtension = searchAsset.Extensions.First();
asset.OriginalType = assetTypesManager.Restore(searchAsset.OriginalTypeCd);
asset.NoteCount = searchAsset.NoteCount;
asset.Status = (AssetStatus)searchAsset.Status;
asset.ClosedGalleryCount = searchAsset.ClosedGalleryCount;
asset.Finalized = searchAsset.Finalized;
asset.TotalGalleryCount = searchAsset.TotalGalleryCount;
asset.Width = searchAsset.Width;
asset.Height = searchAsset.Height;
asset.PageCount = searchAsset.PageCount;
asset.ByteCount = searchAsset.ByteCount;
asset.HasMarkup = searchAsset.HasMarkup;
asset.StorageFolderPath = searchAsset.StorageFolderPath ?? string.Empty;
asset.NewStorageLocation = searchAsset.NewStorageLocation;
asset.Archived = searchAsset.Archived;
if (searchAsset.Lightboxes != null && searchAsset.Lightboxes.Any())
{
var searchLightbox = searchAsset.Lightboxes.First();
asset.LightboxAsset = new LightboxAsset()
{
AddedBy = searchLightbox.AddedBy,
AssetID = asset.ID,
LightboxID = searchLightbox.LightboxID,
SeqOrder = searchLightbox.OrderID
};
}
return asset;
}
}
这使我们的查询总计不到3秒(平均100次点击约2.3秒)。