Elasticsearch 搜索技術深入

序號 內容 鏈接地址
1 SpringBoot整合Elasticsearch7.6.1 https://blog.csdn.net/miaomiao19971215/article/details/105106783
2 Elasticsearch Filter執行原理 https://blog.csdn.net/miaomiao19971215/article/details/105487446
3 Elasticsearch 倒排索引與重建索引 https://blog.csdn.net/miaomiao19971215/article/details/105487532
4 Elasticsearch Document寫入原理 https://blog.csdn.net/miaomiao19971215/article/details/105487574
5 Elasticsearch 相關度評分算法 https://blog.csdn.net/miaomiao19971215/article/details/105487656
6 Elasticsearch Doc values https://blog.csdn.net/miaomiao19971215/article/details/105487676
7 Elasticsearch 搜索技術深入 https://blog.csdn.net/miaomiao19971215/article/details/105487711
8 Elasticsearch 聚合搜索技術深入 https://blog.csdn.net/miaomiao19971215/article/details/105487885
9 Elasticsearch 內存使用 https://blog.csdn.net/miaomiao19971215/article/details/105605379
10 Elasticsearch ES-Document數據建模詳解 https://blog.csdn.net/miaomiao19971215/article/details/105720737

一. 手工控制搜索結果的精準度

1.1 搜索包含查詢條件分詞後所有詞條的數據

搜索包含firstcontent詞條的document:

GET /test_sort/_search
{
  "query": {
    "content": {
      "remark": "first content"
    }
  }
}

升級: 搜索包含firstcontent詞條的document:

GET /test_sort/_search
{
  "query": {
    "match": {
      "content": {
        "query": "first content",
        "operator": "and"
      }
    }
  }
}

上述搜索方式中,如果把operator設置成or,則查詢的接口與不使用operator沒有區別。and表明查詢的document中必須既包含first,又包含content。(注意: 並不強制要求first和content必須緊挨着出現)

1.2 搜索包含查詢條件分詞後一定比例數量的詞條的數據

在搜索document時,希望目標數據包含一定比例的查詢條件分詞後的詞條個數,可以使用minimum_should_match,填入百分比或固定數字來實現。百分比代表目標document需要包含搜索條件中詞條個數的百分比,如果無法整除,則向下匹配。固定數字代表至少需要包含多少個詞條。

GET /test_sort/_search
{
  "query": {
    "match": {
      "content": {
        "query": "first content",
        "minimum_should_match": "50%"
      }
    }
  }
}

minimum_should_match可以和bool should搭配使用,should本身代表着多個條件只需要滿足一個即可,如果使用了minimum_should_match=2,則代表至少滿足2個詞條才能被視爲目標數據。

GET /test_sort/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "content": "first"
          }
        },
        {
          "match": {
            "content": "C++"
          }
        },
        {
          "match": {
            "content": "second"
          }
        }
      ],
      "minimum_should_match": 2
    }
  }
}

1.3 match的底層轉換

官方建議搜索時,儘量使用轉換後的形式,執行效率更高(不需要ES自行轉換了)。 在條件較少時轉換請求的數據結構不會帶來太明顯的性能提升,但如果條件非常多,那麼節約的時間就很可觀了。
轉換前1:

GET /test_sort/_search
{
  "query": {
    "content": {
      "content": "first content"
    }
  }
}

轉換後1:

GET /test_sort/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "content": {
              "value": "first"
            }
          }
        },
        {
          "term": {
            "content": {
              "value": "content"
            }
          }
        }
      ]
    }
  }
}

轉換前2:

GET /test_sort/_search
{
  "query": {
    "match": {
      "content": {
        "query": "first content",
        "operator": "and"
      }
    }
  }
}

轉換後2:

GET /test_sort/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "content": {
              "value": "first"
            }
          }
        },
        {
          "term": {
            "content": {
              "value": "content"
            }
          }
        }
      ]
    }
  }
}

二. boost權重控制

人爲的控制搜索條件在進行相關度分數計算時的權重大小。
比如搜索document中content字段內包含first的數據,如果content包含java或C++,則優先顯示包含它們的數據,並且java的權重是C++的三倍。
boost權重控制一般用於搜索時搭配相關度排序使用。比如: 電商平臺對商品進行綜合排序。將一個商品的銷量、廣告費、評價值、庫存、單價等信息進行比較並綜合排序。排序時,庫存的權重最低,廣告費的權重最高。

