Поиск в индексе с "грязными" данными

Доброго времени суток. Иcпользую ES 7.13. Есть индекс, созданный программой FSCrawler.

В самом индексе есть данные о номерах телефона, но вот беда, их формат может быть:

7(999)111-22-333 или 7-999-111-22-333 или 7.999.111.22.333, то есть в самом индексе могут быть различные разделители в номерах телефона.

Можно ли настроить запрос так, чтобы при поиске по номеру без разделителей, т.е
если искать по 799911122333, то были найдены все вхождения 7(999)111-22-333 ,7-999-111-22-333 ,7.999.111.22.333

Спасибо

Может что то типа такого подойдёт?

GET phones/_search
{
  "query": {
    "bool": {
      "filter": {
        "script": {
          "script": {
            "source": """params.num.equals(/[-()\.]/.matcher(doc['number.keyword'].value).replaceAll(''))""",
            "params": {
              "num": "799911122333"
            }
          }
        }
      }
    }
  }
}

Спасибо за ответ. Никогда раньше не использовал скрипты в ES, у меня выдаёт ошибку ( сам телефон находится в поле content дефолтного индекса, который создал fscrawler

{
  "error" : {
    "root_cause" : [
      {
        "type" : "script_exception",
        "reason" : "runtime error",
        "script_stack" : [
          "org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:65)",
          "org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:27)",
          "params.num.equals(/[-()\\.]/.matcher(doc['content.keyword'].value).replaceAll(''))",
          "                                        ^---- HERE"
        ],
        "script" : "params.num.equals(/[-()\\.]/.matcher(doc['content.keyword'].value).replaceAll(''))",
        "lang" : "painless",
        "position" : {
          "offset" : 40,
          "start" : 0,
          "end" : 81
        }
      }
    ],
    "type" : "search_phase_execution_exception",
    "reason" : "all shards failed",
    "phase" : "query",
    "grouped" : true,
    "failed_shards" : [
      {
        "shard" : 0,
        "index" : "svodki_dch",
        "node" : "ZQ1H_U0gQnKhtBTyKJ0I0Q",
        "reason" : {
          "type" : "script_exception",
          "reason" : "runtime error",
          "script_stack" : [
            "org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:65)",
            "org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:27)",
            "params.num.equals(/[-()\\.]/.matcher(doc['content.keyword'].value).replaceAll(''))",
            "                                        ^---- HERE"
          ],
          "script" : "params.num.equals(/[-()\\.]/.matcher(doc['content.keyword'].value).replaceAll(''))",
          "lang" : "painless",
          "position" : {
            "offset" : 40,
            "start" : 0,
            "end" : 81
          },
          "caused_by" : {
            "type" : "illegal_argument_exception",
            "reason" : "No field found for [content.keyword] in mapping"
          }
        }
      }
    ]
  },
  "status" : 400

А какое имя у индекса? Можно ли посмотреть как выглядит ваш полный запрос к этому индексу?

А так же маппинг?

GET <имя индекса>/_mapping

A в этом поле кроме цифр что-нибудь еще есть? Они всегда запятой разделены?

aleksmaus:
Имя индекса - second.
Запрос к нему (как Вы посоветовали)

GET second/_search
{
  "query": {
    "bool": {
      "filter": {
        "script": {
          "script": {
            "source": """params.num.equals(/[-()\.]/.matcher(doc['content.keyword'].value).replaceAll(''))""",
            "params": {
              "num": "799911122333"
            }
          }
        }
      }
    }
  }
}

Но повторяюсь, к своему стыду, скриптинг я не использовал ранее
Мэппинг такой (создан программой fscrawler):

  "second" : {
    "mappings" : {
      "dynamic_templates" : [
        {
          "raw_as_text" : {
            "path_match" : "meta.raw.*",
            "mapping" : {
              "fields" : {
                "keyword" : {
                  "ignore_above" : 256,
                  "type" : "keyword"
                }
              },
              "type" : "text"
            }
          }
        }
      ],
      "properties" : {
        "attachment" : {
          "type" : "binary"
        },
        "attributes" : {
          "properties" : {
            "group" : {
              "type" : "keyword"
            },
            "owner" : {
              "type" : "keyword"
            }
          }
        },
        "content" : {
          "type" : "text"
        },
        "file" : {
          "properties" : {
            "checksum" : {
              "type" : "keyword"
            },
            "content_type" : {
              "type" : "keyword"
            },
            "created" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "extension" : {
              "type" : "keyword"
            },
            "filename" : {
              "type" : "keyword",
              "store" : true
            },
            "filesize" : {
              "type" : "long"
            },
            "indexed_chars" : {
              "type" : "long"
            },
            "indexing_date" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "last_accessed" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "last_modified" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "url" : {
              "type" : "keyword",
              "index" : false
            }
          }
        },
        "meta" : {
          "properties" : {
            "altitude" : {
              "type" : "text"
            },
            "author" : {
              "type" : "text"
            },
            "comments" : {
              "type" : "text"
            },
            "contributor" : {
              "type" : "text"
            },
            "coverage" : {
              "type" : "text"
            },
            "created" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "creator_tool" : {
              "type" : "keyword"
            },
            "date" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "description" : {
              "type" : "text"
            },
            "format" : {
              "type" : "text"
            },
            "identifier" : {
              "type" : "text"
            },
            "keywords" : {
              "type" : "text"
            },
            "language" : {
              "type" : "keyword"
            },
            "latitude" : {
              "type" : "text"
            },
            "longitude" : {
              "type" : "text"
            },
            "metadata_date" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "modifier" : {
              "type" : "text"
            },
            "print_date" : {
              "type" : "date",
              "format" : "dateOptionalTime"
            },
            "publisher" : {
              "type" : "text"
            },
            "rating" : {
              "type" : "byte"
            },
            "relation" : {
              "type" : "text"
            },
            "rights" : {
              "type" : "text"
            },
            "source" : {
              "type" : "text"
            },
            "title" : {
              "type" : "text"
            },
            "type" : {
              "type" : "text"
            }
          }
        },
        "path" : {
          "properties" : {
            "real" : {
              "type" : "keyword",
              "fields" : {
                "fulltext" : {
                  "type" : "text"
                },
                "tree" : {
                  "type" : "text",
                  "analyzer" : "fscrawler_path",
                  "fielddata" : true
                }
              }
            },
            "root" : {
              "type" : "keyword"
            },
            "virtual" : {
              "type" : "keyword",
              "fields" : {
                "fulltext" : {
                  "type" : "text"
                },
                "tree" : {
                  "type" : "text",
                  "analyzer" : "fscrawler_path",
                  "fielddata" : true
                }
              }
            }
          }
        }
      }
    }
  }
}

Igor_Motov:
В этом поле (content) чего только нет, тк это залитые в Elastic документы ms office, а сделал я это потому, что они до жути не структурированные и их годами формировали нерадивые пользователи, так что проще всего мне к ним обращаться через ES, разделители там могут быть разные (но в основном '/', '.' ,'(' ,')', '*', '-')

Спасибо за разъяснение, изначально подумал, что поле keyword и в нем только номер телефона.

В таком случае можно попробовать custom analyzer, как один из вариантов.

Если ситуация позволяет закрыть индекс на короткий промежуток времени чтобы добавить analyzer в тот же самый индекс, то можно так попробовать:

1. закрыть индекс

POST /second/_close

2. добавить analyzer

PUT /second/_settings
{
  "analysis" : {
    "char_filter": {
        "digits_only": {
          "type": "pattern_replace",
          "pattern": "[^\\d\\s]"
        }
    },
    "analyzer":{
      "digits":{
        "type":"custom",
        "char_filter": "digits_only",
        "tokenizer": "standard",
        "filer": ["trim"]
      }
    }
  }
}

3. открыть индекс

POST /second/_open

4. изменить mapping: добавить новое поле phone с новым analyzer и copy_to к полю content

PUT /second/_mapping
{
  "properties": {
    "content" : {
      "type" : "text",
      "copy_to": "phone"
    },
    "phone": {
      "type": "text",
      "analyzer": "digits"
    }
  }
}

5. pickup mapping changes

POST second/_update_by_query?conflicts=proceed

6. после этого можно использовать новое поле для запроса с номером

GET second/_search
{
  "query" : {
    "match": {
      "phone" : {
        "query": "799911122333"
      }
    }
  }
}

Ну и конечно понятно что в этом поле будут все цифры не только телефоны, и чтобы ограничить телефонами надо докрутить analyzer. Например поумнее сделать regular expressions чтобы выбирал только телефоны.

@Igor_Motov меня поправит если есть какой-то более лучший вариант

1 Like

Спасибо большое за развёрнутый ответ, всё вроде работает!

Единственно непонятно как highlights делать, когда ищешь 799911122333, а находишь 7(999)111-223-33