enterprise-search-site-search-light-1680x980.png

Los motores en Elastic App Search te permiten indexar documentos y brindan capacidades de búsqueda ajustables listas para usar. De forma predeterminada, los motores tienen soporte para una lista de idiomas predefinida. Si tu idioma no está en esa lista, aquí explicamos cómo puedes agregar soporte para idiomas adicionales. Para hacerlo, crearemos un motor de App Search con analizadores configurados para dicho idioma.

Antes de entrar en detalles, definamos qué es un analizador de Elasticsearch:

Un analizador de Elasticsearch es un paquete que contiene tres bloques de creación de nivel inferior: filtros de caracteres, tokenizadores y filtros de tokens. Los analizadores pueden estar integrados o ser personalizados. Los analizadores integrados preempaquetan bloques de creación en analizadores adecuados para distintos idiomas y tipos de texto.

Los analizadores de cada campo se usan para lo siguiente:

  • Índice. Cada campo de documento se procesará con su analizador correspondiente y se dividirá en tokens para facilitar la búsqueda.
  • Búsqueda. Esta consulta de búsqueda se analizará para garantizar una coincidencia adecuada con los campos de indexación que ya se analizaron.

Los motores basados en índices de Elasticsearch te permiten crear motores de App Search a partir de índices existentes de Elasticsearch. Crearemos un índice de Elasticsearch con nuestros propios analizadores y mapeos, y usaremos ese índice en App Search.

Este proceso tiene cuatro pasos:

  1. Crear un índice de Elasticsearch y documentos de índice
  2. Agregar analizador de lenguaje a ese índice
  3. Actualizar el mapeo de índices para usar analizadores
  4. Reindexar los documentos

1. Crear un índice de Elasticsearch y documentos de índice

Para comenzar, tomemos un índice que no se haya optimizado para ningún idioma. Supongamos que es un índice nuevo sin mapeos predefinidos y que se creó al indexar los documentos por primera vez.

En Elasticsearch, el mapeo es el proceso que define cómo un documento y los campos que contiene se almacenan e indexan. Cada documento es una recopilación de campos, cada uno de los cuales tiene su propio tipo de datos. Al mapear los datos, creas una definición de mapeo, la cual contiene una lista de campos pertinentes para el documento.

Volvamos a nuestro ejemplo. El índice se llama books, donde title está en idioma rumano. Elegimos el rumano porque es mi idioma y no está incluido en la lista de idiomas que App Search admite de forma predeterminada.  

POST books/_doc/1
{
  "title": "Un veac de singurătate",
  "author": "Gabriel García Márquez"
}

POST books/_doc/2
{
  "title": "Dragoste în vremea holerei",
  "author": "Gabriel García Márquez"
}

POST books/_doc/3
{
  "title": "Obosit de viaţă, obosit de moarte",
  "author": "Mo Yan"
}

POST books/_doc/4
{
  "title": "Maestrul și Margareta",
  "author": "Mihail Bulgakov"
}

2. Agregar analizador de lenguaje a ese índice

Cuando inspeccionamos el mapeo de índice books, vemos que no está optimizado para rumano. Es evidente porque no hay un campo analysis en el bloque settings y los campos de texto no usan un analizador personalizado.