GET /test_sort/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "first"
          }
        }
      ],
      "should": [
        {
          "match": {
             "content": {
               "query": "java",
               "boost": 3
             }
          }
        },
        {
          "match": {
             "content": {
               "query": "C++",
               "boost": 1
             }
          }
        }
      ]
    }
  }
}

搜索結果爲:

"hits" : [
      {
        "_index" : "test_sort",
        "_type" : "sort_type",
        "_id" : "4",
        "_score" : 3.2636642,
        "_source" : {
          "content" : "first content java"
        }
      },
      {
        "_index" : "test_sort",
        "_type" : "sort_type",
        "_id" : "5",
        "_score" : 1.8323578,
        "_source" : {
          "content" : "first content C++"
        }
      },
      {
        "_index" : "test_sort",
        "_type" : "sort_type",
        "_id" : "1",
        "_score" : 0.48120394,
        "_source" : {
          "content" : "first content",
          "order" : 1
        }
      }
    ]

2.1 多shard環境中相關度分數不準確問題

在 ES的搜索結果中,相關度分數不是一定準確的。在多個shard環境中,使用相同的搜索條件得到的相關度分數可能會有誤差(如果index只有一個主分片,則不會出現誤差)。只要數據量達到一定的程度,那麼相關度分數的誤差就會逐漸趨近於0。

請看以下場景:
現在有兩個主分片: shard0和shard1,它們分別持有10000條document數據,其中,shard0含有100個包含java的document,shard1含有10個包含java的document。ES在shard本地計算相關度分數(不是把數據統一上報到coordinate協調節點後,才進行相關度分數的計算),當以java作爲條件進行搜索時,雖然經過TF計算出的值相同,但使用IDF算法計算相關度分數時,shard0內的得分比shard1低。

如果數據量足夠大,使得含有java的document數據均勻的分佈在所有分片上時,那麼無論在哪個shard上計算相關度分數,得到的結果就都是相同的了。

在開發測試階段,我們可以通過在設置settings時,將number_of_shards的值設置爲1來解決問題,也可以通過增加dfs_query_then_fetch請求參數來解決問題:

GET /test_sort/_search?search_type=dfs_query_then_fetch
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "content": "first"
          }
        }
      ]
    }
  }
}

search_type=dfs_query_then_fetch不建議在生產環境中使用,因爲它會使相關度分數的計算時機從shard本地搜索目標數據,挪到每個shard反饋到協調節點後,由協調節點統一做相關度分數的計算。由於所有的目標結果都會在協調節點彙總,因此協調節點的數據量非常大,而爲了計算相關度分數又不得不把數據全部讀取到內存中,因此這種方式不僅對內存的壓力非常大,還會增加額外的IO開銷。這就是ES官網明明知道多shard時相關度分數不準確的問題,卻不得不在shard本地計算相關度分數的原因。

三. 基於dis_max實現多字段搜索

3.1 dis_max的基本用法

  1. best fields策略: 搜索document中的某一個field字段,儘可能多的匹配搜索搜索條件。

best field的實現手段: dis_max 對每個搜索條件進行評估,以單獨作爲條件進行搜索時,得到的最高相關度分數進行排序。
例子 單獨使用條件1和條件2進行搜索時,各document計算出的相關度分數如下:

document1 document2
條件1 0.5 1.2
條件2 1.1 0.3
最大值 1.1 1.2
  1. most fields策略: 儘可能多的讓document中多個field參與到計算相關度分數中。顯然,這種策略下搜索的精準度沒有best field高。比如某個document中,雖然有一個字段包含了非常多的詞條(由搜索條件提供),但其它字段沒有包含關鍵字。而其它document與之對應的字段中雖然沒有包含這麼多關鍵字,但勝在包含詞條的字段數目多,因此在搜索結果返回時,會被排列到上一個document之前。百度就搜索利用了most fields策略,因爲往往匹配維度多的document比只匹配了寥寥幾個維度(哪怕這些維度匹配的非常精準)的document,對用戶更有用。
    讓我們總結一下
    most_fields的優點: 搜索出的結果更均勻,因爲命中字段多的document排在了前面,命中少的document排在了後面。
    most_fields的缺點: 搜索出的結果沒有best fields那麼精確。
