在公司信息系统中进行搜索 -已经从该短语本身中获取信息。 如果您只有一个,那就太好了,甚至不必考虑正面的用户体验。 如何扭转被搜索引擎宠坏的用户的态度,并创建一种快速,准确,可完全理解的产品? 我们需要学习很多Elasticsearch和一些智能服务,并将其与本指南相结合。
关于如何将基于Elasticsearch的全文搜索固定到现有数据库的文章很多。 但是显然没有足够的文章介绍如何进行真正的智能搜索。
同时,“智能搜索”一词本身已成为流行语,并不习惯于该地点。 那么,为了被视为聪明的搜索引擎应该做什么? 最终,这可以描述为给出用户实际需要的结果,即使该结果与请求的文本不完全匹配。 诸如Google和Yandex之类的流行搜索引擎走得更远,不仅找到所需信息,还直接回答用户问题。
好的,我们不会立即做出最后通decision的决定,但是如何使常规的全文本搜索更接近明智的选择呢?
智力要素
智能搜索-这种情况就是数量可以转化为质量,而许多小的和相当简单的功能可以形成一种魔幻的感觉。
- 纠正用户错误-是错别字,布局错误还是结果数量可疑的请求,但类似于具有更多信息的请求。
- 对于
日 NLP聊天(自然语言处理,不是您想的那样)-如果用户输入了去年的商业报价,那么他是否真的想在所有文档的文本中搜索这些单词,还是仅在去年才真正需要商业报价? ? - 根据先前的查询或常用文档预测输入。
- 结果的呈现是找到的片段的通常亮点,其他信息取决于您要寻找的内容。 由于在上一段中需要商业提案,因此立即显示提案的主题以及它来自哪个组织的做法也许有意义。
- 轻松钻取-使用其他过滤器,构面来优化搜索查询的能力。
介绍性
有一个ECM目录,其中包含许多文档。 该文档由一张带有元信息的卡和一个正文组成,正文可以有多个版本。
目标是以常规方式为搜索引擎用户快速方便地搜索这些文档中的信息。
索引编制
为了寻找合适的东西,您需要首先对其进行索引。
ECM中的文档不是静态的,用户可以修改文本,创建新版本,更改卡中的数据; 新文档不断创建,旧文档有时被删除。
为了在Elasticsearch中保持最新信息,文档需要不断地重新索引。 幸运的是,ECM已经拥有自己的异步事件队列,因此当您更改文档时,只需将其添加到队列中以进行索引。
将ECM文档映射到Elasticsearch文档
ECM中的文档正文可以具有多个版本。 在Elasticsearch中,这可能被认为是嵌套对象的数组,但是使用它们却变得不便-编写查询变得更加困难,当更改其中一个版本时,您需要重新索引所有内容,同一文档的不同版本无法存储在不同的索引中(为什么需要-在下一节中)。 因此,我们将从一张ECM中将一个文档反规范化为具有相同卡但主体不同的多个Elasticsearch文档。
除了卡和主体之外,Elasticsearch文档中还添加了各种服务信息,值得一提:
- 有权使用该文档的组和用户的ID列表-用于进行权利搜索;
- 调用文档的次数-用于调整相关性;
- 最后索引的时间。
指数构成
是的,复数索引。 通常,仅当此信息是不可变的并且与某种时间段(例如,日志)相关联时,才使用几个索引来存储在Elasticsearch中含义相似的信息。 然后,每个月/天或更频繁地根据负载强度创建索引。 在我们的情况下,任何文档都可以更改,并且可以将所有内容存储在一个索引中。
但是-系统中的文档可以使用不同的语言,并且在Elasticsearch中存储多语言数据会带来两个问题:
- 错误的词干。 对于某些单词,将正确找到基数,对于某些单词-错误地(索引中将存在另一个单词),对于某些单词-根本将找不到(索引将被单词形式阻塞)。 对于来自不同语言,具有不同含义的某些单词,依据将是相同的,然后单词的含义将丢失。 连续使用多个茎杆可能会导致对已经计算出的茎杆进行额外的计算。
扼杀-寻找单词的基础。 词根不必是单词的词根或其正常形式。 通常,将相关词投射到一个框架中就足够了。
词法化是词干的一种类型,其中单词的正常(词汇)形式被认为是基础。
- 字频不正确。 ES中的某些相关性确定机制会考虑文档中搜索到的单词的频率(频率越高,相关性越高)和索引中搜索到的单词的频率(频率越高,相关性越低)。 因此,当索引主要是英语文档时,在英语文档中少量传播俄语语音将具有较高的权重,但是值得在索引中混合使用英语和俄语文档,并且权重会降低。
当不同的语言使用不同的字符集(俄语-英语文档使用西里尔字母和拉丁字母)时,第一个问题可以解决-语言提取器仅处理“其”字符。
为了解决第二个问题,我们使用了每种语言都有单独索引的方法。
结合这两种方法,我们获得了语言索引,但仍包含针对不与字符集相交的多种语言的分析器:俄语-英语(以及英语-俄语),波兰语-俄语,德语-俄语,乌克兰语-英语等。 。
为了不预先创建所有可能的索引,我们使用了索引模板-Elasticsearch允许您指定一个包含设置和映射的模板,并指定索引名称模式。 当您尝试将文档编入不存在的索引(其名称与模板的一种模式匹配)时,不仅会创建一个新索引,而且还会将来自相应模板的设置和映射应用于该索引。
索引结构
为了建立索引,我们一次(通过多字段)使用两个分析器:默认用于按精确短语搜索,而自定义则用于其他所有内容:
"ru_en_analyzer": { "filter": [ "lowercase", "russian_morphology", "english_morphology", "word_delimiter", "ru_en_stopwords" ], "char_filter": [ "yo_filter" ], "type": "custom", "tokenizer": "standard"}
使用小写过滤器,一切都变得清楚了,其余的我将告诉您。
过滤器Russian_morphology和english_morphology分别用于俄语和英语文本的形态分析。 它们不是Elasticsearch的一部分,而是作为单独的分析形态插件包含的一部分。 这些lemmatizer将词汇方法与一些启发式方法结合使用,并且比相应语言的内置过滤器更好地工作,MUCH。
POST _analyze { "analyzer": "russian", "text": " " } >>
并且:
POST _analyze { "analyzer": "ru_en_analyzer", "text": " " } >>
非常好奇的word_delimiter过滤器。 例如,当点后没有空格时,它有助于消除错别字。 我们使用以下配置:
"word_delimiter": { "catenate_all": "true", "type": "word_delimiter", "preserve_original": "true" }
yo_filter允许您忽略E和E之间的区别:
"yo_filter": { "type": "mapping", "mappings": [ " => ", " => " ] }
ru_en_stopwords过滤器类型stop-我们的停用词词典。
索引过程
通常,ECM中的文档主体是Office格式的文件:.docx,.pdf等。 要提取文本,将“摄取附件”插件与以下管道一起使用:
{ "document_version": { "processors": [ { "attachment": { "field": "content", "target_field": "attachment", "properties": [ "content", "content_length", "content_type", "language" ], "indexed_chars": -1, "ignore_failure": true } }, { "remove": { "field": "content", "ignore_failure": true } }, { "script": { "lang": "painless", "params": { "languages": ["ru", "en" ], "language_delimeter": "_" }, "source": "..." } }, { "remove": { "field": "attachment", "ignore_failure": true } } ] } }
从管道中的异常情况开始,忽略主体不存在的错误(这种情况发生在加密文档中),并根据文本的语言确定目标索引。 后者是用一种轻松的脚本完成的,我将单独介绍其内容,因为 由于JSON限制,它必须写在一行上。 再加上调试困难(建议的方法是到处抛出异常),这完全变成了痛苦的事情。
if (ctx.attachment != null) { if (params.languages.contains(ctx.attachment.language)) ctx._index = ctx._index + params.language_delimeter + ctx.attachment.language; if (ctx.attachment.content != null) ctx.content = ctx.attachment.content; if (ctx.attachment.content_length != null) ctx.content_length = ctx.attachment.content_length; if (ctx.attachment.content_type != null) ctx.content_type = ctx.attachment.content_type; if (ctx.attachment.language != null) ctx.language = ctx.attachment.language; }
因此,我们总是将文档发送到index_name 。 如果未定义语言或不支持该语言,则文档将位于此索引中,否则它将属于index_name_language 。
我们不存储文件的原始正文,但是启用了_source字段,因为 需要部分更新文档并突出显示找到的文档。
如果自上次建立索引以来仅卡已更改,则我们使用不带管道的更新按查询API对其进行更新。 首先,这不会使可能沉重的文档正文从ECM中拖出;其次,它显着加快了Elasticsearch方面的更新-您不必从办公格式中提取文档文本,这是非常耗费资源的。
因此,Elasticsearch中根本没有文档的更新,从技术上讲,从索引进行更新时,旧文档被取出,更改并再次完全索引。
但是,如果正文更改,则通常会删除旧文档并从头开始建立索引。 这允许文档从一种语言索引移至另一种语言索引。
搜寻
为了便于描述,我将给出最终结果的屏幕截图

