Поиск имени файла с помощью ElasticSearch

Я хочу использовать ElasticSearch для поиска имен файлов (а не содержимого файла). Поэтому мне нужно найти часть имени файла (точное совпадение, без нечеткого поиска).

Пример:
У меня есть файлы со следующими именами:

My_first_file_created_at_2012.01.13.doc
My_second_file_created_at_2012.01.13.pdf
Another file.txt
And_again_another_file.docx
foo.bar.txt

Теперь я хочу найти 2012.01.13, чтобы получить первые два файла.
Поиск file или ile должен вернуть все имена файлов, кроме последнего.

Как я могу сделать это с помощью ElasticSearch?

Это то, что я тестировал, но всегда возвращает нулевые результаты:

curl -X DELETE localhost:9200/files
curl -X PUT    localhost:9200/files -d '
{
  "settings" : {
    "index" : {
      "analysis" : {
        "analyzer" : {
          "filename_analyzer" : {
            "type" : "custom",
            "tokenizer" : "lowercase",
            "filter"    : ["filename_stop", "filename_ngram"]
          }
        },
        "filter" : {
          "filename_stop" : {
            "type" : "stop",
            "stopwords" : ["doc", "pdf", "docx"]
          },
          "filename_ngram" : {
            "type" : "nGram",
            "min_gram" : 3,
            "max_gram" : 255
          }
        }
      }
    }
  },

  "mappings": {
    "files": {
      "properties": {
        "filename": {
          "type": "string",
          "analyzer": "filename_analyzer"
        }
      }
    }
  }
}
'

curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_first_file_created_at_2012.01.13.doc" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_second_file_created_at_2012.01.13.pdf" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "Another file.txt" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "And_again_another_file.docx" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "foo.bar.txt" }'
curl -X POST "http://localhost:9200/files/_refresh"


FILES='
http://localhost:9200/files/_search?q=filename:2012.01.13
'

for file in ${FILES}
do
  echo; echo; echo ">>> ${file}"
  curl "${file}&pretty=true"
done

person Biggie    schedule 23.02.2012    source источник


Ответы (3)


У вас есть различные проблемы с тем, что вы вставили:

1) Неверное сопоставление

При создании индекса вы указываете:

"mappings": {
    "files": {

Но ваш тип на самом деле file, а не files. Если бы вы проверили сопоставление, вы бы сразу увидели это:

curl -XGET 'http://127.0.0.1:9200/files/_mapping?pretty=1' 

# {
#    "files" : {
#       "files" : {
#          "properties" : {
#             "filename" : {
#                "type" : "string",
#                "analyzer" : "filename_analyzer"
#             }
#          }
#       },
#       "file" : {
#          "properties" : {
#             "filename" : {
#                "type" : "string"
#             }
#          }
#       }
#    }
# }

2) Неверное определение анализатора

Вы указали токенизатор lowercase, но он удаляет все, что не является буквой (см. docs), поэтому ваши номера будут полностью удалены.

Вы можете проверить это с помощью analyze API:

curl -XGET 'http://127.0.0.1:9200/_analyze?pretty=1&text=My_file_2012.01.13.doc&tokenizer=lowercase' 

# {
#    "tokens" : [
#       {
#          "end_offset" : 2,
#          "position" : 1,
#          "start_offset" : 0,
#          "type" : "word",
#          "token" : "my"
#       },
#       {
#          "end_offset" : 7,
#          "position" : 2,
#          "start_offset" : 3,
#          "type" : "word",
#          "token" : "file"
#       },
#       {
#          "end_offset" : 22,
#          "position" : 3,
#          "start_offset" : 19,
#          "type" : "word",
#          "token" : "doc"
#       }
#    ]
# }

3) Ngrams в поиске

Вы включаете фильтр токенов ngram как в анализатор индекса, так и в анализатор поиска. Это нормально для анализатора индексов, потому что вы хотите, чтобы ngrams были проиндексированы. Но когда вы ищете, вы хотите искать всю строку, а не каждую энграмму.

Например, если вы проиндексируете "abcd" с помощью ngrams длиной от 1 до 4, вы получите следующие токены:

a b c d ab bc cd abc bcd

Но если вы ищете по "dcba" (что не должно совпадать) и анализируете условия поиска с помощью ngrams, то на самом деле вы ищете по:

d c b a dc cb ba dbc cba

Таким образом, a,b,c и d будут совпадать!

Решение