document1 document2
條件1 0.5 1.2
條件2 1.1 0.3
總和 1.6 1.5

beat fields使用案例: 現在想把條件1和條件2結合在一起使用,共同進行搜索,但是希望document1得到的相關度分數爲1.1,document2得到的相關度分數爲1.2,各個條件互不影響,最終得到的排序結果爲: document2 -> document1

做法是在搜索時使用dis_max。 可以注意到,使用dis_max時,不再配合使用should或must,而是使用queries。

GET test_dis_max/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
           "條件1"
        },
        {
          "條件2"
        }
      ]
    }
  }

3.2 使用tie_breaker參數優化dis_max的搜索效果

dis_max是隻取多個query條件中相關度分數最高的用於排序,忽略其它的query條件對應的分數。在某些情況下,我們還需將其它query條件對應的分數加入到最後相關度分數的計算上,這個時候就可以使用tie_breaker參數來優化dis_max的功能。
tie_breaker參數的含義是:將其它所有搜索條件對應的相關度分數乘以一個比例,再參與到結果的排序中。默認參數值爲0,因此不定義此參數,會導致其它搜索條件不參與到結果的排序中。

GET test_dis_max/_search
{
  "query": {
    "dis_max": {
      "queries": [
        {
           "條件1"
        },
        {
          "條件2"
        }
      ],
      "tie_breaker" : 3
    }
  }

此時,相關度分數的計算如下:

document1 document2
條件1 0.5 1.2
條件2 1.1 0.3
最大值 1.1 1.2
最終結果 1.1 + 0.5*3 = 2.6 1.2 + 0.3*3 = 2.1

最終得到的排序結果爲: document1 -> document2

3.3 使用multi_match簡化dis_max+tie_breaker

可以通過multi_match整合boost, query, minimum_should_match, tie_breaker以及fields來實現搜索。具體方式如下:

GET java/_search
{
  "query": {
    "multi_match":{
      "query": "rod java developer",
      "fields": ["name", "remark^2"],
      "type": "best_fields",
      "tie_breaker": 3,
      "minimum_should_match": "50%"
    }
  }
}

其中^n代表權重,比如remark ^2 等價於 。注意,給字段起名時,儘量避免使用 ^,ES在解析到這個符號時,會把它當作轉義字符。

remark: {
  "query": "rot java developer",
  "boost": 2
}

四. 使用multi_match+most fields實現multi_field搜索

3.1中已經談過most_fields策略和best_fields策略的區別,它們沒有優劣之分,只是使用的場景不同。

GET java/_search
{
  "query": {
    "multi_match":{
      "query": "rod java developer",
      "fields": ["name", "remark^2"],
      "type": "most_fields",
      "minimum_should_match": "50%"
    }
  }
}

五. cross fields 跨字段搜索

cross fields會將搜索條件在每一個fields中進行搜索,一般搭配operator使用,具體請看下面的例子:

GET java/_search
{
  "query": {
    "multi_match":{
      "query": "rod java",
      "fields": ["name", "remark"],
      "type": "cross_fields",
      "operator": "and"
    }
  }
}

當"operator"等於"and"時,ES對查詢條件"rod java"進行分詞後,"rod"和"java"必須都分別在"name"或"remark"字段中匹配上。
當"operator"等於"or"時,ES對查詢條件"rod java"進行分詞後,"rod"或"java"只要任意一個詞條在"name"或"remark"中匹配,那麼匹配的document就是搜索結果之一。

六. copy_to組合field搜索

如果想要實現在多個指定的字段中搜索目標數據,以下三種做法都能滿足需求:

  1. _all ES自動創建_all字段,包含index中所有字段,但由於_all字段包含了許多不需要作爲本次搜索目的的字段,因此作爲搜索字段過於沉重了。
  2. multi_match, type=most_field,operator=or 可以實現需求,但排序可能會受到影響,包含搜索條件詞條較多field的document可能會被排到前面,排序後體現的搜索精準度會下降。
  3. copy_to 在索引的設計階段,通過_mapping copy_to的方式,將多個指定字段同時與某個邏輯字段進行關聯,類似java中的推導類型和Mysql中的視圖,這個類型在es持久化時可能並不存在,但它可以在搜索時,充當搜索條件,相當於把若干個字段中的值拼在了一起。

copy_to的定義方式如下:

PUT index_name/_mapping
{
  "properties": {
    "author_first_name": {
      "type": "text",
      "analyzer": "standard",
      "copy_to": "whole_name"
    },
    "author_last_name": {
      "type": "text",
      "analyzer": "standard",
      "copy_to": "whole_name"
    },
    "address": {
       "type": "text",
       "analyzer": "ik_max_word"
    }
  }
}

來看看index_name的數據結構: GET /index_name/_mapping

{
  "index_name" : {
    "mappings" : {
      "properties" : {
        "address" : {
          "type" : "text",
          "analyzer" : "ik_max_word"
        },
        "author_first_name" : {
          "type" : "text",
          "copy_to" : [
            "whole_name"
          ],
          "analyzer" : "standard"
        },
        "author_last_name" : {
          "type" : "text",
          "copy_to" : [
            "whole_name"
          ],
          "analyzer" : "standard"
        }
      }
    }
  }
}

author_first_name和author_last_name都與whole_name進行了關聯。
在正常的搜索時,whole_name虛擬字段 不會被搜索到:

"hits" : [
      {
        "_index" : "index_name",
        "_type" : "_doc",
        "_id" : "lEqYO3EBDfqTlht4Npnl",
        "_score" : 1.0,
        "_source" : {
          "author_first_name" : "Peter",
          "author_last_name" : "Smith"
        }
      },
      {
        "_index" : "index_name",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "author_first_name" : "Smith",
          "author_last_name" : "Williams"
        }
      }
    ]