全文
我们拥有的主要查询类型是简单查询字符串查询 :
"simple_query_string": { "fields": [ "card.d*.*_text", "card.d*.*_text.exact", "card.name^2", "card.name.exact^2", "content", "content.exact" ], "query": " ", "default_operator": "or", "analyze_wildcard": true, "minimum_should_match": "-35%", "quote_field_suffix": ".exact" }
其中.exact是默认解析器索引的字段。 文档名称的重要性是其他字段的两倍。 "default_operator": "or"
和"minimum_should_match": "-35%"
可让您查找不超过35%的搜索词的文档。
同义字
通常,使用不同的分析器进行索引和搜索,但是它们之间的唯一区别是添加了一个过滤器,以将同义词添加到搜索查询中:
"search_analyzer": { "filter": [ "lowercase", "russian_morphology", "english_morphology", "synonym_filter", "word_delimiter", "ru_en_stopwords" ], "char_filter": [ "yo_filter" ], "tokenizer": "standard" }
"synonym_filter": { "type": "synonym_graph", "synonyms_path": "synonyms.txt" }
会计权
对于基于权限的搜索,主查询嵌入在Bool Query中 ,并添加了过滤器:
"bool": { "must": [ { "simple_query_string": {...} } ], "filter": [ { "terms": { "rights": [ ] } } ] }
我们在索引部分中还记得,索引有一个字段,该字段具有对文档拥有权限的用户和组的ID。 如果此字段与传递的数组有交集,则有权限。
相关性调整
默认情况下,Elasticsearch使用BM25算法使用查询和文档文本来评估结果的相关性。 我们决定再考虑三个因素来影响对预期结果和实际结果的符合性评估:
- 上次编辑文档的时间-越远,需要此文档的可能性就越小;
- 对该文档的调用次数-越多,则需要该文档的可能性就越大;
ECM正文版本具有几种可能的状态:开发,运行和不推荐使用。 理所当然的是,表演比其他表演更重要。
您可以借助功能得分查询来达到此效果:
"function_score": { "functions": [ { "gauss": { "modified_date": { "origin": "now", "scale": "1095d", "offset": "31d", "decay": 0.5 } } }, { "field_value_factor": { "field": "access_count", "missing": 1, "modifier": "log2p" } }, { "filter": { "term": { "life_stage_value_id": { "value": "" } } }, "weight": 1.1 } ], "query": { "bool": {...} } }
结果,对于ceteris paribus来说,我们得到的结果评级修饰语大约依赖于其最后更改日期X和命中次数Y:
外部情报
对于智能搜索功能的一部分,我们需要从搜索查询中提取各种事实 :日期及其应用(创建,修改,批准等),组织名称,所搜索文档的类型等。
还希望将请求分类为特定类别,例如,按组织,员工,法规等的文档。
这两个操作由ECM智能模块DIRECTUM Ario执行 。
智能搜索过程
现在是时候更详细地考虑哪些机制是实施情报要素了。
用户错误纠正
布局的正确性是根据Trigram语言模型确定的-对于一条线,它会计算在英语和俄语文本中满足其三个字符序列的可能性。 如果认为当前布局不太可能,则首先显示具有正确布局的提示:

