讀書筆記:從Lucene到Elasticsearch:全文檢索實戰

BlackHole1發表於2019-01-08

公司專案的的日誌埋點是發到 Elasticsearch 上的,有時開發會去在 kibana 上查詢相關的日誌資訊,用於診斷使用者的問題。但是 kibana 太過重量級,查詢起來比較慢。於是學習了下 Elasticsearch 的用法,此文章個人也只是點到為止。工作時夠用就行了,所有有些地方可能並沒有詳細說明。只能充當入門讀物吧。

當前的筆記只介紹 Elasticsearch 的搜尋部分。

文章中的搜尋都是在 kibanaDev tools 進行查詢的。

準備工作

需要安裝 Elasticsearchkibanaelasticsearch-analysis-ik

具體的安裝方式,這裡就不再闡述了。(安裝完,記得重啟 Elasticsearch

重啟完成後,開啟 kibanaDev tools,輸入下面的DSL程式碼,並執行:

PUT books
{
  "settings": {
    "number_of_replicas": 1,
    "number_of_shards": 3
  },
  "mappings": {
    "IT": {
      "properties": {
        "id": {
          "type": "long"
        },
        "title": {
          "type": "text",
          "analyzer": "ik_max_word"
        },
        "language": {
          "type": "keyword"
        },
        "author": {
          "type": "keyword"
        },
        "price": {
          "type": "double"
        },
        "year": {
          "type": "date",
          "format": "yyyy-MM-dd"
        },
        "description": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
    }
  }
}

複製程式碼

執行好後,下載 books.json 檔案,並進行匯入。如果你安裝的 Elasticsearch 版本小於6.0,使用下面的命令進行匯入 books.json

curl -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @books.json
複製程式碼

如果你的 Elasticsearch 版本大於6.0,則使用下面的命令進行匯入:

curl -H "Content-Type: application/json" -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @books.json
複製程式碼

基本搜尋

返回指定index的所有文件

GET books/_search
{
  "query": {
    "match_all": {}
  }
}
複製程式碼

可以簡寫為:

GET books/search
複製程式碼

查詢指定欄位中包含給定單詞的文件

使用term來進行查詢,term查詢不會被解析,只有查詢的詞和文件中的詞精確匹配才會被搜尋到,應用場景為:查詢人名、地名等需要精準匹配的需求。

查詢title欄位中含有思想的書籍

GET books/_search
{
  "query": {
    "term": {
      "title": "思想"
    }
  }
}
複製程式碼

返回如下:

Imgur

對查詢結果進行分頁

有時查詢時,會返回成千上萬的資料,這種情況下,分頁的作用就出來了。

分頁有兩個屬性,分別是fromsize

  • from: 從何處開始
  • size: 返回的文件最大數量

可以理解為:我從from位置把剩下的文件全部返回,然後size限制了返回的數量。

用js程式碼來詮釋就是:

const from = 100 - 1; // 陣列從0開始,需要減一
const size = 10;
const data = [1, 2, 3, ..., 999, 1000];

const fromDate = data.splice(from);
const result = fromData.splice(0, size);
console.log(result) //=> [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
複製程式碼

限制返回欄位

一般我們查詢時,都是為了觀察某一個欄位,而不是想看全部的欄位。而如果是預設情況下,Elasticsearch 會返回的文件的全部欄位資訊。會對工作造成一定的影響。於是,Elasticsearch 提供了一個介面,用於限制返回的欄位。假設我只需要 titleauthor 欄位:

GET books/_search
{
  "_source": ["title", "author"],
  "query": {
    "term": {
      "title": "java"
    }
  }
}
複製程式碼

結果如圖:

Imgur

基於最小評分過濾

因為 Elasticsearch 在做普通的搜尋時,是採用相關性進行搜尋的,而相關性是由評分 取決的。所以當我們進行模糊搜尋時,Elasticsearch 可能會返回一些相關性不那麼高的文件。所以我們可以通過 Elasticsearch 提供的介面,來設定一個評分最低標準,低於這個標準的文件,將不會出現在結果頁中。

比如,我想搜尋 title 裡包含 java 的文件,並且評分不低於0.7

GET books/_search
{
  "min_score": 0.7,
  "query": {
    "term": {
      "title": "java"
    }
  }
}
複製程式碼

結果如圖:

Imgur

高亮關鍵字

有時,我們會把 Elasticsearch 結果直接匯入到網頁中,這個時候需要高亮關鍵字,讓使用者更加清楚自己想要的東西,Elasticsearch 已經提供了一個介面,比如我想讓搜尋出來的結果中的關鍵字高亮:

GET books/_search
{
  "_source": ["title"],
  "min_score": 0.7,
  "query": {
    "term": {
      "title": "java"
    }
  },
  "highlight": {
    "fields": {
      "title": {}
    }
  }
}
複製程式碼

結果如圖:

Imgur

預設的標籤是<em></em>,如果你想自定義,可以使用: pre_tagspost_tags。最終查詢程式碼為:

GET books/_search
{
  "_source": ["title"],
  "min_score": 0.7,
  "query": {
    "term": {
      "title": "java"
    }
  },
  "highlight" : {
    "pre_tags" : ["<h1>"],
    "post_tags" : ["</h1>"],
    "fields" : {
      "title" : {}
    }
  }
}
複製程式碼

結果如圖:

Imgur

全文查詢

上節基本都是以 term 進行搜尋,但其實 Elasticsearch 提供了很多搜尋方法,本章就是介紹 Elasticsearch 有哪些搜尋方法、分別起的作用。

本章對 common_terms queryquery_string querysimple_query_string query 沒有解釋說明,因為使用起來較少,而且解釋起來較為麻煩。如果想了解,可以參考網上的文章。這裡就不在闡述了。

match query

我們先使用 term 進行一次查詢:

GET books/_search
{
  "_source": ["title", "author"],
  "query": {
    "term": {
      "title": "java程式設計"
    }
  }
}
複製程式碼

你會發現,其結果為空(但是資料庫裡是有這個資料的),如圖:

Imgur

這是因為 term 是匹配分詞後的詞項來進行查詢的。比如剛剛我們查的 java程式設計 ,在 Elasticsearch 進行分詞時,會把 java程式設計 分為:java程式設計 。導致匹配不起來。

用程式碼詮釋的話就是:

const keyword = 'java程式設計';
const data = ['java', '程式設計'];
const result = data.includes(keyword);
console.log(result) //=> false
複製程式碼

現在我們把 term 換成 match 來嘗試下:

GET books/_search
{
  "_source": ["title", "author"],
  "query": {
    "match": {
      "title": "java程式設計"
    }
  }
}
複製程式碼

結果如圖:

Imgur

可以發現,已經有結果了,但是為什麼會有兩個呢?

原因是因為 match 會對你的關鍵字進行分詞,然後去匹配文件分詞後的結果,只要文件裡的詞項能匹配關鍵字分詞後的任何一個,都會返回到結果裡。

程式碼詮釋:

const data = ['java', '程式設計', '思想']; // 分詞後的文件裡的資料
const keywords = ['java', '程式設計', '思想']; // 分詞後的關鍵字

const result = (() => {
  for (let x = 0; x < data.length; x++) {
    const dataItem = data[x];
    for (let y = 0; y < keywords.length; y++) {
      const keywordItem = keywords[y];
      if (dataItem === keywordItem) {
        return true;
      }
    }
  }
  return false;
})()
複製程式碼

如果我只想讓它返回一個呢,並且只能用 match 來做,可以麼?

是可以的,match 提供了一個屬性:operator。可以用這個來幫助完成這個需求:

GET books/_search
{
  "_source": ["title", "author"],
  "query": {
    "match": {
      "title": {
        "query": "java程式設計",
        "operator": "and"
      }
    }
  }
}
複製程式碼

最終的結果如圖:

Imgur

原理是因為 operator 屬性的值為 and,這樣的話,就告訴 Elasticsearch 我要讓我的關鍵字都能和文件裡的詞項匹配上。有一個沒匹配上,我都不要。

如果 operator 屬性的值為 or,那結果就和之前是一樣的了。

match_phrase query

你可以把這個方法理解為自帶了 operator 屬性的值為 andmatch

這個方法有兩個限制條件,只有都滿足,才會在結果中顯示出:

  • 分詞後的所有詞項都在該欄位中,相當於 operator: "and"
  • 順序要一致

順序一致指的是什麼呢?

假設你使用 match 來匹配: 程式設計java,那麼結果還是和上面一樣。所以如果你需要要求順序一致性,那麼你就可以使用 match_phrase 來做。

如果使用 程式設計java 來搜尋:

Imgur

如果使用 java程式設計

Imgur

match_phrase_prefix query

這個方法和 match_phrase 方法類似,不過這個方法可以可以把最後一個詞項作為字首進行匹配,想象一下:使用者在搜尋欄中搜尋 辣雞UZ,然後下面列表中出現了 辣雞UZI

首先 match_phrase_prefix 會先分詞為: 辣雞,然後找了一個文件,再然後匹配 辣雞 後面的字串是否以 UZ 開頭的。這個時候文件滿足條件,就返回出結果。可以假想後面一直有一個(.*)的萬用字元,如:辣雞UZ(.*)

知道原理了,我們現在寫一個查詢語句:

GET books/_search
{
  "_source": ["title", "author"],
  "query": {
    "match_phrase_prefix": {
      "title": "java編"
    }
  }
}
複製程式碼

結果如圖:

Imgur

multi_match query

multi_matchmatch 的升級方法,可以用來搜尋多個欄位。

比如我不想只在 title 裡搜尋 java程式設計,我還想在 description 裡進行搜尋。那應該怎麼做呢?

Elasticsearch 已經提供了 multi_match 專門用來處理這件事情:

GET books/_search
{
  "_source": ["title", "description"],
  "query": {
    "multi_match": {
      "query": "java程式設計",
      "fields": ["title", "description"]
    }
  }
}
複製程式碼

最終結果如圖:

Imgur

並且 multi_match 還支援萬用字元。上面的查詢語句,可以寫成:

GET books/_search
{
  "_source": ["title", "description"],
  "query": {
    "multi_match": {
      "query": "java程式設計",
      "fields": ["title", "*tion"]
    }
  }
}
複製程式碼

詞項查詢

上一章是全文查詢,這一章是詞項查詢。他們倆的區別在於:

  • 全文查詢:會對查詢語句(query)進行分詞,然後匹配文件裡分詞後的資料
  • 詞項查詢:不會對查詢語句進行分詞

term query

第一章節已經介紹過了,這裡就不再闡述了。

terms query

termsterm 查詢的升級版本,可以用來查詢文件中某一欄位,是否包含了其關鍵字。比如,我想查詢 title 欄位中包含了 優化 或者 基礎 的文件:

GET books/_search
{
  "_source": ["title"],
  "query": {
    "terms": {
      "title": ["優化", "基礎"]
    }
  }
}
複製程式碼

其結果如圖:

Imgur

range query

從名字就能猜測出 range 是範圍匹配。可以匹配 numberdatestring (字串範圍查詢比較特殊,比較少用,就不再闡述了)

range 支援以下查詢引數:

  • gt: 大於
  • gte: 大於等於
  • lt: 小於
  • lte: 小於等於

number 範圍查詢

現在我想查詢價格低於70,並大於等於50的書籍。虛擬碼既:(price >= 50 && price < 70)

GET books/_search
{
  "_source": ["title", "price"], 
  "query": {
    "range": {
      "price": {
        "gte": 50,
        "lt": 70
      }
    }
  }
}
複製程式碼

其結果如圖:

Imgur

date 範圍查詢

如果我想查詢,出版日期在 2016-1-12016-12-31 之間的書籍,那麼DSL查詢語句就如同以下這樣:

GET books/_search
{
  "_source": ["title", "publish_time"],
  "query": {
    "range": {
      "publish_time": {
        "gte": "2016-1-1",
        "lte": "2016-12-31",
        "format": "yyyy-MM-dd"
      }
    }
  }
}
複製程式碼

其結果如圖:

Imgur

exists query

匹配有這個屬性的文件。比如我想找到存在 title 欄位的文件:

GET books/_search
{
  "_source": "title", 
  "query": {
    "exists": {
      "field": "title"
    }
  }
}
複製程式碼

結果會返回所有的文件。那麼如何定義 有這個屬性 呢?

定義的規則如下:

  • {"title": "js"}: 存在
  • {"title": ""}: 存在
  • {"title": ["js"]}: 存在
  • {"title": ["js", null]}: 存在(有一個值不為空就行)
  • {"title": null}: 不存在
  • {"title": []} 不存在
  • {"title": [null]} 不存在
  • {"foo": "bar"}: 不存在

perfix query

用來匹配文件分詞後的詞項中的字首。我們先寫個DSL進行匹配下:

GET books/_search
{
  "_source": "description", 
  "query": {
    "prefix": {
      "description": "wi"
    }
  }
}
複製程式碼

其結果如圖:

Imgur

為何 wi 可以匹配到這個呢?因為 Elasticsearch 會對 description 進行分詞,其中會把 winPython 分為 win Python。那麼這兩個就是文件分詞後的詞項,而 prefix 匹配每個詞項的開頭是否匹配,相當於js的 startsWith 方法。用程式碼詮釋的話就是:

const dataItem = ['win', 'python'];
const prefixKeyword = 'wi';

const result = dataItem.some(item => item.startsWith(prefixKeyword));

console.log(result); //=> true
複製程式碼

wildcard query

wildcard 為萬用字元查詢。不過目前只支援 *?。所代表的含義為:

  • *: 零個或多個
  • ?: 一個或多個

注意:wildcard 不是匹配全文,還是會對文件的欄位進行分詞,然後應用於每個詞項

比如,我現在想查詢 wi* 的文件:

GET books/_search
{
  "_source": "description", 
  "query": {
    "wildcard": {
      "description": "wi*"
    }
  }
}
複製程式碼

其結果如圖:

Imgur

首先 Elasticsearch 會先對 description 進行分詞為:winpython。然後 wi* 會應用到每個詞項裡,其中 win 符合規則,則顯示在結果中。

如果我用 win?,則不會有任何的結果,因為 ? 代表的是一個或多個。那麼匹配到 win 的時候,後面沒有字串了,則結果為空。

regexp query

其為正規表示式查詢,原理同 wildcard,這裡就不在闡述了。

fuzzy query

可以把 fuzzy 理解為模糊查詢。比如使用者輸入關鍵字時,一不小心輸入錯了,變成了 javascrpit,那麼 fuzzy 的作用就出來了。它仍可以搜尋到 javascript:

GET books/_search
{
  "_source": "description", 
  "query": {
    "fuzzy": {
      "description": "javascrpit"
    }
  }
}
複製程式碼

其結果如圖:

Imgur

複合查詢

複合查詢就是把簡單的查詢組合在一起,從而實現更加複雜的查詢。並且複合查詢還可以控制另一個查詢的行為。

constant_score query

不太常用,用於對返回結果的文件進行打分。

這裡就不在闡述了,如果感興趣,可見:[Elasticsearch] 控制相關度 (四) - 忽略TF/IDF

bool query

這個查詢方法,還是非常重要的。這個方法提供了以下操作方法:

  • must: 文件必須滿足 must 下面的查詢條件,相當於AND 或者 &&
  • should: 文件可以匹配 should 下的查詢條件,匹配不出來也沒事。相當於 OR 或者 ||
  • must_not: 和 must 相反,必須不滿足 must_not 下面的查詢條件,相當於 !==
  • filter: 其功能和 must 一樣,但是不會打分,也就說不會影響文件的 _score 欄位

現在,我們想要查詢:書籍作者(author)是 葛一鳴,書籍名稱(title)裡包含 java 的書籍,價格(price)不能高於 70 低於 40,並且書籍描述(description)可以包含或者不包含 虛擬機器 的書籍。

GET books/_search
{
  "query": {
    "bool": {
      "filter": {
        "term": {
          "author": "葛一鳴"
        }
      },
      "must": [
        {
          "match": {
            "title": "java"
          }
        }
      ],
      "should": [
        {
          "match": {
            "description": "虛擬機器"
          }
        }
      ],
      "must_not": [
        {
          "range": {
            "price": {
              "gt": 70,
              "lt": 40
            }
          }
        }
      ]
    }
  }
}
複製程式碼

其結果如圖:

Imgur

dis_max query、function_score query、boosting query

這三個就不在闡述了,其主要作用是關係到 _score,也就是關係到查詢的結果的評分。感興趣的,可以在網上搜下。

結尾

此文章是《從Lucene到Elasticsearch:全文檢索實戰》一書的讀書筆記,如果造成侵權,可與我聯絡,我將刪除此文。

這本書是十分棒的,如果大家有興趣想深入瞭解的話,可以去進行購買此書。

作者資訊

Black-Hole: 158blackhole@gmail.com

Blog: www.bugs.cc

Github: github.com/BlackHole1

相關文章