Языковые анализаторы, синонимы, nGram

Всем привет, кто знает как лучше реализовать поиск с учетом русского, английского языков (тобиж нескольких), и применения синонимов, nGram фильтра итд.

Стоит ли выделить анализатор для языков/синонимов/nGram отдельно (и применять их на этапе индексации к мультифилду) или
использовать их комбинацию в филтрах в одном/нескольких анализаторах?

Может подскажите ресурсы с бест практикс, спасибо

Я бы начал с изучения Definitive Guide, после этого поискал бы по этому форуму. Этот вопрос тут уже несколько раз обсуждался. Если будут какие-нибудь конкретные вопросы - то спрашивайте.

использовать их комбинацию в филтрах в одном/нескольких анализаторах?

nGram и стеммниг в одном анализаторе не уживаются, но синонимы можно использовать и в том и в другом анализаторе. Только при nGram синонимы будут хорошо работать только при индексировании.

У меня есть мапинг (без плагина для русс/анг.)

{
  "settings": {
    "analysis": {
      "filter": {
        "russian_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "russian_stemmer": {
          "type": "stemmer",
          "language": "russian"
        },
        "russian_english_stopwords": {
          "type": "stop",
          "stopwords": "а,без,более,бы,был,была,были,было,быть,в,вам,вас,весь,во,вот,все,всего,всех,вы,где,да,даже,для,до,его,ее,если,есть,еще,же,за,здесь,и,из,или,им,их,к,как,ко,когда,кто,ли,либо,мне,может,мы,на,надо,наш,не,него,нее,нет,ни,них,но,ну,о,об,однако,он,она,они,оно,от,очень,по,под,при,с,со,так,также,такой,там,те,тем,то,того,тоже,той,только,том,ты,у,уже,хотя,чего,чей,чем,что,чтобы,чье,чья,эта,эти,это,я,a,an,and,are,as,at,be,but,by,for,if,in,into,is,it,no,not,of,on,or,such,that,the,their,then,there,these,they,this,to,was,will,with"
        },
        "english_stop": {
          "type": "stop",
          "stopwords": "_english_"
        },
        "english_stemmer": {
          "type": "stemmer",
          "language": "english"
        },
        "english_possessive_stemmer": {
          "type": "stemmer",
          "language": "possessive_english"
        },
        "synonym": {
          "type": "synonym",
          "synonyms": [
            "бенеттон => united colors of benetton",
            "нью беланс => new balance",
            "зара => zara",
            "женская, женские, женский, девушкам => женщинам",
            "мужчина, мужские, мужской, мужская, парням, парню => мужчинам"
          ],
          "ignore_case": true,
          "expand": false
        },
        "edgengram": {
          "type": "edge_ngram",
          "min_gram": 3,
          "max_gram": 50
        }
      },
      "analyzer": {
        "index_edgengram": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "edgengram"
          ]
        },
        "russian_english": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "english_possessive_stemmer",
            "lowercase",
            "synonym",
            "russian_stop",
            "english_stop",
            "russian_stemmer",
            "english_stemmer",
            "russian_english_stopwords"
          ]
        }
      }
    }
  },
  "mappings": {
    "page": {
      "properties": {
        "title": {
          "type": "keyword",
          "fields": {
            "org": {
              "type": "text"
            },
            "raw": {
              "type": "text",
              "analyzer": "russian_english",
              "search_analyzer": "russian_english"
            },
            "ng": {
              "type": "text",
              "analyzer": "index_edgengram",
              "search_analyzer": "russian_english"
            }
          },
          "copy_to": [
            "search_data"
          ],
          "ignore_above": 256
        },
        "body": {
          "type": "keyword",
          "fields": {
            "org": {
              "type": "text"
            },
            "raw": {
              "type": "text",
              "analyzer": "russian_english",
              "search_analyzer": "russian_english"
            },
            "ng": {
              "type": "text",
              "analyzer": "index_edgengram",
              "search_analyzer": "russian_english"
            }
          },
          "copy_to": [
            "search_data"
          ],
          "ignore_above": 256
        },        
        "search_data": {
          "type": "text"
        }
      }
    }
  }
}
'