Во-первых, нужно правильно выбрать анализатор. Ваши пользователи, вероятно, будут искать слова, числа или даты, но вряд ли будут ожидать, что ile будет соответствовать file. Вместо этого, вероятно, будет полезнее использовать граничные диаграммы, которые привяжет ngram к началу (или концу) каждого слова.

Кроме того, зачем исключать docx и т. д.? Конечно, пользователь может захотеть выполнить поиск по типу файла?

Итак, давайте разобьем каждое имя файла на более мелкие токены, удалив все, что не является буквой или цифрой (используя токенизатор шаблонов):

My_first_file_2012.01.13.doc
=> my first file 2012 01 13 doc

Затем для анализатора индекса мы также будем использовать пограничные ngram для каждого из этих токенов:

my     => m my
first  => f fi fir firs first
file   => f fi fil file
2012   => 2 20 201 201
01     => 0 01
13     => 1 13
doc    => d do doc

Мы создаем индекс следующим образом:

curl -XPUT 'http://127.0.0.1:9200/files/?pretty=1'  -d '
{
   "settings" : {
      "analysis" : {
         "analyzer" : {
            "filename_search" : {
               "tokenizer" : "filename",
               "filter" : ["lowercase"]
            },
            "filename_index" : {
               "tokenizer" : "filename",
               "filter" : ["lowercase","edge_ngram"]
            }
         },
         "tokenizer" : {
            "filename" : {
               "pattern" : "[^\\p{L}\\d]+",
               "type" : "pattern"
            }
         },
         "filter" : {
            "edge_ngram" : {
               "side" : "front",
               "max_gram" : 20,
               "min_gram" : 1,
               "type" : "edgeNGram"
            }
         }
      }
   },
   "mappings" : {
      "file" : {
         "properties" : {
            "filename" : {
               "type" : "string",
               "search_analyzer" : "filename_search",
               "index_analyzer" : "filename_index"
            }
         }
      }
   }
}
'

Теперь проверьте правильность работы наших анализаторов:

поиск_имени_файла:

curl -XGET 'http://127.0.0.1:9200/files/_analyze?pretty=1&text=My_first_file_2012.01.13.doc&analyzer=filename_search' 
[results snipped]
"token" : "my"
"token" : "first"
"token" : "file"
"token" : "2012"
"token" : "01"
"token" : "13"
"token" : "doc"

индекс_имя_файла:

curl -XGET 'http://127.0.0.1:9200/files/_analyze?pretty=1&text=My_first_file_2012.01.13.doc&analyzer=filename_index' 
"token" : "m"
"token" : "my"
"token" : "f"
"token" : "fi"
"token" : "fir"
"token" : "firs"
"token" : "first"
"token" : "f"
"token" : "fi"
"token" : "fil"
"token" : "file"
"token" : "2"
"token" : "20"
"token" : "201"
"token" : "2012"
"token" : "0"
"token" : "01"
"token" : "1"
"token" : "13"
"token" : "d"
"token" : "do"
"token" : "doc"

ОК - вроде работает правильно. Итак, давайте добавим некоторые документы:

curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_first_file_created_at_2012.01.13.doc" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_second_file_created_at_2012.01.13.pdf" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "Another file.txt" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "And_again_another_file.docx" }'
curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "foo.bar.txt" }'
curl -X POST "http://localhost:9200/files/_refresh"

И попробуйте поиск:

curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1'  -d '
{
   "query" : {
      "text" : {
         "filename" : "2012.01"
      }
   }
}
'

# {
#    "hits" : {
#       "hits" : [
#          {
#             "_source" : {
#                "filename" : "My_second_file_created_at_2012.01.13.pdf"
#             },
#             "_score" : 0.06780553,
#             "_index" : "files",
#             "_id" : "PsDvfFCkT4yvJnlguxJrrQ",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_first_file_created_at_2012.01.13.doc"
#             },
#             "_score" : 0.06780553,
#             "_index" : "files",
#             "_id" : "ER5RmyhATg-Eu92XNGRu-w",
#             "_type" : "file"
#          }
#       ],
#       "max_score" : 0.06780553,
#       "total" : 2
#    },
#    "timed_out" : false,
#    "_shards" : {
#       "failed" : 0,
#       "successful" : 5,
#       "total" : 5
#    },
#    "took" : 4
# }

Успех!

#### ОБНОВЛЕНИЕ ####