搜索時,只需要對whole_name中做條件匹配,ES會對whole_name中拼成的值做倒排索引。ES會自動維護whole_name組合字段,我們作爲使用者不需要對它做任何的維護工作。

七. 近似搜索(proximity search)

GET _search
{
  "query" : {
    "match" : {
      "field" : "hello world"
    }
  }
}

以上寫法使用的搜索類型是精確匹配,目標document對應的fields在分詞後,必須包含完整的hello或者world詞條。倘若搜索hel,顯然搜不到任何的數據。

如果我們在搜索時有特殊的要求,比如:

  1. 使用hello world進行搜索,但目標document中必須完整的包含"hello world"整個詞條,不能分割。
  2. document的fields中包含"hello"和"world"詞條,且兩個單詞離的越近,則相關度分數越高。(比如Java hello world比hello Java world的相關度分數高)
  3. 使用hel進行搜索,希望能夠搜索到包含"hel"的數據。

顯然,使用match搜索無法實現這些要求,因此近似匹配就派上用場了。

7.1 match phrase 短語搜索

使用match phrase時,搜索條件不分詞(宏觀上),ES會在倒排索引中查詢是否有document的fields對應的詞條與搜索條件完全相同。
比如:

GET index_name/_search
{
  "query": {
    "match_phrase": {
      "tags": "hello world"
    }
  }
}

在目標document中,必須完整的匹配"hello world"詞條。
query match被稱爲"full-text search",match phrase與它的區別在於,前者會對搜索條件進行分詞,而後者不會(宏觀上)。

7.1.1 match phrase原理 – term position

ES實現match phrase時,其實也進行了分詞,可能這會讓人產生疑惑:剛剛不是說了match phrase不分詞,而full-text search分詞嗎?
這就涉及到了倒排索引的建立過程了。倒排索引在創建時,除了使用analyzer分詞器將fields分割成若干詞條以外,還會記錄以下屬性(包括但不限於):

  1. 詞條在哪些document中出現過,也就是記錄document的id。
  2. 詞條在document中出現的位置(下標 position)