GET books
{
  "books": {
    "aliases": {},
    "mappings": {
      "properties": {
        "author": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    },
    "settings": {
      "index": {
        "routing": {
          "allocation": {
            "include": {
              "_tier_preference": "data_content"
            }
          }
        },
        "number_of_shards": "1",
        "provided_name": "books",
        "creation_date": "1679310576178",
        "number_of_replicas": "1",
        "uuid": "0KuiDk8iSZ-YHVQGg3B0iw",
        "version": {
          "created": "8080099"
        }
      }
    }
  }
}

Si intentamos crear un motor de App Search con el índice books, tendremos dos problemas. Primero, los resultados de búsqueda no estarán optimizados para rumano y, segundo, las características como el ajuste de precisión estarán deshabilitadas.

Una nota rápida sobre los distintos tipos de motores de Elastic App Search:

[Artículo relacionado: Elasticsearch Search API: A new way to locate App Search documents (API de búsqueda de Elasticsearch: Una nueva forma de ubicar los documentos de App Search]

Cuando creas un motor a partir de un índice de Elasticsearch existente, si los mapeos no respetan las convenciones de App Search, no se habilitarán todas las características para dicho motor. Veamos en más detalle las convenciones de mapeo de App Search, para ello observaremos un motor completamente gestionado por App Search. Este motor tiene dos campos, title y author, y usa el idioma inglés.

GET .ent-search-engine-documents-app-search-books/_mapping/field/title
{
  ".ent-search-engine-documents-app-search-books": {
    "mappings": {
      "title": {
        "full_name": "title",
        "mapping": {
          "title": {
            "type": "text",
            "fields": {
              "date": {
                "type": "date",
                "format": "strict_date_time||strict_date",
                "ignore_malformed": true
              },
              "delimiter": {
                "type": "text",
                "index_options": "freqs",
                "analyzer": "iq_text_delimiter"
              },
              "enum": {
                "type": "keyword",
                "ignore_above": 2048
              },
              "float": {
                "type": "double",
                "ignore_malformed": true
              },
              "joined": {
                "type": "text",
                "index_options": "freqs",
                "analyzer": "i_text_bigram",
                "search_analyzer": "q_text_bigram"
              },
              "location": {
                "type": "geo_point",
                "ignore_malformed": true,
                "ignore_z_value": false
              },
              "prefix": {
                "type": "text",
                "index_options": "docs",
                "analyzer": "i_prefix",
                "search_analyzer": "q_prefix"
              },
              "stem": {
                "type": "text",
                "analyzer": "iq_text_stem"
              }
            },
            "index_options": "freqs",
            "analyzer": "iq_text_base"
          }
        }
      }
    }
  }
}

Verás que el campo title tiene varios subcampos. Los subcampos date, float y location no son campos de texto.

Aquí, lo que nos interesa es cómo configurar los campos de texto que requiere App Search. ¡Hay bastantes campos! En esta página de documentación se explican los campos de texto usados en App Search. Echemos un vistazo a los analizadores que App Search configura para un índice oculto que pertenece a un motor gestionado de App Search:

GET .ent-search-engine-documents-app-search-books/_settings/index.analysis*
{
  ".ent-search-engine-documents-app-search-books": {
    "settings": {
      "index": {
        "analysis": {
          "filter": {
            "front_ngram": {
              "type": "edge_ngram",
              "min_gram": "1",
              "max_gram": "12"
            },
            "bigram_joiner": {
              "max_shingle_size": "2",
              "token_separator": "",
              "output_unigrams": "false",
              "type": "shingle"
            },
            "bigram_max_size": {
              "type": "length",
              "max": "16",
              "min": "0"
            },
            "en-stem-filter": {
              "name": "light_english",
              "type": "stemmer"
            },
            "bigram_joiner_unigrams": {
              "max_shingle_size": "2",
              "token_separator": "",
              "output_unigrams": "true",
              "type": "shingle"
            },
            "delimiter": {
              "split_on_numerics": "true",
              "generate_word_parts": "true",
              "preserve_original": "false",
              "catenate_words": "true",
              "generate_number_parts": "true",
              "catenate_all": "true",
              "split_on_case_change": "true",
              "type": "word_delimiter_graph",
              "catenate_numbers": "true",
              "stem_english_possessive": "true"
            },
            "en-stop-words-filter": {
              "type": "stop",
              "stopwords": "_english_"
            }
          },
          "analyzer": {
            "i_prefix": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "front_ngram"
              ],
              "tokenizer": "standard"
            },
            "iq_text_delimiter": {
              "filter": [
                "delimiter",
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter",
                "en-stem-filter"
              ],
              "tokenizer": "whitespace"
            },
            "q_prefix": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding"
              ],
              "tokenizer": "standard"
            },
            "iq_text_base": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter"
              ],
              "tokenizer": "standard"
            },
            "iq_text_stem": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stop-words-filter",
                "en-stem-filter"
              ],
              "tokenizer": "standard"
            },
            "i_text_bigram": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stem-filter",
                "bigram_joiner",
                "bigram_max_size"
              ],
              "tokenizer": "standard"
            },
            "q_text_bigram": {
              "filter": [
                "cjk_width",
                "lowercase",
                "asciifolding",
                "en-stem-filter",
                "bigram_joiner_unigrams",
                "bigram_max_size"
              ],
              "tokenizer": "standard"
            }
          }
        }
      }
    }
  }
}

