Търсене на име на файл с 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), така че вашите номера се премахват напълно.

Можете да проверите това с 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 да бъдат индексирани. Но когато търсите, искате да търсите в целия низ, а не във всеки ngram.

Например, ако индексирате "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. Вместо това вероятно ще бъде по-полезно да използвате edge ngrams, които ще закотви ngram към началото (или края) на всяка дума.

Също така, защо изключваме docx и т.н.? Със сигурност потребителят може да иска да търси по типа файл?

Така че нека разделим всяко име на файл на по-малки символи, като премахнем всичко, което не е буква или число (използвайки токенизатор на шаблон):

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

След това за анализатора на индекса ще използваме също крайни ngrams за всеки от тези токени:

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"

OK - изглежда, че работи правилно. Така че нека добавим някои документи:

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, анализатор : filename_index } } Тук използвате filename_index, който има edge ngram анализатор по време на търсене. Така че това няма ли да означава, че текстът по време на търсене също ще бъде повреден? - person Anirudh Modi; 26.11.2015
comment
@DrTech: Благодаря за чудесния отговор... Имам някакъв проблем при търсенето. Получавам грешка в приставката за усещане. "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