Elasticsearch入门

    xiaoxiao2023-10-29  23

    一、Elasticsearch简介

    Elasticsearch(ES)是一个基于Lucene构建的开源、分布式、RESTful接口全文搜索引擎和数据分析引擎。Elasticsearch还是一个分布式文档数据库,其中每个字段均是被索引的数据且可被搜索。Elasticsearch可以在很短的时间内存储、搜索和分析大量数据(近实时)。

    ES的优点如下:

    横向可扩展:只需增加一台服务器,做一点配置,启动一下ES进程就可以加入集群。所谓横向扩展(水平扩展)就是通过不断增加及其数量来进行扩展,而垂直扩展(纵向扩展)是通过替换性能更强劲的机器来进行扩展。

    分片机制:同一个索引分成多个分片(Shard),类似于HDFS的块,分而治之提升处理效率。

    高可用:提供复制(replica)机制,即使某台服务器宕机,集群仍旧可用。

    使用简单:只需要一条命令就可以下载文件,然后很快搭建一个站内搜索引擎。

    二、全文搜索及倒排索引

    全文搜索是指计算机搜索程序通过扫描文章中每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当用户查找时,搜索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户。这个过程类似于通过字典查字的过程。Lucene是目前全球使用最广的全文搜索引擎开源库。

    倒排索引源于实际应用中需要根据属性值来查找记录,这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因此叫做倒排索引(inverted index)。带有倒排索引的文件叫做倒排索引文件,简称倒排文件(inverted file)。

    倒排索引中的索引对象是文档或者文档集合中的单词等,用来存储这些单词在一个文档或者一组文档中出现的位置,是对文档或者文档集合的一种最常用的索引机制。

    搜索引擎的关键步骤就是建立倒排索引,倒排索引一般表示为一个关键词,然后是它的频度(出现的次数)、位置(出现在哪一篇文章或者网页中,及有关的日期、作者信息等)。好比一本书的目录,想看哪个章节,直接根据目录就可找到,不必一页一页的找。

    下面通过例子介绍倒排索引的建立及全文检索的过程。

    假设有两篇文章1和文章2:

    文章1:Tom lives in Guangzhou, I live in Guangzhou too. 文章2:He once lived in Shanghai.

    1、分词

    • 英文单词使用空格分隔,比较好处理,中文之间是连在一起的,需要特殊的分词器;

    • 文章中的in、once、too等词没有什么意义(叫做stop word,停用词),可以过滤掉;

    • "He"和"he"、“HE”实际上是一样的,所以需要进行大小写转换;

    • 通常查询"live"时希望把含有"lives"、"lived"的文章也查询出来,所以需要做时态的转换;

    • 文章的标点符号也可以过滤掉。

    在Lucene中,以上步骤通过Analyzer类完成,经过上面的处理,得到的结果如下:

    文章1的关键词:tom live guangzhou i live guangzhou 文章2的关键词:he live shanghai

    2、建立倒排索引

    建立的倒排索引如下所示:

    关键词文章号guangzhou1he

    2

    i1live

    1,2

    shanghai2tom1

    通常仅知道关键词在那哪些文章中出现还不够,还需要知道关键词在文章中出现的次数和位置,通常有两种位置:

    • 字符位置,即记录改词是文章中第几个字符(有点是显示定位关键词快);

    • 关键词位置:即记录该词是文章中第几个关键词(有点是节约索引空间、词组查询快),Lucene中使用的就是这种。

    加上出现频率和出现位置信息后,索引结构如下所示:

    关键词文章号[出现频率]出现位置guangzhou1[2]3,6he2[1]1i1[1]4live

    1[2],

    2[1]

    2,5

    2

    shanghai2[1]3tom1[1]1

    以上就是Lucene索引结构中最核心的部分,注意关键词是按照字符顺序排列的(Lucene没有使用B树结构),因此Lucene可以使用二元搜索算法快速定位关键词。

    3、实现

    Lucene将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存了每个关键词,还保存了指向频率文件和位置文件的指针,通过指针可以快速找到该关键字的频率信息和位置信息。

    Lucene使用了Field的概念,用于表达信息所在位置(如标题中、文章中、URL中)。在建索引中,该field信息也记录在词典文件中,每个关键词都有一个field信息,因为每个关键字一定属于一个或多个field。

    4、压缩算法

    为了减小索引文件的大小,Lucene对索引还使用了压缩技术。

    首先,对词典文件中的关键词进行了压缩,关键词压缩为<前缀长度,后缀>。例如:当前词为“阿拉伯语”,上一个词为“阿拉伯”,那么阿拉伯压缩为<3,语>。

    其次大量用到的是对数字的压缩,数字只保存与上一个值的差值。例如当前文章号是16389,上一个文章号是16382,压缩后保存7。

    5、应用场景

    假设要查询单词live,Lucene先对词典进行二元查找,找到该词通过指向频率文件的指针读出所有文章号,然后返回结果。词典通常非常小,因此整个过程是毫秒级别的。

    三、ES术语及概念

    索引词(term)

    索引词是一个能够被索引的精确值。

    文本(text)

    文本是一段普通的非结构化文字。通常,文本会被分析成一个个的索引词,存储在ES的索引库中。

    分析(analysis)

    分析是将文本转换为索引词的过程,分析的结果依赖于分词器。

    集群(cluster)

    集群由一个或多个节点组成,对外提供索引和搜索功能。一个集群有唯一的名称,默认为“Elasticsearch”。每个节点只能是集群的一部分,当该节点被设置为相同的集群名称时,就会自动加入集群。一个节点只能加入一个集群。

    节点(node)

    一个节点是一个独立的服务,它是集群的一部分,可以存储数据,并参与集群的索引和搜索功能。和集群一样,节点也有唯一的名字,在启动时分配,也可以指定节点名。

    路由(routing)

    当存储一个文档的时候,它会存储在唯一的主分片中,具体哪个分片是通过散列值进行选择。默认情况下,这个值由文档的ID生成。如果文档有一个指定的父文档,则从父文档ID生成。

    分片(shard)

    分片是单个Lucene实例,这是ES管理的比较底层的功能。索引是指向主分片(primary shard)和副本分片(replica shard)的逻辑空间。对于使用,只需指定分片的数量,其他不需要做过多的事情。开发中,对应的对象都是索引,ES会自动管理集群中所有的分片。当发生故障时,ES会把分片移动到不同的节点或者添加新的节点。

    一个索引可以存储很大的数据(可能是TB级别),仅从单个节点搜索可能很慢,而且一台物理机也不一定能存储这么多数据。为了解决这一问题,ES将索引分解成多个分片。当创建一个索引时,可以简单地定义想要的分片数量。每个分片本身是一个全功能的、独立的单元,可以托管在集群中的任意节点。

    主分片(primary shard)

    每个文档都存储在一个分片中。当存储一个文档的时候,系统会首先存储在主分片中,然后会复制到不同的副本。默认情况下,一个索引有5个主分片,可以事先定制分片的数量,分片一旦建立,分片的数量不能修改。

    副本分片(replica shard)

    每一个分片有零个或多个副本。副本主要是分片的复制,副本的数量可以动态的配置增加,副本分片必须部署在与主分片不同的节点上,不能部署在和主分片相同的节点上。副本的作用:

    1、高可用:当主分片失败的时候,可以从副本分片中选择以个作为主分片;

    2、提高性能:查询的时候可以从主分片或者副本分片进行查询;

    3、允许水平分割扩展数据;

    4、允许分配和并行操作。

    复制(replica)

    当某个节点出现问题的时候,复制可以对故障进行转移,保证系统高可用。索引可以拆分成多个分片,索引可以复制零个或多个分片,一旦复制,每个索引就有了主分片和副本分片。分片的数量和副本的数量可以在创建索引时定义。当索引创建后,可以修改副本的数量,但不能改变分片的数量。

    默认情况下,每个索引分配5个分片和一个副本,这意味着集群节点至少需要2个节点,这样就拥有5个主分片和5个副本分片。

    索引(index)

    索引是具有相同结构(大部分field相同)的文档集合。在系统上索引的名字全部小写,通过这个名字可以用来执行索引、搜索、更新和删除操作等。索引结构如下图:

    可以把index想象成关系数据库的数据库。

    类型(type)

    在索引中,可以定义一个或多个类型,类型是索引的逻辑分区。一般情况下,一个类型被定义为具有一组公共字段(具有完全相同的field)的文档。

    可以把type想象成关系数据库中的表。

    文档(document)

    文档是存储在ES中的一个JSON格式的字符串。就像关系数据库中的一行。每个存储在索引中的一个文档都有一个类型和一个ID,每个文档都是一个JSON对象,存储了零个或多个字段,或者键值对。原始的JSON文档被存储在一个叫做_source的字段中。当搜索文档的时候默认返回的就是这个字段。

    映射(mapping)

    映射像关系数据库中的表结构,每一个索引都有一个映射,它定义了索引中的每一个字段类型,以及一个索引范围内的设置。一个映射可以事先被定义,或者在第一次存储文档的时候自动识别。

    字段(field)

    文档中包含零个或多个字段,字段可以是一个简单的值(例如字符串、整数、日期),也可以是一个数组或者对象的嵌套结构。字段类似于关系数据库中表的列。每个字段都对应一个字段类型,如整数、字符串、对象等。字段还可以指定如何分析该字段的值。

    来源字段(source field)

    默认情况下, 原文档会被存储在_source字段中,查询的时候也是返回这个字段。这允许你可以从搜索结果中访问原始的对象,这个对象返回一个精确的JSON字符串,这个对象不显示索引分析后的其他任何数据。

    主键(ID)

    ID是一个文件的唯一标识,如果在存库的时候没有提供ID,系统会自动生成一个ID,文档的index/type/id必须是唯一的。

    四、安装配置

    4.1 安装ES

    安装ES首先需要安装Java,ES需要Java 7或者更高版本的支持。

    首先在ES官网下载安装包,我这里下载的是 5.2.0 版本:

    wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.0.tar.gz

    下载完成后解压:

    tar -xvzf elasticsearch-5.2.0.tar.gz

    解压完成后,ES就安装好了。

    Elasticsearch配置文件在${ELASTICSEARCH_HOME}/config目录下,具体配置可以参考Elasticsearch 的官方文档。

    接下来就可以运行ES,进入到 bin 目录,执行如下命令:

    ./elasticsearch

    结果出现了如下错误:

    ...... org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root ......

    这是由于ElasticSearch可以接收用户输入的脚本并且执行,为了系统安全考虑, 建议创建一个单独的用户用来运行ElasticSearch。创建用户组及用户:

    groupadd esgroup useradd esuser -g esgroup -p elasticsearch

    更改elasticsearch文件夹及内部文件的所属用户及组为esuser:esgroup:

    chown -R esuser:esgroup elasticsearch-5.2.0

    切换到esuser用户再启动:

    su esuser ./elasticsearch

    此时可能会报出如下错误:

    max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

    意思是elasticsearch用户拥有的内存权限太小,至少需要262144。解决办法是,切换到root用户修改配置文件/etc/sysctl.conf,添加如下配置:

    vm.max_map_count=262144

    之后执行如下命令即可:

    sysctl -p

    此时再次启动ES,如果运行成功,将会看到如下输出:

    ...... [2019-05-25T23:39:25,716][INFO ][o.e.c.c.Coordinator ] [ubuntu] setting initial configuration to VotingConfiguration{hVllfCrNQk68-lKjlYBSAQ} [2019-05-25T23:39:25,956][INFO ][o.e.c.s.MasterService ] [ubuntu] elected-as-master ([1] nodes joined)[{ubuntu}{hVllfCrNQk68-lKjlYBSAQ}{01PYo4SOT3S0gKSe8ggdLw}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=2078441472, xpack.installed=true, ml.max_open_jobs=20} elect leader, _BECOME_MASTER_TASK_, _FINISH_ELECTION_], term: 1, version: 1, reason: master node changed {previous [], current [{ubuntu}{hVllfCrNQk68-lKjlYBSAQ}{01PYo4SOT3S0gKSe8ggdLw}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=2078441472, xpack.installed=true, ml.max_open_jobs=20}]} [2019-05-25T23:39:25,996][INFO ][o.e.c.c.CoordinationState] [ubuntu] cluster UUID set to [0Gu9877WQsmZ2YkOIqMWLQ] [2019-05-25T23:39:26,013][INFO ][o.e.c.s.ClusterApplierService] [ubuntu] master node changed {previous [], current [{ubuntu}{hVllfCrNQk68-lKjlYBSAQ}{01PYo4SOT3S0gKSe8ggdLw}{127.0.0.1}{127.0.0.1:9300}{ml.machine_memory=2078441472, xpack.installed=true, ml.max_open_jobs=20}]}, term: 1, version: 1, reason: Publication{term=1, version=1} [2019-05-25T23:39:26,613][WARN ][o.e.x.s.a.s.m.NativeRoleMappingStore] [ubuntu] Failed to clear cache for realms [[]] [2019-05-25T23:39:26,650][INFO ][o.e.h.AbstractHttpServerTransport] [ubuntu] publish_address {127.0.0.1:9200}, bound_addresses {[::1]:9200}, {127.0.0.1:9200} [2019-05-25T23:39:26,651][INFO ][o.e.n.Node ] [ubuntu] started ......

    默认情况下,ES使用9200端口,这个端口是可以修改的。重新打开一个终端,使用curl访问,将会看到下面的输出:

    root@ubuntu:~# curl 127.0.0.1:9200 { "name" : "a4Cl7U7", "cluster_name" : "elasticsearch", "cluster_uuid" : "b0OcjZLURq-4lxUdvDme8w", "version" : { "number" : "5.2.0", "build_hash" : "24e05b9", "build_date" : "2017-01-24T19:52:35.800Z", "build_snapshot" : false, "lucene_version" : "6.4.0" }, "tagline" : "You Know, for Search" }

    此时在外部浏览器中是访问不到ES的,需要修改elasticsearch.yml配置文件如下:

    ...... network.host: 0.0.0.0 ......

    之后重新启动ES,在浏览器中访问http://192.168.11.59:9200/,可以看到如下内容:

    { "name" : "a4Cl7U7", "cluster_name" : "elasticsearch", "cluster_uuid" : "b0OcjZLURq-4lxUdvDme8w", "version" : { "number" : "5.2.0", "build_hash" : "24e05b9", "build_date" : "2017-01-24T19:52:35.800Z", "build_snapshot" : false, "lucene_version" : "6.4.0" }, "tagline" : "You Know, for Search" }

    4.2 安装Kibana

    后续会使用Kibana操作ES(也可以使用HEAD插件),所以需要安装Kibana,直接下载解压即可。注意Kibana的版本需要和ES的版本兼容,我这里下载的Kibana的版本是5.2.0。

    五、对外接口

    ES对外提供的API是HTTP RESTful风格的。

    5.1 创建索引

    PUT secisland

    执行完后可以在浏览器中访问http://192.168.11.59:9200/_cat/indices?v,结果如下:

    可以看到已经成功建立了一个索引secisland,主分片是5个,副本分片是1个,健康度是黄色,状态是活动,文档数量为0。

    关于集群的健康度:

    green:每个索引的primary shard和replica shard都是active状态的;

    yellow:每个索引的primary shard都是active状态的,但是部分replica shard不是active状态,处于不可用的状态;

    red:不是所有索引的primary shard都是active状态的,部分索引有数据丢失了。

    5.2 插入文档

    PUT secisland/secilog/1 { "computer":"secisland", "message":"secisland is an security company" }

    返回值:

    { "_index":"secisland", "_type":"secilog", "_id":"1", "_version":1, "result":"created", "_shards":{ "total":2, "successful":1, "failed":0 }, "_seq_no":0, "_primary_term":1 }

    以上命令会插入一个document,如果此时再次执行一次相同的操作(PUT secisland/secilog/1),则是替换文档,如果只想修改部分字段,可以使用下面的接口。

    5.3 修改文档

    POST secisland/secilog/1/_update { "doc": { "computer": "secisland", "message": "secisland is an security computer. It provides log analysis products!" } }

    返回值:

    { "_index": "secisland", "_type": "secilog", "_id": "1", "_version": 4, "result": "updated", "_shards": { "total": 2, "successful": 1, "failed": 0 } }

    5.4 查询文档

    GET secisland/secilog/1

    返回值:

    { "_index": "secisland", "_type": "secilog", "_id": "1", "_version": 4, "found": true, "_source": { "computer": "secisland", "message": "secisland is an security computer. It provides log analysis products!" } }

    5.5 删除文档

    DELETE secisland/secilog/1/

    返回值:

    { "found": true, "_index": "secisland", "_type": "secilog", "_id": "1", "_version": 5, "result": "deleted", "_shards": { "total": 2, "successful": 1, "failed": 0 } }

    5.6 删除索引

    DELETE secisland

    返回值:

    { "acknowledged": true }

    六、Java接口

    添加依赖:

    <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>transport</artifactId> <version>5.2.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.10.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency>ncy>

    代码:

    package com.wuychn; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.transport.TransportClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.transport.client.PreBuiltTransportClient; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.net.InetAddress; public class ESClient { TransportClient client; @Before public void before() throws Exception { Settings settings = Settings.builder() .put("cluster.name", "elasticsearch") .build(); client = new PreBuiltTransportClient(settings) .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("192.168.11.59"), 9300)); } @After public void after() { client.close(); } /** * 创建员工信息(创建一个document) * * @throws Exception */ @Test public void createEmployee() throws Exception { IndexResponse response = client.prepareIndex("company", "employee", "1") .setSource(XContentFactory.jsonBuilder() .startObject() .field("name", "jack") .field("age", 27) .field("position", "technique") .field("country", "china") .field("join_date", "2017-01-01") .field("salary", 10000) .endObject()) .get(); System.out.println(response.getResult()); } /** * 获取员工信息 */ @Test public void getEmployee() { GetResponse response = client.prepareGet("company", "employee", "1").get(); System.out.println(response.getSourceAsString()); } /** * 修改员工信息 * * @throws Exception */ @Test public void updateEmployee() throws Exception { UpdateResponse response = client.prepareUpdate("company", "employee", "1") .setDoc(XContentFactory.jsonBuilder() .startObject() .field("position", "technique manager") .endObject()) .get(); System.out.println(response.getResult()); } /** * 删除 员工信息 */ @Test public void deleteEmployee() { DeleteResponse response = client.prepareDelete("company", "employee", "1").get(); System.out.println(response.getResult()); } }

     

    最新回复(0)