Si queremos crear un índice que podamos usar en App Search, para un idioma diferente (por ejemplo, noruego, finés o árabe), necesitaríamos analizadores similares. Por ejemplo, debemos asegurarnos de que los filtros de raíz y palabra vacía usen la versión de rumano.

Volvamos al índice books inicial y agreguemos los analizadores correctos.

Ten precaución. En índices existentes, los analizadores son un tipo de configuración de Elasticsearch que solo puede cambiarse cuando un índice está cerrado. Con este enfoque, comenzamos con un índice existente y, por lo tanto, debemos cerrarlo, agregamos analizadores y luego reabrimos el índice.

Nota: Como alternativa, también podrías recrear el índice desde cero con los mapeos correctos y luego indexar todos los documentos. Si eso es lo mejor para tu caso de uso, siéntete libre de omitir las partes de esta guía en las que se habla sobre abrir y cerrar el índice, y reindexar.

Para cerrar el índice, puedes ejecutar POST books/_close. Luego de eso, agregaremos los analizadores:

PUT books/_settings
{
  "analysis": {
    "filter": {
      "front_ngram": {
        "type": "edge_ngram",
        "min_gram": "1",
        "max_gram": "12"
      },
      "bigram_joiner": {
        "max_shingle_size": "2",
        "token_separator": "",
        "output_unigrams": "false",
        "type": "shingle"
      },
      "bigram_max_size": {
        "type": "length",
        "max": "16",
        "min": "0"
      },
      "ro-stem-filter": {
        "name": "romanian",
        "type": "stemmer"
      },
      "bigram_joiner_unigrams": {
        "max_shingle_size": "2",
        "token_separator": "",
        "output_unigrams": "true",
        "type": "shingle"
      },
      "delimiter": {
        "split_on_numerics": "true",
        "generate_word_parts": "true",
        "preserve_original": "false",
        "catenate_words": "true",
        "generate_number_parts": "true",
        "catenate_all": "true",
        "split_on_case_change": "true",
        "type": "word_delimiter_graph",
        "catenate_numbers": "true"
      },
      "ro-stop-words-filter": {
        "type": "stop",
        "stopwords": "_romanian_"
      }
    },
    "analyzer": {
      "i_prefix": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "front_ngram"
        ],
        "tokenizer": "standard"
      },
      "iq_text_delimiter": {
        "filter": [
          "delimiter",
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter",
          "ro-stem-filter"
        ],
        "tokenizer": "whitespace"
      },
      "q_prefix": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding"
        ],
        "tokenizer": "standard"
      },
      "iq_text_base": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter"
        ],
        "tokenizer": "standard"
      },
      "iq_text_stem": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stop-words-filter",
          "ro-stem-filter"
        ],
        "tokenizer": "standard"
      },
      "i_text_bigram": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stem-filter",
          "bigram_joiner",
          "bigram_max_size"
        ],
        "tokenizer": "standard"
      },
      "q_text_bigram": {
        "filter": [
          "cjk_width",
          "lowercase",
          "asciifolding",
          "ro-stem-filter",
          "bigram_joiner_unigrams",
          "bigram_max_size"
        ],
        "tokenizer": "standard"
      }
    }
  }
}

Puedes ver que estamos agregando ro-stem-filter para derivación en rumano, lo que mejorará la relevancia de búsqueda para variaciones de palabras específicas del rumano. Incluimos el filtro de palabras vacías en rumano (ro-stop-words-filter) para asegurarnos de que las palabras vacías en rumano no se tengan en cuenta a los fines de la búsqueda.

Y ahora reabrimos el índice ejecutando POST books/_open.

3. Actualizar el mapeo de índices para usar analizadores