Добавим два документа

curl -XPOST 'http://localhost:9200/test/page' -d'
{"title": "United Colors Of Benetton", "body": "Серый"}
'

curl -XPOST 'http://localhost:9200/test/page' -d'
{"title": "United Colors Of Benetton", "body": "Белый"}
'

Теперь поиск: multi_match + cross_fields

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{"query" :
  {"multi_match": 
    {"query":               "Бенеттон Серый",
    "type":                 "cross_fields",
    "fields":               ["*.org^10", "*.raw^5", "*.ng"],
    "operator":             "and",
    "tie_breaker":          1,
    "minimum_should_match": "80%"}}}
}'

Получаем два документа, а должны один.
Вот так все нормально:

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{"query" :
  {"multi_match": 
    {"query":               "United Colors Of Benetton Серый",
    "type":                 "cross_fields",
    "fields":               ["*.org^10", "*.raw^5", "*.ng"],
    "operator":             "and",
    "tie_breaker":          1,
    "minimum_should_match": "80%"}}}
}'

спасибо

Режим cross_field не рассчитан на работу с полями, использующими разные анализаторы. Посмотрите вот это разъяснение в докумнтации. Там же есть пример, как с этим бороться.

Указав явно analyzer и одно поле, всеравно получаем два документа:

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{"query" :
  {"multi_match": 
     {"query":               "Бенеттон Серый",
     "type":                 "cross_fields",
     "fields":               ["*.raw"],
    "operator":             "and",
     "analyzer": "russian_english",
     "minimum_should_match": "80%"}}}
 }'
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.79400253,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "page",
        "_id" : "AWEj5tVmvfv5mY1-erl9",
        "_score" : 0.79400253,
        "_source" : {
          "title" : "United Colors Of Benetton",
          "body" : "Серый"
        }
      },
      {
        "_index" : "test",
        "_type" : "page",
        "_id" : "AWEj5vIBvfv5mY1-erl-",
        "_score" : 0.7594807,
        "_source" : {
          "title" : "United Colors Of Benetton",
          "body" : "Белый"
        }
      }
    ]
  }
}

Аналогично с этим запросом

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query": "Бенеттон Серый",
            "type": "cross_fields",
            "fields": [
              "*.raw^5"
            ],
            "operator": "and",
            "minimum_should_match": "80%"
          }
        },
        {
          "multi_match": {
            "query": "Бенеттон Серый",
            "type": "cross_fields",
             "operator": "and",
            "fields": [
              "*.ng"
            ]
          }
        }
      ]
    }
  }
}
'

Вот _analyze по этому индексу

curl -XGET 'http://localhost:9200/test/_analyze?pretty' -d'
{
   "analyzer": "russian_english",
   "text" : "Бенеттон Серый"
 }
 '
{
  "tokens" : [
    {
      "token" : "unit",
      "start_offset" : 0,
      "end_offset" : 8,
      "type" : "SYNONYM",
      "position" : 0
    },
    {
      "token" : "сер",
      "start_offset" : 9,
      "end_offset" : 14,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "color",
      "start_offset" : 9,
      "end_offset" : 14,
      "type" : "SYNONYM",
      "position" : 1
    },
    {
      "token" : "benetton",
      "start_offset" : 9,
      "end_offset" : 14,
      "type" : "SYNONYM",
      "position" : 3
    }
  ]
}

Да это известная проблема с многословными синонима. Проблема тут:

Когда запрос обрабатывается, elasticsearch думает, что серый и color - это одно и тоже слово потому, что у них одна и та же позиция. В результате, он генерирует вот такой запрос:

