ElasticSearch

安装

安装 ES

下载压缩包

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.14.0-linux-x86_64.tar.gz

解压

tar -zxvf elasticsearch-7.14.0-linux-x86_64.tar.gz

修改配置文件,开启远程访问

vim elasticsearch-7.14.0/config

修改
network.host: 0.0.0.0

由于 ElasticSearch 不能在 root 用户下启动,所以需要创建一个新用户:

useradd temp

passwd temp

su temp

启动 ElasticSearch

cd elasticsearch-7.14.0/bin

./elasticsearch

port: http 9200 tcp 9300

可能遇到的问题:

  1. 如果启动后 ES 被 killed ,可能是服务器内存不够,可以修改分配给 ES 的 JVM 内存
   vim elasticsearch-7.14.0/config/jvm.options

   #修改
   -Xms1g
   -Xmx1g
  1. 报错:在 root 用户安装了 JDK 的情况下,普通用户运行报错 could not find java in bundled JDK at /home/ElasticSearch 这是因为 root 用户安装的 JDK 是局部环境变量,不是全局的。需要在 /etc/profile 中配置全局环境变量
   vi  /etc/profile

   添加如下内容
   export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.14.1.1-1.el7_9.x86_64 #安装的jdk的位置
   export CLASSPATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
   export PATH=$JAVA_HOME/bin:$PATH

   :wq

   source /etc/profile
  1. 报错:AccessDeniedException
   chmod -R 777 /home/ElasticSearch/elasticsearch-7.14.0  
   # 修改 ES 目录权限
  1. 报错:关于elasticsearch boostrap checks failed错误类型整理及解决方法 – linzepeng – 博客园 (cnblogs.com)
  2. 报错:(34条消息) ES启动异常:the default discovery settings are unsuitable for production use; at least…_lizz666的博客-CSDN博客 这tm绝对是我部署过踩坑最多的中间件,太草了

docker 安装 ES

docker pull elasticsearch:7.14.0

docker run -d -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e "ES_JAVA_OPTS=-Xms1g -Xmx1g" elasticsearch:7.14.0

不得不说这种中间件还是docker方便

安装 Kibana

wget https://artifacts.elastic.co/downloads/kibana/kibana-7.14.0-linux-x86_64.tar.gz

tar -xvzf kibana-7.14.0-linux-x86_64/config/kibana.yml

vim kibana-7.14.0-linux-x86_64/config/kibana.yml
# 修改 server.host: "0.0.0.0"
# 修改 elasticsearch.hosts: ["http://localhost:9200"] (默认不用改)

cd kibana-7.14.0-linux-x86_64/bin

./kibana
# kibana也不能运行在 root 用户上

port: 5601

docker 安装 Kibana

docker pull kibana:7.14.0

docker run -d --name kibana -p 5601:5601 kibana:7.14.0

docker exec -it kibana bash
# 进入 kibana 的容器

vi config/kibana.yml
# 修改配置文件
# 修改 elasticsearch.hosts: [ "ES运行的地址" ]

exit

docker restart kibana

docker-compose 安装 ES & Kibana

cd /home

mkdir ES-Kibana

cd ES-Kibana

vim docker-compose.yml
# 内容如下

vim kibana.yml
# 内容如下

docker-compose.yml

version: "3.8"
volumes:
  data:
  config:
  plugin:
networks:
  es:
services:
  elasticsearch:
    image: elasticsearch:7.14.0
    ports:
      - "9200:9200"
      - "9300:9300"
    networks:
      - "es"
    environment:
      - "discovery.type=single-node"
      - "ES_JAVA_OPTS=-Xms512m -Xmx1g"
    volumes:
      - data:/usr/share/elasticsearch/data
      - config:/usr/share/elasticsearch/config
      - plugin:/usr/share/elasticsearch/plugins

  kibana:
    image: kibana:7.14.0
    ports:
      - "5601:5601"
    networks:
      - "es"
    depends_on:
      - elasticsearch
    environment:
      I18N_LOCALE: zh-CN  # 7.X  后切换中文
    volumes:
      - ./kibana.yml:/usr/share/kibana/config/kibana.yml