GET _analyze
{
  "text": "hello world java",
  "analyzer": "standard"
}
{
  "tokens" : [
    {
      "token" : "hello",
      "start_offset" : 0,
      "end_offset" : 5,
      "type" : "<ALPHANUM>",
      "position" : 0
    },
    {
      "token" : "world",
      "start_offset" : 6,
      "end_offset" : 11,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "java",
      "start_offset" : 12,
      "end_offset" : 16,
      "type" : "<ALPHANUM>",
      "position" : 2
    }
  ]
}

因此,在使用match phrase進行搜索時,首先會對搜索條件進行分詞(比如拆分成 “hello"和"world”),接着找出既包含"hello"又包含"world"的document,最後比較這兩個詞條在目標document中的position,若position的值是連續的,則說明"hello" "world"在目標document中連續出現,相當於目標document的fields中包含了一個完整的"hello world"詞條,否則代表匹配失敗。

7.1.2 match phrase搜索參數 slop

使用match phrase短語搜索時,如果搜索的參數是hello world,但ES中存儲的是hello java C world, 那麼將查不到任何數據。可能有人會說使用full text search可以解決問題,但如果用戶需要在搜索時,搜索的詞條在目標document中越接近,則相關度分數越高,排名越靠前,那麼full text search便無計可施了。
slop用於計算在進行match phrase短語搜索時,單詞移動到目標位置所花費的最少步數。比如 "hello java C world"被analyzer分詞後,會得到以下倒排索引信息:

hello java C world
position 0 1 2 3

但我們搜索的目標是:

hello world
position 0 1

從目標的位置開始,ES至少要把world的下標向右方移動2位才能與當前document中存放的數據重合。 因此,slop=2便是"hello java C world"對應document的移動臨界值,如果在搜索中slop<2,那麼該document將無法被搜索出來。

舉個例子,以下是ES中存儲的數據,現在準備搜索"refuse exist",不難發現,_id=1移動到目標位置的步長等於4,_id=2等於1,_id=3等於2。

hits" : [
      {
        "_index" : "match_phrase_index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "content" : "I refuse to prove that I exist"
        }
      },
      {
        "_index" : "match_phrase_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "content" : "I refuse prove exist"
        }
      },
      {
        "_index" : "match_phrase_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "content" : "I refuse prove cat exist"
        }
      }
    ]

因此,如果使用slop=2進行短語搜索,搜索結構如下:

GET match_phrase_index/_search
{
  "query": {
    "match_phrase": {
      "content": {
        "query": "refuse exist",
        "slop": 2
      }
      
    }
  }
}

由於(_id=2 -> 1) < (_id=3->2),_id=2花費的步數短,相關度分數高,排名自然更靠前,因此最終得到如下結果:

"hits" : [
      {
        "_index" : "match_phrase_index",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.19916546,
        "_source" : {
          "content" : "I refuse prove exist"
        }
      },
      {
        "_index" : "match_phrase_index",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 0.1325897,
        "_source" : {
          "content" : "I refuse prove cat exist"
        }
      }
    ]

如果搜索條件是"exist refuse",ES在搜索時會按照如下方式移動詞條:

0 1 2 3
尚未移動 I refuse prove exist
第一次移動 I refuse exist/prove
第二次移動 I refuse/exist prove
第三次移動 I exist refuse/prove

顯然,移動三次便能得到搜索條件的順序。

一般對英文text做slop,如果對中文做slop就比較麻煩了,因爲中文分詞後不是一空格一個詞,如果準確的移動中文詞條,需要經過多次測試。

7.2 match和proximity search搭配使用 (召回率和精準度)

首先拋出幾個概念

  1. 召回率: 召回率就是搜索結果的比率。比如索引A中有1000個document,如果返回的結果中包含了900個document,那麼召回率就是90%。爲了提升召回率的效果,ES會盡可能的擴大搜索範圍,返回更多的document。
  2. 精準度: 準確的搜索結果,ES會盡可能的不返回無關的文檔。目的是在結果的第一頁中爲用戶呈現最爲相關的文檔。

如果在搜索中只是用match phrase,會導致召回率底下,錯過許多有用的document,因爲搜索結果中必須包含短語(就算是proximity search近似搜索也是如此)。
如果在搜索中只使用match,會導致精準度底下,只要包含任意一個詞條的document都能被返回。

那麼如果我們向兼顧召回率和精準度,就需要把match和proximity search搭配在一起使用了。