其次,以正确的布局执行搜索的其他步骤:

而且,如果使用正确的布局找不到任何内容,则搜索将从原始行开始。
拼写校正是使用短语提示器实现的。 它有一个问题-如果您同时对多个索引执行查询,则建议可能不会返回任何内容,而如果仅对一个索引执行,则会产生结果。 可以通过将置信度设置为0来解决此问题,但随后建议建议以正常形式替换单词。 同意,当您搜索“字母a ”以得到答案时会很奇怪: 也许您正在寻找有关的信?
可以通过在请求中使用两个提示来规避此问题:
"suggest": { "content_suggest": { "text": " ", "phrase": { "collate": { "query": { {{suggestion}} } }, } }, "check_suggest": { "text": "", "phrase": { "collate": { "query": { {{suggestion}} - ({{source_query}}) }, "params": { "source_query": " " } }, } } }
使用的常用参数
"confidence": 0.0, "max_errors": 3.0, "size": 1
如果前一个返回正确的结果,但第二个没有返回结果,则该结果是原始字符串本身,可能带有其他形式的单词,因此无需显示提示。 如果仍然需要提示,则原始搜索短语将与提示合并。 通过仅替换更正的单词和拼写检查程序(使用Hunspell)认为不正确的单词,可以实现此目的。
如果对源字符串的搜索返回了0个结果,则将其替换为通过合并获得的字符串,然后再次执行搜索:

否则,返回的提示字符串仅作为搜索提示返回:

查询分类和事实提取
如前所述,我们使用DIRECTUM Ario,即文本分类服务和事实提取服务。 为此,我们为分析师提供了匿名搜索查询以及我们感兴趣的事实列表。 基于查询和对系统中包含哪些文档的了解,分析人员确定了几个类别,并训练了分类服务以根据查询文本确定类别。 根据结果类别和事实列表,我们制定了使用这些事实的规则。 例如,在“ 每个人 ”类别中,最后一年的短语被视为文档的创建日期,而在“ 按组织 ”类别中,该短语被视为注册日期。 同时, 去年创建的类别应在创建日期之内。
从搜索方面来看,他们进行了配置,在其中注册了类别,将哪些事实应用于哪一个方面过滤器。
输入完成
除了已经提到的布局更正之外,用户和公共文档的先前搜索属于自动完成。

它们是使用另一种类型的“建议者”- 完成建议者来实现的 ,但是每个人都有自己的细微差别。
自动补全:搜索记录
与搜索引擎相比,ECM中的用户数量要少得多,并且可以为其分配足够的常见查询 为什么列宁蘑菇 不可能的。 出于隐私考虑,连续显示所有内容也不值得。 通常的完成建议器只能在索引中搜索整个文档集,但是上下文建议器可以帮助您-一种为每个提示设置上下文并通过这些上下文进行过滤的方法。 如果将用户名用作上下文,则只有他的历史记录可以显示给所有人。
您还需要给用户机会删除他感到羞耻的提示。 作为删除键,我们使用了用户名和工具提示文本。 结果,对于带有提示的索引,我们得到了一个稍微重复的映射:
"mappings": { "document": { "properties": { "input": { "type": "keyword" }, "suggest": { "type": "completion", "analyzer": "simple", "preserve_separators": true, "preserve_position_increments": true, "max_input_length": 50, "contexts": [ { "name": "user", "type": "CATEGORY" } ] }, "user": { "type": "keyword" } } } }
每个新提示的权重都设置为1,并且每次使用非常简单的ctx._source.suggest.weight++
脚本使用Update By Query API重新输入提示时,权重都会增加。
自动完成:文档
但是可能会有很多文件以及权利的可能组合。 因此,与此相反,我们决定在自动完成时不按权限进行过滤,而只对公共文档编制索引。 是的,您不需要从该索引中删除单个提示。 似乎在所有方面的实现都比上一个更容易,即使不是两点:
第一个-完成建议程序仅支持前缀搜索,客户喜欢为所有内容分配商品编号,还有一些.01.01
键入查询时.01.01
。 在这里,连同全名一样,您还可以索引从其派生的n-gram:
{ "extension": "pdf", "name": ".01.01 ", "suggest": [ { "input": "", "weight": 70 }, { "input": " ", "weight": 80 }, { "input": " ", "weight": 90 }, { "input": ".01.01 ", "weight": 100 } ] }
对于故事来说,这并不是很关键,但是如果同一位用户再次搜索某内容,则输入的内容大致相同。 大概吧 。
第二个-默认情况下,所有提示都是相等的,但是我们希望使其中的一些更相等,并且最好使它们与搜索结果的排名一致。 为此,请大致重复使用“ 功能分数查询”中的gauss和field_value_factor函数。
事实证明,这是一条管道:
{ "dir_public_documents_pipeline": { "processors": [ ... { "set": { "field": "terms_array", "value": "{{name}}" } }, { "split": { "field": "terms_array", "separator": "\\s+|$" } }, { "script": { "source": "..." } } ] } }
使用以下脚本:
Date modified = new Date(0); if (ctx.modified_date != null) modified = new SimpleDateFormat('dd.MM.yyyy').parse(ctx.modified_date); long dayCount = (System.currentTimeMillis() - modified.getTime())/(1000*60*60*24); double score = Math.exp((-0.7*Math.max(0, dayCount - 31))/1095) * Math.log10(ctx.access_count + 2); int count = ctx.terms_array.length; ctx.suggest = new ArrayList(); ctx.suggest.add([ 'input': ctx.terms_array[count - 1], 'weight': Math.round(score * (255 - count + 1)) ]); for (int i = count - 2; i >= 0 ; --i) { if (ctx.terms_array[i].trim() != "") { ctx.suggest.add([ "input": ctx.terms_array[i] + " " + ctx.suggest[ctx.suggest.length - 1].input, "weight": Math.round(score * (255 - i))]); } } ctx.remove('terms_array'); ctx.remove('access_count'); ctx.remove('modified_date');
为什么不花很多钱去麻烦管道,而不用更方便的语言编写管道? 因为现在,使用Reindex API ,您可以仅用一条命令就将搜索索引的内容覆盖到提示索引中(当然,只需指定必要的字段即可)。
真正需要的公共文档的组成通常不会更新,因此可以手动启动此命令。
显示结果
分类目录
类别确定可用的构面以及摘要的外观。 它可以通过外部情报自动检测,也可以在搜索栏上方手动选择。
刻面
对于每个人的行为而言,方面都是如此直观的事情,但是,行为由非常平凡的规则来描述。 以下是其中一些:
构面值取决于搜索结果,但是,搜索结果取决于所选构面。 如何避免递归?
在一个构面内选择值不会影响此构面的其他值,但会在其他构面中影响值:

- 用户选择的构面值不应消失,即使另一个构面中的选择将其消灭为0或它们不再位于顶部也是如此:

在弹性方面,小面是通过聚集机制实现的,但是为了遵守所描述的规则,这些聚集必须相互投资并相互过滤。
考虑负责此操作的请求片段:
代码太大 { ... "post_filter": { "bool": { "must": [ { "terms": { "card.author_value_id": [ "1951063" ] } }, { "terms": { "editor_value_id": [ "2337706", "300643" ] } } ] } }, "query": {...} "aggs": { "card.author_value_id": { "filter": { "terms": { "editor_value_id": [ "2337706", "300643" ] } }, "aggs": { "card.author_value_id": { "terms": { "field": "card.author_value_id", "size": 11, "exclude": [ "1951063" ], "missing": "" } }, "card.author_value_id_selected": { "terms": { "field": "card.author_value_id", "size": 1, "include": [ "1951063" ], "missing": "" } } } }, ... "editor_value_id": { "filter": { "terms": { "card.author_value_id": [ "1951063" ] } }, "aggs": { "editor_value_id": { "terms": { "field": "editor_value_id", "size": 11, "exclude": [ "2337706", "300643" ], "missing": "" } }, "editor_value_id_selected": { "terms": { "field": "editor_value_id", "size": 2, "include": [ "2337706", "300643" ], "missing": "" } } } }, ... } }
这是什么:
- post_filter允许您对已经完成的查询的结果施加附加条件,并且不影响聚合的结果。 同样的递归间隙。 包括所有构面的所有选定值。
- 顶级聚合,在示例card.author_value_id和editor_value_id中 。 每个都有:
- 按除您自己之外的所有其他方面的值过滤;
- 选定构面值的嵌套聚合- 防止ni灭 ;
- 其他构面值的嵌套聚合。 我们显示前10名,并请求前11名-确定是否显示“ 显示全部”按钮。
片段
根据所选类别的不同,代码段的外观可能会有所不同,例如,在类别中进行搜索时,同一文档
全部 :

和员工 :

还是记得,我们想看看商业报价的主题以及它来自谁?

为了不从弹性卡上拖出整个卡(这会减慢搜索速度),使用了源过滤 :
{ ... "_source": { "includes": [ "id", "card.name", "card.card_type_value_id", "card.life_stage_value_id", "extension", ... ] }, "query": {...} ... }
为了突出显示文档文本中的单词,使用了快速矢量荧光笔 -为大型文本生成最合适的代码片段,并为名称- 统一荧光笔 -作为对资源和索引结构的最低要求:
"highlight": { "pre_tags": [ "<strong>" ], "post_tags": [ "</strong>" ], "encoder": "html", "fields": { "card.name": { "number_of_fragments": 0 }, "content": { "fragment_size": 300, "number_of_fragments": 3, "type": "fvh" } } },
在这种情况下,名称将全部突出显示,并且从文本中我们可以得到最多300个字符的3个片段。 Fast Vector荧光笔返回的文本通过权宜算法进一步压缩以获得最小化的代码段状态。
倒塌
从历史上看,此ECM的用户习惯于搜索将文档返回给他们,但实际上Elasticsearch在文档的各个版本中进行搜索。 可能会发现在同一查询中会找到几个几乎相同的版本。 这会使结果混乱并使用户感到困惑。 幸运的是,可以使用Field Collapsing机制避免这种行为-某些轻量级的聚合已经可以在完成的结果上使用(在这种情况下,它类似于post_filter, 两个拐杖是一对 )。 崩溃将导致最相关的崩溃对象。
{ ... "query": {...} ... "collapse": { "field": "id" } }
不幸的是,崩溃具有许多令人不愉快的影响,例如,搜索结果的各种数值特征继续返回,就好像没有崩溃一样。 也就是说,结果的数量,构面值的数量-都会有些许不正确,但是用户通常不会注意到这一点,就像疲倦的读者一样,后者以前不太可能阅读过此建议。
结束了。