Una vez que tengamos la configuración de análisis lista, podemos modificar el mapeo del índice. App Search usa plantillas dinámicas para asegurar que los campos nuevos tengan los subcampos y analizadores correctos. A los fines de nuestro ejemplo, solo agregaremos los subcampos a los campos title y author existentes:

PUT books/_mapping
{
  "properties": {
    "author": {
      "type": "text",
      "fields": {
        "delimiter": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "iq_text_delimiter"
        },
        "enum": {
          "type": "keyword",
          "ignore_above": 2048
        },
        "joined": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "i_text_bigram",
          "search_analyzer": "q_text_bigram"
        },
        "prefix": {
          "type": "text",
          "index_options": "docs",
          "analyzer": "i_prefix",
          "search_analyzer": "q_prefix"
        },
        "stem": {
          "type": "text",
          "analyzer": "iq_text_stem"
        }
      }
    },
    "title": {
      "type": "text",
      "fields": {
        "delimiter": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "iq_text_delimiter"
        },
        "enum": {
          "type": "keyword",
          "ignore_above": 2048
        },
        "joined": {
          "type": "text",
          "index_options": "freqs",
          "analyzer": "i_text_bigram",
          "search_analyzer": "q_text_bigram"
        },
        "prefix": {
          "type": "text",
          "index_options": "docs",
          "analyzer": "i_prefix",
          "search_analyzer": "q_prefix"
        },
        "stem": {
          "type": "text",
          "analyzer": "iq_text_stem"
        }
      }
    }
  }
}

4. Reindexar los documentos

El índice books ahora está casi listo para usarse en App Search.

Solo necesitamos asegurarnos de que los documentos que indexamos antes de modificar el mapeo tengan todos los subcampos correctos. Para hacerlo, podemos ejecutar una reindexación con update_by_query:

POST books/_update_by_query?refresh
{
  "query": {
    "match_all": {
    }
  }
}

Como estamos usando una búsqueda match_all, se actualizarán todos los documentos existentes.

Con una solicitud update by query también podemos incluir un parámetro de script para definir cómo actualizar los documentos.

Ten en cuenta que no estamos cambiando los documentos, pero sí queremos reindexar los documentos existentes como están para asegurarnos de que los campos de texto author y title tengan los subcampos correctos. Por lo tanto, no necesitamos incluir un script en nuestra actualización mediante solicitud de búsqueda.

Ahora tenemos un índice optimizado para el idioma que podemos usar en App Search con motores de Elasticsearch. Verás todos los beneficios en acción en las capturas de pantalla siguientes.

Usaremos el título del libro Cien años de soledad como referencia. El título traducido en rumano es Un veac de singurătate. Presta atención a la palabra veac, que es "siglo" en rumano. Haremos una búsqueda con el plural de veac, que es veacuri. Ingestamos este registro de datos en ambos ejemplos que veremos a continuación:

{
  "title": "Un veac de singurătate",
  "author": "Gabriel García Márquez"
}

Cuando un índice no está optimizado para un idioma, el título del libro en rumano Un veac de singurătate se indexa con el analizador estándar, que funciona bien para la mayoría de los idiomas, pero es posible que no siempre encuentre coincidencias con documentos relevantes. Buscar veacuri no muestra ningún resultado debido a que esta entrada de búsqueda no coincide con ningún texto sin formato en el registro de datos.

ajuste de relevancia gestión de campos

Sin embargo, al usar el índice optimizado para el idioma, cuando buscamos veacuri, Elastic App Search encuentra coincidencias con la palabra del idioma rumano veac y devuelve lo que estamos buscando. Los campos de ajuste de precisión también están disponibles dentro de la vista de ajuste de precisión. Observa todo lo resaltado en esta imagen:

ajuste de relevancia ajuste de precisión

Así agregamos soporte en Elastic Enterprise Search para el rumano, mi idioma. El proceso usado en esta guía puede replicarse para crear índices optimizados para cualquier otro idioma que tenga soporte en Elasticsearch. Si deseas conocer la lista completa de analizadores de lenguaje con soporte en Elasticsearch, visita esta página de documentación.

Los analizadores en Elasticsearch son un tema fascinante. Si quieres saber más, aquí tienes algunos recursos: