本章主要介绍GeoMesa
的操作流程,包括数据写入、索引创建、数据查询等,GeoMesa前期的安装部署见GeoMesa安装
一、 Geomesa主要常用类
类 | 描述 |
---|---|
SimpleFeatureBuilder | 用来创建feature,feature是geomesa完整数据的实体类,一个feature对应一条数据 |
SimpleFeature | 空间要素的抽象表达,默认含有geometry字段,根据定义的SimpleFeatureType依次传入相应类型的数据,最后设置Feature的ID即可。 |
SimpleFeatureType | 要素元数据描述,包括字段名、类型、空间参考等,类似表结构中不同字段的类型等信息。 |
DataStore | 要素数据集,定义了用户操作数据的接口。DataStore是数据的核心访问模型,存储了数据集的名称、数据结构与类型、数据访问源等信息,类似一种数据元信息的存储集合,用于定义和描述数据的基本信息。 |
FeatureSource | 用于数据查询 |
FeatureStore | FeatureSource子类,增加数据更新功能 |
SimpleFeatureCollection | 数据要素集合,按需加载 |
Query | 数据查询类,封装了各项查询条件 |
二、 数据写入
2.1 数据表示形式
原始数据样式——人数据:
id | name | 出生时间 | 出生地经度 | 出生地维度 |
---|---|---|---|---|
1 | Tom | 1536041936000 | 1.6432 | -19.123 |
通过引用GeoJson
库,在Web APi
中可以用下面的方法将上述数据定义成一个点数据:
point_feature = Feature(id="1",geometry=Point((1.6432, -19.123)),
properties={"id":"1","dtg":1536041936000,"name": "Tom"})
在GeoTools API
中可以通过SimpleFeatureBuilde
r构造一个人的SimpleFeature
:
SimpleFeatureBuilder builder = new SimpleFeatureBuilder(sft);
builder.set("peopleId", "1");
builder.set("geom", "POINT(1.6432, -19.123)");
builder.set("dtg", 1536041936000);
SimpleFeature point_feature = builder.buildFeature("1");
2.2 写入流程(GeoJson)
使用GeoTools
进行数据写入的基本流程如下:
1. 构建GeoMesa连接,获取DataStore对象
2. 构建数据的属性描述信息,包括属性字段名、字段类型、是否对属性建索引
3. 根据属性描述信息创建SimpleFeatureType,即创建数据的schema
4. 设置时空索引的时间间隔、空间索引字段、时空索引精度
5. DataStore根据SimpleFeatureType真正创建数据表的schema
6. 使用SimpleFeatureBuilder对象创建feature,并写入数据
在写入数据时,可以从csv
、txt
文件中写入,只是在写入之前要先把文件中的数据转换为WKT格式,WKT示例如下图。
2.3 代码示例
/**
* 向Geomesa-Hbase中导入数据
* */
public static void importData() throws Exception{
Map params = new HashMap();
params.put("hbase.zookeepers", "10.1.1.1");
params.put("hbase.catalog", "nicole_test");
//构建GeoMesa连接--DataStroe
DataStore dataStore = DataStoreFinder.getDataStore(params);
//声明简单类型SimpleFeatureType,用于操作GeoMesa中数据属性,如实行索引、属性描述等存放在该对象中
SimpleFeatureType sft = null;
//构建属性描述符,接收String类型的属性描述
//属性字段描述方法-- 字段名:字段类型,字段名:字段类型,*地理信息字段名:地理信息字段类型:空间引用标识符=标识符编码
//若对某一个属性做索引,则 -- 字段名:字段类型:index=true,
StringBuilder attributes = new StringBuilder();
attributes.append("plantNo:String:index=true,");
attributes.append("color:String,");
attributes.append("dateAttr:Date,");
attributes.append("*geom:LineString:srid=4326");
// srid是GIS当中的一个空间参考标识符。而此处的srid=4326表示这些数据对应的WGS 84空间参考系统
//使用SimpleFeatureTypes创建SimpleFeatureType,string-data是自己定义的schema的名称
sft = SimpleFeatureTypes.createType("string-data", attributes.toString());
//设置xz3索引时间间隔为天
sft.getUserData().put("geomesa.xz3.interval", "day");
//设置时空索引时间字段为date
sft.getUserData().put("geomesa.index.dtg", "dateAttr");
//设置索引精度
sft.getUserData().put("geomesa.xz.precision", 10);
//DataStore通过SimpleFeatureType真正创建表的schema
SimpleFeatureType sftCheck = dataStore.getSchema("string-data");
if(sftCheck == null){
dataStore.createSchema(sft);
} else{
System.out.println("schema exists !");
}
//声明SimpleFeatureBuilder对象,用于创建feature,一个feature对应一条数据
SimpleFeatureBuilder builder = new SimpleFeatureBuilder(sft);
//构造一条数据
String plantNo = "A00001";
String geomStr = "LINESTRING(120.777 20.444,120.333 30.188,120.3211 30.1902)";
Date date = new Date(System.currentTimeMillis());
builder.set("plantNo", plantNo);
builder.set("geom", geomStr);
builder.set("dateAttr", date);
//设置FeatureID,该字段要求具有唯一性,推荐使用UUID
String featureId = plantNo + 1;
SimpleFeature simpleFeature = builder.buildFeature(featureId);
//数据写入
//通过DataStore获取featureWriter,设置事务自动提交
FeatureWriter writer = dataStore.getFeatureWriterAppend("string-data", Transaction.AUTO_COMMIT);
//服务端下一次要写入的数据feature,并为其设置我们的数据
SimpleFeature toWrite = writer.next();
toWrite.setAttributes(simpleFeature.getAttributes());
((FeatureIdImpl) toWrite.getIdentifier()).setID(simpleFeature.getID());
//设置是否使用呢客户端提供的featureID作为 FID
toWrite.getUserData().put(Hints.USE_PROVIDED_FID, Boolean.TRUE);
toWrite.getUserData().putAll(simpleFeature.getUserData());
//真正的执行写操作
writer.write();
writer.close();
}
数据写入之后在hbase
中会生成五张表:
nicole_test
nicole_test_string_2ddata_attr_v5
nicole_test_string_2ddata_id
nicole_test_string_2ddata_xz2
nicole_test_string_2ddata_xz3 //因为导入的数据是线,所以这里会生成XZ的表
Geomesa
存储到hbase
中的数据是编码之后的,无法在hbase
中查看到具体的数据样式。 通过java API
查看每个表中的rowkey
:
nicole-test
Rowkey: string-data~attributes
Rowkey: string-data~id
Rowkey: string-data~stats-date
Rowkey: string-data~table.attr.v5
Rowkey: string-data~table.id.v1
Rowkey: string-data~table.xz2.v1
Rowkey: string-data~table.xz3.v1
nicole_test_string_2ddata_attr_v5:
Rowkey: A00001 F] !2I'9465d3a8-8f2d-4949-b0e8-cd20589682fa
nicole_test_string_2ddata_id:
Rowkey: 9465d3a8-8f2d-4949-b0e8-cd20589682fa
nicole_test_string_2ddata_xz2:
Rowkey: �W9465d3a8-8f2d-4949-b0e8-cd20589682fa
nicole_test_string_2ddata_xz3:
Rowkey: F] !2I'9465d3a8-8f2d-4949-b0e8-cd20589682fa
三、 索引创建
索引的创建在构建数据的schema
时进行,分为属性索引创建、空间索引创建和时间索引创建。
注意
①时间索引必然要跟随空间索引,不能单独只有时间索引。即可以单独创建空间索引或者共同创建空间索引和时间索引。
②对同一属性不能重复建索引。
3.1 创建属性索引
属性索引比较简单,只需要在构建某个属性的schema
信息时,在属性信息后面增加index=true
的配置信息即可,如为plantNo属性创建属性索引:
attributes.append("plantNo:String:index=true,");
3.2 创建空间索引
在声明空间信息字段时用*来标识该字段是空间信息字段,配置空间引用标识符和标识符编码,会自动为其创建空间索引,如果该属性是Point
,则创建Z索引,如果是LineString
或者Polygon
则创建XZ索引。
attributes.append("*geom:LineString:srid=4326");
该例子中,属性名是geom,类型是线。
3.3 创建时间索引
创建时间索引时,和前两种索引不同,前两种索引是在构建属性时直接指定,但时间索引需要为SimpleFeatureType对象来进行设置。(若schema
中只有一个Date
类型的属性,GeoMesa
会自动为其构建时间索引,但当有多个Date
类型属性时,需手动指定为哪一个时间属性创建索引。)
时间索引可以设定时间间隔周期(默认是week
),并指定创建时间索引的字段。
//设置z3索引时间间隔为天
sft.getUserData().put("geomesa.z3.interval", "day");
//设置时空索引时间字段为date
sft.getUserData().put("geomesa.index.dtg", "dateAttr");
此外该可以对索引设置精度,该精度会影响存储和检索的效率。
//设置索引精度
sft.getUserData().put("geomesa.xz.precision", 10);
更多对索引的设置如下表。
配置项 | 操作代码 |
---|---|
set schema options | sft.getUserData().put("option.one", "foo"); |
设置属性索引 | sft.getDescriptor("name").getUserData().put("index", "true"); |
有多个时间属性时设置时间索引 | sft2.getUserData().put("geomesa.index.dtg", true); |
设置对时间不设置索引 | sft2.getUserData().put("geomesa.ignore.dtg", true); |
设置Feature ID为uuid | sft.getUserData().put("geomesa.fid.uuid", "true"); |
设置geomesa索引精度 | sft.getDescriptor("geom").getUserData().put("precision", "4"); |
设置column-groups | sft.getDescriptor("name").getUserData().put("column-groups", "a,b"); |
设置Z索引的预分片 | sft. getUserData().put("geomesa.z.splits", "4"); |
设置时间索引的间隔 | sft.getUserData().put("geomesa.z3.interval", "month"); |
设置XZ索引的精度 | sft.getUserData().put("geomesa.xz.precision", 12); |
设置属性索引的Shards | sft.getUserData().put("geomesa.attr.splits", "4"); |
设置属性索引的Cardinality | sft.getDescriptor("name").getUserData().put("cardinality", "high"); |
设置索引的分区依据 | sft.getUserData().put("geomesa.table.partition", "time"); |
四、 数据查询
4.1 查询语法介绍
1.列举CQL查询语言语法:
比较运算法、BETWEEN、比较运算符、LIKE、两属性比较、算法表达式、IN、过滤函数、几何过滤等
2.空间关系查询谓词:
INTERSECTS、DISJOINT、CONTAINS、WITHIN、TOUCHES、CROSSES、EQUALS、BBOX
3.时空查询谓词:
BEFORE、BEFORE OR DURING、 DURING、 DURING OR AFTER、 AFTER
4.属性查询:
针对某一属性字段创建索引之后,可以对该列进行属性查询
5.用户可以配置Query
对象参数指定结果具体返回哪些列:
query.setPropertyNames(returnFields) 其中returnFields是String数组
6. 用户可以配置Query
对象的sortBy
参数指定返回结果的排序:
query.setSortBy(sort)
其中sort=new SortBy[]{CommonFactoryFinder.getFilterFactory2().sort(sortField, order)}
具体谓词的作用和使用方法在此。
4.2 查询计划
Geomesa
查询计划是将GeoTools Query
转换为特定后端的扫描和过滤器的过程,它包括以下几个步骤:
(1)重写cql
过滤器以进行快速评估并进行优化;
(2)CQL
过滤器根据可用索引拆分;
(3)选择一个可用索引来执行查询(索引选取策略);
(4)逻辑查询计划由核心geomesa
索引代码创建(各种索引的查询ranges
生成方式);
(5)为特定后端数据库创建物理查询计划(hbase
转为scans
查询)。
1、查询条件分解
查询Query
可能包含多种过滤条件,在查询计划执行中会将这些条件进行拆分,确定主过滤器(用来确定从hbase
拉数据的scans
)和用来过滤的cql过滤器。主过滤器的选择最终是由索引选择决定的。
举例:
对于上述有属性查询、时间和空间查询的请求,可以分解为两种索引使用方式。
(1):Z3索引+属性
Filter
索引;
(2):Z2索引+时间和属性
Filter
索引。
Geomesa
会对不同的索引组合打分,哪种情况分高使用哪种索引进行查询,具体选择策略见下面。
2、索引选择
索引选择基于需要扫描数据库的数据范围最小原则,因此最佳的查询计划通常是扫描最少行的查询计划。Geomesa
有两种方法可供选择:基于成本的策略和基于启发式策略。
(1)基于成本的策略
Geomesa
在录入数据期间会收集统计数据,并将其存储用于查询计划。收集的统计数据是:
总数 |
---|
默认几何,默认日期和任何索引属性的最小值/最大值(边界) |
默认几何,默认日期和任何索引属性的直方图 |
任何索引属性的频率,按周拆分 |
任何索引属性的前k个 |
Z3直方图基于默认几何和默认日期(如果两者都存在) |
根据geomesa
官网介绍,当前只支持针对Accumulo
数据存储,hbase
暂不支持缓存统计信息。
在该例子BBOX(geom,120.227754,30.215471, 120.227898,30.215885) AND date DURING 2018-12-01T00:00:00.000Z/2018-12-01T03:00:00.000Z AND PlateNo = '苏Z1G31G'
,按照规则,则会选择车牌的属性索引进行查询过滤。
(2)基于启发式策略
启发式扫描可以仅基于查询过滤器用于查询计划。优先事项如下,括号内为耗时常量,属性索引耗时和条件有关。
查询条件 | 耗时常量 |
---|---|
使用ID索引的功能ID谓词 | 1L |
使用属性索引的高基数属性谓词 | 500L/25L/10L |
属性相等谓词使用属性索引 | 100L |
使用Z3 / XZ3指数的时空谓词 | 200L |
使用属性索引的属性范围谓词 | 250L |
使用Z2 / XZ2索引的空间谓词 | 400L |
使用Z3 / XZ3指数的时态谓词 | 401L |
使用属性索引的低基数属性谓词 | 50000L/2500L/1000L |
此外,使用join
属性索引的Accumulo
数据存储将根据查询属性/转换对任何需要连接的谓词进行去优先级排序。
如果多个属性谓词与最高优先级相关联,则无法保证从该组中选择哪一个。
启发式策略则是预先设定好和查询优先级相关的耗时常量值,根据查询过滤的CQL
来确定不同索引耗时的排序,选择耗时最少的索引作为执行查询的索引。属性索引常量选择和CQL
条件相关,非空匹配耗时常量值5000
、等于匹配耗时100
、范围匹配耗时250
,如果在创建属性索引时指定了索引基数HIGH/LOW
,耗时对应调整除10 (HIGH)
, 乘10 (LOW)
,不指定则不变。Geomesa
此时会根据这些索引耗时排序,选择耗时最少的索引。
查询时索引的选择顺序:
1. Feature ID predicates using the ID index
2. High-cardinality attribute predicates using the attribute index
3. Attribute equality predicates using the attribute index
4. Spatio-temporal predicates using the Z3/XZ3 index
5. Attribute range predicates using the attribute index
6. Spatial predicates using the Z2/XZ2 index
7. Temporal predicates using the Z3/XZ3 index
4.3 查询流程
查询数据使用GeoTools
进行查询的基本流程如下:
1. 对想要查询的字段,编写相应的查询条件ECQL语句
2. 获取要查询的要素名称,即写入时SimpleFeatureType的Name,schema的名称
3. 用ECQL创建Filter类型的对象
4. 创建Query对象,将上两步中获取的schema名称和Filter对象作为参数传入
5. 构建GeoMesa连接,创建DataStore
6. 使用dataStore对象获取数据读取器,将Query对象传给数据读取器执行查询,查询结果数据通过读取器对象获取。
4.4 代码示例
/**
* 对数据进行时空查询
* */
public static void queryData() throws Exception{
//先定义查询语句
String during = "dateAttr DURING 2010-04-25T00:00:00.000Z/2020-04-28T00:00:00.000Z";
String bbox = "bbox (geom, 115.31412 ,10.89577, 125.31412, 80.89577)";
String spatioTemp = bbox + " AND " + during;
//声明Query查询对象
Query query = null;
query = new Query("string-data", ECQL.toFilter(spatioTemp));
//构建GeoMesa连接--DataStroe
Map params = new HashMap();
params.put("hbase.zookeepers", "10.3.69.191");
params.put("hbase.catalog", "wcy_test");
DataStore dataStore = DataStoreFinder.getDataStore(params);
//获取读取器reader
FeatureReader reader = dataStore.getFeatureReader(query, Transaction.AUTO_COMMIT);
if(reader.hasNext()){
SimpleFeature feature = reader.next();
String locationWKT = feature.getAttribute("geom").toString();
System.out.println(locationWKT);
} else{
System.out.println("No data");
}
}
4.5 常用查询条件
- 设置最大返回条目
Query query = new Query(typeName, ECQL.toFilter(queryCQL));
query.setMaxFeatures(Integer.parseInt(maxView));
- 设置排序
Query query = new Query(typeName, ECQL.toFilter(queryCQL));
FilterFactoryImpl ff = new FilterFactoryImpl();
query.setSortBy(new SortBy[]{new SortByImpl(ff.property("startTime"), SortOrder.ASCENDING)});
- 统计查询-查总数
Query query = new Query(typeName);
query.getHints().put(QueryHints.STATS_STRING(), "Count()");
- 聚合查询-GroupBy,查每个分组的总数
Query query = new Query(typeName);
query.getHints().put(QueryHints.STATS_STRING(), "GroupBy(\"carID\",Count())");
- 统计查询-查最大最小值
Query query = new Query(typeName);
query.getHints().put(QueryHints.STATS_STRING(), "GroupBy(\"carID\",Count())");