GET /test/_validate/query?pretty&explain&rewrite
{
  "query": {
    "multi_match": {
      "query": "Бенеттон Серый",
      "type": "cross_fields",
      "fields": [
        "*.raw"
      ],
      "operator": "and",
      "analyzer": "russian_english",
      "minimum_should_match": "80%"
    }
  }
}
{
  "valid": true,
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "explanations": [
    {
      "index": "test",
      "valid": true,
      "explanation": "+(title.raw:unit | body.raw:unit) +(title.raw:сер | title.raw:color | body.raw:сер | body.raw:color) +(title.raw:benetton | body.raw:benetton)"
    }
  ]
}

Если мы это запрос перепишем с AND и OR, то получим

(title.raw:unit OR body.raw:unit) AND
(title.raw:сер OR title.raw:color OR body.raw:сер OR body.raw:color)  AND
(title.raw:benetton OR body.raw:benetton)

Эту проблему можно решить заменив многословные синонимы на однословные как-то так:

"united colors of benetton => united_colors_of_benetton, benetton",
"benetton => united_colors_of_benetton",
"бенеттон => united_colors_of_benetton",

Но лучшее решение будет переключиться на synonym_graph фильтр при поиске:

PUT test
{
  "settings": {
    "analysis": {
      "filter": {
        "russian_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "russian_stemmer": {
          "type": "stemmer",
          "language": "russian"
        },
        "russian_english_stopwords": {
          "type": "stop",
          "stopwords": "а,без,более,бы,был,была,были,было,быть,в,вам,вас,весь,во,вот,все,всего,всех,вы,где,да,даже,для,до,его,ее,если,есть,еще,же,за,здесь,и,из,или,им,их,к,как,ко,когда,кто,ли,либо,мне,может,мы,на,надо,наш,не,него,нее,нет,ни,них,но,ну,о,об,однако,он,она,они,оно,от,очень,по,под,при,с,со,так,также,такой,там,те,тем,то,того,тоже,той,только,том,ты,у,уже,хотя,чего,чей,чем,что,чтобы,чье,чья,эта,эти,это,я,a,an,and,are,as,at,be,but,by,for,if,in,into,is,it,no,not,of,on,or,such,that,the,their,then,there,these,they,this,to,was,will,with"
        },
        "english_stop": {
          "type": "stop",
          "stopwords": "_english_"
        },
        "english_stemmer": {
          "type": "stemmer",
          "language": "english"
        },
        "english_possessive_stemmer": {
          "type": "stemmer",
          "language": "possessive_english"
        },
        "search_synonyms" : {
          "type": "synonym_graph",
          "synonyms": [
            "бенеттон => united colors of benetton",
            "нью беланс => new balance",
            "зара => zara",
            "женская, женские, женский, девушкам => женщинам",
            "мужчина, мужские, мужской, мужская, парням, парню => мужчинам"
          ]
        },
        "synonym": {
          "type": "synonym",
          "synonyms": [
            "бенеттон => united colors of benetton",
            "нью беланс => new balance",
            "зара => zara",
            "женская, женские, женский, девушкам => женщинам",
            "мужчина, мужские, мужской, мужская, парням, парню => мужчинам"
          ],
          "ignore_case": true,
          "expand": false
        },
        "edgengram": {
          "type": "edge_ngram",
          "min_gram": 3,
          "max_gram": 50
        }
      },
      "analyzer": {
        "index_edgengram": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "edgengram"
          ]
        },
        "russian_english": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "english_possessive_stemmer",
            "lowercase",
            "synonym",
            "russian_stop",
            "english_stop",
            "russian_stemmer",
            "english_stemmer",
            "russian_english_stopwords"
          ]
        },
        "search_russian_english": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "english_possessive_stemmer",
            "lowercase",
            "search_synonyms",
            "russian_stop",
            "english_stop",
            "russian_stemmer",
            "english_stemmer",
            "russian_english_stopwords"
          ]
        }
      }
    }
  },
  "mappings": {
    "page": {
      "properties": {
        "title": {
          "type": "keyword",
          "fields": {
            "org": {
              "type": "text"
            },
            "raw": {
              "type": "text",
              "analyzer": "russian_english",
              "search_analyzer": "russian_english"
            },
            "ng": {
              "type": "text",
              "analyzer": "index_edgengram",
              "search_analyzer": "russian_english"
            }
          },
          "copy_to": [
            "search_data"
          ],
          "ignore_above": 256
        },
        "body": {
          "type": "keyword",
          "fields": {
            "org": {
              "type": "text"
            },
            "raw": {
              "type": "text",
              "analyzer": "russian_english",
              "search_analyzer": "russian_english"
            },
            "ng": {
              "type": "text",
              "analyzer": "index_edgengram",
              "search_analyzer": "russian_english"
            }
          },
          "copy_to": [
            "search_data"
          ],
          "ignore_above": 256
        },        
        "search_data": {
          "type": "text"
        }
      }
    }
  }
}

