ES101系列03 | 倒排索引、分词器与常用Search API

69 阅读7分钟

本篇文章主要介绍 ElasticSearch 中的倒排索引和分词器,与一些常见的 Search API 与应用实例。

倒排索引

倒排索引是搜索引擎和全文检索系统的核心数据结构,核心思想是通过建立「单词到文档」的映射关系从而实现 Keyword 快速定位包含该词的所有文档。例如当用户搜索「ElasticSearch」时,系统可直接通过倒排索引找到包含这一词汇的所有文档集合。

倒排索引的实现依赖于两个关键结构:单词词典(Term Dictionary)和倒排列表(Posting List)。

flowchart TD
    A["文档集合"] --> B("分词处理")
    B --> C{"单词词典"}
    C --> D["B+树/哈希表"] & E["倒排列表"]
    E --> F["文档ID"] & G["词频 TF"] & H["位置 Position"]

Analysis & Analyzer

Analysis 是通过 Analyzer 来实现的。

分词器

由三部分组成:

  • Character Filters:针对原始文本处理,例如去除 html。
  • Tokenizer:按照规则切分为单词。
  • Token Filter:将切分的的单词进行加工,例如小写、删除 stopwords、增加同义词等。
Character Filters => Tokenizer => Token Filters

ElasticSearch 内置分词器

