regex elasticsearch lucene odata analyzer

regex - ¿Cómo combinar sabiamente el herpes zóster y edgeNgram para proporcionar una búsqueda flexible de texto completo?



elasticsearch lucene (1)

Este es un caso de uso interesante. Aquí está mi opinión:

{ "settings": { "analysis": { "analyzer": { "my_ngram_analyzer": { "tokenizer": "my_ngram_tokenizer", "filter": ["lowercase"] }, "my_edge_ngram_analyzer": { "tokenizer": "my_edge_ngram_tokenizer", "filter": ["lowercase"] }, "my_reverse_edge_ngram_analyzer": { "tokenizer": "keyword", "filter" : ["lowercase","reverse","substring","reverse"] }, "lowercase_keyword": { "type": "custom", "filter": ["lowercase"], "tokenizer": "keyword" } }, "tokenizer": { "my_ngram_tokenizer": { "type": "nGram", "min_gram": "2", "max_gram": "25" }, "my_edge_ngram_tokenizer": { "type": "edgeNGram", "min_gram": "2", "max_gram": "25" } }, "filter": { "substring": { "type": "edgeNGram", "min_gram": 2, "max_gram": 25 } } } }, "mappings": { "test_type": { "properties": { "text": { "type": "string", "analyzer": "my_ngram_analyzer", "fields": { "starts_with": { "type": "string", "analyzer": "my_edge_ngram_analyzer" }, "ends_with": { "type": "string", "analyzer": "my_reverse_edge_ngram_analyzer" }, "exact_case_insensitive_match": { "type": "string", "analyzer": "lowercase_keyword" } } } } } } }

  • my_ngram_analyzer se usa para dividir cada texto en partes pequeñas, el tamaño de las partes depende de su caso de uso. Elegí, para fines de prueba, 25 caracteres. se usa lowercase ya que dijiste que no distingue entre mayúsculas y minúsculas. Básicamente, este es el tokenizer utilizado para substringof(''table 1'',name) . La consulta es simple:

{ "query": { "term": { "text": { "value": "table 1" } } } }

  • my_edge_ngram_analyzer se usa para dividir el texto comenzando desde el principio y esto se usa específicamente para el startswith(name,''table 1'') uso del startswith(name,''table 1'') . De nuevo, la consulta es simple:

{ "query": { "term": { "text.starts_with": { "value": "table 1" } } } }

  • Encontré esta es la parte más complicada: la que endswith(name,''table 1'') . Para esto my_reverse_edge_ngram_analyzer que usa un tokenizador de keyword junto con lowercase y un filtro edgeNGram precedido y seguido por un filtro reverse . Lo que básicamente hace este tokenizer es dividir el texto en edgeNGrams, pero el borde es el final del texto, no el inicio (como con el edgeNGram normal). La consulta:

{ "query": { "term": { "text.ends_with": { "value": "table 1" } } } }

  • para el caso del name eq ''table 1'' , un simple tokenizador de keyword junto con un filtro en lowercase debería hacerlo La consulta:

{ "query": { "term": { "text.exact_case_insensitive_match": { "value": "table 1" } } } }

Con respecto a query_string , esto cambia un poco la solución, porque contaba con el term para no analizar el texto de entrada y para que coincida exactamente con uno de los términos en el índice.

Pero esto se puede "simular" con query_string si se especifica el analyzer apropiado para él .

La solución sería un conjunto de consultas como las siguientes (siempre use ese analizador, cambiando solo el nombre del campo):

{ "query": { "query_string": { "query": "text.starts_with:(/"table 1/")", "analyzer": "lowercase_keyword" } } }

Tenemos una API compatible con OData que delega algunas de sus necesidades de búsqueda de texto completo en un clúster Elasticsearch. Dado que las expresiones OData pueden ser bastante complejas, decidimos simplemente traducirlas a su sintaxis de consulta Lucene equivalente y alimentarla en una consulta query_string .

Admitimos algunas expresiones de filtro OData relacionadas con el texto, como:

  • startswith(field,''bla'')
  • endswith(field,''bla'')
  • substringof(''bla'',field)
  • name eq ''bla''

Los campos con los que hacemos coincidir pueden analyzed , no not_analyzed o ambos (es decir, mediante un campo múltiple). El texto buscado puede ser un solo token (por ejemplo, table ), solo una parte del mismo (por ejemplo, tab ) o varios tokens (por ejemplo, table 1. , table 10 , etc.). La búsqueda debe ser entre mayúsculas y minúsculas.