kibana.yml

server.host: "0"
server.shutdownTimeout: "5s"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
# 由于 kibana 和 ES 在 docker-compose 文件中指定了处于同一个network
# 因此可以用 ES 的服务名来作为 IP 指定服务
monitoring.ui.container.elasticsearch.enabled: true

设置用户名密码

在 /usr/share/elasticsearch/config/elasticsearch.yml 中添加

xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

在 /usr/share/kibana/config/kibana.yml 中添加

elasticsearch.username: "elastic"
elasticsearch.password: "后面要设置的"

重启 ES ,进入 ES 容器,设置用户名密码

[root@f42838410089 elasticsearch]# ./bin/elasticsearch-setup-passwords interactive
# 然后依次设置密码


Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.
You will be prompted to enter passwords as the process progresses.
Please confirm that you would like to continue [y/N]y


Enter password for [elastic]: 
Reenter password for [elastic]: 
Enter password for [apm_system]: 
Reenter password for [apm_system]: 
Enter password for [kibana_system]: 
Reenter password for [kibana_system]: 
Enter password for [logstash_system]: 
Reenter password for [logstash_system]: 
Enter password for [beats_system]: 
Reenter password for [beats_system]: 
Enter password for [remote_monitoring_user]: 
Reenter password for [remote_monitoring_user]: 
Changed password for user [apm_system]
Changed password for user [kibana_system]
Changed password for user [kibana]
Changed password for user [logstash_system]
Changed password for user [beats_system]
Changed password for user [remote_monitoring_user]
Changed password for user [elastic]

核心概念

索引 index

一群相似的文档的集合,索引由名字来标识(全小写)

相当于 MySQL 的表

索引基础操作

# 查看所有索引
GET /_cat/indices    

# 创建索引
PUT /索引名
# 默认创建的索引会在本地创建主数据块和副本数据块,
# 由于主从都在一个服务器上,因此索引的 health 状态会变为 yellow
# 可以暂时设置从索引数量为 0 ,解决这个问题
# PUT /索引名
# {
#    "settings":{
#         "number_of_shards": 1,
#        "number_of_replicas": 0
#    }
# }

# 删除索引
DELETE /索引名

文档 document

一条条基础的数据,用 json 表示

相当于 MySQL 的数据

文档基础操作

# 添加文档,手动指定 _id
POST /索引名/_doc/1
{
    "xxx":"xxxx"
}

# 添加文档,自动生成 _id (UUID)
POST /索引名/_doc
{
    "xxx":"xxxx"
}

# 查询文档  基于 _id 查询
GET /索引名/_doc/<id>

# 删除文档  基于 _id 删除
DELETE /索引名/_doc/<id>

# 更新文档  基于 _id 更新 (删除原始文档,重新添加)
PUT /索引名/_doc/<id>
{
    "xxx":"xxxx"
}
# 更新文档  基于 _id 更新 (在原文档的基础上更新)
POST /索引名/_doc/<id>/_update
{
    "xxx":"xxxx"
}

映射 mapping

定义一个文档和他所包含的字段如何被存储和索引

相当于 MySQL 的表结构

映射基础操作

# 创建索引的同时创建 mapping 映射
PUT /索引名
{
    "mappings": {
        "properties":{
            "id":{
                "type":"integer"
            },
            "title":{
                "type":"keyword"
            },
            "price":{
                "type":"double"
            },
            "description":{
                "type":"text"
            },
            "created_at":{
                "type":"date"
            }
        }
    }
}

# 查看某个索引的映射信息
GET /索引名/_mapping

ES的基本数据类型

字符串类型:keyword 关键字 text 文本

数字类型:integer long float double

布尔类型:boolean

日期类型:date

分词器

推荐 IK 分词器