Я понял, что поиск 2012.01 будет соответствовать как 2012.01.12, так и 2012.12.01, поэтому я попытался изменить запрос, чтобы использовать текстовая фраза. Однако это не сработало. Получается, что фильтр edge ngram увеличивает счетчик позиций для каждого ngram (в то время как я думал, что позиция каждого ngram будет такой же, как и для начала слова).

Проблема, упомянутая в пункте (3) выше, возникает только при использовании запроса query_string, field или text, который пытается сопоставить ЛЮБОЙ токен. Однако для запроса text_phrase он пытается сопоставить ВСЕ токены и в правильном порядке.

Чтобы продемонстрировать проблему, проиндексируйте другой документ с другой датой:

curl -X POST "http://localhost:9200/files/file" -d '{ "filename" : "My_third_file_created_at_2012.12.01.doc" }'
curl -X POST "http://localhost:9200/files/_refresh"

И выполните тот же поиск, что и выше:

curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1'  -d '
{
   "query" : {
      "text" : {
         "filename" : {
            "query" : "2012.01"
         }
      }
   }
}
'

# {
#    "hits" : {
#       "hits" : [
#          {
#             "_source" : {
#                "filename" : "My_third_file_created_at_2012.12.01.doc"
#             },
#             "_score" : 0.22097087,
#             "_index" : "files",
#             "_id" : "xmC51lIhTnWplOHADWJzaQ",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_first_file_created_at_2012.01.13.doc"
#             },
#             "_score" : 0.13137488,
#             "_index" : "files",
#             "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_second_file_created_at_2012.01.13.pdf"
#             },
#             "_score" : 0.13137488,
#             "_index" : "files",
#             "_id" : "XwLNnSlwSeyYtA2y64WuVw",
#             "_type" : "file"
#          }
#       ],
#       "max_score" : 0.22097087,
#       "total" : 3
#    },
#    "timed_out" : false,
#    "_shards" : {
#       "failed" : 0,
#       "successful" : 5,
#       "total" : 5
#    },
#    "took" : 5
# }

Первый результат имеет дату 2012.12.01, которая не является лучшим совпадением для 2012.01. Итак, чтобы сопоставить только эту точную фразу, мы можем сделать:

curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1'  -d '
{
   "query" : {
      "text_phrase" : {
         "filename" : {
            "query" : "2012.01",
            "analyzer" : "filename_index"
         }
      }
   }
}
'

# {
#    "hits" : {
#       "hits" : [
#          {
#             "_source" : {
#                "filename" : "My_first_file_created_at_2012.01.13.doc"
#             },
#             "_score" : 0.55737644,
#             "_index" : "files",
#             "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_second_file_created_at_2012.01.13.pdf"
#             },
#             "_score" : 0.55737644,
#             "_index" : "files",
#             "_id" : "XwLNnSlwSeyYtA2y64WuVw",
#             "_type" : "file"
#          }
#       ],
#       "max_score" : 0.55737644,
#       "total" : 2
#    },
#    "timed_out" : false,
#    "_shards" : {
#       "failed" : 0,
#       "successful" : 5,
#       "total" : 5
#    },
#    "took" : 7
# }

Или, если вы все еще хотите сопоставить все 3 файла (поскольку пользователь может запомнить некоторые слова в имени файла, но в неправильном порядке), вы можете выполнить оба запроса, но увеличить важность имени файла, которое находится в правильном порядке. :

curl -XGET 'http://127.0.0.1:9200/files/file/_search?pretty=1'  -d '
{
   "query" : {
      "bool" : {
         "should" : [
            {
               "text_phrase" : {
                  "filename" : {
                     "boost" : 2,
                     "query" : "2012.01",
                     "analyzer" : "filename_index"
                  }
               }
            },
            {
               "text" : {
                  "filename" : "2012.01"
               }
            }
         ]
      }
   }
}
'

# [Fri Feb 24 16:31:02 2012] Response:
# {
#    "hits" : {
#       "hits" : [
#          {
#             "_source" : {
#                "filename" : "My_first_file_created_at_2012.01.13.doc"
#             },
#             "_score" : 0.56892186,
#             "_index" : "files",
#             "_id" : "ZUezxDgQTsuAaCTVL9IJgg",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_second_file_created_at_2012.01.13.pdf"
#             },
#             "_score" : 0.56892186,
#             "_index" : "files",
#             "_id" : "XwLNnSlwSeyYtA2y64WuVw",
#             "_type" : "file"
#          },
#          {
#             "_source" : {
#                "filename" : "My_third_file_created_at_2012.12.01.doc"
#             },
#             "_score" : 0.012931341,
#             "_index" : "files",
#             "_id" : "xmC51lIhTnWplOHADWJzaQ",
#             "_type" : "file"
#          }
#       ],
#       "max_score" : 0.56892186,
#       "total" : 3
#    },
#    "timed_out" : false,
#    "_shards" : {
#       "failed" : 0,
#       "successful" : 5,
#       "total" : 5
#    },
#    "took" : 4
# }
person DrTech    schedule 24.02.2012
comment
Вау, это не просто решение. Это учебник, который я искал: D THX - person Biggie; 26.02.2012
comment
Большое спасибо за этот отличный ответ! Я только что заметил, что текстовые * запросы устарели в самых последних версиях elasticsearch и должны быть переименованы в match и match_phrase. - person Jörn; 22.08.2014
comment
Большое спасибо за это. До сих пор это было очень полезно (жаль, что ссылки не работают). Я все еще немного запутался в нескольких битах (например, я знаю, что шаблон является RE, но не ясно, что такое p{L}). Я использую его с запросом match, проблема, которую я вижу, заключается в том, что когда я ищу только в поле имени файла, кажется, что он работает, но не при использовании _all :(. Есть идеи? - person Aldo 'xoen' Giambelluca; 15.04.2015
comment
Это прекрасно работает, если в именах файлов есть разделители слов. Проблема в том, что не во всех файлах они есть. На самом деле многие имена файлов сочетают в себе камилизированные слова. Например, если вы попытаетесь проиндексировать OpenJDK.7z, теперь пользователь обычно либо будет искать полное имя файла openjdk, которое будет работать с этим анализатором, либо, вероятно, будет искать jdk, которое этот анализатор не вернет. - person Zaid Amir; 15.04.2015
comment
text_phrase: {filename: {boost: 2, query: 2012.01, Analyzer: filename_index}} } Здесь вы используете filename_index, который имеет анализатор Edge ngram во время поиска. Значит ли это, что текст во время поиска также будет поврежден? - person Anirudh Modi; 26.11.2015
comment
@DrTech: Спасибо за отличный ответ... У меня возникли проблемы при поиске. Я получаю сообщение об ошибке в моем плагине sense. "type": "query_parsing_exception", "reason": "No query registered for [text]", Кто-нибудь еще сталкивался с такой же ошибкой? - person ASN; 27.06.2016
comment
@ASN: измените текст, чтобы он соответствовал, и он должен работать нормально - person corvus; 07.09.2016

Я считаю, что это из-за использования токенизатора.

http://www.elasticsearch.org/guide/reference/index-modules/analysis/lowercase-tokenizer.html

Токенизатор нижнего регистра разделяется по границам слов, поэтому 2012.01.13 будет проиндексирован как «2012», «01» и «13». Поиск строки «2012.01.13» явно не совпадет.

Одним из вариантов было бы добавить токенизацию в поиск. Таким образом, при поиске «2012.01.13» будут использоваться те же токены, что и в индексе, и они будут совпадать. Это также удобно, так как вам не нужно всегда строчными буквами выполнять поиск в коде.

Второй вариант — использовать токенизатор n-грамм вместо фильтра. Это будет означать, что он будет игнорировать границы слов (и вы также получите «_»), однако у вас могут возникнуть проблемы с несоответствием регистра, что, по-видимому, является причиной, по которой вы в первую очередь добавили токенизатор нижнего регистра.

person Chris Rode    schedule 24.02.2012
comment
К 1-му варианту: я думал, что мой filename_analyzer уже будет использоваться при индексации и поиске, потому что я явно не использовал index_analyzer/search_analyzer. Ко 2-му варианту: пробовал так. Но поиск имеет результаты только в том случае, если я окружу ключевые слова "*", например: "*2012*". Причем "*doc*" находит оба doc-файла, а "*.doc*" находит только docx-файл. Есть идеи? - person Biggie; 24.02.2012

У меня нет опыта работы с ES, но в Solr вам нужно указать тип поля как текст. Ваше поле имеет тип строка вместо текст. Строковые поля не анализируются, а сохраняются и индексируются дословно. Попробуйте и посмотрите, сработает ли это.

properties": {
        "filename": {
          "type": "string",
          "analyzer": "filename_analyzer"
        }
person Mikos    schedule 24.02.2012
comment
ES просто использует тип string, и они анализируются по умолчанию. Если вы хотите, чтобы они сохранялись дословно, вам нужно добавить {"index":"not_analyzed"} к отображению - person DrTech; 25.02.2012