GET /test_index/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "f": "java spark"
          }
        }
      ],
      "should": [
        {
          "match_phrase": {
            "f": {
              "query": "java spark",
              "slop" : 50
            }
          }
        }
      ]
    }
  }
}

以上搜索中,既能返回包含java或spark的document,又能把java和spark更臨近的數據排列在結果的最前列,所以兼顧了召回率和精準度。

7.3 proximity search性能優化

full text search(match)和match phrase(包含proximity search)相比較時,match的效率要高得多,畢竟對document進行分詞並創建倒排索引後,match只需要匹配document中是否含有指定詞條,而match phrase還需要比較position是否連續,proximity search就更麻煩了,它還要嘗試着移動詞條的位置。

一般來說,match的性能比match phrase高出10倍左右,比proximity search高出20倍左右。在中小型項目無需擔心性能問題,因爲ES的搜索性能是毫秒級別的,但如果是大型項目,數據量達到PB級,那就需要考慮優化了。

優化的思路是儘量的減少proximity search搜索的document數量。我們可以先使用match儘可能的搜索出需要的數據,再使用proximity search來提高數據的精度,document的相關度分數,從而影響數據的排序,整個過程被稱作rescore(重計分)。此外,重計分之前,我們往往會使用分頁,進一步的縮小需要重計分的數據範圍。因爲用戶一般只關心前幾頁,甚至前十幾頁的數據,無腦的對所有的相關數據進行重計分是沒有必要的。

語法如下:

GET _search
{
  "query": {
    "match": {
      "f": "java spark"
    }
  },
  "rescore": {
    "window_size": 50,
    "query": {
      "rescore_query": {
        "match_phrase": {
          "custom_field_name": {
            "query": "java spark",
            "slop": 10
          }
        }
      }
    }
  },
  "from": 0,
  "size": 10
}

其中,window_size:50指的是在當前的搜索結果之中,對前50條數據進行重計分。from指的是應該跳過的結果數量(默認0),size指的是應該返回的結果數量。custom_field_name指的是字段的名稱。

八. 前綴搜索 prefix search

使用前綴匹配實現搜索時,通常針對keyword字段,也就是不分詞的字段。

爲什麼不能配合match使用前綴搜索?
答: 因爲沒有意義。使用match時,會導致ES在index的倒排索引中搜索數據,既然是倒排索引,那麼便伴隨着分詞,試問在一個document的fields中的所有分詞內去匹配前綴還有什麼意義呢?

語法:

GET /test_index/_search
{
  "query": {
    "prefix": {
      "f.keyword": {
        "value": "Java and"
      }
    }
  }
}

其中,f是字段名稱,f.keyword指的是ES爲f這個字段默認創建的子字段,value中填寫需要搜索的前綴。keyword類型的字段大小寫敏感。
前綴搜索的特點:

  1. 搜索效率極低。
  2. 不會計算相關度分數
  3. 前綴越短,效率越低 (因此如果一定要用前綴搜索,就儘可能的使用長前綴)

九. 通配符搜索 wildcard

ES中的通配符常用的有兩種: ? 和 *。?匹配1個字符,*匹配0~n個字符。通配符既可以在keyword中使用,也可以在涉及倒排索引、分詞的搜索中使用。性能很低,需要掃描完整的索引。
舉例,搜索第二個字符起以"jav j"開頭的數據。

GET /test_index/_search
{
  "query": {
    "wildcard": {
      "f": {
        "value": "?jav j*"
      }
    }
  }
}

十. 正則搜索

ES中常用的正則符號:

  1. [ ] 範圍,如 [0-9] 0~9範圍內的數字
  2. . 一個字符
  3. + 前面的表達式可以出現多次。

舉例,搜索包含大小寫字母出現多次的數據:

GET /test_index/_search
{
  "query": {
    "regexp": {
      "f.keyword": "[A-z].+"
    }
  }
}

[A-z] 代表搜索條件的範圍是是大小寫字母,[A-z].匹配一個字母和一個任意字符,[A-z].+匹配一個字母和多個任意字符。[A-z]+匹配多個字母,.+匹配1至多個任意字符。

使用正則表達式搜索的性能很低。

十一. 搜索推薦 (重點講解max_expansions)