Aquí hay algunos ejemplos del comportamiento que debemos apoyar:

  • startswith(name,''table 1'') debe coincidir con " Tabla 1 ", " tabla 1 00", " Tabla 1 .5", " tabla 1 12 nivel superior"
  • endswith(name,''table 1'') debe coincidir con "Sala 1, Tabla 1 ", " Tabla secundaria 1 ", " tabla 1 ", " Tabla Jeff 1 "
  • substringof(''table 1'',name) debe coincidir con "Big Table 1 back", " table 1 ", " Table 1 ", "Small Table1 2"
  • name eq ''table 1'' debe coincidir con " Tabla 1 ", " TABLA 1 ", " tabla 1 "

Básicamente, tomamos la entrada del usuario (es decir, lo que se pasa al segundo parámetro de startswith / endswith , resp. El primer parámetro de substringof , resp. El valor del lado derecho de la eq ) y tratamos de hacer coincidir exactamente si las fichas coinciden completamente o solo parcialmente.

En este momento, nos estamos librando de una solución torpe resaltada a continuación que funciona bastante bien, pero está lejos de ser ideal.

En nuestra query_string , hacemos coincidir un campo no not_analyzed utilizando la sintaxis de Expresión regular . Dado que el campo no se not_analyzed y la búsqueda no distingue entre mayúsculas y minúsculas, hacemos nuestra propia tokenización mientras preparamos la expresión regular para alimentar la consulta con el fin de llegar a algo como esto, es decir, esto es equivalente al filtro OData endswith(name,''table 8'') (=> coincide con todos los documentos cuyo name termina con "tabla 8")

"query": { "query_string": { "query": "name.raw:/.*(T|t)(A|a)(B|b)(L|l)(E|e) 8/", "lowercase_expanded_terms": false, "analyze_wildcard": true } }

Entonces, a pesar de que esta solución funciona bastante bien y el rendimiento no es tan malo (lo cual fue una sorpresa), nos gustaría hacerlo de manera diferente y aprovechar todo el poder de los analizadores para cambiar toda esta carga en la indexación tiempo en lugar de buscar tiempo. Sin embargo, dado que reindexar todos nuestros datos llevará semanas, nos gustaría investigar primero si hay una buena combinación de filtros de token y analizadores que nos ayudarían a lograr los mismos requisitos de búsqueda enumerados anteriormente.

Mi opinión es que la solución ideal contendría una combinación sabia de herpes zóster (es decir, varias fichas juntas) y edge-nGram (es decir, que coincidan al principio o al final de una ficha). Sin embargo, de lo que no estoy seguro es de si es posible hacer que funcionen juntos para que coincidan con varios tokens, donde el usuario podría no ingresar completamente uno de los tokens). Por ejemplo, si el campo de nombre indexado es "Big Table 123", necesito la substringof(''table 1'',name) para que coincida, por lo que "table" es un token totalmente coincidente, mientras que "1" es solo un prefijo del siguiente ficha

Gracias de antemano por compartir sus células cerebrales en este caso.

ACTUALIZACIÓN 1: después de probar la solución de Andrei

=> startswith exacta ( eq ) y startswith funcionar perfectamente.

A. endswith fallas

La búsqueda de substringof(''table 112'', name) produce 107 documentos. La búsqueda de un caso más específico, como endswith(name, ''table 112'') produce 1525 documentos, mientras que debería producir menos documentos (las coincidencias de sufijos deberían ser un subconjunto de coincidencias de subcadenas). Comprobando con mayor profundidad he encontrado algunos desajustes, como "Social Club, Table 12" (no contiene "112") u "Order 312" (no contiene "table" ni "112"). Supongo que es porque terminan con "12" y ese es un gramo válido para el token "112", de ahí la coincidencia.

B. substringof falla

La búsqueda de substringof(''table'',name) coincide con "Party table", "Alex on big table" pero no coincide con "Table 1", "table 112", etc. La búsqueda de substringof(''tabl'',name) no no coincide con nada

ACTUALIZACIÓN 2

Estaba implícito, pero olvidé mencionar explícitamente que la solución tendrá que funcionar con la consulta query_string , principalmente debido al hecho de que las expresiones OData (por complejas que sean) seguirán siendo traducidas a su equivalente Lucene. Soy consciente de que estamos intercambiando el poder de Elasticsearch Query DSL con la sintaxis de consulta de Lucene, que es un poco menos potente y menos expresiva, pero eso es algo que realmente no podemos cambiar. Sin embargo, estamos bastante cerca.

ACTUALIZACIÓN 3 (25 de junio de 2019):

ES 7.2 introdujo un nuevo tipo de datos llamado search_as_you_type que permite este tipo de comportamiento de forma nativa. Lea más en: https://www.elastic.co/guide/en/elasticsearch/reference/7.2/search-as-you-type.html