medcl/elasticsearch-analysis-ik: The IK Analysis plugin integrates Lucene IK analyzer into elasticsearch, support customized dictionary. (github.com)

安装

  • IK 分词器版本需要与 ES 版本一致
  • Docker 容器运行 ES 安装插件目录为 /usr/share/elasticsearch/plugins (docker-compose中已经创建数据卷映射)
[root@fengye ~]# docker volume ls
DRIVER    VOLUME NAME
local     es-kibana_config
local     es-kibana_data
local     es-kibana_plugin
[root@fengye ~]# docker volume inspect es-kibana_plugin 
[
    {
        "CreatedAt": "2022-03-29T23:49:42+08:00",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "es-kibana",
            "com.docker.compose.version": "2.2.3",
            "com.docker.compose.volume": "plugin"
        },
        "Mountpoint": "/www/server/docker/volumes/es-kibana_plugin/_data",
        "Name": "es-kibana_plugin",
        "Options": null,
        "Scope": "local"
    }
]
[root@fengye ~]# cd /www/server/docker/volumes/es-kibana_plugin/_data
[root@fengye _data]# wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.14.0/elasticsearch-analysis-ik-7.14.0.zip
[root@fengye _data]# mkdir ik-7.14.0
[root@fengye _data]# mv elasticsearch-analysis-ik-7.14.0.zip ik-7.14.0/
[root@fengye _data]# cd ik-7.14.0/
[root@fengye _data]# unzip elasticsearch-analysis-ik-7.14.0.zip

用kibana进行测试

# 请求
POST /_analyze
{
  "analyzer": "ik_max_word",
  "text": "嘉然可爱捏" 
}

# 响应:
{
  "tokens" : [
    {
      "token" : "嘉",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "然",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "可爱",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "捏",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 3
    }
  ]
}

# ik_max_word 拆分力度比 ik_smart 更细 

创建索引时使用 IK 分词器

PUT /索引名
{
    "mappings":{
        "description":{
            "type":"text",
            "analyzer":"ik_max_word"
        }
    }
}

扩展|停用词典

修改 ik 解压目录的 config/IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict">test.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords"></entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <!-- <entry key="remote_ext_dict">words_location</entry> -->
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

添加拓展词典 test.dic ,创建 test.dic 文件,写入内容(一行一个词)

嘉然

测试效果:

{
  "analyzer": "ik_max_word",
  "text": "嘉然可爱捏" 
}

{
  "tokens" : [
    {
      "token" : "嘉然",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "可爱",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "捏",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 2
    }
  ]
}

查询 Query DSL

通过 rest api 传递 json 数据进行高级查询

语法:

GET /索引名/_doc/_search
{
    "xxxx":"xxxx"
}

常用查询语句

查询一个索引的所有文档 march_all

GET /索引名/_search
{
    "query":{
        "match_all":{}
    }
}

基于关键词查询 term

GET /索引名/_search
{
    "query":{
        "term":{
            "description":{
                "value":"test"
            }
        }
    }
}
# text 默认的分词器是 中文单字,英文单词
# 除了 text ,其余都不分词

范围查询 range

GET /索引名/_search
{
    "query":{
        "range":{
            "price":{
                "gte":1400,
                "lte":9999
            }
        }
    }
}
# gt 大于  gte 大于等于
# lt 小于  lte 小于等于

前缀查询 prefix

GET /索引名/_search
{
    "query":{
        "prefix":{
            "title":{
                "value":"ipho"
            }
        }
    }
}
# gt 大于  gte 大于等于
# lt 小于  lte 小于等于

通配符查询 wildcard

GET /索引名/_search
{
    "query":{
        "wildcard":{
            "description":{
                "value":"ipho*"
            }
        }
    }
}
# ? 匹配任意字符    * 匹配多个字符

多id查询 ids

获取多个指定 id 的文档

GET /索引名/_search
{
    "query":{
        "ids":{
            "description":{
                "values":["123","124","125"]
            }
        }
    }
}

