对不起,我意识到这就像一本书。因此,我意识到这甚至可能不是最好的论坛,但是SO通常是我找到答案的第一个也是最好的地方。
这个问题必然是高级和抽象的。我们是ElasticSearch / Nest的新手,所以我们所做的很多事情可能都是疯狂和可怕的(他们几乎肯定是这样),这就是为什么我要寻找一些坚实的东西关于优化我们的应用程序的答案。
在阅读本文并制定响应时请记住,工程/架构问题导致许多选择。由于我们正在使用C#,并且我们的应用程序总是有些流畅,我们会花费大量精力来设计事物,使其能够容忍我们期望发生的变化,同时使这些变更尽可能简单。
也就是说我们使用该语言的结构来尝试确保,例如,添加要索引的新类型将需要在一些地方进行一些更改,对应于该类型的接口的新实现者等;或者,添加要搜索的新字段需要从前端到后端轻松跟进一组更改。
这需要严重依赖泛型,接口,具有反射的属性等,并且绝对避免类型检查,if / else / switch语句和字符串文字之类的事情。这意味着,例如,我们不做任何事情来手动在JSON中构建查询。相反,我们的代码将查看一个对象,查看对象的属性,查看属性上的属性,然后使用该属性来确定应该从该属性和对象创建哪种强类型的QueryContainer。 s值,然后交给NEST做任何事情。
我们的目标是让事情尽快失败,希望在编译时失败,如果它们碰巧在运行时,那么它们会在应用程序启动时立即发生,而不是在执行随机功能时发生。因此,JavaScript开发人员可能会因我们的问题或引起我们选择的问题而完全混淆: - )
结束前言
因此,请考虑以下情况:
类型:action,document,documentText
操作可以与许多文档相关联,文档可以与许多操作相关联。一个动作上有可以搜索的元数据字段,它还包含文档关联(作为ID' s)。文档上有许多可以搜索的元数据字段。该文档还可以搜索其全文。我们只需要/需要返回文档中存在的数据子集以及突出显示特定文档被击中的原因。
我们有一个基本的搜索类型,允许用户输入一个或多个单词。如果该查询与操作中的任何内容匹配,则应返回与该操作关联的任何文档。还应返回具有响应查询的元数据的任何文档。同样,如果文档文本是响应式的,那么也应该返回这些文档。
我们还有一个高级搜索,允许用户搜索特定的操作元数据,文档元数据或文档文本(文本搜索允许"所有单词","任何单词" ,"没有单词","精确短语")。在这种情况下,仅当文档在查询的每个阶段都匹配时才返回文档,即它必须与匹配操作查询的操作相关联,并且文档查询和documentText查询都是响应的。
每种类型都位于自己的索引中。我们希望生产环境拥有数十万到数百万的文档(全文)。
我们目前通过5遍搜索完成此任务:
1)搜索操作(高级搜索的逐字段,基本搜索的_all)。来自:0尺寸:非常大的东西 --->为什么?因为我们需要找到可能与任何匹配操作相关联的所有文档。从该查询的结果集中提取关联的文档ID。那么他们就是......
2)作为IdsQuery和文本查询一起输入documentText查询。此通行证禁用来源和突出显示。然后,以相同的方式将该搜索的结果传递给文档元数据。如果它是一个基本搜索,那么IdsQuery就会成为一个"应该",在高级搜索中,他们必须"必须"。来自:0尺寸:非常大的东西最终,我们只会显示10个结果的页面。但是,我们需要查找响应此查询传递的所有文档,因为无法保证前10个(甚至1,000或10,000)将响应文档元数据传递,在这种情况下,我们冒险得不到我们想要的10次点击的完整结果集。所以,ID再次......
3)进入文档元数据传递,与上面相同。同样,源和突出显示被禁用。来自:用户规定的页面大小:10。这些是最终的结果。那十个人就是......
4)反馈到另一个文档元数据传递。源和突出显示已启用,因此我们可以返回要显示的内容。同样的十个ID是......
5)输入另一个文档文本传递。仅启用突出显示
我们在代码中聚合高光,然后将10个结果传回给用户。
以上问题是它不能很好地扩展。完全没有。我现在有一个拥有一百万份文件的系统。我从测试数据中大量摄取这些文档,其中600,000个是相同的。如果我输入与本文档匹配的单词,我会在服务器端代码中获得以下时序(使用RedGate ANTs性能分析器):
传递1:微不足道(仅因为这个词没有匹配任何东西;最终,这个传递必须像上面那样进行优化,例如,因此不是每个结果都被突出显示而且并非所有来自源的字段都返回到初次通过)
通过2:21 秒
传递3:11.2 秒(请记住,这个包含一个ID为600,000+的IdsQuery,这对我来说有点意义)
通过4:4.8秒(对我来说完全混淆)
传递5:58 毫米秒(几乎是我所期待的)
现在,我想我明白为什么Pass 2需要这么长时间。这是因为它必须搜索,评分和排序600,000个文档(是的,我们想要对这些文档进行评分,因为我们优先考虑文档文本中的命中,尽管我们还不知道如何评分不同的索引)。至少,我以为我理解为什么。但是,如果我输入类似" pdf"这样的内容,可以在某些文档中找到'文本,但大多数文档元数据(作为索引文件名),然后我的第一页快速返回闪电(可能是几毫秒),尽管有500,000多次点击。
我知道你可能想知道:为什么文档文本与文档元数据分开?好问题。 这可能会消失。我们实际只是做了这个改变,因为我们在索引方面遇到了很大的问题。问题实际上是在文件I / O中,也是从PDF中提取文本,这样重新索引一百万个文档需要5-7天,而且这些文档是在相对较短的测试文档上进行的,而不是我们期望在生产中获得600多页的文档。
由于文档元数据的模式预计在我们的前几个版本中非常流畅,因此我们确定必须采用匹配来重新索引未经过更改的文本。虽然这发生在离线状态,但5天仍然是一个非常漫长的时间。此外,我们将这些索引用于其他目的,这些目的与文档文本无关,因此我们可能希望在不丢失文本的情况下索引或重新索引文档元数据。
这可能会消失,因为 a)我们现在知道滚动功能,我们希望这将允许我们重新索引所有内容而无需处理文件I / O和 b)因为我们换掉了我们的PDF提取库,这大大加快了读取过程,所以即使现在文件I / O也可能不是问题。
不幸的是,将文档文本拆分为自己的索引是在文件I / O优化的同时完成的(这最终使我们能够将足够的数据输入系统以揭示我们的大规模扩展问题)。也就是说,如果文档搜索不需要多次传递,那么我们的查询对这个巨大的结果集的持续时间没有任何基准。
所以,最后,有些问题:
我们在哪里可以获得最大的性能提升?现在,我们在黑暗中捅了一下。
我们是否应该将文档文档和文档元数据放回到相同的索引和类型中?这将需要工程工作来重新设计/重新构建我们的索引代码和基础结构,但如果它带来了我们到合理的搜索时间,那就这样吧。
我们如何有效地将结果从一个查询传递到另一个查询传递?使用600,000个结果执行IdsQuery看起来似乎本来就很慢。但即使我们对文档执行上述操作,我们仍然会遇到与操作相同的问题。我知道规范的答案可能是对所有内容进行去规范化,并将它们全部放入同一个索引中并进行一次搜索。但考虑到非规范化的所有固有问题并使所有内容保持同步,我们认为这将是站不住脚的。
我们可以做些什么来优化我们的环境?现在,我们有3个不同的索引,每个索引都有自己的类型。每个索引都有默认的5个分片。通过增加/减少分片数量,将类型放入相同的索引等,可以获得任何收益吗?
Scroll是我们应该用于分页的那种东西吗?我们的产品所有者坚持要求返回所有结果,并允许用户分页到任何内容(甚至第63000页)。我们坚持认为这是完全愚蠢的,并且导致许多对最终用户毫无用处。工程团队宁愿限制结果的数量。 但是, 某些用例需要获取所有结果。那么,滚动API是用于在UI中进行常规分页的东西,还是用于更有效的批处理(例如重新索引)的东西?我们宁愿使用相同的方法/代码来获得这些结果。因此,我们可以限制用户看到的结果,并且需要所有结果的用例可以在后台/离线处理中完成。相反,如果滚动允许用户进行深度分页,那么我们可以对两者使用该方法,而不限制结果集。但是,我不会想象滚动会帮助我们遍历随机页面。
我还没有想到的任何事情?正如我所说,我们是新手,所以我们可能并不了解ElasticSearch的很多功能。有没有办法将不同索引中不同类型的搜索结合到一个查询中,以便ElasticSearch和/或NEST完成所有工作?我们的思维在哪里出错?
欢迎所有输入。
更新
以下是POCO。在大多数情况下,表示元数据的属性是唯一需要在此处考虑的内容。自定义属性仅与在创建索引时创建类型映射相关。这里选择的一些东西是不可搜索的,但是被编入索引以向用户显示。
[ElasticsearchType]
public class IndexDocument : GridViewable {
[Column(Analyzer = ElasticConstants.STANDARD_ENGLISH_ANALYZER)]
public string Name { get; set; }
[Column]
public string Markings { get; set; }
[Column(IncludeInAll = false)]
public string SerialNumber { get; set; }
[Column]
public string Classification { get; set; }
[Column]
public string Category { get; set; }
[Column]
public string Series { get; set; }
[Column]
public string CreatedBy { get; set; }
[Column]
public string CheckedOutTo { get; set; }
[Column]
public string Locations { get; set; }
[Nested]
public IList<IndexNeedToKnow> NeedToKnow { get; } = new ListWithDefault<IndexNeedToKnow>(IndexNeedToKnow.EMPTY_NTK);
[Object]
public IList<IndexAuditLog> AuditLog { get; } = new List<IndexAuditLog>();
[SearchDate]
public string DateOfRecord { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string Disposition { get; set; }
[SearchDate]
public string DeclassificationDate { get; set; }
[SearchDate]
public string FutureDispositionDate { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue, IncludeInAll = false)]
public string HasPii { get; set; }
[SearchDate]
public string FutureReviewDate { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string RecordType { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string Saccp { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string RecordStatus { get; set; }
[Object(IncludeInAll = false)]
public IList<ActionAssociation> AssociatedActions { get; } = new ListWithDefault<ActionAssociation>(ActionAssociation.NO_ACTION);
[String(IncludeInAll = false)]
public string Mime { get; set; }
[Number(IncludeInAll = false)]
public int Version { get; set; }
}
[ElasticsearchType]
public class IndexAction : GridViewable, ISuggestable<IndexActionSuggestionPayload> {
[Column]
public string Name { get; set; }
[Completion(Analyzer = Searcher.LOWERCASE_KEYWORD_ANALYZER, Payloads = true)]
public SuggestionField<IndexActionSuggestionPayload> Suggestion
{
get {
return new SuggestionField<IndexActionSuggestionPayload> {
Input = new[] { Name },
Output = Name,
Payload = new IndexActionSuggestionPayload {
Id = Id,
}
}; }
}
[Column(IncludeInAll = false)]
public string Program { get; set; }
[Column]
public string ActionOfficer { get; set; }
[Column]
public string Manager { get; set; }
[Column]
[SearchDate]
public string Suspense { get; set; }
[Column]
public List<string> Categories { get; set; }
[Column]
public string FiscalYear { get; set; }
[Column]
public string FiscalQuarter { get; set; }
[Column]
public string State { get; set; }
[Column]
[SearchDate]
public string Close { get; set; }
[Column]
public string CreatedBy { get; set; }
public List<IndexApprovers> Approvers { get; set; } = new List<IndexApprovers>();
[Object]
public List<IndexActionTask> ActionTasks { get; set; } = new List<IndexActionTask>();
public string ActivityCount { get; set; }
[SearchDate]
public string ApprovalDate { get; set; }
public string ApprovalRole { get; set; }
[Nested]
public List<IndexOrganization> Organizations { get; set; } = new List<IndexOrganization>();
[FreeText]
public string Description { get; set; }
public List<IndexExternalRefNumber> ExternalReferenceNumbers { get; set; }
[Number(NumberType.Float)]
public string FinalCost { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string IsCovertAction { get; set; }
public string LegalJustification { get; set; }
public List<string> Locations { get; set; }
[FreeText]
public string Notes { get; set; }
public List<string> PointsOfContact { get; set; }
[Number(NumberType.Float)]
public string ProjectedCost { get; set; }
public string ProjectName { get; set; }
[String(NullValue = IndexDefaultValue.DefaultNullValue)]
public string IsStaffingRequired { get; set; }
public string Status { get; set; }
[String(IncludeInAll = false, Index = FieldIndexOption.NotAnalyzed)]
public List<Guid> DocumentIDs{ get; set; } = new List<Guid>();
public string Classification { get; set; }
}
[ElasticsearchType]
public class IndexDocumentText : GridViewable {
[FreeText(IncludeInAll = false)]
public string Text { get; set; }
[Nested]
public IList<IndexNeedToKnow> NeedToKnow { get; } = new ListWithDefault<IndexNeedToKnow>(IndexNeedToKnow.EMPTY_NTK);
}
这里是根据&#39;基本&#39;创建的查询的JSON。搜索:
在indexaction index
上传递1{
"timeout": "120s",
"from": 0,
"size": 10000000,
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{
"match": {
"_all": {
"query": "pornography",
"operator": "and"
}
}
}
]
}
}
]
}
},
"highlight": {
"pre_tags": [
"<b>"
],
"post_tags": [
"</b>"
],
"number_of_fragments": 10,
"fields": {
"name": {},
"name.highlight": {},
"actionOfficer": {},
"actionOfficer.highlight": {},
"manager": {},
"manager.highlight": {},
"suspense": {},
"suspense.highlight": {},
"categories": {},
"categories.highlight": {},
"fiscalYear": {},
"fiscalYear.highlight": {},
"fiscalQuarter": {},
"fiscalQuarter.highlight": {},
"state": {},
"state.highlight": {},
"close": {},
"close.highlight": {},
"activityCount": {},
"approvalDate": {},
"approvalDate.highlight": {},
"approvalRole": {},
"description": {},
"externalReferenceNumbers.name": {},
"finalCost": {},
"finalCost.highlight": {},
"isCovertAction": {},
"legalJustification": {},
"locations": {},
"notes": {},
"pointsOfContact": {},
"projectedCost": {},
"projectedCost.highlight": {},
"projectName": {},
"isStaffingRequired": {},
"status": {},
"actionTasks.completionDate": {},
"actionTasks.dueDate": {},
"actionTasks.description": {},
"actionTasks.owner": {},
"actionTasks.status": {},
"organizations.name": {}
},
"require_field_match": false
}
}
在indexdocumenttext索引上传递2;注意ids查询,它是从前一个传递的结果中提取的(是的,我只是注意到它在那里两次,它不应该是)。底部的过滤器是安全检查的一部分。另外,请注意_source: { "exclude": ["*"] }
。这表明我们(使用Head插件)比"_source" : false}
慢,但NEST莫名其妙地删除了该功能。
{
"timeout": "120s",
"from": 0,
"size": 10000000,
"_source": {
"exclude": [
"*"
]
},
"query": {
"bool": {
"must": [
{
"bool": {
"must": [
{
"ids": {
"values": [
"00000000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-000000000003"
]
}
}
]
}
},
{
"bool": {
"should": [
{
"bool": {
"must": [
{
"match": {
"text": {
"query": "pornography",
"fuzziness": 1,
"operator": "and"
}
}
}
],
"should": [
{
"match": {
"text": {
"query": "pornography",
"fuzziness": 1,
"slop": 20,
"type": "phrase"
}
}
}
]
}
},
{
"ids": {
"values": [
"00000000-0000-0000-0000-000000000000",
"00000000-0000-0000-0000-000000000003"
],
"boost": 2
}
}
]
}
},
{
"bool": {
"filter": [
{
"nested": {
"query": {
"bool": {
"should": [
{
"bool": {
"must": [
{
"match": {
"needToKnow.id": {
"query": "1"
}
}
},
{
"match": {
"needToKnow.type": {
"query": "1"
}
}
}
]
}
},
{
"bool": {
"must": [
{
"match": {
"needToKnow.id": {
"query": "0"
}
}
},
{
"match": {
"needToKnow.type": {
"query": "0"
}
}
}
]
}
}
]
}
},
"path": "needToKnow"
}
}
]
}
}
]
}
}
}
Pass 3看起来大致相同,除了搜索词在_all字段上,我们只需要10个结果,id查询可能是巨大的。高级搜索看起来与这些看起来大致相同,只是bool查询数组中会有更多项目,每个项目都将针对具有特定值的特定字段。此外,在这种情况下,ids
查询将是must
,因为我们只想返回响应每个字段的结果。