使用ElasticSearch进行文件名搜索

32

我想使用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以获取前两个文件。
搜索fileile应该返回除最后一个文件名之外的所有文件名。 如何使用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
3个回答

150

您在粘贴的内容中存在以下问题:

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分词器,但它会删除除字母以外的任何字符,(请参见文档),因此您的数字将被完全删除。

您可以使用分析 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。

例如,如果您使用长度为1到4的ngrams索引"abcd",则将得到以下标记:

a b c d ab bc cd abc bcd

但是,如果您在搜索中使用"dcba"(本不应匹配),并且您还使用ngrams分析搜索词,则实际上正在搜索以下内容:

d c b a dc cb ba dbc cba

因此,abcd 将匹配!

解决方案

首先,您需要选择正确的分析器。您的用户可能会搜索单词、数字或日期,但他们可能不希望 ile 匹配 file。相反,使用edge ngrams可能更有用,它会将ngram锚定到每个单词的开头(或结尾)。

另外,为什么要排除docx等格式?用户很可能想要搜索文件类型呢?

因此,让我们通过删除任何不是字母或数字的内容(使用pattern tokenizer)来将每个文件名拆分成较小的标记:

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"
            }
         }
      }
   }
}
'

现在,测试我们的分析器是否正常工作:

filename_search:

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.122012.12.01,因此我尝试更改查询以使用文本短语查询。然而,这并没有起作用。原来,边缘ngram过滤器对于每个ngram都增加了位置计数(而我原本认为每个ngram的位置与单词开头的位置相同)。

上述第(3)点提到的问题仅在使用query_stringfieldtext查询尝试匹配任何标记时才存在。但是,对于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
# }

21
哇,这不仅是一个解决方案,还是我一直在寻找的教程 :D 谢谢 - Biggie
非常感谢这个。到目前为止,它非常有用(可惜链接已经失效)。我仍然有一点困惑(例如,我知道模式是一个RE,但不清楚p{L}是什么)。我正在使用match查询,我遇到的问题是,当我仅在文件名字段中搜索时,似乎可以工作,但在使用_all时却不能:(。有什么想法吗? - Aldo 'xoen' Giambelluca
如果文件名中有单词分隔符,这个方法可以很好地工作。问题是并不是所有的文件都有单词分隔符。实际上,很多文件名组合了驼峰式单词。例如,如果你尝试索引OpenJDK.7z,用户通常会搜索完整的文件名"openjdk",这对于这种分析器来说是有效的,或者可能会搜索"jdk",这种分析器无法返回正确结果。 - Zaid Amir
1
@DrTech:感谢你的好答案...但是我在搜索时遇到了一些问题。我的 Sense 插件出现了错误。 "type": "query_parsing_exception", "reason": "No query registered for [text]", 还有其他人遇到过这个错误吗? - ASN
1
@ASN:将“text”更改为“match”,它应该可以正常工作。 - corvus
显示剩余2条评论

0

我相信这是由于所使用的分词器造成的。

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-gram分词器而不是过滤器。这意味着它将忽略单词边界(并且您也会得到“_”),但是您可能会遇到大小写不匹配的问题,这可能是您首先添加小写分词器的原因。


对于第一种选择:我认为我的filename_analyzer在索引和搜索时已经被使用了,因为我没有明确地使用index_analyzer/search_analyzer。对于第二种选择:我尝试了这种方法。但是只有当我用“”将关键字括起来时,搜索才会有结果,例如:“2012”。此外,“doc”可以找到两个doc文件,但是“.doc*”只能找到docx文件。有什么想法吗? - Biggie

-2

我没有使用ES的经验,但在Solr中,您需要将字段类型指定为text。 您的字段类型为string而不是text。字符串字段未经过分析,但以逐字方式存储和索引。尝试一下,看看它是否有效。

properties": {
        "filename": {
          "type": "string",
          "analyzer": "filename_analyzer"
        }

ES仅使用string类型,默认情况下会进行分析。如果要让它们按原样存储,需要在映射中添加{"index":"not_analyzed"} - DrTech

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接