Да, круто!
Я подозревал что то не то с синонимами, пробовал и так и сяк, но только вы указали на одинаковый position.
Я смотрел в сторону synonym_graph но варнинги в доке "This functionality is experimental .." испугали меня.
Спасибо большое.

Подскажите еще, можно ли что то сделть в таком случае.
В текущую схему что выше добавим еще один документ:

curl -XPOST 'http://localhost:9200/test/page' -d'
{"title": "джин", "body": "крепкий"}

Простой поиск с index_edgengram

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query": "джинсы",
            "type": "cross_fields",
            "fields": [
              "*.ng"
            ],
            "operator": "and",
            "analyzer": "index_edgengram"
          }
        }
      ]
    }
  }
}
'

и мы получаем "джин" хотя просили "джинсы"

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 1,
    "max_score" : 0.46029136,
    "hits" : [
      {
        "_index" : "test",
        "_type" : "page",
        "_id" : "AWFNQQrjvfv5mY1-erm4",
        "_score" : 0.46029136,
        "_source" : {
          "title" : "джин",
          "body" : "крепкий"
        }
      }
    ]
  }
}

_validate/query?explain:
{"valid":true,"_shards":{"total":1,"successful":1,"failed":0},"explanations":[{"index":"test","valid":true,"explanation":"+(blended(terms:[body.ng:джи, body.ng:джин, body.ng:джинс, body.ng:джинсы, title.ng:джи, title.ng:джин, title.ng:джинс, title.ng:джинсы])) #_type:page"}]

edgeNGram надо применять при индексации, а не при поиске.

поправочка) я имел в виду в вот таком запросе

curl 'http://localhost:9200/test/page/_search?pretty' -d'
{
  "query": {
    "bool": {
      "should": [
        {
          "multi_match": {
            "query": "джинсы",
            "type": "cross_fields",
            "fields": [
              "*.raw^5"
            ],
            "operator": "and",
            "analyzer": "search_russian_english",
            "minimum_should_match": "80%"
          }
        },
        {
          "multi_match": {
            "query": "джинсы",
            "type": "cross_fields",
            "fields": [
              "*.ng"
            ],
            "operator": "and",
            "analyzer": "index_edgengram"
          }
        }
      ]
    }
  }
}
'

Надо менять меппинг на edgeNGram, а не запрос.

не пойму, у нас есть body.ng и title.ng которые мы вовремя индексации разложили

 "ng": {
              "type": "text",
              "analyzer": "index_edgengram",
              "search_analyzer": "russian_english"
            }

понял, просто в самом запросе не нужно указывать анализаторы мы их и так в мапинге раставили.

У вас с search_analyzer стеммер используется. Это может привести к странным результатам. По-хорошему, там должен быть такой-же анализатор, что и при индексировании, но без edgeNGram фильтра. То есть, если вы индексировали с

        "index_edgengram": {
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "edgengram"
          ]
        },

то вам надо искать с

        "search_edgengram": {
          "tokenizer": "standard",
          "filter": [
            "lowercase"
          ]
        },

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.