又叫做"自動補全"。比如搜索Spring k時,希望搜索框中能夠提示Spring kafka和Spring kotlin。
語法:

GET /test_index/_search
{
  "query": {
    "match_phrase_prefix": {
      "f": {
        "query": "Spring k",
        "slop": 5,
        "max_expansions": 5
      }
    }
  }
}

ES在進行搜索推薦時,使用的關鍵字是match_phrase_prefix。首先,ES會去搜索已經分詞的詞條中是否包含搜索條件,接着在slop限制的步長次數內,嘗試着移動搜索條件中的最後一個單詞,使得搜索條件與數據的單詞重合。

max_expansions用於限制被收集的最後一個詞條的變形單詞的個數,這個概念不太容易理解,因此給出一個例子:
有數據如下:

"hits" : [
      {
        "_index" : "test_match_phrase_prefdix",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_source" : {
          "content" : "Elasticsearch Jav and Spring boot Testing and Maven and cat Spark are friends"
        }
      },
      {
        "_index" : "test_match_phrase_prefdix",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 1.0,
        "_source" : {
          "content" : "Tests  Elasticsearch cat and Spring boot and Maven and Spark are friends"
        }
      },
      {
        "_index" : "test_match_phrase_prefdix",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.0,
        "_source" : {
          "content" : "Elasticsearch and Spring cat and Tester Maven and Spark are friends"
        }
      }
    ]

要求: 以"cat test"爲條件進行搜索,讓ES返回搜索推薦。

因此,搜索的語句如下:

GET test_match_phrase_prefdix/_search
{
  "query": {
    "match_phrase_prefix": {
      "content": {
        "query": "cat test",
        "slop": 10,
        "max_expansions": 2
      }
    }
  }
}

現在開始分析ES的搜索過程:

  1. ES將搜索條件進行分詞,得到"cat"和"test"。
  2. ES在倒排索引中找出包含"cat"的文檔。
  3. 在第2步得到的文檔列表中找出test詞條的變形,放入一個容器中。本例中ES會把"Testing",“Tests”,"Tester "放進容器,並按照字典順序進行排序。
  4. 在document中首先定位cat的位置(說白了就是position下標),接着以詞條爲最小單位,移動"test"對應的詞條(比如_id=1的文檔中,移動Testing),若在移動步長的限制值(本例中是10步)範圍內,文檔數據與搜索條件實現了重合(也就是移動成了cat Testing),則當前文檔就是命中的、符合要求的數據。
  5. 搜索語句中max_expansions=2,意味着ES只會搜索容器中前兩個test的變形詞條(也就是"Testing"和"Tests"),而放棄"Tester",因此,即便_doc=3中,Tester只需要移動3個步長就能實現cat Tester,也不會成爲符合要求的數據。

最終搜索出的結果中,不會包含_doc=3:

 "hits" : [
      {
        "_index" : "test_match_phrase_prefdix",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.7947273,
        "_source" : {
          "content" : "Tests  Elasticsearch cat and Spring boot and Maven and Spark are friends"
        }
      },
      {
        "_index" : "test_match_phrase_prefdix",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 0.5328808,
        "_source" : {
          "content" : "Elasticsearch Jav and Spring boot Testing and Maven and cat Spark are friends"
        }
      }
    ]

設計max_expansion的目的在於避免給出過多的搜索推薦。比如用戶輸入"Spring k",index中包含以"k"爲前綴單詞的文檔可能數以萬計,全部返回給用戶顯然不顯示,使用了max_expansion後,就能過濾掉大部分的數據。max_expansion與from size在過濾數據時的側重點不同,前者側重於減少搜索條件中最後一個單詞變形的可能性,而後者只關注數據的下標,對滿足條件的數據進行簡單的過濾。

十二. ngram 分詞機制

ngram是指在指定的長度下,對已經分詞的詞條進行再次拆分。
使用方式如下:

PUT /test_ngram
{
  "settings": {
    "index.max_ngram_diff":30,
    "analysis": {
      "filter": {
        "custom_ngram_filter_mmr": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 30
        }
      },
      "analyzer": {
        "custom_analyzer_mmr": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "custom_ngram_filter_mmr"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "ff": {
        "type": "text",
        "analyzer": "custom_analyzer_mmr",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

分析hello這個單詞

GET test_ngram/_analyze
{
  "text": "hello",
  "analyzer": "custom_analyzer_mmr"
}

可以分出如下單詞:
h,he,hel,hello
e,el,ell,ello,
l,ll,llo,
l,lo,o

edge ngram在ngram的基礎上增加了一條限制,拆分單詞時,只會從首字母向後拆分,比如同樣對hello進行拆分,edge ngram得到的數據如下: h,he,hel,hello

使用edge ngram對分詞後的單詞進行再次拆分的目的在於使用前綴搜索或搜索推薦時,效率更高,因爲再次拆分的單詞與doc_id的對應信息會存儲在倒排索引中,ES可以直接在倒排索引中尋找分詞後的搜索條件,不再需要遍歷每一個詞條並逐一匹配,極大的節省了時間。相對的,由於多出了拆分單詞的動作,因此新增數據花費的時間會變長,並且拆分後的單詞隨倒排索引一起存儲在硬盤上,並加載至內存中,因此會增加硬盤和內存的負擔。

十三. fuzzy模糊搜索(解決拼寫錯誤問題)

搜索條件有時會寫錯,比如不小心將hello world寫成了 hello wold。fuzzy技術用於爲錯誤的拼寫進行糾正。其中,fuzziness代表value的值可以通過多少次操作變成目標詞條。在英文數據中使用fuzzy搜索很有效,但在中文數據中作用很小。
操作的方式有如下三種:

  1. 替換字符
  2. 插入一個字符
  3. 刪除一個字符

舉個例子,ES中存儲的數據是hello,但搜索時不小心寫成了"hleo":

GET /test_ngram/_search
{
  "query": {
    "fuzzy": {
      "ff": {
        "value": "hleo",
        "fuzziness": 2
      }
      
    }
  }
}

爲了把"hleo"修改成"hello",至少需要經歷以下2步操作:

操作步驟 0 1 2 3 4
初始 h l e o
第一步 (在1號和2號位之間插入字符l) h l l e o
第二步 (將1號位和3號位的字符進行交換) h e l l o

如果在搜索hleo時,將fuzziness設置成1,則查不出任何數據。

十四. suggest搜索建議

suggest search (completion suggest): 就建議搜索(又稱搜索建議),也可以叫做自動完成auto-completion。類似百度中的搜索聯想提示功能。

Elasticsearch實現suggest的時候,性能非常高,其構建的不是倒排索引,也不是正排索引,就是純粹的用於進行前綴搜索的一種特殊的數據結構,而且會全部放在內存中,所以使用suggest search進行前綴搜索提示,性能時非常高的。

如果需要使用suggest,則必須在定義index mapping中指定字段開啓suggest。具體方式如下:

PUT /baidu
{
  "mappings": {
      "properties" : {
        "title" : {
          "type": "text",
          "analyzer": "standard",
          "fields": {
            "suggest" : {
              "type" : "completion",
              "analyzer": "standard"
            }
          }
        },
        "content": {
          "type": "text",
          "analyzer": "standard"
        }
      }
  }
}
POST /baidu/_doc/1
{
  "title": "大話西遊電影",
  "content": "大話西遊的電影時隔20年即將在2017年4月重映"
}
POST /baidu/_doc/2
{
  "title": "大話西遊小說",
  "content": "某知名網絡小說作家已經完成了大話西遊同名小說的出版"
}
POST /baidu/_doc/3
{
  "title": "大話西遊手遊",
  "content": "網易遊戲近日出品了大話西遊經典IP的手遊,正在火爆內測中"
}

“type”:“completion”表示爲"title"字段開啓了suggest搜索建議功能。"title.suggest"是"title"的子字段。

使用搜索建議的語句如下:

GET /baidu/_search
{
  "suggest": {
    "suggest_function_name": {
      "prefix": "大話西遊",
      "completion": {
        "field": "title.suggest"
      }
    }
  }
}

“suggest_function_name”是搜索建議函數的函數名,人爲指定。"prefix"填寫搜索內容的前綴。completion field用來指定需要實現搜索建議功能的字段名稱。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章