模糊查询 fuzzy

模糊查询含有指定关键字的文档

GET /索引名/_search
{
    "query":{
        "fuzzy":{
            "title":"hello"
        }
    }
}
# 搜索关键词长度小于等于 2 不允许模糊 
# 搜索关键词长度为 3-5 允许一次模糊 
# 搜索关键词长度大于 5 允许两次模糊 

布尔查询 bool

GET /索引名/_search
{
    "query":{
        "bool":{
            "must":[
                {"term":{
                    "price":{
                        "value":4999
                    }
                }}
            ]
        }
    }
}
# must 相当于 &&      全真
# should 相当于 ||       一个真
# must_not 相当于 !   全假

多字段查询 multi_match

GET /索引名/_search
{
    "query":{
        "multi_match":{
            "query":"iphone",
            "fields":["title","description"]
        }
    }
}
# 如果字段类型 field 分词,那么查询的 query 也会分词后进行查询

默认字段分词查询 query_string

GET /索引名/_search
{
    "query":{
        "query_string":{
            "default_field":"description",
            "query":"test"
        }
    }
}
# 如果字段分词,查询条件也分词

高亮查询 highlight

将指定字段中的关键词高亮显示(只有能分词的字段才可以高亮)

默认使用 <em> 标签将关键词包裹

GET /索引名/_search
{
    "query":{
        "term":{
            "description":{
                "value":"iphone"
            }
        }
    },
    "highlight":{
        "pre_tags":["<span style='color:red;'>"],
        "post_tags":["</span>"]
        "fields":{
            "*":{}
        }
    }
}
# 如果字段分词,查询条件也分词

分页查询 from size

默认查询只返回前十条

size: 每一页多少个数据

from: 起始位置 from = (pageNum-1)*size

GET /索引名/_search
{
    "query":{
        "term":{
            "description":{
                "value":"iphone"
            }
        }
    },
    "size":100,
    "from":0
}

排序查询 from

GET /索引名/_search
{
    "query":{
        "term":{
            "description":{
                "value":"iphone"
            }
        }
    },
    "sort":[
        {
            "price":{
                "order":"desc"
            }
        }
    ]
}
# 降序 desc      升序 asc

返回指定字段 _source

GET /索引名/_search
{
    "query":{
        "term":{
            "description":{
                "value":"iphone"
            }
        }
    },
    "_source":["title","description"]
}

返回结果样例

{
  "took" : 346,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "HnOeWYABh7tgu1RN4hRV",
        "_score" : 1.0,
        "_source" : {
          "title" : "test",
          "price" : 123
        }
      }
    ]
  }
}

过滤查询 filter

ES中有两种查询:query 和 filter

query 查询出来的结果会根据索引出现的次数、位置等信息进行打分,然后根据得分进行排名后返回结果

filter 则直接返回结果,因此 filter 效率要高于 query(同时 ES 也会缓存常用的 filter )

一般应用时,应先使用过滤操作过滤数据,然后使用查询匹配数据。

filter 必须配合 bool 查询使用:

GET /索引名/_search
{
    "query":{
        "bool":{
            "must":[
                {
                    "match_all":{}
                }
            ],
            "filter":{
                "term":{
                    "description":"iphone"
                }
            }
        }
    }
}

过滤类型:term、terms、ranage、exists、ids、bool

整合 SpringBoot

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置客户端

@Configuration
public class ESClientConfig extends AbstractElasticsearchConfiguration {
    @Value("${elasticsearch.host}")
    private String host;

    @Value("${elasticsearch.port}")
    private String port;

    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(host + ":" + port)
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

客户端对象

  • ElasticsearchOperations 通过偏向 oop 的方式操作
  • RestHighLevelClient 类似 kibana ,通过rest操作(推荐)

ElasticsearchOperations

创建映射实体类

import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "products")
// 指定文档的索引名称
public class Product {
    @Id
    // 指定字段作为 _id
    private Integer id;