分词器使用场景分词逻辑
Standard通用文本处理,支持大多数语言(默认选择)。按 Unicode 标准分词,移除标点符号,转小写,支持多语言基础处理。
Simple快速简单分词,忽略标点符号和数字。在非字母字符处分割文本,删除非字母字符,转小写(如 Hello-World["hello", "world"])。
Whitespace按空格严格分割,保留原始格式(如代码、特定标识)。仅按空格分割,保留大小写和标点(如 Quick-Brown["Quick-Brown"])。
Stop需过滤常见停用词(如英文中的“the”、“is”)的文本。按非字母字符分割出连续字母词条,转小写后移除停用词(如 The fox["fox"])。
Keyword需精确匹配的字段(如 ID、状态码)。将整个输入作为单一词条,不进行任何处理(如 Hello World["Hello World"])。
Pattern需自定义分隔规则(如按特定符号分割)的文本。通过正则表达式(默认 \W+)分割文本,转小写(可自定义正则)。
Language(如 english针对特定语言优化(如英文词干提取、停用词过滤)。按语言规则分词,处理停用词、转小写、词干提取等(如 running["run"])。

analyzer API

通过 analyzer API 能够快速得到分词结果进行测试,以下提供了一些例子可以去到 Kibana 的 Dev Tools 进行使用。

#standard
GET _analyze
{
  "analyzer": "standard",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#simple
GET _analyze
{
  "analyzer": "simple",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#stop
GET _analyze
{
  "analyzer": "stop",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#whitespace
GET _analyze
{
  "analyzer": "whitespace",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#keyword
GET _analyze
{
  "analyzer": "keyword",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#pattern
GET _analyze
{
  "analyzer": "pattern",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

#english
GET _analyze
{
  "analyzer": "english",
  "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}

示例:standard 分词器的分词结果

{
  "tokens" : [
    {
      "token" : "2",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<NUM>",
      "position" : 0
    },
    {
      "token" : "running",
      "start_offset" : 2,
      "end_offset" : 9,
      "type" : "<ALPHANUM>",
      "position" : 1
    },
    {
      "token" : "quick",
      "start_offset" : 10,
      "end_offset" : 15,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "brown",
      "start_offset" : 16,
      "end_offset" : 21,
      "type" : "<ALPHANUM>",
      "position" : 3
    },
    {
      "token" : "foxes",
      "start_offset" : 22,
      "end_offset" : 27,
      "type" : "<ALPHANUM>",
      "position" : 4
    },
    {
      "token" : "leap",
      "start_offset" : 28,
      "end_offset" : 32,
      "type" : "<ALPHANUM>",
      "position" : 5
    },
    {
      "token" : "over",
      "start_offset" : 33,
      "end_offset" : 37,
      "type" : "<ALPHANUM>",
      "position" : 6
    },
    {
      "token" : "lazy",
      "start_offset" : 38,
      "end_offset" : 42,
      "type" : "<ALPHANUM>",
      "position" : 7
    },
    {
      "token" : "dogs",
      "start_offset" : 43,
      "end_offset" : 47,
      "type" : "<ALPHANUM>",
      "position" : 8
    },
    {
      "token" : "in",
      "start_offset" : 48,
      "end_offset" : 50,
      "type" : "<ALPHANUM>",
      "position" : 9
    },
    {
      "token" : "the",
      "start_offset" : 51,
      "end_offset" : 54,
      "type" : "<ALPHANUM>",
      "position" : 10
    },
    {
      "token" : "summer",
      "start_offset" : 55,
      "end_offset" : 61,
      "type" : "<ALPHANUM>",
      "position" : 11
    },
    {
      "token" : "evening",
      "start_offset" : 62,
      "end_offset" : 69,
      "type" : "<ALPHANUM>",
      "position" : 12
    }
  ]
}

Search API

  • URI Search
  • Request Body Search

示例

# URI Search
GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie

# Request Body Search
POST kibana_sample_data_ecommerce/_search
{
	"profile": true,
	"query": {
		"match_all": {}
	}
}

指定查询的索引

语法范围
/_search所有索引
/index1/_searchindex1
/index1,index2/_searchindex1 和 index2
/index*/_search以 index 开头的索引

URI Search

示例

GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
{
	"profile":"true"
}

参数:

  • q 指定查询语句,使用 Query String Syntax。
  • df 指定默认字段,不指定时会对所有字段进行查询。
  • sort 代表排序。
  • from 和 size 用于分页。
  • profile 可以查看查询是如何被执行的。

泛查询

GET /movies/_search?q=2012
{
	"profile":"true"
}

未指定 df 参数时 ES 会搜索所有字段,可能触发跨字段匹配,性能消耗较大。分析结果如下:

{  
    "type": "DisjunctionMaxQuery",  
    "description": "(title.keyword:2012 | id.keyword:2012 | year:[2012 TO 2012] | genre:2012 | @version:2012 | @version.keyword:2012 | id:2012 | genre.keyword:2012 | title:2012)"  
}

可以看到在所有字段进行了匹配。

显式字段查询

GET /movies/_search?q=title:2012
{
	"profile":"true"
}

通过 title:2012 显式指定字段,精准限定搜索范围,比泛查询更高效。分析结果如下:

{  
    "type": "TermQuery",  
    "description": "title:2012"  
}

短语分割问题

GET /movies/_search?q=title:Beautiful Mind
{
	"profile":"true"
}

实际执行 title:Beautiful OR Mind,空格被识别为 OR 逻辑,返回包含任意词的文档。

精确短语匹配

GET /movies/_search?q=title:"Beautiful Mind"
{
	"profile":"true"
}

使用双引号包裹词组,强制进行短语搜索,要求词语按顺序完整出现。

分组查询

GET /movies/_search?q=title:(Beautiful Mind)
{
	"profile":"true"
}

括号实现逻辑分组,等效于 title:Beautiful OR title:Mind,优先执行组内操作。

布尔运算符

GET /movies/_search?q=title:(Beautiful AND Mind)

显式布尔查询,要求同时包含两个词(AND 逻辑)。

范围查询

GET /movies/_search?q=title:beautiful AND year:[2002 TO 2018%7D

[2002 TO 2018} 表示闭区间包含 2002,开区间不包含 2018(%7D 为 URL 编码的 } 符号)。

通配符搜索

GET /movies/_search?q=title:b*
{
	"profile":"true"
}

b* 匹配以 b 开头的任意长度字符,支持 ? 匹配单个字符,注意通配符在前端影响性能。

模糊匹配

GET /movies/_search?q=title:beautiful~1
{
	"profile":"true"
}

GET /movies/_search?q=title:"Lord Rings"~2
{
	"profile":"true"
}
  • ~1 允许 1 个字符的编辑距离(拼写纠错)。
  • "Lord Rings"~2 表示短语中允许间隔 2 个单词。

Request Body Search & Query DSL

通常生成环境都使用这种方法,更加强大、功能更丰富。

示例

POST movies/_search
{
  "from":0,
  "size":10,
  "query": {
    "match": {
      "title": {
        "query": "last christmas",
        "operator": "and"
      }
    }
  }
}

更多 DSL 语法请参考 官方文档

基本 Match 查询

POST movies/_search
{
  "query": {
    "match": {
      "title": "last christmas"
    }
  }
}
  • 默认使用 OR 逻辑匹配分词结果。
  • 自动对搜索词进行分词处理。

精确 AND 匹配

POST movies/_search
{
  "query": {
    "match": {
      "title": {
        "query": "last christmas",
        "operator": "and"
      }
    }
  }
}

通过 operator:"and" 强制要求所有分词必须同时存在。

短语搜索

POST movies/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "one love"
      }
    }
  }
}
  • 要求词语按顺序完整出现。
  • 等效于 URI Search 中的引号。

模糊短语匹配

POST movies/_search
{
  "query": {
    "match_phrase": {
      "title": {
        "query": "one love",
        "slop": 1
      }
    }
  }
}

slop 参数允许词语间隔位置数(此处允许间隔 1 个词)。

Query String 搜索

GET /movies/_search
{
  "query": {
    "query_string": {
      "default_field": "title",
      "query": "Beafiful AND Mind"
    }
  }
}
  • 支持 AND/OR/NOT 布尔逻辑。

多字段搜索

GET /movies/_search
{
  "query": {
    "query_string": {
      "fields": ["title","year"],
      "query": "2012"
    }
  }
}
  • fields 数组定义多个搜索字段。
  • 自动进行跨字段联合查询。

Simple Query 搜索

GET /movies/_search
{
  "query": {
    "simple_query_string": {
      "query": "Beautiful +mind",
      "fields": ["title"]
    }
  }
}
  • + 代替 AND 操作。
  • 自动忽略无效语法。
  • 适合直接暴露给前端搜索框使用。

跨索引查询

POST /movies,404_idx/_search?ignore_unavailable=true
{
  "query": {
    "match_all": {}
  }
}
  • 逗号分隔多个索引名称。
  • ignore_unavailable=true 忽略不存在索引。

源过滤

POST kibana_sample_data_ecommerce/_search
{
  "_source":["order_date"],
  "query": {
    "match_all": {}
  }
}

_source 过滤返回字段。

脚本字段

GET kibana_sample_data_ecommerce/_search
{
  "script_fields": {
    "new_field": {
      "script": {
        "lang": "painless",
        "source": "doc['order_date'].value+'hello'"
      }
    }
  }
}

动态计算返回字段。

写在最后

这是该系列的第三篇,主要讲解 ElasticSearch 倒排索引、分词器、Search API 的内容,可以自己去到 Kibana 的 Dev Tool 实战操作,未来会持续更新该系列,欢迎关注👏🏻。

同时欢迎关注公众号:LanTech指南。不定时分享职场思考、独立开发日志、大厂方法论和后端经验❤️

参考

  1. github.com/onebirdrock…
  2. www.elastic.co/elasticsear…