    @Field(type = FieldType.Keyword)
    private String title;

    @Field(type = FieldType.Float)
    private Double price;

    @Field(type = FieldType.Text,analyzer = "ik_max_word")
    // 指定映射类型和分词器
    private String description;
    //get set...
}

使用

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private ElasticsearchOperations elasticsearchOperations;

    @Test
    void contextLoads() {
        Product product = new Product();
        product.setId(1);
        product.setTitle("iphone");
        product.setPrice(9999.0);
        product.setDescription("iphone with IOS");

        elasticsearchOperations.save(product);
        // save() 当文档 id 不存在时,创建文档
        // 当文档 id 存在时,更新文档

        Product res = elasticsearchOperations.get("1", Product.class);

        elasticsearchOperations.delete(product);
    }
}

RestHighLevelClient

创建索引

import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Map;

/**
 * @author: 風楪fy
 * @create: 2022/4/24 10:47
 **/
@Service
public class ESService {
    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 判断索引是否存在
     *
     * @param indexName 索引名称
     * @return
     * @throws IOException
     */
    public boolean isIndexExist(String indexName) throws IOException {
        GetIndexRequest request = new GetIndexRequest(indexName);
        return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
    }

    /**
     * 创建索引
     *
     * @param indexName   索引名
     * @param mappingJson 映射
     * @throws IOException
     */
    public void createIndex(String indexName, String mappingJson) throws IOException {
        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
        if (mappingJson != null) {
            createIndexRequest.mapping(mappingJson, XContentType.JSON);
        }
        restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
    }

    /**
     * 向指定索引中添加文档
     *
     * @param indexName 索引名
     * @param document  被添加的 JSON 文档
     * @param id        指定要添加的文档的 id,为 null 时 ES 会自动生成
     * @throws IOException
     */
    public void addDocument(String indexName, String document, String id) throws IOException {
        IndexRequest request = new IndexRequest(indexName);
        request.id(id).source(document, XContentType.JSON);
        restHighLevelClient.index(request, RequestOptions.DEFAULT);
    }

    /**
     * 根据 id 更新文档 (在原文档的基础上更新)
     *
     * @param indexName 索引名
     * @param id        id
     * @param document  更新内容
     */
    public void updateDocument(String indexName, String id, String document) throws IOException {
        UpdateRequest updateRequest = new UpdateRequest(indexName, id);
        updateRequest.doc(document, XContentType.JSON);
        restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
    }

    /**
     * 删除指定文档
     *
     * @param indexName
     * @param id
     */
    public void deleteDocument(String indexName, String id) throws IOException {
        DeleteRequest deleteRequest = new DeleteRequest(indexName, id);
        restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
    }

    /**
     * 根据 id 返回文档
     *
     * @param indexName
     * @param id
     * @return
     * @throws IOException
     */
    public Map<String, Object> getDocumentById(String indexName, String id) throws IOException {
        GetRequest getRequest = new GetRequest(indexName, id);
        GetResponse documentFields = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
        return documentFields.getSource();
    }


    /**
     * 封装分页条件查询
     *
     * @param indexName
     * @param queryBuilder
     * @param pageNum      起始位置(从0开始)
     * @param pageSize     每一页的数量
     * @return
     * @throws IOException
     */
    private SearchResponse query(String indexName, QueryBuilder queryBuilder, int pageNum, int pageSize) throws IOException {
        SearchRequest searchRequest = new SearchRequest(indexName);

        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(queryBuilder)
                .from((pageNum - 1) * pageSize)
                .size(pageSize);

        searchRequest.source(searchSourceBuilder);
        return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    }

    /**
     * term 查询
     *
     * @param indexName
     * @param fieldName
     * @param terms
     * @return
     * @throws IOException
     */
    public SearchResponse termQuery(String indexName, String fieldName, String... terms) throws IOException {
        return this.query(indexName, QueryBuilders.termsQuery(fieldName, terms), 0, 10);
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